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:
@@ -17,8 +17,16 @@ Add a README row for `dot kde apply`.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] `dot kde apply` pushes every manifest entry's declared value onto the live system via `kwriteconfig6`
|
- [x] `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)
|
- [x] Re-running `dot kde apply` against a system already matching the manifest changes nothing (idempotent)
|
||||||
- [ ] `dot kde apply help` prints usage without writing anything
|
- [x] `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
|
- [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
|
||||||
- [ ] README has a row for `dot kde apply`
|
- [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.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ function _dot_kde_usage
|
|||||||
echo "usage: dot kde <command>
|
echo "usage: dot kde <command>
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
apply push manifest entries onto the live system
|
||||||
save write live KDE settings into the manifest
|
save write live KDE settings into the manifest
|
||||||
help show this message
|
help show this message
|
||||||
|
|
||||||
@@ -17,6 +18,9 @@ function _dot_kde
|
|||||||
set -l helper_dir (status dirname)
|
set -l helper_dir (status dirname)
|
||||||
|
|
||||||
switch "$argv[1]"
|
switch "$argv[1]"
|
||||||
|
case apply
|
||||||
|
python3 $helper_dir/kde.py apply $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
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ SAVE_USAGE = """usage: dot kde save [identifier]
|
|||||||
(no args) refresh every already-declared manifest entry from the live system
|
(no args) refresh every already-declared manifest entry from the live system
|
||||||
help show this message"""
|
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"])
|
Setting = namedtuple("Setting", ["file", "group", "key"])
|
||||||
|
|
||||||
|
|
||||||
@@ -138,6 +143,22 @@ def read_live_value(setting, default):
|
|||||||
return result.stdout.rstrip("\n")
|
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):
|
def save_one(identifier, kcfg_map):
|
||||||
setting = parse_identifier(identifier)
|
setting = parse_identifier(identifier)
|
||||||
mechanism, default = resolve_mechanism(setting, kcfg_map)
|
mechanism, default = resolve_mechanism(setting, kcfg_map)
|
||||||
@@ -146,6 +167,14 @@ def save_one(identifier, kcfg_map):
|
|||||||
return read_live_value(setting, default)
|
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):
|
def cmd_save(args, manifest_path, schema_dir):
|
||||||
if args and args[0] == "help":
|
if args and args[0] == "help":
|
||||||
print(SAVE_USAGE)
|
print(SAVE_USAGE)
|
||||||
@@ -172,6 +201,28 @@ def cmd_save(args, manifest_path, schema_dir):
|
|||||||
return 0
|
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):
|
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))):
|
||||||
@@ -191,6 +242,9 @@ def main(argv):
|
|||||||
if command == "save":
|
if command == "save":
|
||||||
return cmd_save(rest, manifest_path, schema_dir)
|
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
|
# 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.
|
||||||
|
|||||||
@@ -392,6 +392,71 @@ dot kde save nodots >/dev/null 2>&1
|
|||||||
set -l bad_identifier_status $status
|
set -l bad_identifier_status $status
|
||||||
@test "dot kde save rejects an identifier without file.group.key structure" $bad_identifier_status -ne 0
|
@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
|
# --- 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
|
||||||
|
|||||||
1
.github/README.md
vendored
1
.github/README.md
vendored
@@ -20,6 +20,7 @@ fish -c 'dot init'
|
|||||||
| `dot init` | Bootstraps the dotfiles repo on a new machine. |
|
| `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 <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 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. |
|
||||||
|
|||||||
Reference in New Issue
Block a user