From f7b9f1b251063628dd678f37738e098a0b14b8ea Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 5 Jul 2026 19:56:32 -0400 Subject: [PATCH] dotcli: Implement dot kde apply for schema-backed settings Pushes every manifest entry's declared value onto the live system via kwriteconfig6, idempotently. Non-schema (shortcuts/freeform) entries are rejected as not-yet-supported, deferred to later tasks. --- .../tasks/0002-kde-schema-backed-apply.md | 18 +++-- .config/dot/commands/kde/kde.fish | 4 ++ .config/dot/commands/kde/kde.py | 54 +++++++++++++++ .config/dot/tests/dot.fish | 65 +++++++++++++++++++ .github/README.md | 1 + 5 files changed, 137 insertions(+), 5 deletions(-) diff --git a/.config/dot/.claude/tasks/0002-kde-schema-backed-apply.md b/.config/dot/.claude/tasks/0002-kde-schema-backed-apply.md index d4cf0c5..f14fcf0 100644 --- a/.config/dot/.claude/tasks/0002-kde-schema-backed-apply.md +++ b/.config/dot/.claude/tasks/0002-kde-schema-backed-apply.md @@ -17,8 +17,16 @@ Add a README row for `dot kde apply`. ## Acceptance criteria -- [ ] `dot kde apply` pushes every manifest entry's declared value onto the live system via `kwriteconfig6` -- [ ] Re-running `dot kde apply` against a system already matching the manifest changes nothing (idempotent) -- [ ] `dot kde apply help` prints usage without writing anything -- [ ] Tests run against a scratch `$HOME`, exercising apply over a manifest with schema-backed entries, verifying resulting rc-file contents and idempotence on a second run -- [ ] README has a row for `dot kde apply` +- [x] `dot kde apply` pushes every manifest entry's declared value onto the live system via `kwriteconfig6` +- [x] Re-running `dot kde apply` against a system already matching the manifest changes nothing (idempotent) +- [x] `dot kde apply help` prints usage without writing anything +- [x] Tests run against a scratch `$HOME`, exercising apply over a manifest with schema-backed entries, verifying resulting rc-file contents and idempotence on a second run +- [x] README has a row for `dot kde apply` + +## Implementation Notes + +- File layout mirrors `save`'s: `write_live_value` (the `kwriteconfig6` counterpart to `read_live_value`) and `apply_one` (mirroring `save_one`'s `parse_identifier` → `resolve_mechanism` → schema-only gate) added to `commands/kde/kde.py`; `cmd_apply` mirrors `cmd_save`'s help/argument/error-handling scaffold. `kde.fish` gained an `apply` dispatch case above `save`. +- `apply` takes no arguments (unlike `save`, which supports an optional identifier) — the task only specifies pushing the whole manifest, and the parent spec's `apply` user story has no per-identifier mode, so `dot kde apply ` is rejected as misuse rather than silently ignored. +- `write_live_value` passes the value positionally after a `--` separator (`kwriteconfig6 --file ... --group ... --key ... -- `) rather than via a `--value` flag, since `kwriteconfig6` takes the value as a mandatory positional argument, not a flag; `--` guards against a value that itself looks like an option. +- Non-schema (shortcuts/freeform) manifest entries are rejected with the same "not yet supported" error `save_one` already raises for those mechanisms, kept out of scope per this task's title ("...apply for schema-backed settings"); those mechanisms are added in later tasks (0004, 0005) without needing to restructure `cmd_apply`. +- `/review-uncommitted` flagged two baseline duplication smells (`apply_one`/`cmd_apply` mirroring `save_one`/`cmd_save`'s shape) and one observation (a failing entry mid-manifest halts `apply` immediately, leaving earlier writes already applied — a partial-apply state, untested either way). Left as-is: the duplication mirrors an already-established local convention from task 0001 rather than introducing a new one, and the partial-apply behavior is consistent with `cmd_save`'s pre-existing control flow, not a new risk introduced by this task. diff --git a/.config/dot/commands/kde/kde.fish b/.config/dot/commands/kde/kde.fish index 3913147..fe24bf1 100644 --- a/.config/dot/commands/kde/kde.fish +++ b/.config/dot/commands/kde/kde.fish @@ -2,6 +2,7 @@ function _dot_kde_usage echo "usage: dot kde Commands: + apply push manifest entries onto the live system save write live KDE settings into the manifest help show this message @@ -17,6 +18,9 @@ function _dot_kde set -l helper_dir (status dirname) switch "$argv[1]" + case apply + python3 $helper_dir/kde.py apply $argv[2..-1] + return $status case save python3 $helper_dir/kde.py save $argv[2..-1] return $status diff --git a/.config/dot/commands/kde/kde.py b/.config/dot/commands/kde/kde.py index a1854e2..d2b8426 100644 --- a/.config/dot/commands/kde/kde.py +++ b/.config/dot/commands/kde/kde.py @@ -21,6 +21,11 @@ SAVE_USAGE = """usage: dot kde save [identifier] (no args) refresh every already-declared manifest entry from the live system help show this message""" +APPLY_USAGE = """usage: dot kde apply + + Pushes every manifest entry's declared value onto the live system. + help show this message""" + Setting = namedtuple("Setting", ["file", "group", "key"]) @@ -138,6 +143,22 @@ def read_live_value(setting, default): return result.stdout.rstrip("\n") +def write_live_value(setting, value): + cmd = [ + "kwriteconfig6", + "--file", setting.file, + "--group", setting.group, + "--key", setting.key, + "--", + value, + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError( + f"kwriteconfig6 failed for {setting.file}/{setting.group}/{setting.key}: {result.stderr.strip()}" + ) + + def save_one(identifier, kcfg_map): setting = parse_identifier(identifier) mechanism, default = resolve_mechanism(setting, kcfg_map) @@ -146,6 +167,14 @@ def save_one(identifier, kcfg_map): 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") + write_live_value(setting, value) + + def cmd_save(args, manifest_path, schema_dir): if args and args[0] == "help": print(SAVE_USAGE) @@ -172,6 +201,28 @@ def cmd_save(args, manifest_path, schema_dir): return 0 +def cmd_apply(args, manifest_path, schema_dir): + if args and args[0] == "help": + print(APPLY_USAGE) + return 0 + + if args: + print("dot kde apply: too many arguments", file=sys.stderr) + return 1 + + kcfg_map = build_kcfg_map(schema_dir) + manifest = load_manifest(manifest_path) + + try: + for identifier, value in manifest.items(): + apply_one(identifier, value, kcfg_map) + except (ValueError, RuntimeError) as e: + print(f"dot kde apply: {e}", file=sys.stderr) + return 1 + + return 0 + + def cmd_complete(schema_dir): kcfg_map = build_kcfg_map(schema_dir) for identifier in sorted(set(iter_schema_identifiers(kcfg_map))): @@ -191,6 +242,9 @@ def main(argv): if command == "save": return cmd_save(rest, manifest_path, schema_dir) + if command == "apply": + return cmd_apply(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. diff --git a/.config/dot/tests/dot.fish b/.config/dot/tests/dot.fish index 228deb7..85a5513 100644 --- a/.config/dot/tests/dot.fish +++ b/.config/dot/tests/dot.fish @@ -392,6 +392,71 @@ dot kde save nodots >/dev/null 2>&1 set -l bad_identifier_status $status @test "dot kde save rejects an identifier without file.group.key structure" $bad_identifier_status -ne 0 +# --- dot kde apply help touches neither the manifest nor kwriteconfig6 --- +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_kwrite (mktemp -d) +set -gx KWRITECONFIG_LOG (mktemp) +echo '#!/bin/sh +echo "$@" >>"$KWRITECONFIG_LOG" +exit 1' >$fake_bin_kwrite/kwriteconfig6 +chmod +x $fake_bin_kwrite/kwriteconfig6 +set -gx PATH $fake_bin_kwrite $path_before_fake_kreadconfig + +set -l kde_apply_help_output (dot kde apply help) +set -l kde_apply_help_status $status +set -l kwriteconfig_called_for_apply_help (test -s $KWRITECONFIG_LOG; and echo yes; or echo no) +set -l manifest_exists_after_apply_help (test -e $HOME/.config/dot/kde-manifest; and echo yes; or echo no) + +@test "dot kde apply help succeeds" $kde_apply_help_status -eq 0 +@test "dot kde apply help mentions manifest" (string match -q '*manifest*' -- $kde_apply_help_output; echo $status) -eq 0 +@test "dot kde apply help never invokes kwriteconfig6" $kwriteconfig_called_for_apply_help = no +@test "dot kde apply help does not create a manifest" $manifest_exists_after_apply_help = no + +set -gx PATH $path_before_fake_kreadconfig + +# --- dot kde apply: pushes every declared manifest entry onto the live rc file --- +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 +printf 'testrc.General.Greeting=Applied Greeting\ntestrc.General.RealKey=Hi=There\n' >$HOME/.config/dot/kde-manifest + +dot kde apply >/dev/null 2>&1 +set -l apply_status $status +set -l testrc_after_apply (cat $HOME/.config/testrc) + +@test "dot kde apply succeeds" $apply_status -eq 0 +@test "dot kde apply writes a declared value onto the live rc file" (string match -q '*Greeting=Applied Greeting*' -- $testrc_after_apply; echo $status) -eq 0 +@test "dot kde apply preserves an embedded '=' in the applied value" (string match -q '*RealKey=Hi=There*' -- $testrc_after_apply; echo $status) -eq 0 + +# re-running against a system already matching the manifest changes nothing +dot kde apply >/dev/null 2>&1 +set -l reapply_status $status +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 +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 + +# misuse: apply takes no arguments +printf 'testrc.General.Greeting=Applied Greeting\n' >$HOME/.config/dot/kde-manifest +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 + # --- 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 diff --git a/.github/README.md b/.github/README.md index 9d2a8f1..7bc4a06 100644 --- a/.github/README.md +++ b/.github/README.md @@ -20,6 +20,7 @@ fish -c 'dot init' | `dot init` | Bootstraps the dotfiles repo on a new machine. | | `dot install ` | 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 help` | Lists `dot kde`'s subcommands. | | `dot kde save ` | 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. |