dotcli: Add dot kde diff for schema-backed settings, fix apply/diff completions

Implements the broad, read-only schema-backed scan for dot kde diff:
walks every (rcfile, group, key) reachable through the kcfg mapping
table, compares live values against their schema-declared defaults,
and reports each mismatch tagged declared or undeclared. Never writes.

Also fixes dot kde completions, which only ever offered save/help —
apply (added in a prior task) and diff were both missing.
This commit is contained in:
2026-07-05 20:23:39 -04:00
parent 272866c7e5
commit 8570826927
6 changed files with 127 additions and 9 deletions

View File

@@ -19,9 +19,17 @@ Add a README row for `dot kde diff`.
## Acceptance criteria ## Acceptance criteria
- [ ] `dot kde diff` reports every schema-backed setting whose live value differs from its schema-declared default - [x] `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 - [x] Each reported mismatch is tagged declared or undeclared based on manifest presence
- [ ] `dot kde diff` makes no writes under any circumstances - [x] `dot kde diff` makes no writes under any circumstances
- [ ] `dot kde diff help` prints usage without scanning - [x] `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) - [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)
- [ ] README has a row for `dot kde diff` - [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 "`.

View File

@@ -3,6 +3,7 @@ function _dot_kde_usage
Commands: Commands:
apply push manifest entries onto the live system 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 save write live KDE settings into the manifest
help show this message help show this message
@@ -21,6 +22,9 @@ function _dot_kde
case apply case apply
python3 $helper_dir/kde.py apply $argv[2..-1] python3 $helper_dir/kde.py apply $argv[2..-1]
return $status return $status
case diff
python3 $helper_dir/kde.py diff $argv[2..-1]
return $status
case save case save
python3 $helper_dir/kde.py save $argv[2..-1] python3 $helper_dir/kde.py save $argv[2..-1]
return $status return $status

View File

@@ -26,6 +26,15 @@ APPLY_USAGE = """usage: dot kde apply
Pushes every manifest entry's declared value onto the live system. Pushes every manifest entry's declared value onto the live system.
help show this message""" 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. Read-only -- never writes the manifest or the live
system.
help show this message"""
Setting = namedtuple("Setting", ["file", "group", "key"]) Setting = namedtuple("Setting", ["file", "group", "key"])
@@ -223,6 +232,36 @@ def cmd_apply(args, manifest_path, schema_dir):
return 0 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})")
return 0
def cmd_complete(schema_dir): def cmd_complete(schema_dir):
kcfg_map = build_kcfg_map(schema_dir) kcfg_map = build_kcfg_map(schema_dir)
for identifier in sorted(set(iter_schema_identifiers(kcfg_map))): for identifier in sorted(set(iter_schema_identifiers(kcfg_map))):
@@ -245,6 +284,9 @@ def main(argv):
if command == "apply": if command == "apply":
return cmd_apply(rest, manifest_path, schema_dir) 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 # Internal, not a user-facing `dot kde` subcommand -- called directly by
# completions/dot.fish to source candidates from the live schema, never # completions/dot.fish to source candidates from the live schema, never
# dispatched to via kde.fish. # dispatched to via kde.fish.

View File

@@ -457,6 +457,67 @@ dot kde apply extra-arg >/dev/null 2>&1
set -l apply_extra_arg_status $status set -l apply_extra_arg_status $status
@test "dot kde apply rejects an unexpected argument" $apply_extra_arg_status -ne 0 @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.
printf '[General]\nGreeting=Bonjour\nRealKey=ChangedAlias\n' >$HOME/.config/testrc
printf 'testrc.General.Greeting=Bonjour\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 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 # --- kde.py complete: tab-completion candidates, sourced from the live
# schema mapping table rather than a hardcoded list. This is the # schema mapping table rather than a hardcoded list. This is the
# underlying data completions/dot.fish shells out to; the fish # underlying data completions/dot.fish shells out to; the fish

View File

@@ -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)" complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l restore" -f -a "(__fish_print_pacman_packages)"
# --- dot kde --- # --- 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 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 save help" -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 diff -d "scan for settings whose live value differs from its default"
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 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 # 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. # 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)" 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
View File

@@ -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 <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 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 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 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 <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. | | `dot kde save` | Refreshes every already-declared manifest entry's value from the live system. |