docs: Add specs and tasks for previously scoped work in todo.md

This commit is contained in:
2026-07-05 17:44:14 -04:00
parent b233fe41e7
commit 0d6f0d2fc9
13 changed files with 463 additions and 225 deletions

View File

@@ -0,0 +1,24 @@
---
spec: dot-kde
---
## What to build
Extend the subcommand-discovery mechanism to glob one directory level
deeper, so a `dot` subcommand can live as `commands/<name>/<name>.fish`
alongside a companion file (e.g. a Python helper), not just as a flat
`commands/<name>.fish`. This mechanism exists in two places today
(`dot.fish`'s `__dot_help` and `completions/dot.fish`'s
`__dot_custom_subcommands`), intentionally duplicated rather than shared
(fish autoload constraints) — both must be updated together and stay in
sync. Existing flat-file subcommands must keep working unchanged.
This is pure prefactoring: no KDE-specific behavior is introduced here.
## Acceptance criteria
- [ ] `dot help` lists a subcommand that lives at `commands/<name>/<name>.fish`
- [ ] `dot <name>` sources and dispatches to `commands/<name>/<name>.fish`'s `_dot_<name>` function
- [ ] Tab-completion (`__dot_custom_subcommands`) lists a nested-directory subcommand
- [ ] Existing flat-file subcommands (`dot install`) are still discovered and dispatched correctly
- [ ] `tests/dot.fish` covers a nested-directory dummy command dispatching correctly, alongside the existing flat-file dispatch case

View File

@@ -0,0 +1,53 @@
---
spec: dot-kde
blocked-by: 0000-nested-subcommand-discovery
---
## What to build
Stand up `dot kde` itself: the fish dispatcher plus its Python helper,
living together under `commands/kde/` per the nested-subcommand layout
from the prior task. Establish the manifest file (flat text file directly
under `~/.config/dot/`, one `identifier=value` line each, split on the
first `=` only; identifier split on the first two `.`s into
`file.group.key`, leaving the key free to contain further dots or spaces).
Implement the KConfigXT schema-backed mechanism: reads and writes go
through `kreadconfig6`/`kwriteconfig6`, and the "default" value for a
setting comes from its `.kcfg` schema. Build the `(rcfile → [kcfg files])`
mapping table by scanning the system's KConfigXT schema directory for
files that statically declare their target rc file
(`<kcfgfile name="...">`), plus a small hand-maintained list for the
exceptions that only declare their target file at runtime
(`<kcfgfile arg="true">``kwin.kcfg` is a known example). The schema
directory location must be overridable (e.g. via an environment variable),
defaulting to the real system path, so tests can point it at a fixture
directory of synthetic `.kcfg` files instead.
Structure identifier resolution as a dispatchable decision (rc file is
`kglobalshortcutsrc` → shortcuts; else resolves via the mapping table →
schema-backed; else → freeform) even though only the schema-backed branch
is implemented yet — later tasks add the other two branches without
restructuring this.
Implement `dot kde save` for schema-backed settings, in both modes:
run with no arguments, refresh every already-declared manifest entry's
value from the live system; run with an explicit identifier, read its
current live value and add it to the manifest as a new declared entry.
Add `dot kde help` and `dot kde save help`, following the project's
check-for-`help`-before-`argparse` convention at each dispatch level.
Add README rows for `dot kde help`, `dot kde save <identifier>`, and
`dot kde save` (no arguments).
## 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 `<kcfgfile name="...">`, 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 <identifier>` 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 <identifier>`, and `dot kde save`

View File

@@ -0,0 +1,24 @@
---
spec: dot-kde
blocked-by: 0001-kde-schema-backed-save
---
## What to build
Implement `dot kde apply` for schema-backed settings: read every entry in
the manifest and write its declared value onto the live system via
`kwriteconfig6`. Re-running it against an already-applied system must be a
no-op with no unintended side effects — this is the idempotence the
feature depends on for safe re-runs after a KDE update or on a freshly
built machine. Add `dot kde apply help`, following the project's
check-for-`help`-before-`argparse` convention.
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`

View File

@@ -0,0 +1,27 @@
---
spec: dot-kde
blocked-by: 0001-kde-schema-backed-save
---
## What to build
Implement `dot kde diff`'s broad, read-only scan for schema-backed
settings: walk every `(rcfile, group, key)` reachable through the
mapping table built in the prior task, compare each live value
(`kreadconfig6`) against its schema-declared default, and report every
mismatch. Each reported mismatch is tagged as declared (its identifier is
present in the manifest — an intentional, already-tracked deviation) or
undeclared (never explicitly declared). `diff` never writes anything.
Add `dot kde diff help`, following the project's
check-for-`help`-before-`argparse` convention.
Add a README row for `dot kde diff`.
## Acceptance criteria
- [ ] `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
- [ ] `dot kde diff` makes no writes under any circumstances
- [ ] `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)
- [ ] README has a row for `dot kde diff`

View File

@@ -0,0 +1,32 @@
---
spec: dot-kde
blocked-by: [0002-kde-schema-backed-apply, 0003-kde-schema-backed-diff]
---
## What to build
Add the freeform mechanism as a dispatch branch across `save`, `apply`,
and `diff`: for settings with no KConfigXT schema (e.g. `kxkbrc`'s
`Options=` line), read and write via `kreadconfig6`/`kwriteconfig6`, with
"default" defined as "the key is absent" rather than any schema-declared
value. In the identifier-resolution decision from the first schema-backed
task, this is the fallback branch: an identifier whose `(rcfile, group,
key)` doesn't resolve through the mapping table is freeform. Because
there's no schema to enumerate, freeform settings can only be checked by
`diff` when already declared in the manifest — they never participate in
undeclared broad-scan discovery.
As the real-world validation for this task, bring the machine's live,
already-hand-set `kxkbrc` caps-lock/Escape swap
(`Options=caps:escape_shifted_capslock`) under tracking via
`dot kde save`, and confirm `dot kde apply`/`dot kde diff` behave
correctly against it.
## Acceptance criteria
- [ ] An identifier whose `(rcfile, group, key)` has no schema match is treated as freeform rather than erroring
- [ ] `dot kde save <identifier>` and `dot kde save` (refresh) work for freeform entries
- [ ] `dot kde apply` writes freeform entries via `kwriteconfig6`, idempotently
- [ ] `dot kde diff` reports a freeform mismatch when its identifier is already declared in the manifest, and never surfaces an undeclared freeform setting via broad scan
- [ ] Tests run against a scratch `$HOME`, covering freeform save/apply/diff using a fixture rc file with no corresponding schema
- [ ] The live `kxkbrc` caps-lock/Escape swap is tracked via `dot kde save` and the manifest committed to the dotfiles repo

View File

@@ -0,0 +1,38 @@
---
spec: dot-kde
blocked-by: [0002-kde-schema-backed-apply, 0003-kde-schema-backed-diff]
---
## What to build
Add the shortcuts mechanism as a dispatch branch across `save`, `apply`,
and `diff`: identifiers rooted at `kglobalshortcutsrc` are resolved not by
editing the rc file directly but through KDE's `kglobalaccel` D-Bus
service — `shortcut(actionId)` for the current value, `defaultShortcut
(actionId)` for the default, and `setShortcut(actionId, keys, flags)`
with `flags = NoAutoloading` for writes (so a declared value always wins
over any previously saved shortcut). `actionId` is the 4-element
`[componentUnique, actionUnique, componentFriendly, actionFriendly]`
tuple; only the two `Unique` fields are stored in the manifest, and the
two friendly-name fields are resolved dynamically at call time by looking
up the component's shortcut list.
Per the spec's testing decisions, this mechanism is deliberately excluded
from the automated test suite (it depends on a live, already-running
session service that isn't practically substitutable without disproportionate
mock infrastructure) — verify it manually against the real session instead.
As the real-world validation, apply the planned screenshot/session-lock
keybind changes (Spectacle bindings, moving Lock Session off `Meta+L` to
`Meta+X`) through `dot kde save`/`dot kde apply`, and update the
corresponding rows in `keybindings.md` in the same change, per the
project's cross-cutting keybindings convention.
## Acceptance criteria
- [ ] An identifier whose rc file is `kglobalshortcutsrc` dispatches to the `kglobalaccel` D-Bus mechanism rather than the schema-backed or freeform paths
- [ ] `dot kde save <identifier>` and `dot kde save` (refresh) read a shortcut's current value via `shortcut(actionId)`, resolving the friendly-name fields dynamically
- [ ] `dot kde apply` writes a declared shortcut via `setShortcut(actionId, keys, NoAutoloading)`, verified manually to take effect immediately in the running session
- [ ] `dot kde diff` reports a declared shortcut mismatch by comparing against `defaultShortcut(actionId)`, verified manually
- [ ] The Spectacle and Lock-Session (`Meta+X`) keybind changes are applied through `dot kde save`/`apply` and tracked in the manifest
- [ ] `keybindings.md` is updated to reflect the new bindings in the same change

View File

@@ -0,0 +1,60 @@
---
spec: dot-setup-folders
---
## What to build
A new `dot setup` subcommand family, following the project's existing
nested-subcommand dispatch convention. Bare `dot setup` (no arguments) runs
every machine-setup task unconditionally; `dot setup <task>` runs just that
one task. The only task that exists yet is `folders`.
`dot setup folders` brings the 8 standard XDG user directories under the
project's short-name convention (`Desktop→.desktop`, `Documents→doc`,
`Downloads→dwn`, `Music→mus`, `Pictures→pic`, `Videos→vid`, `Templates` and
`Public` both →`.ignoreme`). The desired short names live in a tracked
`user-dirs.dirs` file (a plain dotfile, not generated from a table each run).
A separate small hardcoded table maps each of the 8 standard XDG categories
to its legacy full-named folder, used only to locate content an XDG-defaults
install would have left behind, and merge it into the already-tracked
short-named target.
This slice covers the core happy path: a legacy folder found empty (strictly:
no entries at all, including dotfiles/metadata) is merged into its
short-named target silently, with no confirmation needed. As part of the same
`Pictures→pic` pass, a nested `Screenshots` folder is renamed to lowercase
`screenshots`, landing at `pic/screenshots`. After all folder moves complete,
run `xdg-user-dirs-update` (no arguments) once to notify running apps/portals.
`~/wrk` gets no XDG variable of its own and is out of scope for any mapping;
the existing ad hoc `~/Projects` folder is left alone.
Non-empty legacy folders and filename collisions are out of scope for this
slice (covered by later tasks) — for now it's acceptable for a non-empty
legacy folder to be handled in whatever minimal way unblocks the empty-folder
path (e.g. left untouched with a message), since the confirmation gate and
collision safety are built out next.
Wire the new command into the project's standard subcommand checklist: a
`_dot_setup_usage` help function reachable via `dot setup help` (and
`dot setup folders help` for the nested task), the completions/help-glob
duplication point, and a README command-table row.
## Acceptance criteria
- [ ] `dot setup folders` on a fresh scratch `$HOME` (all 8 legacy folders
present and empty) renames them to their short-name targets per the
mapping table, including `Pictures/Screenshots→pic/screenshots`, and
leaves the tracked `user-dirs.dirs` short names in place
- [ ] The fake `xdg-user-dirs-update` (PATH-prepended, logging its invocation
per the project's existing fake-`sudo`/fake-`pacman` testing pattern)
is invoked exactly once after a successful migration
- [ ] Bare `dot setup` on a fresh scratch `$HOME` runs the `folders` task as
part of running everything
- [ ] `dot setup folders help` and `dot setup help` print usage and make no
filesystem changes
- [ ] Re-running `dot setup folders` after a clean migration is a no-op
(idempotent)
- [ ] `~/.github/README.md` has a command-table row for `dot setup`
(and its `folders` task) with paths relative to `$HOME`
- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and
`fishtape ~/.config/dot/tests/dot.fish` passes

View File

@@ -0,0 +1,30 @@
---
spec: dot-setup-folders
blocked-by: 0006-setup-dispatcher-and-folders-core
---
## What to build
Extend `dot setup folders`'s migration so a legacy folder found non-empty
(any entry at all, including a stray dotfile or KDE metadata like a
`.directory` file, counts as non-empty) stops and prints what would be moved,
then refuses to proceed unless an explicit `--yes` flag was passed on the
command line — no interactive prompt. With `--yes`, the migration proceeds
for that folder the same way the empty-folder path already does.
This applies uniformly across all 8 mapped categories, including the nested
`Pictures/Screenshots→pic/screenshots` rename from the prior slice: a
non-empty `Screenshots` folder is also gated behind the same confirmation
rule.
## Acceptance criteria
- [ ] A legacy folder with real content (a real file, not just an empty
directory) refuses to migrate without `--yes`, prints what would have
been moved, and leaves the folder and its contents untouched
- [ ] The same legacy folder migrates successfully when `--yes` is passed
- [ ] A legacy folder containing only a stray dotfile/metadata file (e.g. a
fake `.directory`) is still treated as non-empty and triggers the same
confirmation gate
- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and
`fishtape ~/.config/dot/tests/dot.fish` passes

View File

@@ -0,0 +1,32 @@
---
spec: dot-setup-folders
blocked-by: 0007-folders-non-empty-confirmation
---
## What to build
Make the `--yes`-confirmed merge from the prior slice collision-safe: when a
legacy folder and its short-named target both contain an entry with the same
name, use no-clobber move semantics so the target's existing file is never
silently overwritten. Report which files were skipped due to a collision, and
leave the legacy folder in place (don't remove it) whenever any collision
occurred during that folder's migration, rather than deleting a folder that
still holds something that couldn't be merged.
This closes the gap left by the old bash `setup_folders`'s naive `mv $from/*
$to`, which had no collision protection at all.
## Acceptance criteria
- [ ] A filename collision between a legacy folder and its already-populated
short-named target is skipped, not overwritten (the target's existing
file is preserved byte-for-byte)
- [ ] The skipped collision is reported to the user
- [ ] The legacy folder is left in place (not removed) when a collision
occurred, even though `--yes` was given and other non-colliding files
in it were moved
- [ ] Re-running `dot setup folders` after a collision was reported and left
in place behaves consistently (doesn't lose the previously-skipped
file, doesn't re-move already-migrated files)
- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and
`fishtape ~/.config/dot/tests/dot.fish` passes