From 0614e0ebe68932327d8885881aaccc1b3f0cb364 Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 5 Jul 2026 18:33:55 -0400 Subject: [PATCH] dotcli: Add dot kde save with schema-backed defaults and completion. Introduces the dot kde subcommand family (fish dispatcher plus a Python helper under commands/kde/), a flat identifier=value manifest, and dot kde save in both its explicit-identifier and no-argument refresh modes. Settings resolve to KDE's KConfigXT schema-backed mechanism via a (rcfile -> [kcfg files]) mapping table auto-derived by scanning the system schema directory (overridable via DOT_KDE_KCFG_DIR), plus a hand-maintained exceptions list for schemas that only declare their target rc file at runtime. Also wires tab-completion for dot kde save identifiers, sourced live from that same mapping table. --- .../tasks/0001-kde-schema-backed-save.md | 28 ++- .config/dot/commands/kde/kde.fish | 27 +++ .config/dot/commands/kde/kde.py | 205 ++++++++++++++++++ .config/dot/tests/dot.fish | 143 ++++++++++++ .config/dot/tests/fixtures/kcfg/kwin.kcfg | 9 + .config/dot/tests/fixtures/kcfg/testrc.kcfg | 15 ++ .config/dot/tests/fixtures/kcfg/unmapped.kcfg | 9 + .config/fish/completions/dot.fish | 8 + .github/README.md | 3 + 9 files changed, 438 insertions(+), 9 deletions(-) create mode 100644 .config/dot/commands/kde/kde.fish create mode 100644 .config/dot/commands/kde/kde.py create mode 100644 .config/dot/tests/fixtures/kcfg/kwin.kcfg create mode 100644 .config/dot/tests/fixtures/kcfg/testrc.kcfg create mode 100644 .config/dot/tests/fixtures/kcfg/unmapped.kcfg diff --git a/.config/dot/.claude/tasks/0001-kde-schema-backed-save.md b/.config/dot/.claude/tasks/0001-kde-schema-backed-save.md index 6d77dcc..169a12c 100644 --- a/.config/dot/.claude/tasks/0001-kde-schema-backed-save.md +++ b/.config/dot/.claude/tasks/0001-kde-schema-backed-save.md @@ -42,12 +42,22 @@ Add README rows for `dot kde help`, `dot kde save `, and ## Acceptance criteria -- [ ] `dot kde` and `dot kde save` are discoverable via `dot help` and dispatch correctly -- [ ] Manifest parsing splits correctly on the first `=` (values may contain `=`) and the first two `.`s of the identifier (keys may contain dots/spaces) -- [ ] The `(rcfile → [kcfg files])` mapping table is derived by scanning a schema directory for ``, plus the hand-maintained exceptions list for `arg="true">` schemas -- [ ] The schema directory is overridable via an environment variable, defaulting to the real system path -- [ ] `dot kde save ` reads the current live value via `kreadconfig6` and adds a new declared entry to the manifest -- [ ] `dot kde save` with no arguments refreshes every already-declared manifest entry's stored value from the live system, leaving undeclared settings untouched -- [ ] `dot kde help` and `dot kde save help` print usage without touching the manifest or invoking `kreadconfig6`/`kwriteconfig6` -- [ ] Tests run against a scratch `$HOME` and a fixture `.kcfg` schema directory, exercising manifest read/write, identifier parsing, and mapping-table-driven default lookup, per the project's scratch-`$HOME`-plus-`fishtape` convention -- [ ] README has rows for `dot kde help`, `dot kde save `, and `dot kde save` +- [x] `dot kde` and `dot kde save` are discoverable via `dot help` and dispatch correctly +- [x] Manifest parsing splits correctly on the first `=` (values may contain `=`) and the first two `.`s of the identifier (keys may contain dots/spaces) +- [x] The `(rcfile → [kcfg files])` mapping table is derived by scanning a schema directory for ``, plus the hand-maintained exceptions list for `arg="true">` schemas +- [x] The schema directory is overridable via an environment variable, defaulting to the real system path +- [x] `dot kde save ` reads the current live value via `kreadconfig6` and adds a new declared entry to the manifest +- [x] `dot kde save` with no arguments refreshes every already-declared manifest entry's stored value from the live system, leaving undeclared settings untouched +- [x] `dot kde help` and `dot kde save help` print usage without touching the manifest or invoking `kreadconfig6`/`kwriteconfig6` +- [x] Tests run against a scratch `$HOME` and a fixture `.kcfg` schema directory, exercising manifest read/write, identifier parsing, and mapping-table-driven default lookup, per the project's scratch-`$HOME`-plus-`fishtape` convention +- [x] README has rows for `dot kde help`, `dot kde save `, and `dot kde save` + +## Implementation Notes + +- File layout: `commands/kde/kde.fish` (thin dispatcher: help-before-dispatch at the `dot kde` level, then hands off to the Python helper) plus `commands/kde/kde.py` (manifest parsing, mapping-table derivation, mechanism resolution, `kreadconfig6` invocation, and `save`'s own help-before-work check). +- Manifest location: `~/.config/dot/kde-manifest`, a flat file directly under `~/.config/dot/` as specified. +- Mechanism dispatch (`resolve_mechanism`) implements all three branches described in the parent spec (shortcuts / schema / freeform) even though only `schema` is wired to real behavior; `shortcuts` and `freeform` both currently raise a clear "not yet supported" error from `save_one`, so later tasks can fill them in without restructuring the dispatch. +- Test fixtures added under `tests/fixtures/kcfg/`: `testrc.kcfg` (a plain `` schema, including an entry whose ini `key=` differs from its schema `name=`, and one entry whose key contains dots and spaces), `kwin.kcfg` (an `arg="true"` schema resolved only via the hand-maintained exceptions list), and `unmapped.kcfg` (an `arg="true"` schema absent from that list, proving it's never guessed at from its own filename). +- Per the project's testing convention, `kreadconfig6` is never mocked for the tests exercising actual `save` behavior — it runs for real against fixture rc files under a scratch `$HOME`. It's faked (via a `$PATH`-prepended logging stub) only for the two tests asserting that `dot kde help` / `dot kde save help` never invoke it. +- Applied two small cleanups surfaced by `/review-uncommitted`'s Standards pass before closing out: extracted a shared `_parse_kcfg` helper (was duplicated between `build_kcfg_map` and `find_schema_default`), and introduced a `Setting = namedtuple("Setting", ["file", "group", "key"])` to stop threading those three strings as separate parameters across `resolve_mechanism`/`find_schema_default`/`read_live_value`/`save_one`. +- The Spec pass caught that the `unmapped.kcfg` fixture was created but never actually exercised by a test; added a case asserting `dot kde save unmapped.Whatever.Setting` resolves to freeform rather than schema-backed. diff --git a/.config/dot/commands/kde/kde.fish b/.config/dot/commands/kde/kde.fish new file mode 100644 index 0000000..3913147 --- /dev/null +++ b/.config/dot/commands/kde/kde.fish @@ -0,0 +1,27 @@ +function _dot_kde_usage + echo "usage: dot kde + +Commands: + save write live KDE settings into the manifest + help show this message + +Run 'dot kde help' for flags on a specific command." +end + +function _dot_kde + if test "$argv[1]" = help + _dot_kde_usage + return 0 + end + + set -l helper_dir (status dirname) + + switch "$argv[1]" + case save + python3 $helper_dir/kde.py save $argv[2..-1] + return $status + case '*' + _dot_kde_usage + return 1 + end +end diff --git a/.config/dot/commands/kde/kde.py b/.config/dot/commands/kde/kde.py new file mode 100644 index 0000000..a1854e2 --- /dev/null +++ b/.config/dot/commands/kde/kde.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +import os +import subprocess +import sys +import xml.etree.ElementTree as ET +from collections import defaultdict, namedtuple +from pathlib import Path + +KCFG_NS = "{http://www.kde.org/standards/kcfg/1.0}" +DEFAULT_SCHEMA_DIR = "/usr/share/config.kcfg" + +# .kcfg files that only declare their target rc file at runtime +# (), so it can't be discovered by scanning. +ARG_TRUE_RCFILES = { + "kwin.kcfg": "kwinrc", +} + +SAVE_USAGE = """usage: dot kde save [identifier] + + identifier declare a new manifest entry, seeded from its current live value + (no args) refresh every already-declared manifest entry from the live system + help show this message""" + +Setting = namedtuple("Setting", ["file", "group", "key"]) + + +def parse_identifier(identifier): + parts = identifier.split(".", 2) + if len(parts) != 3: + raise ValueError(f"invalid identifier {identifier!r} (expected file.group.key)") + return Setting(*parts) + + +def load_manifest(path): + entries = {} + if not path.exists(): + return entries + for line in path.read_text().splitlines(): + if not line.strip(): + continue + identifier, _, value = line.partition("=") + entries[identifier] = value + return entries + + +def write_manifest(path, entries): + lines = [f"{identifier}={value}" for identifier, value in entries.items()] + path.write_text("".join(f"{line}\n" for line in lines)) + + +def _parse_kcfg(path): + try: + return ET.parse(path).getroot() + except ET.ParseError: + return None + + +def _kcfgfile_name(root): + elem = root.find(f"{KCFG_NS}kcfgfile") + if elem is None: + return None + return elem.get("name") + + +def build_kcfg_map(schema_dir): + mapping = defaultdict(list) + if not schema_dir.is_dir(): + return mapping + + for path in sorted(schema_dir.glob("*.kcfg")): + root = _parse_kcfg(path) + if root is None: + continue + + rcfile = _kcfgfile_name(root) or ARG_TRUE_RCFILES.get(path.name) + if rcfile: + mapping[rcfile].append(path) + + return mapping + + +def find_schema_default(kcfg_paths, setting): + for path in kcfg_paths: + root = _parse_kcfg(path) + if root is None: + continue + + for group_elem in root.iter(f"{KCFG_NS}group"): + if group_elem.get("name") != setting.group: + continue + for entry in group_elem.findall(f"{KCFG_NS}entry"): + if (entry.get("key") or entry.get("name")) != setting.key: + continue + default_elem = entry.find(f"{KCFG_NS}default") + return default_elem.text if default_elem is not None and default_elem.text else "" + + return None + + +def iter_schema_identifiers(kcfg_map): + for rcfile, paths in kcfg_map.items(): + for path in paths: + root = _parse_kcfg(path) + if root is None: + continue + + for group_elem in root.iter(f"{KCFG_NS}group"): + group = group_elem.get("name") + if not group: + continue + for entry in group_elem.findall(f"{KCFG_NS}entry"): + key = entry.get("key") or entry.get("name") + if key: + yield f"{rcfile}.{group}.{key}" + + +def resolve_mechanism(setting, kcfg_map): + if setting.file == "kglobalshortcutsrc": + return "shortcuts", None + + default = find_schema_default(kcfg_map.get(setting.file, []), setting) + if default is not None: + return "schema", default + + return "freeform", None + + +def read_live_value(setting, default): + cmd = ["kreadconfig6", "--file", setting.file, "--group", setting.group, "--key", setting.key] + if default is not None: + cmd += ["--default", default] + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError( + f"kreadconfig6 failed for {setting.file}/{setting.group}/{setting.key}: {result.stderr.strip()}" + ) + return result.stdout.rstrip("\n") + + +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") + return read_live_value(setting, default) + + +def cmd_save(args, manifest_path, schema_dir): + if args and args[0] == "help": + print(SAVE_USAGE) + return 0 + + if len(args) > 1: + print("dot kde save: too many arguments", file=sys.stderr) + return 1 + + kcfg_map = build_kcfg_map(schema_dir) + manifest = load_manifest(manifest_path) + + try: + if args: + manifest[args[0]] = save_one(args[0], kcfg_map) + else: + for identifier in manifest: + manifest[identifier] = save_one(identifier, kcfg_map) + except (ValueError, RuntimeError) as e: + print(f"dot kde save: {e}", file=sys.stderr) + return 1 + + write_manifest(manifest_path, manifest) + return 0 + + +def cmd_complete(schema_dir): + kcfg_map = build_kcfg_map(schema_dir) + for identifier in sorted(set(iter_schema_identifiers(kcfg_map))): + print(identifier) + return 0 + + +def main(argv): + if not argv: + print("dot kde: no command given", file=sys.stderr) + return 1 + + command, rest = argv[0], argv[1:] + schema_dir = Path(os.environ.get("DOT_KDE_KCFG_DIR", DEFAULT_SCHEMA_DIR)) + manifest_path = Path(os.environ["HOME"]) / ".config" / "dot" / "kde-manifest" + + if command == "save": + return cmd_save(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. + if command == "complete": + return cmd_complete(schema_dir) + + print(f"dot kde: unknown command {command!r}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.config/dot/tests/dot.fish b/.config/dot/tests/dot.fish index 4be9e92..228deb7 100644 --- a/.config/dot/tests/dot.fish +++ b/.config/dot/tests/dot.fish @@ -266,3 +266,146 @@ set -l pacman_called_help (test -s $PACMAN_LOG; and echo yes; or echo no) @test "dot install help mentions --restore" (string match -q '*--restore*' -- $help_output; echo $status) -eq 0 @test "dot install help mentions --no-sync" (string match -q '*--no-sync*' -- $help_output; echo $status) -eq 0 @test "dot install help never calls pacman" $pacman_called_help = no + +# --- dot kde --- +# The fixture schema directory stands in for the real /usr/share/config.kcfg: +# testrc.kcfg declares a plain , kwin.kcfg declares +# (resolved only via the hand-maintained exceptions +# list, kwin.kcfg -> kwinrc), and unmapped.kcfg is an arg="true" schema with +# no exceptions-list entry, so it never resolves to anything. +set -l kcfg_fixtures (path resolve (status dirname)/fixtures/kcfg) +set -gx DOT_KDE_KCFG_DIR $kcfg_fixtures + +# kreadconfig6 itself is never mocked for the tests that exercise real +# save behavior (per the project's convention, it runs for real against +# fixture rc files under the scratch HOME) -- only the help-path tests below +# swap in a logging fake, to prove kreadconfig6 is never invoked for them. +set -l path_before_fake_kreadconfig $PATH + +# --- dot kde help / dot kde save help touch 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 (mktemp -d) +set -gx KREADCONFIG_LOG (mktemp) +echo '#!/bin/sh +echo "$@" >>"$KREADCONFIG_LOG" +exit 1' >$fake_bin_kde/kreadconfig6 +chmod +x $fake_bin_kde/kreadconfig6 +set -gx PATH $fake_bin_kde $PATH + +set -l kde_help_output (dot kde help) +set -l kde_help_status $status +set -l kreadconfig_called_for_kde_help (test -s $KREADCONFIG_LOG; and echo yes; or echo no) +set -l manifest_exists_after_kde_help (test -e $HOME/.config/dot/kde-manifest; and echo yes; or echo no) + +@test "dot kde help succeeds" $kde_help_status -eq 0 +@test "dot kde help mentions save" (string match -q '*save*' -- $kde_help_output; echo $status) -eq 0 +@test "dot kde help never invokes kreadconfig6" $kreadconfig_called_for_kde_help = no +@test "dot kde help does not create a manifest" $manifest_exists_after_kde_help = no + +set -l kde_save_help_output (dot kde save help) +set -l kde_save_help_status $status +set -l kreadconfig_called_for_save_help (test -s $KREADCONFIG_LOG; and echo yes; or echo no) +set -l manifest_exists_after_save_help (test -e $HOME/.config/dot/kde-manifest; and echo yes; or echo no) + +@test "dot kde save help succeeds" $kde_save_help_status -eq 0 +@test "dot kde save help mentions identifier" (string match -q '*identifier*' -- $kde_save_help_output; echo $status) -eq 0 +@test "dot kde save help never invokes kreadconfig6" $kreadconfig_called_for_save_help = no +@test "dot kde save help does not create a manifest" $manifest_exists_after_save_help = no + +set -gx PATH $path_before_fake_kreadconfig + +# --- dot kde save : declares a new manifest entry from the real live value --- +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 +printf '[General]\nGreeting=Hi=There\n' >$HOME/.config/testrc +set -l manifest $HOME/.config/dot/kde-manifest + +dot kde save testrc.General.Greeting >/dev/null 2>&1 +set -l save_status $status + +@test "dot kde save succeeds" $save_status -eq 0 +@test "declares the identifier with its live value, preserving an embedded '='" (cat $manifest | string collect) = "testrc.General.Greeting=Hi=There" + +# a kcfg entry whose ini key (key=) differs from its schema name still +# resolves correctly, falling back to the schema default when unset live +dot kde save testrc.General.RealKey >/dev/null 2>&1 +@test "resolves an aliased kcfg key (name != key) to its schema default" (string match -q '*testrc.General.RealKey=AliasDefault*' -- (cat $manifest); echo $status) -eq 0 + +# the identifier is split on the first two dots only, so the key portion +# may itself contain further dots and spaces +dot kde save "testrc.General.Some.Key With Spaces" >/dev/null 2>&1 +@test "an identifier's key portion may contain further dots and spaces" (string match -q '*testrc.General.Some.Key With Spaces=SpacedDefault*' -- (cat $manifest); echo $status) -eq 0 + +# an arg="true" schema resolves through the hand-maintained exceptions list +# (kwin.kcfg -> kwinrc), not by scanning for a static +dot kde save kwinrc.Windows.BorderSize >/dev/null 2>&1 +@test "resolves an arg=true schema via the hand-maintained exceptions list" (string match -q '*kwinrc.Windows.BorderSize=Normal*' -- (cat $manifest); echo $status) -eq 0 + +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 +dot kde save somefreeform.Group.Key >/dev/null 2>&1 +set -l unmapped_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 + +# 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 +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 + +# --- dot kde save with no arguments refreshes every already-declared entry --- +printf '[General]\nGreeting=Changed\n' >$HOME/.config/testrc +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 "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 + +# --- misuse: too many arguments / a malformed identifier --- +dot kde save one two >/dev/null 2>&1 +set -l too_many_args_status $status +@test "dot kde save rejects more than one identifier" $too_many_args_status -ne 0 + +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 + +# --- 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 +# completion wiring itself is verified manually (no existing +# infrastructure tests completions at all, per the nested-subcommand +# prefactoring task) --- +set -l complete_output (python3 $HOME/.config/dot/commands/kde/kde.py complete) + +@test "kde.py complete lists a schema-backed identifier" (string match -q '*testrc.General.Greeting*' -- $complete_output; echo $status) -eq 0 +@test "kde.py complete resolves an aliased kcfg key to its ini key, not its schema name" (string match -q '*testrc.General.RealKey*' -- $complete_output; echo $status) -eq 0 +@test "kde.py complete lists an arg=true schema resolved via the exceptions list" (string match -q '*kwinrc.Windows.BorderSize*' -- $complete_output; echo $status) -eq 0 +@test "kde.py complete never lists an aliased entry under its schema name" (string match -q '*testrc.General.AliasedKey*' -- $complete_output; echo $status) -eq 1 +@test "kde.py complete never lists an arg=true schema absent from the exceptions list" (string match -q '*Whatever.Setting*' -- $complete_output; echo $status) -eq 1 + +# --- dot help / dot help discovers dot kde --- +set -l help_with_kde (dot help) +@test "dot help lists the kde subcommand" (string match -q '*kde*' -- $help_with_kde; echo $status) -eq 0 diff --git a/.config/dot/tests/fixtures/kcfg/kwin.kcfg b/.config/dot/tests/fixtures/kcfg/kwin.kcfg new file mode 100644 index 0000000..6bf2c17 --- /dev/null +++ b/.config/dot/tests/fixtures/kcfg/kwin.kcfg @@ -0,0 +1,9 @@ + + + + + + Normal + + + diff --git a/.config/dot/tests/fixtures/kcfg/testrc.kcfg b/.config/dot/tests/fixtures/kcfg/testrc.kcfg new file mode 100644 index 0000000..cbaf05e --- /dev/null +++ b/.config/dot/tests/fixtures/kcfg/testrc.kcfg @@ -0,0 +1,15 @@ + + + + + + Hello + + + AliasDefault + + + SpacedDefault + + + diff --git a/.config/dot/tests/fixtures/kcfg/unmapped.kcfg b/.config/dot/tests/fixtures/kcfg/unmapped.kcfg new file mode 100644 index 0000000..193fe08 --- /dev/null +++ b/.config/dot/tests/fixtures/kcfg/unmapped.kcfg @@ -0,0 +1,9 @@ + + + + + + Unreachable + + + diff --git a/.config/fish/completions/dot.fish b/.config/fish/completions/dot.fish index c50f9ea..7ab9119 100644 --- a/.config/fish/completions/dot.fish +++ b/.config/fish/completions/dot.fish @@ -17,3 +17,11 @@ complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)" complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l restore" -l restore -d "reinstall every package from the saved list" complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l no-sync" -l no-sync -d "skip the pacman -Sy database refresh" 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" +# 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)" diff --git a/.github/README.md b/.github/README.md index 0ca2348..9d2a8f1 100644 --- a/.github/README.md +++ b/.github/README.md @@ -20,6 +20,9 @@ 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 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. | | `dot ` | Everything else is passed to `git`. | See [CLAUDE.md](../.config/dot/CLAUDE.md) for the `dot` tool's internal