Compare commits
3 Commits
0d6f0d2fc9
...
ae4ec34822
| Author | SHA1 | Date | |
|---|---|---|---|
| ae4ec34822 | |||
| 0614e0ebe6 | |||
| 8ce46a7b98 |
56
.config/dot/.claude/spec/gitea-axi-integration.md
Normal file
56
.config/dot/.claude/spec/gitea-axi-integration.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Today, `to-spec`, `to-tasks`, and `implement` track specs and tasks as local files (`.claude/spec/<slug>.md`, `.claude/tasks/<NNNN>-<slug>.md`) scoped to a single git working tree.
|
||||||
|
That means task state and context don't survive across the machine boundary — a spec or task can't be picked up from a different clone, referenced from a PR, or handed to a differently-scoped agent session without manually carrying the files over.
|
||||||
|
There's also no natural place for `review-uncommitted`'s findings to live once produced, other than the terminal output, which the operator has to capture manually if they want it preserved as a record.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Once `gitea-axi` (see the companion `gitea-axi` spec) exists, replace the local-file storage in this project's skill-based task-management pipeline with Gitea issues and pull requests: specs and tasks become labeled issues, "readiness" becomes a label state, and implemented work becomes a pull request that `review-uncommitted` comments on directly.
|
||||||
|
The workflow-specific semantics (label names, state transitions, PR-to-issue linking) live entirely in the skills' own prose, calling `gitea-axi`'s generic primitives — `gitea-axi` itself stays unaware of this project's conventions.
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
1. As the operator, I want `to-spec` to open a Gitea issue containing the spec instead of writing a local file, so that the spec is visible and referenceable outside my local working tree.
|
||||||
|
2. As the operator, I want the spec issue labeled to mark it ready for task breakdown, so that a later session can find it without me telling it the issue number.
|
||||||
|
3. As the operator, I want a new session to be able to locate and read a spec issue by its readiness label, so that I can hand off spec-to-task work across sessions without manually passing context.
|
||||||
|
4. As the operator, I want `to-tasks` to open one Gitea issue per task instead of writing local task files, so that each task is independently discoverable and referenceable the same way the spec is.
|
||||||
|
5. As the operator, I want each task issue to retain a reference back to its parent spec issue, so that the `spec` traceability that today's local task-file frontmatter provides isn't lost in the move to issues.
|
||||||
|
6. As the operator, I want `to-tasks` to remove the spec issue's readiness label once tasks are created from it, so that the state machine reflects "spec has already been broken down" and isn't reprocessed.
|
||||||
|
7. As the operator, I want to ask a new session to implement "the next task" and have it find the right task issue by its readiness label, so that I don't have to look up and paste an issue number myself.
|
||||||
|
8. As the operator, I want `implement` to read a task issue's full details before starting work, so that it has the same context a local task file would have given it.
|
||||||
|
9. As the operator, I want `implement` to open a pull request (carrying the implementation commit) once work is done, instead of leaving only an uncommitted or committed local diff, so that the work is reviewable and mergeable through Gitea like any other PR.
|
||||||
|
10. As the operator, I want `review-uncommitted` to fetch its diff and spec context from the pull request and its linked issue when run in this workflow, so that I don't need a local spec file for it to work against.
|
||||||
|
11. As the operator, I want `review-uncommitted`'s three-axis findings posted as a comment on the pull request, so that they're visible as a permanent record on the PR itself, not just in my terminal.
|
||||||
|
12. As the operator, I want the label taxonomy and state machine (spec/task readiness, PR-to-issue linking conventions) to be easy to change later, so that I can iterate on the workflow without touching `gitea-axi`'s code.
|
||||||
|
13. As the operator, I want PR granularity (one commit vs. several, one task vs. several per PR) decided case-by-case between me and the agent at `implement` time, rather than fixed by a rule baked into the skill.
|
||||||
|
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
- Depends on `gitea-axi` existing first (see the companion spec) — this spec only covers how this project's skills consume it, not the tool itself.
|
||||||
|
- Affected skills: `to-spec`, `to-tasks`, `implement`, `review-uncommitted`. Each swaps its local-file I/O (`Read`/`Write`/`Edit` against `.claude/spec/` and `.claude/tasks/`) for calls to `gitea-axi`'s generic issue/PR primitives.
|
||||||
|
- `to-spec` opens an issue (instead of writing `.claude/spec/<feature-slug>.md`) carrying the same spec content and format, labeled to mark it as newly created and ready for breakdown.
|
||||||
|
- `to-tasks` reads the spec issue, opens one issue per task slice (instead of `.claude/tasks/<NNNN>-<slice-slug>.md`), each carrying a reference back to the parent spec issue (replacing the current `spec` frontmatter field), labels each task issue as ready for implementation, and removes the readiness label from the spec issue once done.
|
||||||
|
- `implement` locates its target task issue (by number if given, or by readiness label/query if asked for "the next task"), reads it in place of a local task file, does the work, and opens a pull request carrying the implementation commit — in place of just staging locally and leaving the commit to the operator.
|
||||||
|
- `review-uncommitted` gains a Gitea-aware path: when working against a PR, it fetches PR diff/metadata and the linked spec/task issue instead of `git diff HEAD` and a local spec file, and posts its aggregated Risk/Standards/Spec report as a single PR comment once done (per the companion spec's decision to keep this a single comment, not per-finding inline comments).
|
||||||
|
- Label taxonomy and exact naming (today referred to provisionally as "spec"/"ready-for-agent") are explicitly left open — to be finalized when these skill updates are actually implemented, not fixed by this spec.
|
||||||
|
- PR granularity (commits per PR, tasks per PR) is explicitly left as a case-by-case decision made between the operator and the agent at `implement` time — not a fixed rule this spec encodes.
|
||||||
|
|
||||||
|
## Testing Decisions
|
||||||
|
|
||||||
|
- Skills are prose (`SKILL.md` files), not unit-testable code — there is no automated test seam for the skill updates themselves. Verification is behavioral: running each updated skill against a real (or disposable) Gitea instance end-to-end and confirming the resulting issues, PRs, labels, and comments match what the prose describes.
|
||||||
|
- The one seam that is testable in the traditional sense is `gitea-axi` itself, already covered by the companion spec — these skill updates are downstream consumers of that seam, not a new one.
|
||||||
|
- No prior art in this repo for testing prompt-based skills; `~/.config/dot/tests/dot.fish` (fishtape, end-to-end against fixtures) is the closest pattern, but it tests code, not prose, so it doesn't transfer directly.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Building `gitea-axi` itself (fully covered by the companion `gitea-axi` spec).
|
||||||
|
- Deciding the actual label taxonomy and state machine names — deferred to implementation time.
|
||||||
|
- Deciding PR granularity rules — deferred to case-by-case decisions at `implement` time.
|
||||||
|
- Inline per-finding PR review comments for `review-uncommitted` (deferred enhancement, noted in the companion spec).
|
||||||
|
- Any change to `codebase-design`, `domain-modeling`, `test-driven-development`, or other skills not in the four listed above.
|
||||||
|
|
||||||
|
## Further Notes
|
||||||
|
|
||||||
|
- This spec assumes `gitea-axi`'s generic primitives (issue create/read/find-by-label/update-labels, PR create/get/comment) are sufficient for the four listed skills. If implementation reveals a missing primitive, it should be added to `gitea-axi` itself (kept generic) rather than special-cased here.
|
||||||
|
- This is an opinionated, single-adopter view of `gitea-axi` — it intentionally isn't part of the `gitea-axi` spec itself, since that tool is meant to stay usable by others regardless of this project's specific workflow conventions.
|
||||||
70
.config/dot/.claude/spec/gitea-axi.md
Normal file
70
.config/dot/.claude/spec/gitea-axi.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Coding agents that need to drive a Gitea-hosted workflow (issues, pull requests, labels) today have two poor options.
|
||||||
|
The official `tea` CLI is human-oriented: it has no token-efficiency, no contextual guidance, and no agent-facing error conventions.
|
||||||
|
Gitea's MCP servers expose the full API surface (dozens of tools) rather than being tuned for token or turn efficiency.
|
||||||
|
There is no Gitea-focused tool built to the same "agent ergonomics" standard that `gh-axi` established for GitHub.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Build `gitea-axi`: a thin, generic CLI wrapper around the official `tea` binary that reshapes its output according to the 10 AXI (Agent eXperience Interface) principles — token-efficient output, minimal default schemas, structured errors, contextual next-steps, and so on.
|
||||||
|
It gives coding agents an ergonomic, low-token way to drive issues and pull requests on any Gitea instance.
|
||||||
|
It ships both as an installable npm CLI and as an installable Agent Skill, so any agent session can adopt it with one install step.
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
1. As a coding agent, I want to create a Gitea issue with a title, body, and labels, so that I can record work items for later retrieval.
|
||||||
|
2. As a coding agent, I want to find issues by label (and other basic filters), so that I can locate relevant work without already knowing its issue number.
|
||||||
|
3. As a coding agent, I want to read an issue's full body, labels, and comments, so that I can load its context into a session.
|
||||||
|
4. As a coding agent, I want to add and remove labels on an existing issue, so that I can reflect state transitions as work progresses.
|
||||||
|
5. As a coding agent, I want to create a pull request from the current branch, so that completed work becomes reviewable.
|
||||||
|
6. As a coding agent, I want to fetch a pull request's metadata and diff, so that review tooling can operate on it without re-deriving it from git.
|
||||||
|
7. As a coding agent, I want to post a comment on a pull request, so that findings or notes are visible as a permanent reference on the PR itself.
|
||||||
|
8. As a coding agent, I want command output in a token-minimized format (TOON, minimal default fields, truncated large fields with an escape hatch), so that repeated calls across a long-running session don't consume excessive context.
|
||||||
|
9. As a coding agent, I want pre-computed aggregates in list/read output, so that I don't need follow-up calls just to derive obvious derived fields.
|
||||||
|
10. As a coding agent, I want explicit empty-state output when a query returns nothing, so that "no results" is never ambiguous with an error or a hang.
|
||||||
|
11. As a coding agent, I want structured errors with actionable suggestions and meaningful exit codes instead of prose failures, so that I can self-correct without the operator's help.
|
||||||
|
12. As a coding agent, I want mutations to be idempotent and to never prompt interactively, so that unattended, scripted use never stalls or double-applies.
|
||||||
|
13. As a coding agent, I want contextual next-step suggestions appended after output, so that I know what to call next without being taught the tool from scratch every session.
|
||||||
|
14. As a coding agent, I want a consistent per-subcommand `--help`, so that I can discover the interface on demand rather than needing it pre-loaded in context.
|
||||||
|
15. As an operator, I want gitea-axi run with no arguments to show live, actionable repository state instead of a help screen, so that I get immediate value without memorizing flags.
|
||||||
|
16. As an operator, I want gitea-axi to reuse my existing `tea` login configuration (including multi-instance profiles), so that I don't manage a second set of credentials.
|
||||||
|
17. As an operator, I want gitea-axi's command surface to stay generic, with no workflow-specific behavior baked in, so that it's useful across different projects and label/workflow conventions without code changes.
|
||||||
|
18. As an operator, I want gitea-axi published to npm and as an installable Agent Skill, so that I (and others) can adopt it with a single install step.
|
||||||
|
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
- New standalone repository — not bundled into any other tool or CLI framework.
|
||||||
|
- Developed against the operator's personal Gitea instance; push-mirrored to GitHub for npm publishing and public discoverability/contribution.
|
||||||
|
- Language/runtime: TypeScript on Node, matching the `gh-axi` reference implementation this design is modeled on.
|
||||||
|
- Implementation strategy: wrap the `tea` binary as a subprocess, invoking it with `--output json` (or the most structured format it supports) and reshaping that output — not a from-scratch Gitea API client. This reuses `tea`'s auth, multi-instance login, and full command coverage for free.
|
||||||
|
- **Flagged risk**: subprocess-wrapping-a-CLI can become fragile or slow at higher call volumes or in edge cases (partial output, non-JSON error text, version drift in `tea`'s own output shape). If this proves to be a real problem in practice, the fallback is a direct Gitea HTTP API client (as Gitea's own MCP server already does) — noted here so it isn't re-litigated from scratch if revisited.
|
||||||
|
- Auth: no independent credential handling. Every command shells out through `tea`, so it relies entirely on `tea login add` already being configured, including `tea`'s own `--login`/multi-instance profile resolution.
|
||||||
|
- Command surface: generic Gitea primitives only — issue create/read/find-by-label/update-labels, PR create/get/comment (see User Stories above for the full list). No project-specific or workflow-specific commands (e.g. nothing that hardcodes a particular label taxonomy or state machine).
|
||||||
|
- Output ergonomics follow the 10 AXI principles (https://axi.md/, https://github.com/kunchenguid/axi), grouped as:
|
||||||
|
- Efficiency: TOON-formatted stdout (~40% fewer tokens than JSON), minimal default schemas (3-4 fields per list item), truncated large fields with size hints and an escape hatch to fetch full content.
|
||||||
|
- Robustness: pre-computed aggregates to avoid round trips, explicit empty-state messages, structured errors and exit codes, idempotent mutations, no interactive prompts, fail loudly on unknown flags.
|
||||||
|
- Discoverability: opt-in session integration plus an on-demand skill, no-args shows live data rather than help text, contextual next-step suggestions appended after output.
|
||||||
|
- Help: consistent per-subcommand `--help`.
|
||||||
|
- Distribution: published to npm as a global-installable CLI, and packaged as an installable Agent Skill (installable the same way as `gh-axi`'s, e.g. via `npx skills`) — both built together from the start, not phased.
|
||||||
|
|
||||||
|
## Testing Decisions
|
||||||
|
|
||||||
|
- Good tests exercise the actual command-line interface (argv in, stdout/exit-code out) — the one seam every caller depends on — not internal functions, and not a mock of the `tea` subprocess call itself (that would only prove gitea-axi calls `tea` with certain arguments, not that the output is correctly reshaped).
|
||||||
|
- Tests should run the real, built CLI against either a disposable/fixture Gitea instance or a recorded fixture of `tea`'s own JSON output.
|
||||||
|
- Prior art: `~/.config/dot/tests/dot.fish` tests `dot`'s subcommands end-to-end with fishtape, building a throwaway bare-git remote fixture per scenario rather than mocking `git`. The equivalent here is a disposable Gitea fixture (or recorded `tea` output) rather than mocking `tea`.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Any workflow-specific commands or hardcoded label/state semantics (tracked separately — see the companion `gitea-axi-integration` spec for one concrete adopter's usage).
|
||||||
|
- Inline per-line PR review comments (a possible future addition; the primitive here is a plain PR comment).
|
||||||
|
- A from-scratch Gitea HTTP API client bypassing `tea` (deferred fallback if the subprocess-wrapping approach proves fragile — see flagged risk above).
|
||||||
|
- Multi-instance orchestration beyond what `tea`'s own login profiles already provide.
|
||||||
|
- A `dot` (or any other host CLI's) subcommand wrapping this tool — it is intentionally a standalone, independently distributed tool.
|
||||||
|
|
||||||
|
## Further Notes
|
||||||
|
|
||||||
|
- AXI ("Agent eXperience Interface") is an existing framework: https://axi.md/ and https://github.com/kunchenguid/axi. Its reference implementation, `gh-axi` (https://github.com/kunchenguid/gh-axi), wraps GitHub's `gh` CLI the same way this spec proposes wrapping `tea`, and reports (its own benchmarks) 100% task success vs. 86% for raw `gh`, and 66% cheaper / 74% fewer input tokens / half the interaction turns vs. GitHub's official MCP server on the same 17-task benchmark.
|
||||||
|
- The official Gitea MCP server (https://gitea.com/gitea/gitea-mcp) was evaluated and rejected as the primary approach: roughly 45 consolidated tools, actively maintained, but — by analogy to the gh-axi-vs-GitHub-MCP benchmark — generic MCP servers expose the full API surface rather than being tuned for token/turn efficiency, and using one directly would forfeit control over output shape.
|
||||||
|
- Raw `tea` was also evaluated and rejected as the long-term approach (though it remains the dependency this tool wraps): it already supports `--output json/yaml/csv/tsv`, so it's scriptable, but its schemas are human-oriented, not agent-ergonomic (no truncation, no contextual next-steps, no token minimization).
|
||||||
|
- Name collision check (as of this writing): `gitea-axi` is unclaimed on both npm and GitHub.
|
||||||
@@ -17,8 +17,15 @@ This is pure prefactoring: no KDE-specific behavior is introduced here.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] `dot help` lists a subcommand that lives at `commands/<name>/<name>.fish`
|
- [x] `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
|
- [x] `dot <name>` sources and dispatches to `commands/<name>/<name>.fish`'s `_dot_<name>` function
|
||||||
- [ ] Tab-completion (`__dot_custom_subcommands`) lists a nested-directory subcommand
|
- [x] Tab-completion (`__dot_custom_subcommands`) lists a nested-directory subcommand
|
||||||
- [ ] Existing flat-file subcommands (`dot install`) are still discovered and dispatched correctly
|
- [x] 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
|
- [x] `tests/dot.fish` covers a nested-directory dummy command dispatching correctly, alongside the existing flat-file dispatch case
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- The dispatch check in `dot.fish` tries the flat file first, then falls back to `commands/<name>/<name>.fish` — a flat file always wins if both somehow exist for the same name.
|
||||||
|
- The nested-directory scan requires the file basename to match its containing directory's name (`commands/foo/foo.fish`), not just any `.fish` file one level deep — this matches the acceptance criteria's exact convention and avoids misclassifying a stray companion file (e.g. a `.py` helper) as its own subcommand.
|
||||||
|
- Tab-completion's nested-directory listing was verified manually (sourcing `completions/dot.fish` and calling `__dot_custom_subcommands` directly) rather than via an automated test — `tests/dot.fish` has no existing infrastructure for testing completions at all, even for pre-existing flat commands, so adding one here would be out of scope for this prefactoring task.
|
||||||
|
- Updated `CLAUDE.md`'s "Architecture" and "Adding a subcommand" sections to document the new nested-directory convention, since it previously only described the flat-file dispatch contract.
|
||||||
|
|||||||
@@ -42,12 +42,22 @@ Add README rows for `dot kde help`, `dot kde save <identifier>`, and
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] `dot kde` and `dot kde save` are discoverable via `dot help` and dispatch correctly
|
- [x] `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)
|
- [x] 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
|
- [x] 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
|
- [x] 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
|
- [x] `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
|
- [x] `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`
|
- [x] `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
|
- [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
|
||||||
- [ ] README has rows for `dot kde help`, `dot kde save <identifier>`, and `dot kde save`
|
- [x] README has rows for `dot kde help`, `dot kde save <identifier>`, 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 `<kcfgfile name="...">` 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.
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ shared: fish only autoloads a function from a file named after that function,
|
|||||||
so a helper defined inside `dot.fish` would be undefined if tab-completion
|
so a helper defined inside `dot.fish` would be undefined if tab-completion
|
||||||
ran before `dot` had ever been sourced in the session. Keep both copies in
|
ran before `dot` had ever been sourced in the session. Keep both copies in
|
||||||
sync when the listing logic changes.
|
sync when the listing logic changes.
|
||||||
|
Both copies also glob one directory level deeper, matching `~/.config/dot/commands/<name>/<name>.fish`, so a subcommand's companion file (e.g. a Python helper) can live alongside it in its own directory.
|
||||||
|
|
||||||
`dot init`:
|
`dot init`:
|
||||||
|
|
||||||
@@ -76,7 +77,9 @@ sync when the listing logic changes.
|
|||||||
### Adding a subcommand
|
### Adding a subcommand
|
||||||
|
|
||||||
Beyond `init`, `dot` looks for `~/.config/dot/commands/<name>.fish`, sources
|
Beyond `init`, `dot` looks for `~/.config/dot/commands/<name>.fish`, sources
|
||||||
it, and calls `_dot_<name>`. These files are deliberately kept out of
|
it, and calls `_dot_<name>`.
|
||||||
|
A subcommand needing a companion file can instead live nested one level deeper, as `~/.config/dot/commands/<name>/<name>.fish` — both layouts dispatch identically.
|
||||||
|
These files are deliberately kept out of
|
||||||
`~/.config/fish/functions/` (fish's autoload path) so they never become
|
`~/.config/fish/functions/` (fish's autoload path) so they never become
|
||||||
independently invokable top-level commands or clutter tab-completion outside
|
independently invokable top-level commands or clutter tab-completion outside
|
||||||
of `dot` itself.
|
of `dot` itself.
|
||||||
|
|||||||
27
.config/dot/commands/kde/kde.fish
Normal file
27
.config/dot/commands/kde/kde.fish
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
function _dot_kde_usage
|
||||||
|
echo "usage: dot kde <command>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
save write live KDE settings into the manifest
|
||||||
|
help show this message
|
||||||
|
|
||||||
|
Run 'dot kde <command> 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
|
||||||
205
.config/dot/commands/kde/kde.py
Normal file
205
.config/dot/commands/kde/kde.py
Normal file
@@ -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
|
||||||
|
# (<kcfgfile arg="true">), 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:]))
|
||||||
@@ -73,6 +73,19 @@ end" >$HOME/.config/dot/commands/mark.fish
|
|||||||
dot mark >/dev/null 2>&1
|
dot mark >/dev/null 2>&1
|
||||||
@test "dispatches to a command file under ~/.config/dot/commands/" (cat $marker) = marked
|
@test "dispatches to a command file under ~/.config/dot/commands/" (cat $marker) = marked
|
||||||
|
|
||||||
|
# --- dispatches to a nested commands/<name>/<name>.fish, same as a flat file
|
||||||
|
set -gx HOME (mktemp -d)
|
||||||
|
dot init --url $remote >/dev/null 2>&1
|
||||||
|
|
||||||
|
mkdir -p $HOME/.config/dot/commands/nested
|
||||||
|
set -l nested_marker (mktemp)
|
||||||
|
echo "function _dot_nested
|
||||||
|
echo nested-marked >$nested_marker
|
||||||
|
end" >$HOME/.config/dot/commands/nested/nested.fish
|
||||||
|
|
||||||
|
dot nested >/dev/null 2>&1
|
||||||
|
@test "dispatches to a nested commands/<name>/<name>.fish" (cat $nested_marker) = nested-marked
|
||||||
|
|
||||||
# --- dot help ---
|
# --- dot help ---
|
||||||
set -gx HOME (mktemp -d)
|
set -gx HOME (mktemp -d)
|
||||||
dot init --url $remote >/dev/null 2>&1
|
dot init --url $remote >/dev/null 2>&1
|
||||||
@@ -93,6 +106,14 @@ end" >$HOME/.config/dot/commands/mark.fish
|
|||||||
set -l help_with_custom (dot help)
|
set -l help_with_custom (dot help)
|
||||||
@test "dot help lists custom commands found under ~/.config/dot/commands/" (string match -q '*mark*' -- $help_with_custom; echo $status) -eq 0
|
@test "dot help lists custom commands found under ~/.config/dot/commands/" (string match -q '*mark*' -- $help_with_custom; echo $status) -eq 0
|
||||||
|
|
||||||
|
mkdir -p $HOME/.config/dot/commands/nested
|
||||||
|
echo "function _dot_nested
|
||||||
|
echo nested
|
||||||
|
end" >$HOME/.config/dot/commands/nested/nested.fish
|
||||||
|
|
||||||
|
set -l help_with_nested (dot help)
|
||||||
|
@test "dot help lists a nested-directory subcommand" (string match -q '*nested*' -- $help_with_nested; echo $status) -eq 0
|
||||||
|
|
||||||
# --- dot install ---
|
# --- dot install ---
|
||||||
# pacman and sudo are faked out via a bin dir prepended to PATH: sudo just
|
# pacman and sudo are faked out via a bin dir prepended to PATH: sudo just
|
||||||
# execs its arguments, and pacman logs each invocation to $PACMAN_LOG (one
|
# execs its arguments, and pacman logs each invocation to $PACMAN_LOG (one
|
||||||
@@ -245,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 --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 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
|
@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 <kcfgfile name="testrc">, kwin.kcfg declares
|
||||||
|
# <kcfgfile arg="true"> (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 <identifier>: 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 <identifier> 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 <kcfgfile name>
|
||||||
|
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
|
||||||
|
|||||||
9
.config/dot/tests/fixtures/kcfg/kwin.kcfg
vendored
Normal file
9
.config/dot/tests/fixtures/kcfg/kwin.kcfg
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0">
|
||||||
|
<kcfgfile arg="true"/>
|
||||||
|
<group name="Windows">
|
||||||
|
<entry name="BorderSize" type="String">
|
||||||
|
<default>Normal</default>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
|
</kcfg>
|
||||||
15
.config/dot/tests/fixtures/kcfg/testrc.kcfg
vendored
Normal file
15
.config/dot/tests/fixtures/kcfg/testrc.kcfg
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0">
|
||||||
|
<kcfgfile name="testrc"/>
|
||||||
|
<group name="General">
|
||||||
|
<entry name="Greeting" type="String">
|
||||||
|
<default>Hello</default>
|
||||||
|
</entry>
|
||||||
|
<entry name="AliasedKey" key="RealKey" type="String">
|
||||||
|
<default>AliasDefault</default>
|
||||||
|
</entry>
|
||||||
|
<entry name="Some.Key With Spaces" type="String">
|
||||||
|
<default>SpacedDefault</default>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
|
</kcfg>
|
||||||
9
.config/dot/tests/fixtures/kcfg/unmapped.kcfg
vendored
Normal file
9
.config/dot/tests/fixtures/kcfg/unmapped.kcfg
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0">
|
||||||
|
<kcfgfile arg="true"/>
|
||||||
|
<group name="Whatever">
|
||||||
|
<entry name="Setting" type="String">
|
||||||
|
<default>Unreachable</default>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
|
</kcfg>
|
||||||
@@ -2,6 +2,13 @@ function __dot_custom_subcommands
|
|||||||
echo init
|
echo init
|
||||||
echo help
|
echo help
|
||||||
path basename $HOME/.config/dot/commands/*.fish 2>/dev/null | path change-extension ''
|
path basename $HOME/.config/dot/commands/*.fish 2>/dev/null | path change-extension ''
|
||||||
|
|
||||||
|
for d in $HOME/.config/dot/commands/*/
|
||||||
|
test -d $d; or continue
|
||||||
|
set -l name (path basename $d)
|
||||||
|
test -f $d$name.fish; or continue
|
||||||
|
echo $name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)"
|
complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)"
|
||||||
@@ -10,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 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 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)"
|
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)"
|
||||||
|
|||||||
@@ -14,11 +14,18 @@ function dot --wraps=git --description 'Manage dotfiles via a bare repo checked
|
|||||||
|
|
||||||
set -l commands_dir $HOME/.config/dot/commands
|
set -l commands_dir $HOME/.config/dot/commands
|
||||||
set -l command_file $commands_dir/$argv[1].fish
|
set -l command_file $commands_dir/$argv[1].fish
|
||||||
|
set -l nested_command_file $commands_dir/$argv[1]/$argv[1].fish
|
||||||
|
|
||||||
if test -n "$argv[1]" -a -f "$command_file"
|
if test -n "$argv[1]"
|
||||||
source $command_file
|
if test -f "$command_file"
|
||||||
_dot_$argv[1] $argv[2..-1]
|
source $command_file
|
||||||
return $status
|
_dot_$argv[1] $argv[2..-1]
|
||||||
|
return $status
|
||||||
|
else if test -f "$nested_command_file"
|
||||||
|
source $nested_command_file
|
||||||
|
_dot_$argv[1] $argv[2..-1]
|
||||||
|
return $status
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
git --git-dir=$dotfiles_dir --work-tree=$HOME $argv
|
git --git-dir=$dotfiles_dir --work-tree=$HOME $argv
|
||||||
@@ -109,6 +116,13 @@ Commands:
|
|||||||
echo " "(path basename $f | path change-extension '')
|
echo " "(path basename $f | path change-extension '')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
for d in $HOME/.config/dot/commands/*/
|
||||||
|
test -d $d; or continue
|
||||||
|
set -l name (path basename $d)
|
||||||
|
test -f $d$name.fish; or continue
|
||||||
|
echo " $name"
|
||||||
|
end
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
Run 'dot <command> help' for flags on a specific command.
|
Run 'dot <command> help' for flags on a specific command.
|
||||||
|
|
||||||
|
|||||||
3
.github/README.md
vendored
3
.github/README.md
vendored
@@ -20,6 +20,9 @@ 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 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. |
|
||||||
| `dot <git>` | Everything else is passed to `git`. |
|
| `dot <git>` | Everything else is passed to `git`. |
|
||||||
|
|
||||||
See [CLAUDE.md](../.config/dot/CLAUDE.md) for the `dot` tool's internal
|
See [CLAUDE.md](../.config/dot/CLAUDE.md) for the `dot` tool's internal
|
||||||
|
|||||||
Reference in New Issue
Block a user