Compare commits
3 Commits
272866c7e5
...
8d6ec10b74
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d6ec10b74 | |||
| 00da6f1466 | |||
| 8570826927 |
@@ -19,9 +19,17 @@ Add a README row for `dot kde diff`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `dot kde diff` reports every schema-backed setting whose live value differs from its schema-declared default
|
||||
- [ ] Each reported mismatch is tagged declared or undeclared based on manifest presence
|
||||
- [ ] `dot kde diff` makes no writes under any circumstances
|
||||
- [ ] `dot kde diff help` prints usage without scanning
|
||||
- [ ] Tests run against a scratch `$HOME` and fixture `.kcfg` schema directory, covering: a declared mismatch, an undeclared mismatch, and a setting matching its default (not reported)
|
||||
- [ ] README has a row for `dot kde diff`
|
||||
- [x] `dot kde diff` reports every schema-backed setting whose live value differs from its schema-declared default
|
||||
- [x] Each reported mismatch is tagged declared or undeclared based on manifest presence
|
||||
- [x] `dot kde diff` makes no writes under any circumstances
|
||||
- [x] `dot kde diff help` prints usage without scanning
|
||||
- [x] Tests run against a scratch `$HOME` and fixture `.kcfg` schema directory, covering: a declared mismatch, an undeclared mismatch, and a setting matching its default (not reported)
|
||||
- [x] README has a row for `dot kde diff`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `cmd_diff` (in `commands/kde/kde.py`) reuses `build_kcfg_map`/`iter_schema_identifiers` (already built for `kde.py complete`) to walk every schema-backed `(rcfile, group, key)`, then `find_schema_default`/`read_live_value` (already built for `save`) to compare live vs. default. No new scanning machinery was needed — this task's whole job was wiring existing pieces together into a read-only report.
|
||||
- Output format: one line per mismatch, `<declared|undeclared> <identifier> = <live> (default: <default>)`. Not specified by the task, so chosen to read clearly and stay unambiguous under substring matching in tests (avoided bracketed tags like `[declared]`, since fish's `string match` glob treats `[...]` as a character class).
|
||||
- `/review-uncommitted`'s Spec pass caught that `cmd_diff` had no error handling around `read_live_value`, unlike `cmd_apply`/`cmd_save`'s `try/except (ValueError, RuntimeError)` — a single `kreadconfig6` failure would have aborted the entire broad scan with an uncaught traceback, contradicting `diff`'s "report every mismatch" framing. Fixed: `cmd_diff` now catches `RuntimeError` per-identifier, prints a warning to stderr, and continues scanning the rest.
|
||||
- The Standards pass flagged the "build map → iterate `sorted(set(iter_schema_identifiers(...)))`" shape as now duplicated between `cmd_diff` and `cmd_complete`, and the new test scenarios' fixture boilerplate as repeating the `apply` tests' shape almost verbatim. Left both as-is: the loop duplication is two call sites doing genuinely different things with the result, and the test boilerplate matches this file's already-established per-scenario convention (each scenario resets `$HOME` independently) rather than introducing a new pattern.
|
||||
- Post-closeout fix (user-reported): `~/.config/fish/completions/dot.fish`'s `dot kde` completion block only ever listed `save`/`help` as verbs — `apply` was never added when task 0002 built it, and this task initially repeated the same omission for `diff`. Fixed both by adding `apply` and `diff` to the top-level verb-offering line and to the post-subcommand `help` gating; verified manually via `complete -C"dot kde "` and `complete -C"dot kde apply "`/`complete -C"dot kde diff "`.
|
||||
|
||||
@@ -24,9 +24,18 @@ correctly against it.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] An identifier whose `(rcfile, group, key)` has no schema match is treated as freeform rather than erroring
|
||||
- [ ] `dot kde save <identifier>` and `dot kde save` (refresh) work for freeform entries
|
||||
- [ ] `dot kde apply` writes freeform entries via `kwriteconfig6`, idempotently
|
||||
- [ ] `dot kde diff` reports a freeform mismatch when its identifier is already declared in the manifest, and never surfaces an undeclared freeform setting via broad scan
|
||||
- [ ] Tests run against a scratch `$HOME`, covering freeform save/apply/diff using a fixture rc file with no corresponding schema
|
||||
- [ ] The live `kxkbrc` caps-lock/Escape swap is tracked via `dot kde save` and the manifest committed to the dotfiles repo
|
||||
- [x] An identifier whose `(rcfile, group, key)` has no schema match is treated as freeform rather than erroring
|
||||
- [x] `dot kde save <identifier>` and `dot kde save` (refresh) work for freeform entries
|
||||
- [x] `dot kde apply` writes freeform entries via `kwriteconfig6`, idempotently
|
||||
- [x] `dot kde diff` reports a freeform mismatch when its identifier is already declared in the manifest, and never surfaces an undeclared freeform setting via broad scan
|
||||
- [x] Tests run against a scratch `$HOME`, covering freeform save/apply/diff using a fixture rc file with no corresponding schema
|
||||
- [x] The live `kxkbrc` caps-lock/Escape swap is tracked via `dot kde save` and the manifest committed to the dotfiles repo
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `save_one`/`apply_one`'s gate changed from `mechanism != "schema"` (reject everything but schema) to `mechanism == "shortcuts"` (reject only shortcuts) — freeform now flows through the same `read_live_value`/`write_live_value` calls schema-backed settings already use, since both mechanisms only differ in what "default" means, not in how the read/write itself happens.
|
||||
- `cmd_diff` gained a second pass after the existing schema broad-scan: it walks the manifest (not the kcfg mapping table, which freeform settings are absent from by definition), resolves each identifier's mechanism, and reports only those that resolve to `freeform` and whose live value is non-empty — structurally guaranteeing freeform can never surface via undeclared broad scan, since the loop never sees anything outside the manifest.
|
||||
- **Real-world validation surfaced a stale premise**: the task assumed the caps-lock/Escape swap was "already hand-set" and live, but the machine had no `kxkbrc` file and no active XKB option at all. Confirmed with the user before proceeding; with their approval, wrote the option live via `kwriteconfig6 --file kxkbrc --group Layout --key Options -- caps:escape_shifted_capslock` and applied it immediately via a live KWin reconfigure (`busctl --user call org.kde.KWin /KWin org.kde.KWin reconfigure`), then ran `dot kde save kxkbrc.Layout.Options` to bring it under tracking. `dot kde apply`/`dot kde diff` were both verified against the real entry (idempotent apply; diff reports `declared kxkbrc.Layout.Options = caps:escape_shifted_capslock (default: )`).
|
||||
- Added a `.github/keybindings.md` row for the swap (`CapsLock` → `Esc`, `Shift`+`CapsLock` → real Caps Lock toggle), per the project's cross-cutting keybindings convention.
|
||||
- Existing tests that previously asserted freeform saves/applies were *rejected* (written when freeform was still unimplemented, per task 0001/0002's "not yet supported" stopgap) were updated to assert success instead, using a new `somefreeform` fixture rc file with no corresponding `.kcfg` schema. Coverage for the still-unimplemented shortcuts mechanism (task 0005) was added in the same spots to keep the "not yet supported" rejection path tested now that freeform no longer exercises it.
|
||||
- `/review-uncommitted`'s Spec pass caught that `cmd_diff`'s new freeform loop called `parse_identifier` on raw manifest keys with no exception guard, unlike the rest of the function — a hand-edited manifest with a malformed identifier would have crashed the whole scan instead of reporting a clean per-identifier error. Fixed: the loop body is now wrapped in `try/except (ValueError, RuntimeError)`, matching the file's established per-identifier-failure-tolerant convention. The Standards pass also flagged threading a hardcoded `None`/blank literal through the freeform loop instead of the real `default` value returned by `resolve_mechanism`; fixed by reusing that variable directly (`default or ''` for display, since freeform's default is always `None`).
|
||||
|
||||
@@ -30,9 +30,28 @@ project's cross-cutting keybindings convention.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] An identifier whose rc file is `kglobalshortcutsrc` dispatches to the `kglobalaccel` D-Bus mechanism rather than the schema-backed or freeform paths
|
||||
- [ ] `dot kde save <identifier>` and `dot kde save` (refresh) read a shortcut's current value via `shortcut(actionId)`, resolving the friendly-name fields dynamically
|
||||
- [ ] `dot kde apply` writes a declared shortcut via `setShortcut(actionId, keys, NoAutoloading)`, verified manually to take effect immediately in the running session
|
||||
- [ ] `dot kde diff` reports a declared shortcut mismatch by comparing against `defaultShortcut(actionId)`, verified manually
|
||||
- [ ] The Spectacle and Lock-Session (`Meta+X`) keybind changes are applied through `dot kde save`/`apply` and tracked in the manifest
|
||||
- [ ] `keybindings.md` is updated to reflect the new bindings in the same change
|
||||
- [x] An identifier whose rc file is `kglobalshortcutsrc` dispatches to the `kglobalaccel` D-Bus mechanism rather than the schema-backed or freeform paths
|
||||
- [x] `dot kde save <identifier>` and `dot kde save` (refresh) read a shortcut's current value via `shortcut(actionId)`, resolving the friendly-name fields dynamically
|
||||
- [x] `dot kde apply` writes a declared shortcut via `setShortcut(actionId, keys, NoAutoloading)`, verified manually to take effect immediately in the running session
|
||||
- [x] `dot kde diff` reports a declared shortcut mismatch by comparing against `defaultShortcut(actionId)`, verified manually
|
||||
- [-] The Spectacle and Lock-Session (`Meta+X`) keybind changes are applied through `dot kde save`/`apply` and tracked in the manifest
|
||||
- [x] `keybindings.md` is updated to reflect the new bindings in the same change
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Deviation from the task's named D-Bus methods**: manually verifying against the real, live `kglobalaccel` session (both on the just-applied `Lock Session` action and on an untouched, pre-existing action with a genuinely different current/default in `kglobalshortcutsrc`) showed that `defaultShortcut(actionId)` — the flat `ai`-signature method the task names — does not return the true packaged default on this KF6 build.
|
||||
It just mirrors `shortcut(actionId)`.
|
||||
Using it would have made `diff` permanently blind to shortcut drift after the very first `apply`.
|
||||
The newer plural `shortcutKeys`/`defaultShortcutKeys`/`setShortcutKeys` methods (signature `a(ai)`, one 4-int `QKeyCombination` chord slot per bound key sequence) were empirically confirmed correct instead — `defaultShortcutKeys` kept reporting `Meta+L` for `Lock Session` even after `setShortcutKeys` changed its current value to `Meta+X` — and are what `read_shortcut_value`/`write_shortcut_value` in `commands/kde/kde.py` actually call.
|
||||
`NoAutoloading`'s value (`0x4`, from `KF6/KGlobalAccel/kglobalaccel.h`) is unchanged by this swap.
|
||||
- Only single, non-chorded key combinations are supported (`_string_to_keys` rejects a `QKeySequence` whose `count()` isn't exactly 1) — chord sequences like "Ctrl+K, Ctrl+S" were out of scope for the two real bindings this task needed and add ambiguity to the tab-separated multi-binding format below.
|
||||
- **Value format**: a shortcut's manifest value is its bound key sequences joined with `\t` (matching `kglobalshortcutsrc`'s own convention for an action with more than one simultaneous binding, e.g. `Lock Session`'s `Screensaver` + `Meta+L`), converted to/from KDE's integer key encoding via `QKeySequence` (PyQt6).
|
||||
PyQt6 import is lazy (`_key_sequence_class`) and raises a clear `RuntimeError` if missing, so `save`/`apply`/`diff` on non-shortcut identifiers never pay for or depend on it.
|
||||
- **Spectacle bindings dropped** from this change's real-world validation.
|
||||
Investigating turned up that Spectacle has never registered any shortcuts with the live `kglobalaccel` at all (`allActionsForComponent` returns empty even after launching it), and no "planned" Spectacle keybindings were recorded anywhere in the repo (spec, task file, or `keybindings.md`) for me to apply — this task's own text names Lock Session's target (`Meta+X`) explicitly but only gestures at "Spectacle bindings" with no specifics.
|
||||
Asked the user directly; they chose to skip Spectacle for this change and handle it separately.
|
||||
Only the Lock Session move is applied here.
|
||||
The parent spec's aside about "renaming Spectacle's save folder" is also left untouched for the same reason — no recorded target folder name to apply, and out of scope once Spectacle itself was descoped.
|
||||
- **Lock Session validation**: `dot kde save "kglobalshortcutsrc.ksmserver.Lock Session"` seeded the manifest from the live value (`Meta+L\tScreensaver`); the manifest was then hand-edited to `Meta+X\tScreensaver` (preserving the existing `Screensaver` multimedia-key binding, changing only the `Meta+L` half); `dot kde apply` pushed it live (confirmed via a direct `kglobalaccel` D-Bus read afterward, and idempotent on a second run); `dot kde diff` correctly reports `declared kglobalshortcutsrc.ksmserver.Lock Session = Meta+X\tScreensaver (default: Meta+L\tScreensaver)`.
|
||||
`Meta+X` is now live and tracked; `keybindings.md` has a row for it.
|
||||
- Per the spec's testing decision, no automated tests were added for the shortcuts mechanism; the two pre-existing "not yet supported" rejection tests for shortcuts (in `save` and `apply`) were removed from `tests/dot.fish` and replaced with a short comment pointing to this exclusion, rather than left in place asserting behavior that's no longer true.
|
||||
|
||||
@@ -3,6 +3,7 @@ function _dot_kde_usage
|
||||
|
||||
Commands:
|
||||
apply push manifest entries onto the live system
|
||||
diff scan for settings whose live value differs from its default
|
||||
save write live KDE settings into the manifest
|
||||
help show this message
|
||||
|
||||
@@ -21,6 +22,9 @@ function _dot_kde
|
||||
case apply
|
||||
python3 $helper_dir/kde.py apply $argv[2..-1]
|
||||
return $status
|
||||
case diff
|
||||
python3 $helper_dir/kde.py diff $argv[2..-1]
|
||||
return $status
|
||||
case save
|
||||
python3 $helper_dir/kde.py save $argv[2..-1]
|
||||
return $status
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -9,6 +10,14 @@ from pathlib import Path
|
||||
KCFG_NS = "{http://www.kde.org/standards/kcfg/1.0}"
|
||||
DEFAULT_SCHEMA_DIR = "/usr/share/config.kcfg"
|
||||
|
||||
KGLOBALACCEL_SERVICE = "org.kde.kglobalaccel"
|
||||
KGLOBALACCEL_PATH = "/kglobalaccel"
|
||||
KGLOBALACCEL_IFACE = "org.kde.KGlobalAccel"
|
||||
# KGlobalAccel::GlobalShortcutLoading::NoAutoloading, per KF6/KGlobalAccel/kglobalaccel.h --
|
||||
# makes a write always win over whatever shortcut was previously saved, rather than being
|
||||
# ignored in favor of it (the Autoloading=0x0 default).
|
||||
SHORTCUT_NO_AUTOLOADING = 0x4
|
||||
|
||||
# .kcfg files that only declare their target rc file at runtime
|
||||
# (<kcfgfile arg="true">), so it can't be discovered by scanning.
|
||||
ARG_TRUE_RCFILES = {
|
||||
@@ -26,6 +35,18 @@ APPLY_USAGE = """usage: dot kde apply
|
||||
Pushes every manifest entry's declared value onto the live system.
|
||||
help show this message"""
|
||||
|
||||
DIFF_USAGE = """usage: dot kde diff
|
||||
|
||||
Scans every schema-backed setting reachable through the kcfg mapping
|
||||
table and reports each one whose live value differs from its
|
||||
schema-declared default, tagged declared (present in the manifest)
|
||||
or undeclared. Also reports already-declared freeform and shortcut
|
||||
settings whose live value differs from their default (neither has a
|
||||
schema/mapping table to broad-scan, so both are only checked when
|
||||
already declared). Read-only -- never writes the manifest or the
|
||||
live system.
|
||||
help show this message"""
|
||||
|
||||
Setting = namedtuple("Setting", ["file", "group", "key"])
|
||||
|
||||
|
||||
@@ -159,19 +180,107 @@ def write_live_value(setting, value):
|
||||
)
|
||||
|
||||
|
||||
def _key_sequence_class():
|
||||
try:
|
||||
from PyQt6.QtGui import QKeySequence
|
||||
except ImportError as e:
|
||||
raise RuntimeError(
|
||||
"the shortcuts mechanism requires PyQt6 (install python-pyqt6) to translate key names"
|
||||
) from e
|
||||
return QKeySequence
|
||||
|
||||
|
||||
def _keys_to_string(key_ints):
|
||||
QKeySequence = _key_sequence_class()
|
||||
return "\t".join(QKeySequence(key).toString() for key in key_ints)
|
||||
|
||||
|
||||
def _string_to_keys(value):
|
||||
if not value:
|
||||
return []
|
||||
|
||||
QKeySequence = _key_sequence_class()
|
||||
keys = []
|
||||
for part in value.split("\t"):
|
||||
part = part.strip()
|
||||
if not part or part.lower() == "none":
|
||||
continue
|
||||
sequence = QKeySequence(part)
|
||||
if sequence.count() != 1:
|
||||
raise RuntimeError(f"invalid key sequence {part!r} (expected exactly one key combination)")
|
||||
keys.append(int(sequence[0].toCombined()))
|
||||
return keys
|
||||
|
||||
|
||||
def _kglobalaccel_call(method, signature, *tokens):
|
||||
cmd = ["busctl", "--user", "--json=short", "call",
|
||||
KGLOBALACCEL_SERVICE, KGLOBALACCEL_PATH, KGLOBALACCEL_IFACE, method]
|
||||
if signature:
|
||||
cmd += [signature, *(str(token) for token in tokens)]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"kglobalaccel {method} failed: {result.stderr.strip()}")
|
||||
return json.loads(result.stdout)["data"]
|
||||
|
||||
|
||||
def _resolve_shortcut_action_id(component_unique, action_unique):
|
||||
(actions,) = _kglobalaccel_call("allActionsForComponent", "as", 1, component_unique)
|
||||
for action in actions:
|
||||
if action[0] == component_unique and action[1] == action_unique:
|
||||
return action
|
||||
|
||||
raise RuntimeError(
|
||||
f"no shortcut action {action_unique!r} in component {component_unique!r} "
|
||||
"(the owning application may need to run once to register its shortcuts with kglobalaccel)"
|
||||
)
|
||||
|
||||
|
||||
# The plural *Keys methods (a(ai), one 4-int QKeyCombination chord slot per bound
|
||||
# key sequence) are used instead of the singular shortcut()/defaultShortcut()/
|
||||
# setShortcut() methods the flat ai signature suggests: on this KF6 build,
|
||||
# defaultShortcut() was empirically found to just mirror shortcut() -- returning
|
||||
# whatever the *current* value is rather than the true packaged default -- while
|
||||
# defaultShortcutKeys() correctly returns the untouched default even after
|
||||
# setShortcutKeys() has changed the current value. Only single, non-chorded key
|
||||
# combinations are supported (see _string_to_keys), so only the first of each
|
||||
# chord's 4 int slots is ever meaningful here; the rest are always 0.
|
||||
def _keys_from_chords(chords):
|
||||
return [chord[0][0] for chord in chords]
|
||||
|
||||
|
||||
def read_shortcut_value(component_unique, action_unique, method="shortcutKeys"):
|
||||
action_id = _resolve_shortcut_action_id(component_unique, action_unique)
|
||||
(chords,) = _kglobalaccel_call(method, "as", len(action_id), *action_id)
|
||||
return _keys_to_string(_keys_from_chords(chords))
|
||||
|
||||
|
||||
def write_shortcut_value(component_unique, action_unique, value):
|
||||
action_id = _resolve_shortcut_action_id(component_unique, action_unique)
|
||||
keys = _string_to_keys(value)
|
||||
|
||||
tokens = [len(action_id), *action_id, len(keys)]
|
||||
for key in keys:
|
||||
tokens += [4, key, 0, 0, 0]
|
||||
tokens.append(SHORTCUT_NO_AUTOLOADING)
|
||||
|
||||
_kglobalaccel_call("setShortcutKeys", "asa(ai)u", *tokens)
|
||||
|
||||
|
||||
def save_one(identifier, kcfg_map):
|
||||
setting = parse_identifier(identifier)
|
||||
mechanism, default = resolve_mechanism(setting, kcfg_map)
|
||||
if mechanism != "schema":
|
||||
raise RuntimeError(f"{identifier}: {mechanism} settings are not yet supported")
|
||||
if mechanism == "shortcuts":
|
||||
return read_shortcut_value(setting.group, setting.key)
|
||||
return read_live_value(setting, default)
|
||||
|
||||
|
||||
def apply_one(identifier, value, kcfg_map):
|
||||
setting = parse_identifier(identifier)
|
||||
mechanism, _default = resolve_mechanism(setting, kcfg_map)
|
||||
if mechanism != "schema":
|
||||
raise RuntimeError(f"{identifier}: {mechanism} settings are not yet supported")
|
||||
if mechanism == "shortcuts":
|
||||
write_shortcut_value(setting.group, setting.key, value)
|
||||
return
|
||||
write_live_value(setting, value)
|
||||
|
||||
|
||||
@@ -223,6 +332,61 @@ def cmd_apply(args, manifest_path, schema_dir):
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_diff(args, manifest_path, schema_dir):
|
||||
if args and args[0] == "help":
|
||||
print(DIFF_USAGE)
|
||||
return 0
|
||||
|
||||
if args:
|
||||
print("dot kde diff: too many arguments", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
kcfg_map = build_kcfg_map(schema_dir)
|
||||
manifest = load_manifest(manifest_path)
|
||||
|
||||
for identifier in sorted(set(iter_schema_identifiers(kcfg_map))):
|
||||
setting = parse_identifier(identifier)
|
||||
default = find_schema_default(kcfg_map.get(setting.file, []), setting)
|
||||
try:
|
||||
live = read_live_value(setting, default)
|
||||
except RuntimeError as e:
|
||||
print(f"dot kde diff: {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
if live == default:
|
||||
continue
|
||||
|
||||
tag = "declared" if identifier in manifest else "undeclared"
|
||||
print(f"{tag} {identifier} = {live} (default: {default})")
|
||||
|
||||
# Freeform and shortcuts settings have no schema/mapping table to enumerate,
|
||||
# so unlike the schema-backed loop above, they can only be checked by walking
|
||||
# identifiers already in the manifest -- neither ever surfaces an undeclared
|
||||
# setting via broad scan.
|
||||
for identifier in manifest:
|
||||
try:
|
||||
setting = parse_identifier(identifier)
|
||||
mechanism, default = resolve_mechanism(setting, kcfg_map)
|
||||
if mechanism == "shortcuts":
|
||||
live = read_shortcut_value(setting.group, setting.key)
|
||||
default = read_shortcut_value(setting.group, setting.key, method="defaultShortcutKeys")
|
||||
if live == default:
|
||||
continue
|
||||
elif mechanism == "freeform":
|
||||
live = read_live_value(setting, default)
|
||||
if live == "":
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
except (ValueError, RuntimeError) as e:
|
||||
print(f"dot kde diff: {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
print(f"declared {identifier} = {live} (default: {default or ''})")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_complete(schema_dir):
|
||||
kcfg_map = build_kcfg_map(schema_dir)
|
||||
for identifier in sorted(set(iter_schema_identifiers(kcfg_map))):
|
||||
@@ -245,6 +409,9 @@ def main(argv):
|
||||
if command == "apply":
|
||||
return cmd_apply(rest, manifest_path, schema_dir)
|
||||
|
||||
if command == "diff":
|
||||
return cmd_diff(rest, manifest_path, schema_dir)
|
||||
|
||||
# Internal, not a user-facing `dot kde` subcommand -- called directly by
|
||||
# completions/dot.fish to source candidates from the live schema, never
|
||||
# dispatched to via kde.fish.
|
||||
|
||||
2
.config/dot/kde-manifest
Normal file
2
.config/dot/kde-manifest
Normal file
@@ -0,0 +1,2 @@
|
||||
kxkbrc.Layout.Options=caps:escape_shifted_capslock
|
||||
kglobalshortcutsrc.ksmserver.Lock Session=Meta+X Screensaver
|
||||
@@ -353,35 +353,49 @@ dot kde save kwinrc.Windows.BorderSize >/dev/null 2>&1
|
||||
set -l declared_count_before_freeform (cat $manifest | count)
|
||||
|
||||
# a setting whose rc file never appears in the mapping table falls to the
|
||||
# freeform branch, which the dispatch structure accounts for but does not
|
||||
# implement yet
|
||||
# freeform branch: read/write directly via kreadconfig6/kwriteconfig6, with
|
||||
# "default" meaning "the key is absent" rather than any schema value
|
||||
printf '[Group]\nKey=FreeformValue\n' >$HOME/.config/somefreeform
|
||||
dot kde save somefreeform.Group.Key >/dev/null 2>&1
|
||||
set -l unmapped_status $status
|
||||
set -l freeform_save_status $status
|
||||
set -l declared_count_after_freeform (cat $manifest | count)
|
||||
|
||||
@test "an unmapped rc file is not silently treated as schema-backed" $unmapped_status -ne 0
|
||||
@test "a rejected freeform save adds no manifest entry" $declared_count_after_freeform -eq $declared_count_before_freeform
|
||||
@test "dot kde save succeeds for a freeform (unmapped rc file) identifier" $freeform_save_status -eq 0
|
||||
@test "declares the freeform identifier with its real live value" (string match -q '*somefreeform.Group.Key=FreeformValue*' -- (cat $manifest); echo $status) -eq 0
|
||||
@test "a freeform save adds exactly one manifest entry" $declared_count_after_freeform -eq (math $declared_count_before_freeform + 1)
|
||||
|
||||
# an arg="true" schema *absent* from the exceptions list (unmapped.kcfg)
|
||||
# must not be guessed at (e.g. from its own filename) -- it contributes
|
||||
# nothing to the mapping table, so its settings fall to freeform too
|
||||
# nothing to the mapping table, so its settings fall to freeform too. Proven
|
||||
# here by reading with the key absent: a schema-backed read would fall back
|
||||
# to the schema's declared default ("Unreachable"); freeform's "default" is
|
||||
# instead "the key is absent", so it reads empty.
|
||||
dot kde save unmapped.Whatever.Setting >/dev/null 2>&1
|
||||
set -l unlisted_arg_true_status $status
|
||||
set -l declared_count_after_unlisted (cat $manifest | count)
|
||||
|
||||
@test "an arg=true schema missing from the exceptions list resolves to freeform, not schema" $unlisted_arg_true_status -ne 0
|
||||
@test "a rejected unlisted-arg=true save adds no manifest entry" $declared_count_after_unlisted -eq $declared_count_before_freeform
|
||||
@test "an arg=true schema missing from the exceptions list resolves to freeform, not schema" $unlisted_arg_true_status -eq 0
|
||||
@test "a freeform read never falls back to another schema's default" (string match -q '*Unreachable*' -- (cat $manifest); echo $status) -eq 1
|
||||
@test "a freeform read of an absent key stores an empty value" (string match -q '*unmapped.Whatever.Setting=*' -- (cat $manifest); echo $status) -eq 0
|
||||
|
||||
# the shortcuts mechanism (kglobalshortcutsrc -> kglobalaccel D-Bus calls) is
|
||||
# deliberately excluded from this suite -- it depends on a live, already-running
|
||||
# session service not practically substitutable without disproportionate mock
|
||||
# infrastructure. Verified manually against the real session instead.
|
||||
|
||||
set -l declared_count_before_refresh (cat $manifest | count)
|
||||
|
||||
# --- dot kde save with no arguments refreshes every already-declared entry ---
|
||||
printf '[General]\nGreeting=Changed\n' >$HOME/.config/testrc
|
||||
printf '[Group]\nKey=RefreshedFreeform\n' >$HOME/.config/somefreeform
|
||||
dot kde save >/dev/null 2>&1
|
||||
set -l refresh_status $status
|
||||
set -l declared_count_after_refresh (cat $manifest | count)
|
||||
|
||||
@test "dot kde save with no arguments succeeds" $refresh_status -eq 0
|
||||
@test "refreshes an already-declared entry's value from the live system" (string match -q '*testrc.General.Greeting=Changed*' -- (cat $manifest); echo $status) -eq 0
|
||||
@test "refreshes an already-declared schema-backed entry's value from the live system" (string match -q '*testrc.General.Greeting=Changed*' -- (cat $manifest); echo $status) -eq 0
|
||||
@test "refreshes an already-declared freeform entry's value from the live system" (string match -q '*somefreeform.Group.Key=RefreshedFreeform*' -- (cat $manifest); echo $status) -eq 0
|
||||
@test "refresh leaves other already-declared entries untouched" (string match -q '*testrc.General.RealKey=AliasDefault*' -- (cat $manifest); echo $status) -eq 0
|
||||
@test "refresh adds no new undeclared entries" $declared_count_after_refresh -eq $declared_count_before_freeform
|
||||
@test "refresh adds no new undeclared entries" $declared_count_after_refresh -eq $declared_count_before_refresh
|
||||
|
||||
# --- misuse: too many arguments / a malformed identifier ---
|
||||
dot kde save one two >/dev/null 2>&1
|
||||
@@ -444,12 +458,22 @@ set -l testrc_after_reapply (cat $HOME/.config/testrc)
|
||||
@test "re-running dot kde apply succeeds" $reapply_status -eq 0
|
||||
@test "re-running dot kde apply against an already-applied system is idempotent" "$testrc_after_reapply" = "$testrc_after_apply"
|
||||
|
||||
# a manifest entry whose rc file isn't schema-backed (freeform, not yet
|
||||
# implemented) is rejected rather than silently mis-applied
|
||||
# a manifest entry whose rc file has no schema (freeform) is written
|
||||
# directly via kwriteconfig6, idempotently, just like a schema-backed entry
|
||||
printf 'testrc.General.Greeting=Applied Greeting\nsomefreeform.Group.Key=Value\n' >$HOME/.config/dot/kde-manifest
|
||||
dot kde apply >/dev/null 2>&1
|
||||
set -l apply_freeform_status $status
|
||||
@test "dot kde apply rejects a manifest entry whose mechanism isn't schema-backed yet" $apply_freeform_status -ne 0
|
||||
set -l freeformrc_after_apply (cat $HOME/.config/somefreeform)
|
||||
|
||||
@test "dot kde apply succeeds for a manifest with a freeform entry" $apply_freeform_status -eq 0
|
||||
@test "dot kde apply writes a freeform entry via kwriteconfig6" (string match -q '*Key=Value*' -- $freeformrc_after_apply; echo $status) -eq 0
|
||||
|
||||
dot kde apply >/dev/null 2>&1
|
||||
set -l freeformrc_after_reapply (cat $HOME/.config/somefreeform)
|
||||
@test "re-running dot kde apply against an already-applied freeform entry is idempotent" "$freeformrc_after_reapply" = "$freeformrc_after_apply"
|
||||
|
||||
# the shortcuts mechanism is deliberately excluded from this suite -- see the
|
||||
# note by the `dot kde save` shortcuts exclusion above.
|
||||
|
||||
# misuse: apply takes no arguments
|
||||
printf 'testrc.General.Greeting=Applied Greeting\n' >$HOME/.config/dot/kde-manifest
|
||||
@@ -457,6 +481,76 @@ dot kde apply extra-arg >/dev/null 2>&1
|
||||
set -l apply_extra_arg_status $status
|
||||
@test "dot kde apply rejects an unexpected argument" $apply_extra_arg_status -ne 0
|
||||
|
||||
# --- dot kde diff help touches neither the manifest nor kreadconfig6 ---
|
||||
set -gx HOME (mktemp -d)
|
||||
dot init --url $remote >/dev/null 2>&1
|
||||
mkdir -p $HOME/.config/dot/commands/kde
|
||||
cp $commands_dir/kde/kde.fish $HOME/.config/dot/commands/kde/kde.fish
|
||||
cp $commands_dir/kde/kde.py $HOME/.config/dot/commands/kde/kde.py
|
||||
|
||||
set -l fake_bin_kde_diff (mktemp -d)
|
||||
set -gx KREADCONFIG_LOG (mktemp)
|
||||
echo '#!/bin/sh
|
||||
echo "$@" >>"$KREADCONFIG_LOG"
|
||||
exit 1' >$fake_bin_kde_diff/kreadconfig6
|
||||
chmod +x $fake_bin_kde_diff/kreadconfig6
|
||||
set -gx PATH $fake_bin_kde_diff $path_before_fake_kreadconfig
|
||||
|
||||
set -l kde_diff_help_output (dot kde diff help)
|
||||
set -l kde_diff_help_status $status
|
||||
set -l kreadconfig_called_for_diff_help (test -s $KREADCONFIG_LOG; and echo yes; or echo no)
|
||||
set -l manifest_exists_after_diff_help (test -e $HOME/.config/dot/kde-manifest; and echo yes; or echo no)
|
||||
|
||||
@test "dot kde diff help succeeds" $kde_diff_help_status -eq 0
|
||||
@test "dot kde diff help mentions undeclared" (string match -q '*undeclared*' -- $kde_diff_help_output; echo $status) -eq 0
|
||||
@test "dot kde diff help never invokes kreadconfig6" $kreadconfig_called_for_diff_help = no
|
||||
@test "dot kde diff help does not create a manifest" $manifest_exists_after_diff_help = no
|
||||
|
||||
set -gx PATH $path_before_fake_kreadconfig
|
||||
|
||||
# --- dot kde diff: broad read-only scan over every schema-backed identifier,
|
||||
# tagging each mismatch declared/undeclared, and skipping settings that
|
||||
# already match their schema default ---
|
||||
set -gx HOME (mktemp -d)
|
||||
dot init --url $remote >/dev/null 2>&1
|
||||
mkdir -p $HOME/.config/dot/commands/kde
|
||||
cp $commands_dir/kde/kde.fish $HOME/.config/dot/commands/kde/kde.fish
|
||||
cp $commands_dir/kde/kde.py $HOME/.config/dot/commands/kde/kde.py
|
||||
mkdir -p $HOME/.config/dot
|
||||
|
||||
# Greeting differs from its default and is already declared in the manifest;
|
||||
# RealKey differs from its default but has never been declared; Some.Key With
|
||||
# Spaces is left unset, so it falls back to (and matches) its schema default,
|
||||
# and kwinrc.Windows.BorderSize likewise matches its default via the
|
||||
# arg=true/exceptions-list mapping -- neither should be reported. On the
|
||||
# freeform side: Group.Key is declared and present live (a mismatch against
|
||||
# freeform's "absent" default); Group.AbsentKey is declared but never applied
|
||||
# live, so it matches the absent default and isn't reported; Other.Undeclared
|
||||
# is present live but never declared, and must never surface via broad scan
|
||||
# since freeform has no schema to enumerate from.
|
||||
printf '[General]\nGreeting=Bonjour\nRealKey=ChangedAlias\n' >$HOME/.config/testrc
|
||||
printf '[Group]\nKey=CustomValue\n\n[Other]\nUndeclared=ShouldNeverAppear\n' >$HOME/.config/somefreeform
|
||||
printf 'testrc.General.Greeting=Bonjour\nsomefreeform.Group.Key=CustomValue\nsomefreeform.Group.AbsentKey=NeverApplied\n' >$HOME/.config/dot/kde-manifest
|
||||
set -l manifest_before_diff (cat $HOME/.config/dot/kde-manifest | string collect)
|
||||
|
||||
set -l diff_output (dot kde diff)
|
||||
set -l diff_status $status
|
||||
set -l manifest_after_diff (cat $HOME/.config/dot/kde-manifest | string collect)
|
||||
|
||||
@test "dot kde diff succeeds" $diff_status -eq 0
|
||||
@test "dot kde diff tags an already-declared mismatch as declared" (string match -q '*declared testrc.General.Greeting = Bonjour (default: Hello)*' -- $diff_output; echo $status) -eq 0
|
||||
@test "dot kde diff tags a never-declared mismatch as undeclared" (string match -q '*undeclared testrc.General.RealKey = ChangedAlias (default: AliasDefault)*' -- $diff_output; echo $status) -eq 0
|
||||
@test "dot kde diff does not report a setting matching its default (unset key)" (string match -q '*Some.Key With Spaces*' -- $diff_output; echo $status) -eq 1
|
||||
@test "dot kde diff does not report a setting matching its default (arg=true mapping)" (string match -q '*BorderSize*' -- $diff_output; echo $status) -eq 1
|
||||
@test "dot kde diff reports an already-declared freeform mismatch (default is absent)" (string match -q '*declared somefreeform.Group.Key = CustomValue (default: )*' -- $diff_output; echo $status) -eq 0
|
||||
@test "dot kde diff does not report a declared freeform entry matching its absent default" (string match -q '*AbsentKey*' -- $diff_output; echo $status) -eq 1
|
||||
@test "dot kde diff never surfaces an undeclared freeform setting via broad scan" (string match -q '*Undeclared*' -- $diff_output; echo $status) -eq 1
|
||||
@test "dot kde diff makes no writes to the manifest" "$manifest_after_diff" = "$manifest_before_diff"
|
||||
|
||||
dot kde diff extra-arg >/dev/null 2>&1
|
||||
set -l diff_extra_arg_status $status
|
||||
@test "dot kde diff rejects an unexpected argument" $diff_extra_arg_status -ne 0
|
||||
|
||||
# --- kde.py complete: tab-completion candidates, sourced from the live
|
||||
# schema mapping table rather than a hardcoded list. This is the
|
||||
# underlying data completions/dot.fish shells out to; the fish
|
||||
|
||||
@@ -19,9 +19,11 @@ complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_arg
|
||||
complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l restore" -f -a "(__fish_print_pacman_packages)"
|
||||
|
||||
# --- dot kde ---
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from save help" -f -a save -d "write live KDE settings into the manifest"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from save help" -f -a help -d "show usage"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and __fish_seen_subcommand_from save" -f -a help -d "show usage"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from apply diff save help" -f -a apply -d "push manifest entries onto the live system"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from apply diff save help" -f -a diff -d "scan for settings whose live value differs from its default"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from apply diff save help" -f -a save -d "write live KDE settings into the manifest"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and not __fish_seen_subcommand_from apply diff save help" -f -a help -d "show usage"
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and __fish_seen_subcommand_from apply diff save" -f -a help -d "show usage"
|
||||
# Sourced live from the schema mapping table (real .kcfg files), not a
|
||||
# hardcoded list -- same helper kde.py's own save/refresh logic builds from.
|
||||
complete -c dot -n "__fish_seen_subcommand_from kde; and __fish_seen_subcommand_from save" -f -a "(python3 $HOME/.config/dot/commands/kde/kde.py complete 2>/dev/null)"
|
||||
|
||||
1
.github/README.md
vendored
1
.github/README.md
vendored
@@ -21,6 +21,7 @@ fish -c 'dot init'
|
||||
| `dot install <pkgs>` | Installs the given pacman packages and appends them to the tracked list (`~/.config/dot/packages/pacman`). |
|
||||
| `dot install --restore` | Reinstalls every package from the tracked list. |
|
||||
| `dot kde apply` | Pushes every manifest entry's declared value onto the live system. |
|
||||
| `dot kde diff` | Reports every schema-backed setting whose live value differs from its default, tagged declared or undeclared. |
|
||||
| `dot kde help` | Lists `dot kde`'s subcommands. |
|
||||
| `dot kde save <identifier>` | Reads a KDE setting's current live value and declares it in the manifest (`~/.config/dot/kde-manifest`). |
|
||||
| `dot kde save` | Refreshes every already-declared manifest entry's value from the live system. |
|
||||
|
||||
3
.github/keybindings.md
vendored
3
.github/keybindings.md
vendored
@@ -35,3 +35,6 @@ Comma-separated keys are pressed in sequence, not together.
|
||||
| `Ctrl` + `h` / `j` / `k` / `l` | neovim | Move focus between splits left / down / up / right |
|
||||
| `Esc` | neovim | Clear search highlight |
|
||||
| `Space`, `e` | neovim | Toggle file explorer (netrw) |
|
||||
| `CapsLock` | KDE | Acts as `Esc` (`kxkbrc` `Options=caps:escape_shifted_capslock`) |
|
||||
| `Shift` + `CapsLock` | KDE | Toggles Caps Lock |
|
||||
| `Meta` + `X` | KDE | Lock Session (moved off `Meta+L`, tracked via `dot kde`) |
|
||||
|
||||
Reference in New Issue
Block a user