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.
This commit is contained in:
2026-07-05 19:56:32 -04:00
parent c28029681d
commit f7b9f1b251
5 changed files with 137 additions and 5 deletions

View File

@@ -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 <extra-arg>` is rejected as misuse rather than silently ignored.
- `write_live_value` passes the value positionally after a `--` separator (`kwriteconfig6 --file ... --group ... --key ... -- <value>`) 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.

View File

@@ -2,6 +2,7 @@ function _dot_kde_usage
echo "usage: dot kde <command>
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

View File

@@ -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.

View File

@@ -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

1
.github/README.md vendored
View File

@@ -20,6 +20,7 @@ fish -c 'dot init'
| `dot init` | Bootstraps the dotfiles repo on a new machine. |
| `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 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. |