Compare commits

..
138 Commits
Author SHA1 Message Date
Gitea Actions 08e66c81ca chore(docs): regenerate manual, man page, and component registry 2026-09-04 21:28:58 +00:00
rootiest c009bbf257 Merge pull request 'feat(docs): GitHub mirror icon, README-sourced doc sections, auto-generated TOC' (#129) from feat/docs-readme-sections into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m0s
CI / build-docs (push) Successful in 3m54s
Reviewed-on: #129
2026-09-04 21:23:06 +00:00
rootiest e6d3fd80b3 style(docs): add fish to logo icon 2026-09-04 17:14:17 -04:00
rootiest 54375a9530 style(docs): match Gitea icon size to GitHub's 24px (1.5rem) 2026-09-04 16:50:16 -04:00
rootiest 2ee2806e01 fix(docs): make the Gitea icon override actually apply, tune GitHub to 1.5x
UnoCSS's .i-pajamas:gitea rule is unlayered CSS; our override lived in
@layer starlight.core, and unlayered rules always beat layered ones
regardless of specificity or source order. The Gitea icon has silently
stayed at UnoCSS's 1em default since it was first added -- confirmed via
computed style in the browser, not just reading the stylesheet source.
!important restores the override across the layer boundary.

Also drops the GitHub icon from 2x to 1.5x per visual feedback.
2026-09-04 16:49:04 -04:00
rootiest 23cae4bd10 chore(docs): regenerate man page
pandoc wasn't available when this branch's earlier commit ran
build-manual.py --concat; regenerate docs/fish-config.1 from the
current docs/fish-config.md now that it is.
2026-09-04 16:43:27 -04:00
rootiest 8c21d34943 style(docs): double the Gitea/GitHub header icon size
--sl-nav-height is a fixed CSS var, not driven by icon content, so both
icons can grow without changing the header bar's height.
2026-09-04 16:43:27 -04:00
rootiest 4912c4052f feat(docs): add GitHub mirror icon, README-sourced doc sections, auto-generated TOC
Adds a GitHub social icon to the docs site header alongside the existing
Gitea one, and documents in the README's Contributing section that
git.rootiest.dev is the base repo while the GitHub copy is a one-way
mirror, so forks/issues/PRs should go through Gitea.

Adds Testing, Contributing, Attribution, and License sections to the
manual/man page/site, sourced directly from README.md via a new
`<!-- README: Heading -->` placeholder mechanism in build-manual.py, so
the README stays the single source of truth for those sections instead
of a hand-maintained copy drifting out of sync.

Also converts docs/manual/00-table-of-contents.md from a hand-typed list
to a generated one (mt.walk()-driven), fixing a numbering drift where
Components Reference was omitted and every section after it was off by
one relative to its own manTitle.
2026-09-04 16:34:22 -04:00
Gitea Actions c5bbbf06e3 chore(docs): regenerate manual, man page, and component registry 2026-09-04 02:52:27 +00:00
rootiest 76afcd5e13 Merge pull request 'feat(privacy): add DO_NOT_TRACK and DISABLE_TELEMETRY env vars under C3 privacy' (#128) from feat/do-not-track-privacy-env into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m53s
CI / build-docs (push) Successful in 9m30s
Reviewed-on: #128
2026-09-04 02:40:53 +00:00
rootiest 28a88a9bdf feat(privacy): add DO_NOT_TRACK and DISABLE_TELEMETRY env vars under C3 privacy 2026-09-03 22:35:05 -04:00
Gitea Actions d278a47b32 chore(docs): regenerate manual, man page, and component registry 2026-09-03 23:52:10 +00:00
rootiest dc3236fddb Merge pull request 'test(agents-vault): make the suite hermetic against git configuration' (#127) from fix/vault-test-git-identity into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m58s
CI / build-docs (push) Successful in 9m19s
Reviewed-on: #127
2026-09-03 23:40:56 +00:00
rootiest 73d41da07d test(agents-vault): inject failures in a way that survives uid 0
Five fixtures forced a failure with chmod 500 on a parent directory. That
is silently useless for root: uid 0 bypasses the mode bits, the operation
succeeds, and a test asserting a failure path then reports the tool as
broken rather than the injection as ineffective.

CI runs the suite as root inside a container, so 21 checks failed there
while passing for every developer and every reviewer -- the same shape as
the git-identity gap in the previous commit. A fixture that quietly stops
injecting is worse than one that never existed: it reads as coverage.

Shim the single command each failure hinges on, matched by a path fragment
so nothing else in the run is disturbed. A command that exits 1 on purpose
does so for every uid. Which command matters: ensure_symlink removes an
existing link before it creates the new one, so the two adopt fixtures
have to fail at that rm to leave the live link in place, and shimming ln
would be too late. The restore fixture deletes the link first and so does
hinge on ln, and the failing --remote fixture hinges on git set-url.

The one remaining chmod 500 asserts a success, not a failure, and its real
proof is the direct assertions on where the stash landed.

Reproduce the root case locally without a container:
  unshare -r fish tests/test-agents-vault.fish

Verified: 296/317 as root before, 317/317 after; 317/317 unchanged as an
ordinary user; and neutering the shims reproduces exactly 296/317, the
count CI reported.
2026-09-03 19:34:01 -04:00
rootiest 75fbfa1e16 test(agents-vault): make the suite hermetic against git configuration
The suite was hermetic against $HOME and ~/.claude but not against git
config. new_repo set an identity on the repos the tests create; nothing
set one on the repos the tool creates -- the scaffolded vault and the
AGENTS/ sub-repo. Those inherited a global user.name/user.email on a
developer machine and committed fine, so the gap was invisible to every
local run and every review; on a runner with no gitconfig they died with
"Author identity unknown", and the suite reported the tool own correct
"nothing recorded" handling as 71 failures.

Supply the identity through the environment, which reaches the git calls
inside agents-vault and agents-init as well as the ones the tests make.

Pin commit.gpgsign off for the mirror-image reason: a developer with
signing enabled would otherwise have tool-created repos reach for a key,
and a hardware token would prompt for a touch partway through the run.

Pin init.defaultBranch because the rebase fixtures build an upstream and a
clone and need the two to agree on a branch name. A global saying main and
a bare default of master disagree, and the fixture then fails to create the
rebase the test is about, reporting a tool failure that never happened.

Verified with GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null:
267/317 before, 317/317 after; unchanged at 317/317 with a normal config.
2026-09-03 19:20:11 -04:00
rootiest 3c967c0cee Merge pull request 'feat(agents-vault): back up curated agent memory to a host-scoped vault repo' (#126) from feat/agent-memory-vault into main
CI / github-mirror (push) Skipped
CI / test (push) Failing after 1m19s
CI / build-docs (push) Skipped
Reviewed-on: #126
2026-09-03 23:09:55 +00:00
rootiest f9d400699f fix(agents-vault): keep the user's ssh command and drop the quadratic walk
The connect bound was delivered by injecting GIT_SSH_COMMAND, and an
environment variable outranks git's core.sshCommand -- so the guard,
which read only the environment, did not merely miss a configured ssh
command, it overruled one. A vault remote reachable only as
`ssh -i ~/.ssh/vault_key` failed to authenticate on every push, autopush
and --push alike, for the sake of a ten-second timeout. Both spellings
now count, and `set -qx` rather than `set -q` on the environment side so
an unexported fish variable -- which git never sees -- does not leave the
push with neither the user's ssh command nor a bound.

The agy knowledge walk appended each find with `set -a`, which rewrites
the whole variable every time; 500 files cost 21ms but 20,000 cost 58s,
on a path that runs in front of every agent launch. The walk now prints
NUL-separated and the list is built once, which is flat: the same 20,000
files take 707ms. NUL rather than newline because a filename may legally
contain one. What the walk collects, and its symlink and dot-led
semantics, are byte-for-byte unchanged.

Autopush is bounded by timeout(1) alone, so without it the launch path
was quietly back to an open-ended network call. It now says so and skips
the push instead; --push was never wrapped and is unaffected.
2026-09-03 18:56:47 -04:00
rootiest d9b56790c5 fix(agents-vault): keep the knowledge walk and the launch push inside their bounds
The agy knowledge allowlist walked the store with `**` and copied with
plain cp, so a symlink inside the store was both followed and dereferenced.
The extension rule still bounded what kind of file was collected, but not
whose: a link to a home directory hands over settings.json, CLAUDE.md and
every cached .json in it, and those reached a commit. A link to / made the
walk itself unbounded, on the path that runs before every agent launch.
The tree is now walked a level at a time and nothing that is a symlink is
followed or copied.

Autopush had the same shape one layer out. Neither GIT_TERMINAL_PROMPT nor
GIT_ASKPASS closes a socket, and git has no connect timeout to set: against
a blackholed address a push took 135s with http.lowSpeedLimit and
http.lowSpeedTime set as well as without them. ssh can time itself out and
is now told to; the autopush pull and push are additionally capped with
timeout(1). An explicit --push stays uncapped, since it is watched and has
to report what a real transfer really did.

Also: scaffold /.migrate-stash into .gitignore beside /.adopt-stash, which
the comment already claimed was covered; and drop the live memory path
during a slug migration only when it is a link. Reached from the
path-derived fallback candidate it can be a real populated directory, where
rm -f correctly refuses -- but said so in rm's voice, so a --silent run that
had succeeded printed what read as an error.
2026-09-03 18:56:46 -04:00
rootiest 16ea31289d fix(agents-vault): repair slug migration, keep the network off the launch path
Six findings from the whole-branch review, all of which end in the same
place: a backup tool reporting success while nothing was backed up.

Slug migration nested the old entry inside the new one. The clear before
the rename was gated on the destination's claude/memory subdirectory
rather than on the destination itself, so an entry that exists without
one survived, `git mv A B` moved A *inside* B, and the mkdir below
fabricated a fresh empty memory directory for the live link to point at.
The real memory ended up one level deeper than --status and --restore
ever look, and the run returned 0. That shape is not exotic: git cannot
track an empty directory, so an entry committed while its memory was
empty comes back from a clone as projects/<slug>/origin and nothing
else -- and cloning the vault is this feature's own recovery path. The
destination is now moved aside the way --adopt already does it rather
than deleted (widening the rm -rf would have destroyed the clone's
origin log), its provenance is folded into the migrated entry, and every
failure path rolls back and reports.

The launch path pulled over the network. Both wrappers call agents-vault
synchronously before starting an agent, and the pull in the shared sync
helper was unguarded once an upstream existed: against a blackholed
remote it blocked the launch indefinitely and then aborted the commit,
so an offline laptop silently stopped being backed up at all. Committing
never needed a remote, so the pull moved to the push path, which was
already opt-in for exactly this reason. A failure there now distinguishes
a real rebase conflict (rebase-merge/ or rebase-apply/ present) from an
unreachable remote instead of calling both a conflict, and both network
calls set GIT_TERMINAL_PROMPT=0 and GIT_ASKPASS so they fail fast rather
than prompt with nobody watching. The helper still refuses to commit a
rebase in progress, and leaves it standing rather than aborting one it
did not start. This also restores agents-init's pre-refactor ability to
commit while offline.

The agy knowledge copy was unfiltered. The allowlist held at the agy root
and nowhere below it, so a planted .credentials.json inside knowledge/
was committed verbatim while the documentation promised nothing new
upstream added could leak in. Only *.md and *.json are copied now --
which is what the store actually holds -- so lock files, transcripts and
conversation databases are excluded by having no business in a backup
rather than by being known about. The scaffolded .gitignore also ignored
only the SQLite sidecars and not the databases, which is worse than
ignoring neither: a torn database landed in history with the write-ahead
log that would have completed it deliberately excluded. Both changes are
template-only, on a feature that has never shipped.

agents-init reported success when nothing was committed. It ended on a
branchless `if` with no arm for a failed commit, which fish resolves to
0 -- the same false zero already fixed in agents-vault, left in the
function the refactor was rewriting. It now has the arm and an explicit
final status.

The --adopt forward-failure path with no stash left a raw coreutils `mv:`
line and no statement that the adopt had been abandoned cleanly; it is
branded like every other error exit in the function.

Tests: the suite now clones a vault with git and runs agents-vault
against the clone, instead of trusting hand-built fixtures to have shapes
git can actually produce -- that blind spot shipped both of the merge
blockers. The "present but empty" migration fixture is rebuilt as the
origin-only directory a clone leaves behind, with the hand-built shape
kept as a separate case. Reverting each fix drops the suite from 285 to
279 (migration), 261 (network), 275 (knowledge allowlist) and 283
(agents-init status).
2026-09-03 18:56:46 -04:00
rootiest 2ad5bf75d2 feat(agents-vault): sync the vault from the claude and agy wrappers
Both wrappers stay behind the C1 guard, so disabling
__fish_config_op_aliases still passes straight through to the real binary.

Launch commits but never pushes, keeping the network and any credential
prompt off the critical path; pushing is left to the Claude Code SessionEnd
hook. agy has no such hook, so its memory lands one launch later.
2026-09-03 18:56:46 -04:00
rootiest 11f4551fa5 fix(agents-vault): repair the adopt rollback and see dot-led entries
The adopt rollback restored the worktree but not the index. Every move it
makes is a plain rename as far as git is concerned -- the stash move out
from under the index most of all -- so a rolled-back adopt left a
half-applied rename staged against a clean vault. No bytes were at risk
and the next ordinary run's `git add -A` healed it, but a hand
`git commit` in that window recorded the half-applied state. Both
rollback sites now re-read projects/ once the worktree is whole again,
the stash restore included. projects/ is named whole rather than the two
entries, because `git add` refuses a pathspec that matches nothing --
which one of the two always is, once it has been moved back -- and then
stages neither.

The stash itself moves from the vault root into .git/, where neither the
entry walk nor `git add -A` can reach it, so a crash between the two
moves can no longer leave junk at the vault root for the next run to
commit. A vault whose .git is not a directory falls back to the root,
which the scaffolded .gitignore now covers.

--status and --restore walked the vault with a fish glob, which does not
match dot-led names. A dot-led slug is both reachable and sanctioned: the
sibling-bare-mirror idiom (`git remote add origin ../mirror.git`) keys as
..-mirror, a dot-led host keys as .hidden.example.com-o-r, and --adopt
accepts a leading dot on purpose. Such an entry is scaffolded, linked,
committed and pushed normally, yet --status under-reported it and batch
--restore left that project unlinked, both without saying so. Both walks
now list the directory instead. The --adopt completion gains -A for the
same reason: an entry that cannot be completed reads as one that is not
there.

The now-fatal push failure is painted as an error rather than a warning,
matching its sibling on the commit path.

The header notes that --adopt does not pin a name. The slug is re-derived
on every run, so the next ordinary run migrates the adopted entry back to
the canonical key, memory and live link following. Behaviour unchanged;
only the documentation gap is closed.

Tests, 173 -> 209 checks. The whole stash branch of --adopt was
uncovered, because the existing atomicity test adopts onto a slug with no
entry at all: a successful stash-adopt and a stash-adopt whose relink
fails are both pinned now, the latter asserting an empty
`git status --porcelain` and a still-reachable live memory. The stash
location is pinned by making the vault root unwritable for the duration,
which only a stash at the root would need. agents-vault's own propagation
of a failed sync had no test at all -- the third recurrence of fish's
branchless-`if` false zero here -- so both ways it can fail are now
driven end to end: a rejecting pre-commit at the vault's own
core.hooksPath, and a real rebase conflict against a bare remote. A
dot-led entry is asserted in --status and in --restore.
2026-09-03 18:56:46 -04:00
rootiest 090779ae5d fix(agents-vault): make push failure fatal and adopt atomic
A push that fails against a configured remote warned on stderr and then
fell through to the trailing branchless `if`, which resolves to 0, so
`--push` reported a successful backup while nothing had left the machine.
That is the exact loss the vault exists to prevent. It now returns
non-zero, verified against a real unreachable remote rather than a mock.

The same audit found two more false zeros in this function, both fixed:
the commit block warned about a rebase conflict and walked past it, and
swallowed a hook-rejected commit entirely (neither branch of its if/else
if matched, since the error goes to stderr rather than stdout); and
--restore reported a relink failure and then returned 0 regardless. All
three now feed one flag and the function ends on an explicit status
rather than on whatever the last branchless `if` left behind.

--adopt is now atomic. A rename that landed while the relink failed left
the memory intact at the new slug but unreferenced: the next ordinary run
found no live link, recomputed the old slug, found nothing there, and
fabricated a fresh empty entry, so the agent wrote history-less memory
from then on. No bytes were lost, but continuity was, with no automated
recovery. The live link is no longer removed first -- ensure_symlink
repins a link that points elsewhere on its own -- a contentless target
entry is moved aside rather than deleted, the origin note is appended
only after the relink succeeds, and a failed relink rolls the rename back
so the vault is exactly as it was.

The --adopt validator no longer refuses a leading dot. _agents_repo_slug
legitimately emits one for a dot-led subdomain, so refusing it made such
an entry impossible to adopt; inside projects/ it is a hidden directory,
not an escape. The traversal cases are still refused: no slash survives
the charset, and "." and ".." are refused by name.

Adds the RETURNS section the header was missing. --status prints a
structured report, which this repo's convention treats as return value
rather than as progress output.
2026-09-03 18:56:46 -04:00
rootiest 45f0fb9688 feat(agents-vault): add status, restore, adopt, remote, and push
--status reports link health, orphaned entries, and how far the vault is
ahead of its remote, which is how an unpushed backup gets noticed. --adopt
rebinds a machine-specific local-* entry by hand. --push is explicit;
autopush stays opt-in via __fish_agent_vault_autopush.

Three corrections to the planned shape:

--status is dispatched ahead of the scaffold instead of behind it. As
planned it sat after the tool install, the agy knowledge copy, and the
global memory link, so asking for a report would first sync global state
and claim ~/.claude/memory. It is now read-only and reports a missing
vault rather than creating one. The global-state block moved below the
mode dispatch so it runs only on a default or --link run; the mutating
modes still need the vault repo, so they sit between the scaffold and it.

--adopt validates its slug before using it. It is interpolated into
"$vault/projects/$slug" and handed to `git mv`, so --adopt=../../../etc
walked straight out of the vault. Only the charset the slug formula emits
is accepted, with no slash and no leading dot.

--remote captures the git exit status explicitly rather than chaining an
`or` off the block terminator. That construct does work in fish, but it
reads as the silent-false-success shape that a hook-rejected commit once
produced here, and it stops working the moment the `else` goes away.

Also pins the dangling-global-symlink case the suite never covered: for a
broken ~/.claude/memory link both -d and -e are false, so the -L disjunct
in the global-memory guard is the only thing that notices it. That is the
state a buggy earlier run left on a real machine; the test asserts it is
detected, repinned into the vault, and exits 0.
2026-09-03 18:56:46 -04:00
rootiest 4c7334c46e fix(agents-vault): keep a global-state fault from aborting project backup
The global block runs before the per-project link and the commit, but its
mkdir and link failures returned 1 outright. Global memory is optional and
frequently absent, so a stray file or a permission problem at
~/.claude/memory would abort the per-project memory backup and its commit
for every project, on every agent launch -- a fault in the secondary
feature killing the primary one.

Both failures now warn to stderr and continue, matching the treatment the
agy copy already had; the whole global block is best-effort by design.
Continuing is safe because _agents_repo_ensure_symlink validates and
refuses before mutating anything. $changed is set only when the link
actually succeeded, and nothing is recorded that would make a later run
believe the global memory is linked when it is not.

The live-side test widens from -d to -e so a stray regular file where the
global memory directory belongs is reported on every run instead of being
silently skipped and mistaken for the absent-by-default case.

Also documents that the agy knowledge copy is merge-only: a fact deleted
upstream persists in the vault and a restore brings it back. Whether the
vault should mirror deletions is a retention decision for the repo owner;
the gap is worth stating either way.
2026-09-03 18:56:46 -04:00
rootiest 1c9cedb8f3 feat(agents-vault): back up agy and global Claude state
agy partitions by conversation UUID rather than by workspace, so it has no
per-project slice and is tracked globally. Its knowledge store is copied
rather than symlinked because it sits beside SQLite databases with WAL
sidecars. Claude's global memory directory is symlinked into the vault the
same way per-project memory is, including the emergent-restore direction.

Paths are allowlisted so credentials, transcripts, and session state cannot
be swept in.

Two variables keep the tests off the real home: the new
__fish_agent_vault_claude_home overrides ~/.claude (whose memory/
subdirectory is the global one), distinct from the existing
__fish_agent_vault_claude_root, which overrides ~/.claude/projects.
Without it a test run on a machine that has a real global memory directory
would move it into a mktemp vault and leave a dangling symlink behind. The
suite now points every run at a throwaway home by default and asserts the
real paths are untouched.

cp cannot report whether anything actually differed, so the copy is only
counted as a change when it leaves the vault's global/agy/ subtree dirty.
Marking it changed unconditionally would print a --quiet summary line on
every agent launch and make the flag meaningless.
2026-09-03 18:56:45 -04:00
rootiest a018d7997c fix(agents-vault): dedupe local-slug formula, drop dead code, widen migration coverage
The slug-migration fallback (used when there is no live symlink to read
the previous slug from) recomputed the local-* candidate by lowercasing
the basename only, while _agents_repo_slug sanitizes it. The two formulas
had drifted, so the fallback silently found nothing for any project
directory whose basename needed sanitizing.

Extract the formula into a single private helper,
_agents_repo_local_slug, and have both _agents_repo_slug's no-remote
branch and agents-vault's migration fallback call it, so there is one
place left to drift.

Also drop two dead lines the review flagged: an unused  local,
and an unreachable mkdir -p (path dirname ...) — slugs never contain a
path separator, so dirname always resolves to a directory that already
exists by that point.

Widen migration test coverage: the current-entry-present-but-empty case,
a remote URL rewrite, a remote removal, and a dirty-basename fallback
test that fails without the sanitization fix and passes with it.
2026-09-03 18:56:45 -04:00
rootiest b7ff4e0981 feat(agents-vault): migrate entries when a project's slug changes
Adding a remote to a previously remote-less project changes its slug. Left
unhandled, the link step repinned the live memory symlink to a fresh empty
entry and orphaned the real memory.

The previous slug is read from the live symlink target rather than guessed,
which covers a remote being added, rewritten, or removed. When both the old
and new entries hold content the migration is ambiguous, so nothing moves
and the user is directed to --adopt.
2026-09-03 18:56:45 -04:00
rootiest 2d8db42b12 docs(fish-config.index): index the agent memory vault variables
config-help resolves keywords through this hand-maintained index; the
new "Agent Memory Vault" section in 07-customization.md had no entries
here yet. Adds agent-vault, __fish_agent_vault_dir, and
__fish_agent_vault_autopush, following the __fish_scrollback_history_dir
precedent (bare variable names as keys).
2026-09-03 18:56:45 -04:00
rootiest 19126316a7 fix(agents-vault): always link the current project's memory
The guard around the symlink step only linked when the live Claude
project directory already existed, which is exactly backwards for the
clone-onto-a-new-machine restore case: a freshly cloned vault entry
would be silently left unlinked and a starting agent would write fresh,
history-less memory instead. _agents_repo_ensure_symlink already makes
its own parent directories and is idempotent, so nothing depended on
the guard; it is removed and the link is now attempted unconditionally.

Also stop swallowing a refused or failed link as success: the helper's
exit status is now checked, and agents-vault reports its own error and
exits 1 instead of silently continuing with no link in place.

Smaller fixes from the same review pass:
- check the exit status of _agents_repo_install_tools and the
  core.hooksPath git config write, instead of discarding both
- give the vmem mkdir failure a stderr message like every other fatal
  in the function
- guard hostname with type -q and add it to DEPENDENCIES
- .version creation now sets changed, so --link (which skips the
  commit step) reports it in --quiet mode
- reword --link's help/doc text: it still scaffolds the vault and
  links memory, it only skips the final commit
- drop the unused c_dim color variable
- move the __fish_agent_vault_dir / __fish_agent_vault_autopush
  documentation below Opinionated Components so its NOTE: callout
  (now flush-left so it actually renders as a Starlight Aside, per
  review) doesn't become the first Note aside in the page and steal
  the existing test's assertions about the original 4-bullet one

Adds two tests: pre-seeded vault entry with no live directory at all
(the restore path the guard was breaking), and a forced link failure
asserting agents-vault now exits 1 instead of 0.
2026-09-03 18:56:45 -04:00
rootiest 2c185f7e23 feat(agents-vault): scaffold the vault and link project memory
Creates the vault repo on demand, reusing the AGENTS version bumper and
hook shims, then links the current project's live memory directory into
its slug-keyed entry and commits.

Because the live directory becomes a symlink into the vault, backup and
restore are the same operation: a cloned vault relinks itself on the next
run in each project, with no manifest and no batch restore step.

Also fixes _agents_repo_install_tools' progress messages, which hardcoded
the literal "AGENTS/.agents-tools/" even for callers writing elsewhere:
they now name repo_dir's own basename, so agents-vault reports its own
directory instead of a false AGENTS/ path.
2026-09-03 18:56:45 -04:00
rootiest b0585d00ad refactor(agents-init): use the shared repo helpers
Renames _agents_init_install_tools to _agents_repo_install_tools now that
the vault shares it, collapses the two duplicated root-symlink blocks into
one loop, and routes the auto-commit through _agents_repo_sync so a failed
rebase can no longer be committed as conflict markers.
2026-09-03 18:56:45 -04:00
rootiest b5d2c9ba87 fix(agents-vault): surface commit-hook rejection as exit 1
_agents_repo_sync fell off the end of its final if-block when git commit
failed, e.g. a pre-commit or commit-msg hook rejecting it (this repo runs
ggshield and Git-LFS hooks). fish's if construct sets status 0 when the
condition is false and there is no else branch, so a rejected commit was
being reported as success rather than as the documented "not a git
repository" exit 1 it was assumed to fall through to.

Add an explicit else branch that emits a stderr diagnostic and returns 1,
and widen the EXIT STATUS/DESCRIPTION docs to cover this path under the
existing code 1 rather than adding a fourth code, since later tasks
already consume the 0/1/2 contract.
2026-09-03 18:56:44 -04:00
rootiest 51543cb7ca feat(agents-vault): add sync helper that refuses to commit conflicts
agents-init currently swallows a failed rebase and then stages and commits
whatever is in the tree, which records conflict markers under a routine
message. No AGENTS repo has a remote today so the pull never runs, but the
vault gives these repos remotes and arms it.

The shared helper aborts the rebase, commits nothing, and returns 2. It
also redirects git's own stdout during the pull/abort: git prints
"CONFLICT (content): ..." to stdout, not stderr, so without this the
message would leak into the helper's own stdout instead of staying
diagnostic-only.

The conflict fixture commits "ours" locally before diverging, since an
uncommitted worktree change has nothing for --autostash's rebase step to
replay -- it fast-forwards cleanly and only the stash pop would conflict.
2026-09-03 18:56:44 -04:00
rootiest 3fd9476fbc feat(agents-vault): add directory-only symlink helper
Enforces the rails the vault depends on: only directories are linked
(agent editing tools refuse to write through a symlinked file), a missing
target is refused rather than turned into a dangling link, and adopting a
populated live directory copies without clobbering.

Also fixes slug sanitization in _agents_repo_slug to apply the same
[^a-z0-9._-] → - mapping to the fallback (no-remote) branch, ensuring
local project slugs are filesystem-safe and won't leak special chars like
spaces or exclamation marks.
2026-09-03 18:56:44 -04:00
rootiest 1dc0e5293d feat(agents-vault): derive vault slugs from normalized remote URLs
Keys a project by its remote rather than its path so the key survives a
machine change or a directory rename. Falls back to a path-derived
local-* key when no remote exists.

Adds a hermetic test harness that builds throwaway repos under mktemp.
2026-09-03 18:56:44 -04:00
rootiest 335fdff433 Merge pull request 'feat(ci): sync the label taxonomy to the GitHub mirror automatically' (#125) from ci/sync-mirror-labels into main
Sync labels to mirror / sync-labels (push) Successful in 26s
2026-09-01 03:37:16 +00:00
rootiest a78c6a604d Merge pull request 'docs(contributing): add issue templates and define the labeling standard' (#124) from docs/issue-templates-and-labels into main 2026-09-01 03:36:02 +00:00
rootiest aed37d5a75 docs(contributing): note that mirror labels now sync automatically
The mirror section told contributors a label added on Gitea "must be
created on the mirror too -- no automation does it for you." That's no
longer true, and a stale instruction to do something by hand is worse than
none, since it invites a manual edit that the next scheduled sync would
overwrite anyway.

Describe what the sync actually does: what it creates, updates, and prunes,
that an in-use label is never deleted, the --dry-run and --self-test flags,
and the rename caveat that follows from matching labels by name. Also
record the GH_MIRROR_TOKEN secret the workflow needs and its exact scopes,
since that's the one part of this that can't be automated.

The exclusive-labels caveat below it is unaffected and stays as written.
2026-08-31 23:32:16 -04:00
rootiest cff4f7b414 feat(ci): sync the label taxonomy to the GitHub mirror automatically
Labels don't travel with a mirror push -- mirroring copies files, not
repository settings -- but they matter on the GitHub side anyway, because
GitHub reads the same .github/ISSUE_TEMPLATE/ files and silently drops a
labels: entry naming a label it doesn't have. Until now the only thing
keeping the two sets aligned was remembering to do it by hand, which is
exactly the kind of thing that gets forgotten and then fails invisibly.

Add scripts/sync-labels.py and a workflow that runs it daily, on any change
to the script itself, and on manual dispatch. Gitea stays the source of
truth: labels are managed there and GitHub is made to match.

- **Creates and updates** anything missing or drifted. Colors and
  descriptions are normalized before comparison -- Gitea returns colors
  bare, GitHub sometimes with a leading '#', and a description may be null
  on one side and "" on the other -- so a steady state is a true no-op
  rather than a rewrite of all 33 labels every run.
- **Deletes only unused extras.** An extra label on the mirror is removed
  only when no issue or PR there carries it; one in use is reported with
  its count and left alone. An unattended scheduled job must not be able to
  strip a label off somebody's issue.
- **Refuses to run on an empty source**, since treating that as truth would
  propose deleting every label on the mirror.
- **--dry-run** prints the plan and changes nothing; **--self-test** checks
  the diff logic offline against fixtures, and gates the sync step in CI so
  a broken diff can't mutate anything.

Stdlib only, so the CI step installs nothing beyond python3. The job is
gated with the same github.server_url check ci.yml uses -- without it the
mirror would queue this forever against a runner that only exists on Gitea.

Labels are matched by name, so a rename reads as delete-plus-create; the
new name is created and the old is pruned only if unused. The two forges
share no stable label ID, so a rename can't be tracked across them.
2026-08-31 23:32:16 -04:00
rootiest 877c973e87 docs(contributing): make the issue templates work on the GitHub mirror too
The templates were written against Gitea's schema alone, but the mirror
serves the same .github/ISSUE_TEMPLATE/ files to GitHub, where two of them
would have silently failed:

- **config.yaml -> config.yml.** Gitea accepts either spelling
  (modules/structs/issue.go: `base == "config.yaml" || base == "config.yml"`),
  GitHub only recognizes config.yml. Under the old name the mirror's
  template chooser would have shown neither the contact links nor the
  blank-issue setting.
- **bug.yaml `about:` -> bug.yml `description:`.** GitHub requires
  `description` on a YAML issue form; Gitea's IssueTemplate.About carries
  the comment "Using description in a template file is compatible" and
  falls back to it at modules/issue/template/unmarshal.go:126. So
  `description` is the one spelling both forges accept. The markdown
  templates keep `about:`, which is correct for their format on both.

Both files now carry a comment explaining the constraint, so neither gets
"tidied" back into a broken state.

Also add a contact link pointing at the canonical Gitea tracker, so someone
arriving from the mirror is steered to the right place before they file,
and document mirror parity in CONTRIBUTING.md: the labels must be created
on both sides by hand, since mirroring copies files rather than repository
settings and GitHub drops a labels: entry naming a label it doesn't have.
Note too that GitHub has no exclusive labels, so the one-of rule on
Priority/, Reviewed/, and Status/ holds only by convention there.
2026-08-31 23:16:41 -04:00
rootiest ce3c44a053 docs(contributing): define the issue and PR labeling standard
The repo carried Gitea's stock label set with no written rule for applying
it, so labels were effectively unused. Establish the standard: every issue
and PR carries exactly one Kind/ and at least one Area/, and document what
each group means, which are exclusive, and who applies them.

Extend the taxonomy to make that workable:

- **Kind/Refactor, Kind/Chore, Kind/Performance** — the stock Kind/ group
  couldn't describe a refactor, a chore, or a perf change, which is much of
  this repo's history. The group now maps one-to-one onto the Conventional
  Commits types already in use, so a PR's label and title agree.
- **A new non-exclusive Area/ group** over the ten subsystems (functions,
  completions, config, docs, tests, CI, integrations, prompt & theme,
  components, scripts). This is what makes the tracker searchable by
  subsystem; Kind/ alone can't answer "what's outstanding in the docs
  pipeline?".
- **good first issue and help wanted**, deliberately unscoped rather than
  under Status/. Status/ is exclusive, and an issue is often both blocked
  and open for someone to pick up; keeping these outside the group lets
  them coexist with a real status.

Priority/, Reviewed/, and Status/ are documented as exclusive and
maintainer-applied, with the rules that keep them meaningful — no
Priority/ label means ordinary priority, Reviewed/Confirmed means actually
reproduced, and a Status/ label is removed once it stops being true.

Also add labeling to the standing PR rules in Branching & Pull Requests,
so it's visible where the rest of the PR checklist lives.
2026-08-31 23:09:01 -04:00
rootiest af764903e7 docs(contributing): add issue templates for bugs, features, and docs
Issues had no template at all, so a report arrived in whatever shape the
reporter chose — most often without a fish version, a reproduction, or the
full error text, which is what actually stalls a bug.

Add three templates under .github/ISSUE_TEMPLATE/, beside the PR template
so the GitHub mirror offers the same set:

- **bug.yaml** — a Gitea issue form rather than markdown. Version, OS,
  area, reproduction, expected and actual behavior are required fields, so
  an unactionable report can't be submitted in the first place. The Area
  dropdown exists because contributors without push access can't set an
  Area/ label themselves.
- **feature.md** and **docs.md** — comment-guided markdown in the same
  house style as PULL_REQUEST_TEMPLATE.md, since what they ask for is
  open-ended prose. feature.md carries `## Acceptance criteria`, the
  issue-side counterpart to a PR's `## Verification`. docs.md insists on
  the docs/manual/** source rather than the generated page, which the next
  CI run would overwrite.
- **config.yaml** — keeps blank issues enabled for what the three don't
  cover, and links the contributing guide and the customization docs.

Each template pre-applies its Kind/ label. Document the set, the plain-
description title convention (an issue states a problem; the conventional
subject belongs on the PR that closes it), and the triage split in a new
CONTRIBUTING.md § Issues.
2026-08-31 23:07:44 -04:00
rootiest fcb9e0c468 Merge pull request 'docs(contributing): add PR description template and convention' (#123) from docs/pr-template into main
Reviewed-on: #123
2026-09-01 02:42:37 +00:00
rootiest 1dc86b9ff0 docs(contributing): document the WIP: title prefix for in-progress PRs
Gitea treats a `WIP:` title prefix as a draft marker and refuses to merge
while it's present. Verified empirically against this instance by
temporarily retitling PR #123: the API flipped `draft` to true and
`mergeable` to false, and both reverted when the prefix was removed.

The distinction from the verification merge gate is the point, so it's
stated explicitly in both files: `WIP:` means more code changes are
coming, NOT "finished but unverified". A complete branch with unticked
`## Verification` boxes is an ordinary PR — that case is already covered
by the merge gate and does not want the prefix. The two signals are
independent and can occur in any combination.

Documented as a merge rule under Branching & Pull Requests, next to the
verification gate, and in the template's title guidance where an author
picking a title will actually encounter it.
2026-08-31 22:41:12 -04:00
rootiest ea8b13b38a docs(contributing): rename Manual Verification to Verification, make it a merge gate
The old name implied the list was only for by-hand steps, which sat badly
with the fact that most entries in practice are programmatic — the test
suite, `fish_indent`, `docs/verify-manual.py`, CI. Dropping "Manual" lets
one list carry both kinds, and gives the checkbox state real meaning:

- **Checked** — verified, whether it ran programmatically or the author
  performed it by hand.
- **Unchecked** — an outstanding manual check the reviewer still has to
  perform. Left visible rather than dropped.
- **The list is the merge gate.** A PR isn't merged until every box is
  checked; added as an explicit rule under Branching & Pull Requests
  alongside the other merge rules.

Because unchecked boxes now block the merge, the guidance also states that
only resolvable checks belong here — one nobody can run would block the PR
indefinitely. Genuinely unverifiable caveats and assumptions go in
`## Notes`, which already existed in the optional-sections list.

Incidentally realigns with PRs 121-122, which had already shortened the
heading to `## Verification`; that part of the recent drift was right.
2026-08-31 22:35:30 -04:00
rootiest 2f49960149 docs(contributing): establish Closes #N issue-linking convention
The repo has no issue-tracking history yet — a grep across all 122 prior
PRs found zero `Closes #N` references — so this is forward-looking rather
than derived from precedent. Establishing it now means the first PR that
does close an issue has a rule to follow instead of inventing one.

- Placement is a trailing line at the end of `## Summary`, not the bottom
  of the body, because `## Manual Verification` is always last.
- `Fixes`/`Resolves` noted as equivalent; `Refs #N` for a related issue
  that should stay open.
- Guidance calls out that the keyword must be repeated per issue, since a
  bare `#43` after a comma links without closing.
- Lives in the template's HTML comments, so a PR with no associated issue
  leaves no stray `Closes #` behind.

Also drops an unverified claim that the GitHub mirror pre-loads the
template; PRs are opened on Gitea, and the mirror is push-only.
2026-08-31 22:32:52 -04:00
rootiest f52bfaa55e docs(contributing): add PR description template and convention
Codifies the PR body format the repo has converged on across its first
122 pull requests, which until now lived only as an implicit pattern
agents and contributors had to reverse-engineer from prior PRs.

- **`.github/PULL_REQUEST_TEMPLATE.md`** — `## Summary`, optional `##`
  sections, then `## Manual Verification` as a checkbox list. Gitea falls
  back to `.github/` when `.gitea/` is absent, and the GitHub mirror reads
  the same path, so one file covers both.
- **`CONTRIBUTING.md`** — new "Pull request descriptions" subsection under
  Branching & Pull Requests, documenting the same three-part structure.

Section names were chosen by frequency across all 122 PRs: `## Summary`
(86) and `## Manual Verification` (63) are the clear majority. The later
`## Manual Verification Checklist` (12, PRs 90-113), `## Test plan` (11)
and `## Verification` (3, PRs 121-122) variants are drift away from that
baseline, not a newer standard, so the template restores the dominant
form. Checkboxes ship unchecked but are meant to be checked before the
PR opens, matching the 585-to-12 ratio of `[x]` to `[ ]` in merged PRs.
2026-08-31 22:27:26 -04:00
Gitea Actions dfb353af09 chore(docs): regenerate manual, man page, and component registry 2026-09-01 02:16:32 +00:00
rootiest 5328529e18 Merge pull request 'fix(help): full index audit and singular/plural keyword matching' (#122) from docs/index-full-audit into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 49s
CI / build-docs (push) Successful in 3m35s
2026-09-01 02:11:53 +00:00
rootiest 81d26e095a Merge pull request 'feat(help): render code spans in the pager instead of printing backticks' (#121) from feat/consistent-code-spans-in-concat into main
CI / github-mirror (push) Skipped
CI / build-docs (push) Canceled after 0s
CI / test (push) Canceled after 40s
2026-09-01 02:11:27 +00:00
rootiest 6cf690e637 fix(help): resolve singular and plural section keywords
The heading scan matches a keyword contained in a heading, so a plural
could never reach a singular heading: `customization` found "7.
CUSTOMIZATION" and `customizations` found nothing at all. The index
lookup was exact-match, so it could not cover the gap either without an
alias per word.

Both now try the keyword as typed first, then its singular/plural
forms. Each variant is tried against every heading before the next one
is considered, so a loose plural cannot beat an exact hit further down
the document.
2026-08-31 22:07:06 -04:00
rootiest f7a9ff9d23 chore(docs): regenerate manual
The man page is left to CI, which has pandoc.
2026-08-31 22:02:41 -04:00
rootiest 577ad993ea feat(help): render inline code spans instead of printing backticks
config-help pipes the manual through bat, which dims the backticks and
leaves the span content the same colour as the prose around it -- so a
delimiter carried no information and every span cost the reader two
literal characters. 1236 of them across the document.

Each span is now rendered bold and the delimiters dropped, in every
branch of the viewer chain:

- ov + bat, and ov alone, style the spans (bat's output wraps each
  backtick in its own SGR sequence; raw Markdown is matched directly)
- bat alone flattens them on the way in instead, because bat escapes
  any SGR sequence handed to it as input
- less and cat style them, less gaining -R to render the result
- man -l needs nothing; pandoc consumed the backticks at build time

Both substitutions are line-preserving, so the tail-slice that opens
the pager on a requested section still lands on it.
2026-08-31 22:02:37 -04:00
rootiest f0de5378fe docs(manual): keep backticks out of verbatim blocks and off line breaks
Two shapes reach a reader as literal punctuation rather than markup:

A backtick inside a four-space block. The block is verbatim in every
renderer -- pandoc sets it monospace, prettify() fences it for the site
-- so the backtick is a character on the page. Twelve such lines are
cleaned; each sat at the start of its column, so the alignment of the
C5 capture table and the component summaries is unchanged.

A span split over a line break. Markdown pairs it happily, but
config-help pairs backticks one line at a time, so `fish-deps\nupdate`
showed both halves. The sentence is reflowed.

Both are now enforced, the first by test_concat_section_five_stays_
verbatim and the second by test_concat_code_spans_never_straddle_a_line.
2026-08-31 22:02:28 -04:00
rootiest b01124f99d feat(docs): run codespans over the man-page pipeline too
A token was typeset by whichever pipeline happened to render it: the
site marked tmux and local.fish through codespans, while the man page
and config-help marked only what the SSOT had backticked by hand. Run
the same pass in build_concat() so prose is marked identically wherever
it is rendered (549 -> 662 spans in the concat).

codespans now treats a four-space block as code. The site never meets
one -- prettify() has already turned it into a fence by then -- but the
concat keeps the indented form pandoc wants, and its contents are
verbatim: without this the table of contents alone would come out with
ov, bat, less and cat wrapped inside a code block. Section 5 is
unaffected for the same reason; its entries arrive as indented blocks
that pandoc already sets in a monospace font.

test_codespans_is_site_only asserted the opposite guarantee and was
passing only because its example, -r/--resume, sits inside one of those
newly-protected blocks. It is replaced by tests for what is now true:
indented blocks stay verbatim, prose spans reach the concat, and
section 5 carries no backticks.

The man page is left for CI to regenerate; pandoc is not needed to
build the concat.
2026-08-31 21:56:55 -04:00
rootiest 2ce8bebf29 docs(functions): drop backticks from doc-headers
CONTRIBUTING states doc-headers are written as plain text -- the header
is read as-is by config-help, by funcsave, and by anyone opening the
file, and docs/codespans.py adds the site's inline code spans at render
time. 22 files had drifted from that, carrying 41 hand-written spans
that reached config-help and the man page as literal backtick
characters inside an otherwise verbatim block.

The one span whose content ended in a space is requoted rather than
dropped, so "read> " keeps reading as a prompt string.
2026-08-31 21:56:45 -04:00
rootiest 618ce00f9e docs(index): full audit of the config-help keyword index
Cover the ~50 previously-unindexed headings (function reference entries
like play-media, steam-dl, bd-pull, cffetch/ffetch, config-toggle, dops,
rand_string, fish_prompt/fish_mode_prompt/fish_right_prompt, lD, mv, p,
fast/fast-cli, gip4/gip6, fzf-update, sponge_filter_secrets,
fzf_configure_bindings, ld; the C1-C6 component sub-category headings
filesystem/network/monitor/shell-tools/dev-tools,
plugin-management/pkg-wrappers/venv/telemetry/sync,
key-bindings/environment/prompt, terminal-abbrs/window-mgmt/
notifications/history-logs/pkg-upgrade, terminal-capture/
multiplexer-capture/pkg-logs, first-run/greeting-message; plus
Config Variables' "Other", the Integrations "Scrollback History", the
dependency catalog's Optional/Terminal Emulators tiers, the C0/always
override note, and "Reading the source directly").

Expanded aliases for existing headings: play-media also reachable as
video/audio/mpv/vlc, steam-dl as steam, cffetch/ffetch share fetch/
fastfetch/neofetch/sysinfo, rand_string as random/password/randomword,
gip4/gip6 as ipv4/ipv6, dops as docker-ps, ld as lazydocker, bd-pull as
beads, fast-cli as speedtest, sponge_filter_secrets as secret-filter.

Fixed two pre-existing duplicate normalized keys (key-bindings/
keybindings and man-page/manpage both collapsed to a single entry) and
repointed config-toggle from the generic config-settings heading to its
own dedicated "### config-toggle" heading now that one exists.

Judgment calls on ambiguous/repeated heading text (left unindexed, or
indexed via a distinguishing keyword instead of the literal name):
- "### Sub-categories" repeats 7 times (once per opinionated-components
  overview and once per C1-C6 page); an index entry pointing at that
  text always resolves to the first occurrence, so none of the 7 are
  indexed under that name. Each parent (C1-C6, and the Minimal Mode
  section) already has its own keyword.
- "### search" appears twice: the pkg search subcommand (kept, existing
  `search` key) and the C1 command-shadow sub-category. The C1 one
  isn't separately reachable for the same reason.
- "## Integrations" appears twice (Section 1 integrations writeup and
  the Dependency Catalog's Integrations tier table); only the first
  (already indexed) is reachable.
- Where a C1-C6 sub-category heading text collides after normalization
  with an existing higher-value keyword (key-bindings, network,
  notifications, venv), the new entry uses a distinguishing keyword
  (bindings-toggle, network-shadow, notifications-toggle, venv-toggle)
  instead of contesting the existing one.
- `prompt` now points at the fuller "## Prompt and Theme" section
  rather than the one-line C3 sub-category blurb; the latter is
  reachable via `prompt-toggle`.
2026-08-31 21:56:15 -04:00
rootiest 1aef0ffca8 Merge pull request 'fix(ci): skip queued-forever jobs on the GitHub mirror' (#120) from ci/skip-duplicate-run-on-github-mirror into main
Reviewed-on: #120
2026-09-01 00:21:20 +00:00
rootiest a89a5576a3 fix(ci): skip queued-forever jobs on the GitHub mirror
The test/build-docs jobs target a self-hosted runner (racknerd-mini)
that only exists on the Gitea instance. When GitHub re-runs this same
workflow on the mirror, those jobs sit queued forever with no matching
runner, so the commit never gets a completed status.

Gate both jobs to skip when github.server_url is github.com, and add a
trivial github-mirror job (runs-on: ubuntu-latest, which GitHub does
provide) that only runs on the mirror, so the check completes instead
of hanging.
2026-08-31 20:19:40 -04:00
rootiest b754709f02 docs(site): add inline code spans to generated Starlight pages
CI / test (push) Successful in 1m4s
CI / build-docs (push) Successful in 3m57s
Function doc-headers are authored as plain text -- `config-help`,
`funcsave` and anyone opening the `.fish` file read them as-is -- so they
carry no backticks. The site inherited that and rendered `-a/--all` and
`__fish_config_op_aliases` as ordinary prose.

docs/codespans.py adds the spans at render time, as the last step of
prettify(), so only the site sees them; build_concat() (man page,
config-help) is byte-for-byte unchanged.

Recognised shapes: flags and flag pairs, `$vars`, SCREAMING_SNAKE env
vars, snake_case identifiers, paths and filenames, key chords, command
shadow chains (`ls->eza`), runs of tool names, whole command lines in a
table column of command lines, and known command names -- drawn from the
`_fdc_*` catalog in functions/_fish_deps_catalog.fish, the functions/
listing, and a standard-command list, minus the names that also read as
English.

Fenced blocks, existing code spans, headings, link targets, URLs,
component markup and <FileTree> bodies are passed through untouched, and
every rule bails out rather than guess.
2026-08-31 20:04:19 -04:00
Gitea Actions f7cfad559c chore(docs): regenerate manual, man page, and component registry 2026-08-25 05:54:10 +00:00
rootiest e97b3ee6ab Merge pull request 'fix(docs): reformat Prompt and Theme section and fix MDX code-block bug' (#119) from docs/customization-prompt-theme-formatting into main
CI / test (push) Successful in 52s
CI / build-docs (push) Successful in 3m38s
Reviewed-on: #119
2026-08-25 05:49:37 +00:00
rootiest d44dbd3564 Merge branch 'main' into docs/customization-prompt-theme-formatting 2026-08-25 05:49:26 +00:00
rootiest 2aad1aca98 Merge pull request 'feat(docs): expand functions/ and completions/ in the Starlight home tree' (#118) from feat/docs-filetree-dynamic-listing into main
CI / test (push) Successful in 49s
CI / build-docs (push) Successful in 3m7s
Reviewed-on: #118
2026-08-25 05:35:32 +00:00
rootiest 9969f43ba3 fix(docs): reformat Prompt and Theme section and fix MDX code-block bug
Verified every claim in the Prompt and Theme section against the actual
source and corrected several inaccuracies: the Starship wrapper's missing
C3 gate, the fallback prompt's vi-mode states and segment order, the FZF
theme's real location (conf.d/theme.fish, not integrations/fzf.fish) and
color set, and the Catppuccin theme-switch example using the wrong
fish_config subcommand (choose, not save). The right-prompt Docker-context
example was rewritten to show that it's independent of exit status.

While reformatting, found that any block build-manual.py couldn't classify
as shell/table/tree fell back to plain 4-space markdown indentation, which
silently renders as squashed, unreadable paragraph text on any page that
also contains an <Aside> or <FileTree> — MDX has no indented-code-block
syntax, unlike plain Markdown. This affected 07-customization.mdx plus four
other pages. Fixed the fallback to emit a fenced ```text block instead,
since fences work in both MDX and plain Markdown; this also gives every
affected block Starlight's normal code-block styling instead of a bare grey
slab.

docs/fish-config.md is intentionally left stale here — CI regenerates and
auto-commits it from docs/manual/** on push to main.
2026-08-25 01:28:18 -04:00
rootiest 82d78d19f5 feat(docs): expand functions/ and completions/ in the Starlight home tree
List every file in functions/ and completions/ inline under the home
page's file tree instead of a one-line summary. The listing is read
live off disk during --site generation, so it never needs manual
upkeep, and only affects the Starlight build — the plain-text
manual/man page (--concat) still renders the compact summary.
2026-08-25 01:07:07 -04:00
rootiest 38924d5c3c docs(readme): link to CONTRIBUTING.md 2026-08-22 01:01:12 -04:00
rootiest 057d9913b9 Merge pull request 'docs(contributing): add CONTRIBUTING.md formalizing repo standards' (#117) from docs/contributing-guide into main
Reviewed-on: #117
2026-08-22 04:58:07 +00:00
rootiest 62167a439e docs(contributing): clarify fork workflow for outside contributors
The branch-directly-off-main workflow assumes push access to the
repo; contributors without it should fork and PR from there instead.
2026-08-22 00:54:20 -04:00
rootiest f4c4922f0e docs(contributing): add CONTRIBUTING.md formalizing repo standards
Documents the branching/PR workflow, commit conventions, fish function
doc-header and colored --help conventions, the docs generation
pipeline, testing, and the secrets/machine-config placement rule, so
these practices live somewhere durable instead of only in commit
history and conversation memory.
2026-08-22 00:53:54 -04:00
rootiest 857faebeba Merge pull request 'feat(help): standardize colored --help output across functions' (#116) from feat/colored-help-text into main
CI / test (push) Successful in 56s
CI / build-docs (push) Successful in 3m26s
Reviewed-on: #116
2026-08-22 04:24:44 +00:00
rootiest b1a0c6c488 Merge pull request 'fix(scrub): restore missing line continuation in aggressive_patterns' (#115) from fix/scrub-aggressive-patterns-backslash into main
CI / build-docs (push) Canceled after 0s
CI / test (push) Canceled after 22s
Reviewed-on: #115
2026-08-22 04:24:30 +00:00
rootiest 500dd8a735 feat(help): standardize colored --help output across functions
Add the established c_head/c_cmd/c_flag/c_dim/c_arg color scheme to
--help (or usage-on-error) output in play-media and 13 other functions
that lacked it or used an ad hoc scheme: bkg, detach, replay, p, y,
spark, wake-lock, open-url, repo-open, dng2avif, dockup, fish-deps
(__fish_deps_help), and scrub.

Also tweak the standard itself:
- c_cmd now uses plain `set_color --bold` instead of `--bold white`,
  so the command name adapts to the terminal's foreground instead of
  forcing white text that washes out on light-background themes.
  Applied across all functions already using the pattern.
- jobrunner's reset variable renamed from c_rst to c_reset to match
  the naming used everywhere else.
2026-08-22 00:19:43 -04:00
rootiest 9e74cb8f0a fix(scrub): restore missing line continuation in aggressive_patterns
A missing trailing backslash after the Thumbs.db:encryptable pattern
terminated the `set -l aggressive_patterns` array early. The AI-tool
patterns (.gemini*, .claude*, .antigravity*, .remember*) were then
executed as a bogus command instead of being appended to the array,
so scrub -a never actually purged them, and every scrub invocation
printed a spurious "Unknown command" error.
2026-08-22 00:18:55 -04:00
Gitea Actions 4006a2f855 chore(docs): regenerate manual, man page, and component registry 2026-08-21 06:57:00 +00:00
rootiest 8b9d0ac1f4 Merge pull request 'feat(media): add play-media fzf picker for audio/video files' (#114) from feat/play-media-fzf-picker into main
CI / test (push) Successful in 1m0s
CI / build-docs (push) Successful in 3m37s
2026-08-21 06:52:23 +00:00
rootiest 51fc669c02 feat(media): add play-media fzf picker for audio/video files
Fuzzy-filters audio/video files under the current directory via fd,
previews them with thumbnails pulled from the freedesktop thumbnail
cache (or ffprobe metadata as a fallback), and plays the selection
through the best available player: --player flag, $play_media_player,
xdg-mime default, then a short mpv/vlc fallback list. Adds mpv/vlc as
optional fish-deps entries.
2026-08-21 02:47:45 -04:00
rootiest c25bb3b105 Merge pull request 'fix(ci): use working-directory instead of cd everywhere' (#113) from fix-ci-deploy-step-group-label into main
Reviewed-on: #113
2026-08-21 05:39:42 +00:00
rootiest 4d7ad64c3a fix(ci): drop remaining cd for working-directory consistency
The only other cd left in the workflow mixed two working directories
in one step: docs/build-manual.py --site runs from the repo root, then
npm ci/astro build need docs/site. Split into two steps so each can
use working-directory instead, keeping the whole file cd-free and
consistent with the Cloudflare deploy step's fix.
2026-08-21 01:23:34 -04:00
rootiest 3c27e52ada fix(ci): use working-directory instead of cd for Cloudflare deploy
Gitea/GitHub Actions auto-groups a multi-line run: script under
"Run <first line>". With `cd docs/site` as the first line, the
collapsed log tree showed that instead of the actual wrangler deploy
command. Moving the directory change to the step's working-directory
key drops cd from the script entirely, so the group label now reflects
the command that's actually running.
2026-08-21 01:21:29 -04:00
rootiest 70b5868e3e Merge pull request 'fix(ci): reduce apt-get noise/fragility and allow per-job dispatch' (#112) from ci-robustness-and-dispatch into main
Reviewed-on: #112
2026-08-21 05:10:01 +00:00
rootiest c47b51cd6e feat(ci): allow triggering test or build-docs individually
workflow_dispatch already ran the whole pipeline manually, but there
was no way to fire just one job (e.g. re-run docs generation without
re-running the fish test suite) the way the old standalone
build-docs.yml let you. Add a job choice input (all/test/build-docs,
defaulting to all) and gate each job on it via `if:`, while leaving
the push-triggered path's needs: test gating untouched.
2026-08-21 00:58:30 -04:00
rootiest c4e225b007 fix(ci): retry apt-get update and skip recommended packages
Two more sources of CI noise/fragility alongside the debconf frontend
fix: fish's install pulls in man-db/groff-base/xsel as recommends,
triggering a slow mandb rebuild for tooling nothing here uses;
--no-install-recommends skips that. apt-get update had no retry
policy, so a transient blip against the PPA mirror failed the whole
job; -o Acquire::Retries=3 gives it a few chances first.
2026-08-21 00:54:54 -04:00
rootiest f7598e5e23 fix(ci): set DEBIAN_FRONTEND=noninteractive for apt-get installs
apt-get install was probing for a Dialog then Readline debconf frontend
before falling back to Teletype on the non-interactive CI runner,
adding noise and failed-negotiation log lines to every run. Passing
DEBIAN_FRONTEND=noninteractive directly on the sudo command line (env
vars set via step-level `env:` don't survive sudo's env_reset) skips
the negotiation and goes straight to the frontend that actually works
here.
2026-08-21 00:52:55 -04:00
rootiest a5cc8cbd36 Merge pull request 'feat(fzf): add preview, dirs, and image support to @@ inline picker' (#110) from enhance-fzf-inline-picker-preview into main
CI / test (push) Successful in 54s
CI / build-docs (push) Successful in 3m34s
Reviewed-on: #110
2026-08-21 04:47:16 +00:00
rootiest 616596726d feat(fzf): add preview, dirs, and image support to @@ inline picker
The @@ picker only listed files with no preview. It now lists both
files and directories via fd (matching _fzf_search_directory), and
shows a bat-highlighted or image-rendered preview through
_fzf_preview_file. Image previews use a kitty-graphics-protocol,
chafa, viu, timg fallback chain via the new _fzf_preview_image
helper, benefiting the Ctrl+F directory search and git-status
pickers as well since they share the same preview helper.
2026-08-21 00:46:29 -04:00
rootiest 7466188d35 chore(ci): rename build-docs.yml to ci.yml
The workflow now runs the fish config test suite before building and
publishing docs, so "build-docs" no longer describes its full scope.
2026-08-20 01:15:10 -04:00
rootiest 2608a6cd72 Merge pull request 'test(ci): add fish config test suite and gate docs build on it' (#109) from ci-fish-config-tests into main
Generate documentation / test (push) Successful in 56s
Generate documentation / build-docs (push) Successful in 3m28s
Reviewed-on: #109
2026-08-20 05:04:20 +00:00
rootiest 642c981e4b test(ci): add fish config test suite and gate docs build on it
Adds tests/run-tests.fish (syntax lint over every .fish file, plus a
sandboxed interactive load) and tests/functional.fish (10 checks
covering XDG/PATH/CDPATH setup, key bindings, abbreviations, core
functions, exit rewiring, and the opinionated-component registry).

The sandbox copies config-relevant files into a scratch HOME/XDG tree
rather than symlinking the checkout, since this repo also serves as a
live ~/.config/fish and a symlink would let universal-variable writes
leak into the real fish_variables file.

Wires the suite into build-docs.yml as a `test` job that `build-docs`
now depends on, so a broken config can no longer get published to the
docs site. Documents the workflow in the README's new Testing section.
2026-08-20 01:03:40 -04:00
Gitea Actions 451097d384 chore(docs): regenerate manual, man page, and component registry 2026-08-20 04:44:19 +00:00
rootiest 1ec69309cf Merge pull request 'fix(bindings): trigger fzf inline picker with a lookbehind @ instead of a @@ chord' (#108) from fix-fzf-picker-at-key-delay into main
Generate documentation / build-docs (push) Successful in 3m46s
Reviewed-on: #108
2026-08-20 04:40:38 +00:00
rootiest 27f780e733 docs(manual): use cat as the token-boundary example for @@ 2026-08-20 00:38:22 -04:00
rootiest 9368a4864c fix(bindings): trigger fzf inline picker with a lookbehind @ instead of a @@ chord
Binding the raw @@ chord made a lone @ an ambiguous prefix, so fish
(with fish_sequence_key_delay_ms unset) held every typed @ indefinitely
until a disambiguating keystroke arrived, breaking things like
`ssh user@host`. Binding the single @ key instead and checking whether
the current token is already a bare @ removes the ambiguity entirely:
plain @ always self-inserts instantly, and a second consecutive @
triggers the picker in place, with no forced delimiter before you can
keep typing. Vi's normal/visual modes are left unbound, matching their
existing (no-op) @ behavior.
2026-08-20 00:38:02 -04:00
rootiest 6689e69ece fix(ci): install fish from the official 4.x PPA
Ubuntu 24.04's default repo only carries fish 3.7, but
docs/manual/06-dependency-catalog.md states fish >= 4.0 as a
project dependency. ppa:fish-shell/release-4 is the fish-shell
maintainers' own PPA and explicitly supports 24.04.
2026-08-18 22:03:23 -04:00
Gitea Actions 5f55438581 chore(docs): regenerate manual, man page, and component registry 2026-08-19 01:58:54 +00:00
rootiest 0ef99e4874 fix(verify-manual): use a real temp file instead of /dev/stdin for the registry round-trip test
Generate documentation / build-docs (push) Successful in 3m30s
fish 3.7 (Ubuntu 24.04's packaged version, used in CI) rejects
'source /dev/stdin' when it's backed by a pipe, with
"'/dev/stdin' is not a file" -- fish 4.8 (local dev) accepts it.
Writing the generated registry to a real temp file sources
identically across fish versions.
2026-08-18 21:55:23 -04:00
rootiest df929c5335 fix(ci): install fish in build-docs workflow
Generate documentation / build-docs (push) Failing after 50s
The registry round-trip test spawns a real fish subprocess to verify
conf.d/__fish_config_op_registry.fish sources correctly, but the
runner image has no fish binary, so the workflow failed with
FileNotFoundError.
2026-08-18 21:50:43 -04:00
rootiest 6ef0859e43 perf(logo-svg): further optimized logo SVG image 2026-08-18 21:49:13 -04:00
rootiest 01f4e376ae Merge pull request 'feat(config): add sub-category granularity to opinionated components' (#107) from worktree-opinionated-subcategories into main
Generate documentation / build-docs (push) Failing after 43s
2026-08-19 01:42:25 +00:00
rootiest 31d04a2fe0 chore(ci): reflect registry regeneration in auto-commit message 2026-08-18 21:37:12 -04:00
rootiest 8866236bda fix(config-settings): align Enter continuation line in --help output
The wrapped description for the Enter key was indented one column past
every other description in the Navigation block. Drop the extra leading
space so it lines up at column 17 with the rest.
2026-08-18 18:13:01 -04:00
rootiest f7b207aaf4 fix(verify-manual): move assertion-free check to warn_*, exclude guard infra from false-positive warning
test_c0_tags_never_combine_with_contradiction_unwarned could never fail (its
own docstring said so) yet inflated the test_* pass count. Rename it to
warn_c0_tags_never_combine_with_contradiction, matching the file's existing
warn_* convention, and call it explicitly from main() alongside the other
three warn_* checks.

warn_functions_without_component also permanently false-positived on
functions/__fish_config_op_enabled.fish: the file's own function signature
and EXAMPLE prose contain the literal guard name, tripping the "calls the
guard" substring check against itself. Exclude the guard's own supporting
infrastructure files (__fish_config_op_enabled.fish,
__fish_config_op_cascade.fish, __fish_config_op_registry_lookup.fish) by
name -- an EXAMPLE-section-only exclusion wouldn't have worked here since
the false match is the function's own `function __fish_config_op_enabled`
definition line, not just its EXAMPLE block.
2026-08-18 18:12:57 -04:00
rootiest 1cb6f37b5b fix(registry): quote keys, dedupe tags, and document sourcing order; CI commits regenerated registry
- Quote both keys and values in the generated __fish_config_op_registry.fish
  keys array so a future identity/tag containing a glob-special character
  (*, ?, [) can't be silently dropped by fish's set, desyncing the
  positionally-paired keys/values arrays.
- Deduplicate the tag list per site in build_registry() so an identity
  collision across sources (e.g. auto-pull tagged autoexec/sync in both
  functions/auto-pull.fish and conf.d/auto-pull.fish) no longer produces a
  duplicated tag in the committed registry.
- Add a comment to the generated file's header noting it must sort first
  among conf.d/*.fish guard-callers, since that ordering is currently
  implicit in the filename rather than stated anywhere.
- CI's auto-commit step now also stages conf.d/__fish_config_op_registry.fish
  alongside the docs it already regenerates, so a # COMPONENT header change
  pushed without a manual __fish_config_op_registry_rebuild self-heals
  instead of silently drifting from what CI just regenerated.
2026-08-18 18:12:49 -04:00
rootiest a8468f3402 docs(manual): document sub-categories in customization, reference index, and troubleshooting
Sub-categories were previously undocumented outside README and the
per-category reference pages. Add a paragraph to 07-customization.md (the
canonical opinionated-components page) explaining the sub-category cascade,
a short note to the 08-components-reference index pointing readers at each
category's sub-category list, and a troubleshooting note that
__fish_config_op_<category>_<subcategory> variables follow the same
check/reset pattern as category variables.
2026-08-18 18:12:41 -04:00
rootiest 040c98c0a7 fix(config-settings,docs): address final whole-branch review findings
Bundled fix wave for six findings from the sub-category-granularity
whole-branch review:

- config-settings: reset in_subcat on Tab/Shift-Tab so up/down keep
  routing to cur_row instead of freezing on the Sponge/Paths pages
  after a drill-down
- verify-manual: _parsed_components() now delegates to
  generate_component_registry.collect_components() instead of a
  lossy dict.update() merge, closing a taxonomy-check blind spot on
  identity collisions
- verify-manual: add test_committed_registry_matches_headers to
  catch the committed registry drifting from current # COMPONENT
  headers
- build-docs workflow: trigger on conf.d/** and config.fish edits,
  not just functions/**
- __config_settings_draw_subcat: show (Universal)/(Session) in the
  drill-down title so the persistence scope is visible before toggling
- __config_settings_draw / config-settings: mention the Enter
  sub-category drill-down in the on-screen hint and --help text
- generate_component_registry: send the "wrote ..." progress line to
  stderr so it no longer corrupts --concat's stdout output
2026-08-18 16:46:46 -04:00
rootiest 3c0bba1737 fix(config-settings): fix silent varname resolution failure and stale panel_h in sub-category drill-down
Two bugs invisible to single-frame rendering, only reachable via the
interactive event loop:

- right/l and left/h resolved the sub-category variable name with
  "$toggle_vars[(math ...)]"_(...) inside a quoted string -- fish
  cannot expand a command-substitution index there ("Invalid index
  value"), so the set never ran and varname silently kept the parent
  category variable. Every arrow-key press on a sub-category row
  toggled the parent category instead. Fixed by hoisting the category
  variable into a plain local first, the same technique the down/j
  case already used for its page index.

- __cs_dispatch_draw left panel_h fixed at 16 regardless of what it
  actually drew, but the new sub-category page is n+7 lines (9-13,
  always < 16). Every redraw/cleanup erase computed its height from
  the stale constant, erasing too many rows and corrupting whatever
  was above the panel. Fixed by having the dispatcher record the
  actual printed height into panel_h after every draw, including
  resetting it back to 16 on the value pages even when in_subcat is
  stale from a Tab away without an intervening Escape.
2026-08-18 03:22:19 -04:00
rootiest 489e3cc7de feat(config-settings): render and toggle sub-categories in the drill-down page
Adds __config_settings_draw_subcat.fish, rendering a category's own
toggle plus its sub-category rows, sized dynamically from
__config_settings_subcats instead of the fixed 6-row category layout.
Wires the real per-category row count into config-settings.fish's
up/down handling (replacing Task 17's stub) and adds the
hyphen-to-underscore sub-category variable resolution to the
left/right toggle handlers, mirroring the translation the guard
function already applies.

Two box-drawing bugs surfaced while adapting the design doc's draft to
the real static sub-category data and were fixed rather than
transcribed: the title-dashes offset was off by 5, and several real
sub-category labels/descriptions (e.g. "Notifications", 13 chars;
several descriptions past 30 chars) exceed the narrower width tiers'
fixed field widths, so both fields are now defensively truncated
before padding to keep every row exactly iw+2 wide regardless of
content length.
2026-08-18 03:11:17 -04:00
rootiest 7ad3b90503 feat(config-settings): add sub-category drill-down navigation state 2026-08-18 03:01:22 -04:00
rootiest db21efee0c fix(config): split tricks.fish overrides-tricks into manpager/bang sites
overrides-tricks conflated two distinct C3 concerns under one shared site
(overrides/environment), so __fish_config_op_overrides_key_bindings off
left tricks.fish's bang-bang bindings active while abbr.fish/puffer.fish/
config.fish's equivalent sites correctly disabled -- a half-dismantled
bang-bang system per docs/manual's own atomic-gating claim. Split into
tricks-manpager (overrides/environment, matches PAGER/EDITOR/CDPATH) and
tricks-bang (overrides/key-bindings, matches abbr.fish/puffer.fish/
config.fish's bang-related sites, per
docs/manual/08-components-reference/03-c3-key-and-environment-overrides.md).

Also fixes stale prose in config.fish's top-of-file comment: the guard
signature is now <identity> [<site>], not <category>.
2026-08-18 02:56:35 -04:00
rootiest 207540c093 fix(config): restore cachyos-tricks site to its documented overrides category
The site was mistagged aliases/filesystem in the prior commit. The original
code's own comment above the guarded block ("Surgically overriding the
distro config is opinionated (C3 overrides): skip it entirely when
overrides are disabled") documents this as an overrides decision, and it
gated __fish_config_op_overrides pre-migration. Retag to
overrides/environment (matching tricks.fish's own sibling site for the
equivalent action) so aliases=on/overrides=off keeps skipping
tricks.fish/ls/lt/cleanup/copy, as originally documented -- no silent
user-facing behavior change.
2026-08-18 02:45:32 -04:00
rootiest 6b2d3bf491 refactor(config): migrate multi-category files and config.fish to named sites
Adds multi-site `# COMPONENT` headers and converts every guard call site in
conf.d/tricks.fish, conf.d/abbr.fish, functions/smart_exit.fish,
conf.d/yay-wrapper.fish, conf.d/paru-wrapper.fish, conf.d/wakatime.fish,
conf.d/first_run.fish, and config.fish (9 sites across 3 categories) to the
self-identifying `(status current-function|basename) <site>` calling
convention. Regenerates conf.d/__fish_config_op_registry.fish.
2026-08-18 02:39:47 -04:00
rootiest c3a2e35465 refactor(config): migrate C5 logging call sites to self-identifying guard 2026-08-18 02:22:31 -04:00
rootiest e02919de21 fix(config): translate hyphens to underscores in sub-category guard variable names
__fish_config_op_enabled built the sub-category override variable name by
concatenating the tag's slug verbatim, e.g. __fish_config_op_integrations_window-mgmt.
Fish variable names cannot contain hyphens, so any hyphenated sub-category
slug (14 of the 24 in the taxonomy) silently could never be overridden --
an unset such variable safely fell through via set -q, but attempting to
set it always errored with "invalid variable name", masking the defect
since no prior smoke test exercised an explicit sub-category-level toggle.
The registry tag itself (as authored in # COMPONENT headers and the
taxonomy docs) stays hyphenated; only the derived fish variable name is
translated.
2026-08-17 21:53:48 -04:00
rootiest 516b2ba26c refactor(config): migrate C4 integrations call sites to self-identifying guard 2026-08-17 21:49:45 -04:00
rootiest a981fa7f0a refactor(config): migrate C3 overrides call sites to self-identifying guard 2026-08-17 21:37:35 -04:00
rootiest 760b8e68fd refactor(config): migrate C2 autoexec call sites to self-identifying guard 2026-08-17 21:23:12 -04:00
rootiest 04fd832974 refactor(config): migrate C1 aliases call sites to self-identifying guard 2026-08-17 21:12:56 -04:00
rootiest 77c68558b2 docs(readme): mention sub-category toggles 2026-08-17 21:07:25 -04:00
rootiest 0554a3dddc fix(docs): tighten taxonomy slug regex to exclude the Sub-categories heading 2026-08-17 21:04:06 -04:00
rootiest fbb6b6e740 feat(docs): validate # COMPONENT tags against the sub-category taxonomy 2026-08-17 21:02:11 -04:00
rootiest 2cf61b0590 docs: author the sub-category taxonomy and C0 explanation 2026-08-17 20:55:36 -04:00
rootiest d748289c11 feat(docs): regenerate component registry before building the manual 2026-08-17 20:49:42 -04:00
rootiest d2e07effc1 feat(config): add manual registry-rebuild command 2026-08-17 20:46:55 -04:00
rootiest 9d6923f225 fix(docs): merge, not overwrite, COMPONENT lines on identity collision
collect_components() used dict.update(), which let conf.d silently
overwrite functions/ (or vice versa) when the same bare identity
appears in both, e.g. functions/auto-pull.fish and
conf.d/auto-pull.fish. The runtime guard can only ever look up the
bare status current-function/basename string, so both call sites
genuinely share one identity and their raw COMPONENT lines must be
concatenated, not replaced.
2026-08-17 20:42:04 -04:00
rootiest c0628dbd4c feat(docs): add component registry generator 2026-08-17 20:34:48 -04:00
rootiest 31b47ddfc5 feat(docs): parse # COMPONENT headers in manualtools 2026-08-17 20:29:02 -04:00
rootiest 9642cd69db feat(config): rewrite guard as self-identifying with C0 and site support 2026-08-17 20:23:49 -04:00
rootiest 7fa5ca2568 feat(config): add component registry lookup helper 2026-08-17 20:19:42 -04:00
rootiest 94a4fbe45f feat(config): add sub-category cascade evaluator 2026-08-17 20:15:29 -04:00
rootiest 04a02d089f style(docs-site): size the Gitea header icon up to 1.5rem
Generate documentation / build-docs (push) Successful in 3m26s
At the default 1em (16px) it read as noticeably smaller than the
other header controls, especially next to the logo. Bumped to a fixed
1.5rem — well within the nav bar's existing content height, so the
header itself doesn't grow.
2026-08-17 16:18:02 -04:00
rootiest 3ef83d1747 Merge pull request 'docs(site): use real Gitea logo for header social link' (#106) from feat-starlight-gitea-icon into main
Generate documentation / build-docs (push) Successful in 3m43s
Reviewed-on: #106
2026-08-17 20:05:56 +00:00
rootiest f865ad766d perf(docs-site): minify header logo SVG
logo.svg was 56KB of unminified, high-precision path data with no
embedded raster. Running it through SVGO (default preset, multipass)
cuts it to ~20KB with no visual difference.

perf(docs-site): further minify header logo SVG. Cut to ~8.9KB with no visual difference.
2026-08-17 16:03:05 -04:00
rootiest f32e0d0ce5 feat(docs-site): add starlight-plugin-icons, use real Gitea logo in header
The header social link used Starlight's generic `code-branch` icon.
Wires up starlight-plugin-icons + UnoCSS (Iconify) and overrides
SocialIcons to render `pajamas:gitea` for the Gitea link instead, while
falling back to Starlight's default icon set for anything else. Sidebar
and codeblock icon support are enabled but unused for now.
2026-08-17 15:30:42 -04:00
Gitea Actions 8a937124d3 chore(docs): regenerate manual and man page 2026-08-17 19:12:59 +00:00
rootiest d8feaf7143 Merge pull request 'docs(fisher-plugins): clarify sponge's history-purge security model, link plugin repos' (#105) from docs-sponge-privacy-clarity into main
Generate documentation / build-docs (push) Successful in 3m34s
Reviewed-on: #105
2026-08-17 19:09:27 +00:00
rootiest 22dcd31c05 docs(fisher-plugins): clarify sponge's history-purge security model, link plugin repos
Explains that a matched command is actively deleted from history and
force-saved within about one prompt cycle by default, not deferred to
shell exit, and calls out sponge_purge_only_on_exit as the setting that
would change that (a killed/crashed session never triggers fish_exit).
Also hyperlinks the Fisher-managed and bundled plugin names to their
upstream repos on the Starlight site.
2026-08-17 15:06:19 -04:00
Gitea Actions cbce46b63f chore(docs): regenerate manual and man page 2026-08-14 20:47:11 +00:00
rootiest 8ab69f53ed Merge pull request 'feat(deps): add Optional/Terminal Emulator tiers, fix docker prompt hang and ov install path' (#104) from feat-fish-deps-optional-tiers into main
Generate documentation / build-docs (push) Successful in 3m8s
Reviewed-on: #104
2026-08-14 20:44:10 +00:00
146 changed files with 12390 additions and 1226 deletions
+151
View File
@@ -0,0 +1,151 @@
# The key below is `description`, NOT `about`.
#
# GitHub requires `description` on a YAML issue form and rejects the
# template without it; Gitea wants `about` but explicitly accepts
# `description` as a compatible alias. `description` is therefore the
# only spelling that works on both the canonical repo and the mirror.
# The markdown templates beside this one still use `about`, which is
# correct for their format on both forges.
name: Bug report
description: Something in the config is broken or behaves unexpectedly
labels:
- Kind/Bug
body:
- type: markdown
attributes:
value: |
Thanks for filing a bug.
**Title it as a plain description of the problem**, not as a
conventional-commit subject — `mv clobbers a symlink when the target
exists`, not `fix(mv): ...`. The commit format belongs on the PR that
fixes this; the `Kind/` and `Area/` labels carry type and scope here.
Before filing, please confirm the problem survives a fresh shell
(`exec fish`) — a stale function definition in a long-lived session is
the single most common false alarm.
- type: input
id: fish-version
attributes:
label: fish version
description: Output of `fish --version`. This config targets fish 4.x.
placeholder: fish, version 4.0.2
validations:
required: true
- type: input
id: os
attributes:
label: Operating system
description: Distribution and version, or macOS release.
placeholder: Arch Linux (CachyOS), kernel 6.12.4
validations:
required: true
- type: input
id: terminal
attributes:
label: Terminal emulator
description: >-
Only matters for rendering, key bindings, and color problems. Leave it
blank if the bug has nothing to do with those.
placeholder: kitty 0.42.1
validations:
required: false
- type: dropdown
id: area
attributes:
label: Area
description: >-
Which part of the config is affected? Pick the closest match — a
maintainer translates this into the matching `Area/` label at triage,
since contributors without push access can't set labels themselves.
Choose "Not sure" rather than guessing.
options:
- Not sure
- Functions (functions/)
- Completions (completions/)
- Config and startup (config.fish, conf.d/)
- Docs (docs/manual/, man page, docs site)
- Tests (tests/)
- CI (.github/workflows/)
- Integrations (integrations/)
- Prompt and theme (themes/)
- Opinionated components (C1-C6 toggles)
- Scripts (scripts/)
validations:
required: true
- type: textarea
id: summary
attributes:
label: What's broken
description: One or two sentences. Name the function or file if you know it.
placeholder: >-
`mv` replaces an existing symlink instead of prompting, so the link
target is lost with no confirmation.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: >-
Exact commands, starting from a fresh shell, that someone else can
paste and run. Include any setup needed to reach the broken state.
render: fish
placeholder: |
exec fish
mkdir -p /tmp/repro; cd /tmp/repro
touch real; ln -s real link
mv real link
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What you thought those commands would do.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: >-
What happened instead. Paste the complete output, including any error
text and stack traces — truncated errors are the usual reason a bug
report stalls in Status/Need More Info.
render: text
validations:
required: true
- type: checkboxes
id: preflight
attributes:
label: Pre-flight
options:
- label: I searched the existing issues and this isn't already reported.
required: true
- label: I reproduced this in a fresh shell (`exec fish`), not a long-lived session.
required: true
- label: I ran `fish tests/run-tests.fish` and noted the result below (or in the output above).
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: >-
Anything else worth knowing: a private overlay in
`~/.config/.user-dots/fish/` that may be involved, opinionated
components you've disabled, the last commit where it worked. Never
paste credentials, tokens, or machine-specific paths you'd rather not
publish.
validations:
required: false
+21
View File
@@ -0,0 +1,21 @@
# Read by both Gitea (the canonical repo) and GitHub (the mirror).
#
# The .yml extension is required: Gitea accepts config.yaml or config.yml,
# but GitHub only recognizes config.yml and silently ignores config.yaml.
# Don't "tidy" this back to .yaml — the chooser on the mirror stops working.
#
# Blank issues stay enabled deliberately: the three templates cover bugs,
# features, and docs, and anything else (a chore, a refactor, a question)
# is better served by an empty box than by a template that doesn't fit.
blank_issues_enabled: true
contact_links:
- name: Canonical repository and issue tracker
url: https://git.rootiest.dev/rootiest/fish-config/issues
about: fish-config is developed on Gitea. If you came from the GitHub mirror, please file here instead — the mirror is read-only and issues opened there are easy to miss.
- name: Contributing guide
url: https://git.rootiest.dev/rootiest/fish-config/src/branch/main/CONTRIBUTING.md
about: Branch naming, commit conventions, coding standards, and the label taxonomy.
- name: Customization and personal overrides
url: https://git.rootiest.dev/rootiest/fish-config/src/branch/main/docs/manual/07-customization.md
about: Want to change behavior on just your machine? Use your private overlay — no issue needed.
+80
View File
@@ -0,0 +1,80 @@
---
name: Documentation issue
about: Something in the manual, man page, config-help, or docs site is wrong, missing, or unclear
labels:
- Kind/Documentation
---
<!--
Title this as a plain description of the problem:
config-help shows literal backticks in the customization section
not `docs(help): ...`. See CONTRIBUTING.md § Labels.
Docs in this repo are GENERATED. docs/manual/** plus the doc-header
comments above each function are the single source of truth;
docs/fish-config.md and docs/fish-config.1 are build output and are never
hand-edited. So a fix always lands in the source, not in the page where you
saw the problem — the Location section below asks for both.
Delete these comments as you fill it in.
-->
## Location
<!--
Where you saw it, and where it actually comes from.
- **Where you saw it** — the docs site URL, the `config-help <topic>` you
ran, `man fish-config`, or the README section.
- **Source file** — the docs/manual/** page, or the function whose
doc-header feeds it (e.g. `functions/mv.fish`). If you're not sure which,
say so and leave it to triage rather than guessing.
If the problem appears in one output but not the others — correct on the
site, broken in the pager — say which, since that usually points at the
rendering pass (docs/codespans.py) rather than the source text.
-->
## Problem
<!--
What's wrong. Quote the current text so it can be found and compared.
Common shapes, if it helps you place yours:
- **Wrong** — documents behavior the code doesn't have.
- **Stale** — described a flag or path that has since changed.
- **Missing** — a function, flag, or setting with no entry at all. Note
that a function with no `# CATEGORY` header is omitted from the manual
deliberately, so "missing" may be an intentional opt-out.
- **Unclear** — accurate, but a reader can't act on it. Say what you
expected to learn and what you concluded instead.
- **Renders wrong** — a broken code span, a mangled table, a bad anchor.
-->
## Suggested fix
<!--
Proposed wording or structure, if you have one — a diff-shaped
before/after is ideal, but a rough sketch is welcome too. "I don't know
what it should say, only that this confused me" is a legitimate and useful
report; keep the heading and say that.
Two constraints on any text under docs/manual/, both enforced by
docs/verify-manual.py:
- No backticks inside an indented block.
- No backtick span wrapped across a line break.
Doc-headers in .fish files take no backticks at all — docs/codespans.py
adds code spans when it renders. See CONTRIBUTING.md § Documentation
Pipeline.
-->
## Notes
<!--
Anything else — related issues (`Refs #42`), the commit that introduced the
problem, other pages with the same mistake. Drop this heading if empty.
-->
+111
View File
@@ -0,0 +1,111 @@
---
name: Feature or enhancement request
about: Propose new functionality, or an improvement to something that already exists
labels:
- Kind/Feature
---
<!--
Title this as a plain description of what you want, NOT as a
conventional-commit subject:
A picker for switching themes without editing config.fish
not `feat(theme): add theme picker`. That format belongs on the PR that
implements this; here, the Kind/ and Area/ labels carry type and scope.
See CONTRIBUTING.md § Labels.
This template applies Kind/Feature. If you're proposing an improvement to
something that already exists rather than genuinely new functionality, say
so in the Summary — a maintainer will swap the label to Kind/Enhancement
at triage. Contributors without push access can't set labels directly.
Keep every heading below except Alternatives considered and Notes, which
you can drop if they'd be empty. Delete these comments as you go.
-->
## Summary
<!--
What you want, in one or two sentences. Lead with the capability, not the
implementation — "a way to preview a theme before committing to it" rather
than "add a --preview flag to theme-set".
-->
## Problem
<!--
What's awkward, slow, or impossible today. Be concrete about the situation
that led you here: the sequence of commands you run now, what you have to
remember, or what goes wrong. A proposal is only as good as the problem it
names, and this section is what a reviewer weighs the cost against.
-->
## Proposed behavior
<!--
The concrete shape of the thing. Where they apply:
- The command or function name, and its flags.
- What it prints on success, and what it does on the error paths.
- What happens with no arguments, or with a missing dependency.
- Whether it's interactive, and what it falls back to when it isn't.
A short usage sketch in a ```fish block is worth several paragraphs.
-->
## Alternatives considered
<!--
Other approaches you weighed and why you set them aside — including
"solve it in my own ~/.config/.user-dots/fish/local.fish instead", which is
the right answer for anything genuinely specific to one machine or one
person's taste. See CONTRIBUTING.md § Secrets & Machine-Specific Config.
Drop this heading if there were no real alternatives.
-->
## Scope
<!--
Answer these — they determine how the change has to be built, and getting
them wrong late is expensive:
- Does this shadow a builtin or an existing command?
- Does it run at startup, or bind a key, or set an environment variable?
- Does it need a new external dependency, and what should happen when that
dependency is missing?
- Is it opinionated enough that users should be able to turn it off? If any
of the above is yes, it likely needs a `# COMPONENT` header and an
`__fish_config_op_enabled` guard — see CONTRIBUTING.md § Opinionated
Components.
- Does it need a manual entry (a `# CATEGORY` header), and under which of
the docs/manual/05-functions/ categories?
-->
## Acceptance criteria
<!--
What must be true for this issue to close, as a checkbox list. This is the
issue-side counterpart to a PR's ## Verification: it's the shared
definition of done, agreed before the work starts rather than argued about
after.
- One observable outcome per line — behavior a reader could check, not
implementation steps.
- Cover the error and fallback paths, not just the happy one.
- Include the docs and tests the change will owe.
Leave the boxes unchecked; they get ticked as the work lands.
-->
- [ ]
- [ ]
## Notes
<!--
Anything else: prior art in other shells or dotfiles, links to the relevant
upstream tool's docs, related issues (`Refs #42`). Drop this heading if
there's nothing to add.
-->
+109
View File
@@ -0,0 +1,109 @@
<!--
PR title: Conventional Commits — type(scope): description
e.g. fix(mv): bring header and completions up to repo conventions
Lowercase after the colon, imperative mood, no trailing period.
Append `!` after the scope for a breaking change: feat(logging)!: ...
Types in use: feat, fix, docs, test, chore, perf, refactor, style.
See CONTRIBUTING.md § Commit Conventions.
Still writing code? Prefix the title with `WIP:` —
WIP: feat(media): add play-media fzf picker
Gitea recognizes the prefix, marks the PR a draft, and blocks merging
until you remove it. The prefix goes before the whole conventional
subject, and comes off when the branch is complete.
`WIP:` means MORE CHANGES ARE COMING. It is not for a finished branch
waiting on verification — that is what an unchecked box in Verification
already signals. A complete branch with open checkboxes is a normal PR,
not a WIP.
Keep the two headings below, in this order, and delete these comments.
Optional sections may be added between them (see the note above
"Verification").
-->
## Summary
<!--
What changed and why. A short prose paragraph, 2-5 bullets, or a
paragraph followed by bullets — whichever fits the change.
- Name concrete paths and identifiers in backticks (`functions/mv.fish`,
`$__fish_config_op_autoexec`), not vague descriptions.
- Lead a bullet with a **bold phrase** when it covers a distinct file or
behavior, so the list scans at a glance.
- Explain the reasoning, not just the diff — a reader should understand
why this approach over the obvious alternative.
- Say so explicitly when there is no behavioral change, when a file is
generated by the docs pipeline, or when the change is scoped to the
site build only.
If this PR resolves a tracked issue, close it with a trailing line at the
end of this section — not at the very bottom, since Verification is
always last:
Closes #42
Gitea and GitHub both auto-close the issue on merge. `Fixes #N` and
`Resolves #N` work identically; pick whichever reads correctly. Use one
line per issue (`Closes #42, closes #43` — the keyword must be repeated,
a bare `#43` after a comma is only a link and will NOT close). For an
issue that is related but should stay open, reference it without a
keyword: `Refs #42`. Omit all of this entirely when no issue is involved
— do not leave an empty `Closes #` behind.
-->
<!--
OPTIONAL SECTIONS — add any `##` heading here that the change actually
needs, and skip them entirely for a straightforward one. Used in this
repo, roughly in order of frequency:
## Root cause — for a bug fix, what was actually wrong
## Why — motivation, when it isn't obvious
## How it works — mechanism, for a non-trivial new feature
## Behavior — user-visible behavior before/after
## Changes — a longer breakdown than Summary can carry
## Docs — documentation touched by this change
## Notes — caveats, follow-ups, deferred work
## Scope note — what this deliberately does NOT cover
## Opinionated guard (C1-C6)
— which tier gates this, and behavior when off
## ⚠️ Breaking Change — required whenever the title carries `!`;
state the break and the migration path
A "Why not <alternative>?" heading is also common when a reviewer would
reasonably ask why the obvious approach was rejected.
-->
## Verification
<!--
Every check this change needs, as a checkbox list. Always the last
section.
A CHECKED box means verified — either it ran programmatically (test
suite, linter, docs verifier, CI) or the author performed it by hand and
confirmed the result. Check these off before opening the PR.
An UNCHECKED box is an outstanding manual check the reviewer still has to
perform. Leave anything you could not verify yourself unchecked rather
than dropping it, so it stays visible.
**This list is the merge gate: the PR does not merge until every box is
checked.** So only put things here that can actually be resolved — a
check nobody is able to run blocks the PR forever. Genuinely unverifiable
caveats, assumptions, and known limitations belong in a `## Notes`
section instead, where they inform the review without gating it.
- One check per line, imperative, with the exact command in backticks
and the expected result stated.
- Cover the regression path, not just the happy path: the old behavior
still working, the opinionated toggle disabled, the error branch.
- Include the repo's standing gates when the change touches what they
guard: `fish -n <file>`, `fish_indent`, `python3 docs/verify-manual.py`,
`python3 docs/build-manual.py --site`, `fish tests/run-tests.fish`.
- Reset any universal variable you set during a check.
-->
- [ ]
- [ ]
-82
View File
@@ -1,82 +0,0 @@
name: Generate documentation
on:
push:
branches:
- main
paths:
- "docs/manual/**"
- "docs/build-manual.py"
- "docs/manualtools.py"
- "docs/verify-manual.py"
- "docs/site/**"
- "functions/**"
workflow_dispatch:
jobs:
build-docs:
runs-on: racknerd-mini
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
- name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y pandoc python3-yaml
- name: Generate concatenated markdown
run: python3 docs/build-manual.py --concat -o docs/fish-config.md
# Regeneration MUST run before verification: verify-manual.py's
# test_concat_roundtrips_original compares a freshly-built concat
# against docs/fish-config.md on disk. Before this step ran, that
# file was still the stale pre-push copy, so any ordinary edit under
# docs/manual/** failed the round-trip check before anything was
# regenerated. Do not reorder this back — verification still gates
# pandoc and the auto-commit below, it just no longer requires a
# contributor to hand-sync the generated file before pushing.
- name: Verify manual integrity
run: python3 docs/verify-manual.py
- name: Compile man page
run: |
pandoc --standalone \
--from markdown \
--to man \
docs/fish-config.md \
-o docs/fish-config.1
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Build documentation site
run: |
python3 docs/build-manual.py --site
cd docs/site
npm ci
npx astro build
- name: Deploy to Cloudflare Pages
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
run: |
cd docs/site
npx --yes wrangler pages deploy dist/ \
--project-name=fish-config-docs \
--branch=main \
--commit-dirty=true
- name: Commit generated docs
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea"
git add docs/fish-config.md docs/fish-config.1
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "chore(docs): regenerate manual and man page"
git push
+148
View File
@@ -0,0 +1,148 @@
name: CI
on:
push:
branches:
- main
paths:
- "docs/manual/**"
- "docs/build-manual.py"
- "docs/manualtools.py"
- "docs/verify-manual.py"
- "docs/site/**"
- "functions/**"
- "conf.d/**"
- "config.fish"
- "completions/**"
- "integrations/**"
- "tests/**"
workflow_dispatch:
inputs:
job:
description: "Job to run"
required: false
default: all
type: choice
options:
- all
- test
- build-docs
jobs:
# This workflow file is mirrored to GitHub as-is, but the runner label
# below (racknerd-mini) only exists on the Gitea instance -- on GitHub
# the job just sits queued forever with no matching runner, so the
# mirror never gets a completed status. Gate the real jobs to Gitea and
# let the github-mirror job below stand in on GitHub instead.
test:
if: |
github.server_url != 'https://github.com' &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'test')
runs-on: racknerd-mini
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
- name: Install fish
run: |
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y software-properties-common
sudo add-apt-repository -y ppa:fish-shell/release-4
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish
- name: Run fish config tests
run: fish tests/run-tests.fish
build-docs:
needs: test
if: |
github.server_url != 'https://github.com' &&
always() &&
(github.event.inputs.job == 'build-docs' ||
((github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all') &&
needs.test.result == 'success'))
runs-on: racknerd-mini
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
- name: Install dependencies
run: |
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y software-properties-common
sudo add-apt-repository -y ppa:fish-shell/release-4
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish
- name: Generate concatenated markdown
run: python3 docs/build-manual.py --concat -o docs/fish-config.md
# Regeneration MUST run before verification: verify-manual.py's
# test_concat_roundtrips_original compares a freshly-built concat
# against docs/fish-config.md on disk. Before this step ran, that
# file was still the stale pre-push copy, so any ordinary edit under
# docs/manual/** failed the round-trip check before anything was
# regenerated. Do not reorder this back — verification still gates
# pandoc and the auto-commit below, it just no longer requires a
# contributor to hand-sync the generated file before pushing.
- name: Verify manual integrity
run: python3 docs/verify-manual.py
- name: Compile man page
run: |
pandoc --standalone \
--from markdown \
--to man \
docs/fish-config.md \
-o docs/fish-config.1
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Generate site content
run: python3 docs/build-manual.py --site
- name: Build documentation site
working-directory: docs/site
run: |
npm ci
npx astro build
- name: Deploy to Cloudflare Pages
working-directory: docs/site
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
run: |
npx --yes wrangler pages deploy dist/ \
--project-name=fish-config-docs \
--branch=main \
--commit-dirty=true
- name: Commit generated docs
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea"
git add docs/fish-config.md docs/fish-config.1 conf.d/__fish_config_op_registry.fish
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "chore(docs): regenerate manual, man page, and component registry"
git push
# Stand-in for the GitHub mirror so the commit gets a completed status
# instead of the real jobs above sitting queued forever for a
# self-hosted runner that only exists on the Gitea instance.
github-mirror:
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
steps:
- name: Note that CI runs on Gitea
run: |
echo "This repository mirrors from Gitea (git.rootiest.dev), where CI actually runs."
echo "See the commit's status on the Gitea instance for the real test/build-docs results."
+71
View File
@@ -0,0 +1,71 @@
name: Sync labels to mirror
# Labels do not travel with a mirror push -- mirroring copies files, not
# repository settings -- but they matter on the GitHub side anyway, because
# GitHub reads the same .github/ISSUE_TEMPLATE/ files and silently drops a
# labels: entry naming a label it does not have. Gitea is the source of
# truth; this makes the mirror match.
on:
schedule:
# 06:00 UTC daily. Label churn is rare, so a slower cadence than this
# would leave the mirror wrong for most of a working day after an edit.
- cron: "0 6 * * *"
push:
branches:
- main
paths:
# Exercise the sync as soon as its own logic changes, rather than
# waiting for the next scheduled run to find out it is broken.
- "scripts/sync-labels.py"
- ".github/workflows/sync-labels.yml"
workflow_dispatch:
inputs:
dry_run:
description: "Report the plan without changing anything"
required: false
default: false
type: boolean
jobs:
sync-labels:
# This file is mirrored to GitHub as-is. The runner label below only
# exists on the Gitea instance, so on GitHub the job would sit queued
# forever against a runner that will never pick it up -- the same
# problem the github-mirror stand-in in ci.yml exists to solve. A
# skipped job costs nothing and produces no stuck status.
if: github.server_url != 'https://github.com'
runs-on: racknerd-mini
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
- name: Install Python
run: |
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install \
--no-install-recommends -y python3
# Cheap, offline, and no token needed. Catches a broken diff before
# anything is allowed to mutate labels on the mirror.
- name: Check the diff logic
run: python3 scripts/sync-labels.py --self-test
- name: Sync labels
env:
GH_MIRROR_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
run: |
if [ -z "$GH_MIRROR_TOKEN" ]; then
echo "::error::GH_MIRROR_TOKEN is not set in this repository's Actions secrets."
echo "Create a fine-grained GitHub token scoped to rootiest/fish-config with"
echo "Issues: read and write, plus Pull requests: read, and add it as"
echo "GH_MIRROR_TOKEN under Settings -> Actions -> Secrets."
exit 1
fi
if [ "${{ inputs.dry_run }}" = "true" ]; then
python3 scripts/sync-labels.py --dry-run
else
python3 scripts/sync-labels.py
fi
+570
View File
@@ -0,0 +1,570 @@
# Contributing to fish-config
This is a personal dotfiles repo, but it's run with practices meant to scale
to a team of contributors, not just one person's solo habits. This document
formalizes those practices so they live somewhere durable instead of only in
commit history and conversation memory. It will grow as the project does —
treat it as a living document, not a final word.
## Table of Contents
- [Getting Started](#getting-started)
- [Issues](#issues)
- [Branching & Pull Requests](#branching--pull-requests)
- [Labels](#labels)
- [Commit Conventions](#commit-conventions)
- [Fish Coding Standards](#fish-coding-standards)
- [Opinionated Components](#opinionated-components)
- [Documentation Pipeline](#documentation-pipeline)
- [Testing](#testing)
- [Secrets & Machine-Specific Config](#secrets--machine-specific-config)
- [License](#license)
---
## Getting Started
You'll need [fish 4.x](https://fishshell.com/). Clone the repo and run the
test suite to confirm your environment is sane:
```fish
fish tests/run-tests.fish
```
If you're touching anything under `docs/manual/`, you'll also want `pandoc`,
`python3-yaml`, and Node 24+ to exercise the full doc-build pipeline locally
(see [Documentation Pipeline](#documentation-pipeline)) — otherwise CI will
catch problems on push.
## Issues
Issues live on the Gitea repo. Three templates cover the common cases, each
pre-applying its `Kind/` label; blank issues stay enabled for everything else
— a chore, a refactor, a question, a tracking issue.
| Template | Format | Use it for | Applies |
|---|---|---|---|
| **Bug report** | web form | Something is broken or behaves unexpectedly | `Kind/Bug` |
| **Feature or enhancement request** | markdown | New functionality, or an improvement to what exists | `Kind/Feature` |
| **Documentation issue** | markdown | The manual, man page, `config-help`, or docs site is wrong, missing, or unclear | `Kind/Documentation` |
They live in `.github/ISSUE_TEMPLATE/`, next to the PR template, so the
GitHub mirror offers the same set. The bug report is a Gitea *issue form*
a real web form with required fields — because a bug report missing its
version, reproduction, or full error text can't be acted on, and a form
refuses to submit without them. The other two are markdown templates in the
same comment-guided style as `.github/PULL_REQUEST_TEMPLATE.md`, since what
they ask for is open-ended prose that structure would only get in the way of.
GitHub reads these same files on the mirror, and its schema differs from
Gitea's in two places, so both are pinned to the spelling that works on both
and each file says so in a comment: the chooser config must be `config.yml`
(GitHub ignores `config.yaml`), and `bug.yml` declares `description:` rather
than `about:` (GitHub requires it; Gitea accepts it as an alias). The two
markdown templates keep `about:`, which is correct for their format on both.
### Issue titles
**Issue titles are plain descriptions of the problem, not Conventional
Commits subjects.**
```text
mv clobbers a symlink when the target exists ← yes
fix(mv): prompt before replacing an existing symlink ← no
```
An issue states a problem; a commit states a change. The type and scope that
`fix(mv):` would carry are already on the issue as its `Kind/` and `Area/`
labels, and the conventional subject belongs on the PR that closes it, where
it becomes the commit message. Writing the fix into the title also presumes
one, which is the wrong end to start from for anything still being diagnosed.
### What an issue owes
- **A bug** needs a reproduction someone else can paste and run, starting
from a fresh shell, plus the complete error output. A stale function
definition in a long-lived session is the most common false alarm, so
confirm it survives `exec fish` first. `Status/Need More Info` is where
reports without a reproduction end up.
- **A feature** needs `## Acceptance criteria` — the checkbox list of what
must be true for the issue to close. It is the issue-side counterpart to a
PR's `## Verification`: a definition of done agreed before the work starts
rather than argued about after, and the PR's checks usually grow out of it.
- **A docs issue** needs to name the `docs/manual/**` source, not just the
page where the problem showed up. `docs/fish-config.md` and
`docs/fish-config.1` are generated, and a fix applied there is overwritten
by the next CI run — see [Documentation
Pipeline](#documentation-pipeline).
### Triage
Reporters aren't expected to label anything. Contributors without push access
can't, and the templates apply the `Kind/` label by themselves; the rest is
the maintainer's job when the issue is triaged — add the `Area/` label (the
bug form's **Area** dropdown is how a reporter tells you, since no forge can
map a form field to a label), set a `Priority/` if it isn't ordinary, and
apply `Reviewed/Confirmed` once a bug actually reproduces. See
[Labels](#labels).
When a PR resolves an issue it closes it with a trailing `Closes #N` line —
see [Pull request descriptions](#pull-request-descriptions).
## Branching & Pull Requests
**If you don't have push access to this repo**, fork it and open your PR
from a branch on your fork back to `main` here — everything below about
branch naming and commit hygiene still applies, it just happens on your
fork instead of a branch of this repo directly. The rest of this section
assumes you *do* have push access (maintainers, regular contributors).
- **Branch off `main` before starting work.** Don't accumulate uncommitted
changes directly on `main`. (If you already started editing before
branching, that's fine — create the branch now, before your first commit;
branching doesn't touch the working tree.)
- **Merge target is `main`, via PR.** Contributors open the PR; the repo
owner merges it. Don't merge your own PR.
- **Label every PR.** At minimum one `Kind/` and one `Area/`, same as an
issue — see [Labels](#labels). If you can't set labels, say what the
change is in the description and a maintainer applies them.
- **Don't merge until the `## Verification` checklist is fully checked.**
Unchecked boxes are outstanding manual checks, not decoration. See
[Pull request descriptions](#pull-request-descriptions) below.
- **Prefix in-progress PRs with `WIP:`.** If the branch still has code
changes coming, open it as `WIP: type(scope): description`. Gitea
recognizes the prefix, flags the PR as a draft, and refuses to merge it
until the prefix is removed; drop it once the branch is complete.
`WIP:` signals **more changes are coming** — not "done but unverified".
A finished branch waiting on manual checks is an ordinary PR whose
`## Verification` boxes aren't all ticked yet; that's already the merge
gate above and doesn't need the prefix. The two are independent: a PR
can be WIP with everything ticked, or complete with checks outstanding.
- **Keep feature branches focused.** If you stumble onto something unrelated
to your current task while working (a pre-existing bug, a stray cleanup),
don't fold it into the same commit or PR. Handle it with one of these,
in order of preference:
1. **Separate branch, separate PR, merged to `main` independently.** The
default for anything that doesn't overlap the code your current branch
touches. The two PRs review and merge independently, in either order.
2. **Separate branch and PR, then sync `main` back into your feature
branch** once it merges. Use this only when the unrelated fix actually
touches the same file/function as your feature branch, or your feature
branch depends on the corrected behavior to work or test correctly.
3. **Commit directly to `main`.** Reserved for changes too small to
justify a branch, or genuinely urgent fixes. **Always ask for explicit
approval before doing this** — there's no standing exception, no matter
how trivial the change looks.
### Pull request descriptions
Fill in `.github/PULL_REQUEST_TEMPLATE.md` — Gitea pre-loads it into the
description box when you open a PR. Every PR carries, in this order:
- **`## Summary`** — what changed and why, as a short paragraph, 2-5
bullets, or both. Name concrete paths and identifiers in backticks, and
explain the reasoning rather than restating the diff.
- **Optional `##` sections** — add what the change actually needs
(`Root cause`, `Why`, `How it works`, `Behavior`, `Docs`, `Notes`,
`Scope note`, `Opinionated guard (C1-C6)`), and skip them entirely for a
straightforward change. A breaking change (title ending in `!` before
the colon) must include `## ⚠️ Breaking Change` with the migration path.
- **`## Verification`** — always last. Every check this change needs, as a
checkbox list, each with the exact command and its expected result.
A **checked** box means verified, whether programmatically (test suite,
linter, docs verifier, CI) or by hand; check those off before opening the
PR. An **unchecked** box is an outstanding manual check the reviewer
still has to perform — leave anything you couldn't verify yourself
unchecked rather than dropping it.
**This list is the merge gate: a PR isn't merged until every box is
checked.** Only list checks that can actually be resolved — one nobody
can run blocks the PR indefinitely. Put genuinely unverifiable caveats,
assumptions, and known limitations in `## Notes` instead, where they
inform the review without gating it.
When a PR resolves a tracked issue, close it with a trailing `Closes #42`
line at the end of `## Summary` — not at the very bottom of the body, since
`## Verification` is always last. `Fixes #N` and `Resolves #N` behave
identically. Repeat the keyword for each issue (`Closes #42, closes #43`); a
bare `#43` is only a link and won't close anything. To point at a related
issue that should stay open, drop the keyword and use `Refs #42`. Leave the
line out entirely when no issue is involved.
## Labels
**Every issue and every pull request carries exactly one `Kind/` label and at
least one `Area/` label.** Everything else is optional, and most of it is
applied by a maintainer at triage rather than by whoever opened the thing.
Labels are scoped: the `Group/Name` form renders as a two-tone chip in Gitea,
and for the three *exclusive* groups below Gitea enforces one-at-a-time by
swapping the old label out when you apply a new one.
### `Kind/` — what this is
Required, and by convention exactly one. Gitea doesn't enforce one-of here,
so pick the dominant character of the change instead of stacking two.
| Label | For |
|---|---|
| `Kind/Bug` | Something is not working |
| `Kind/Feature` | New functionality |
| `Kind/Enhancement` | Improves functionality that already exists |
| `Kind/Documentation` | Documentation changes |
| `Kind/Testing` | The test suite itself |
| `Kind/Refactor` | Restructures code without changing behavior |
| `Kind/Chore` | Tooling, dependencies, housekeeping |
| `Kind/Performance` | Makes existing behavior faster or lighter |
| `Kind/Security` | A security issue |
These deliberately mirror the Conventional Commits types in [Commit
Conventions](#commit-conventions), so a PR's label and its title agree:
`fix``Kind/Bug`, `feat``Kind/Feature` or `Kind/Enhancement`, `docs`
`Kind/Documentation`, `test``Kind/Testing`, `refactor`
`Kind/Refactor`, `chore``Kind/Chore`, `perf``Kind/Performance`.
### `Area/` — what it touches
Required, and non-exclusive on purpose: a change that adds a function, its
completions, and a manual entry gets all three.
| Label | Covers |
|---|---|
| `Area/Functions` | `functions/` |
| `Area/Completions` | `completions/` |
| `Area/Config` | `config.fish`, `conf.d/` — startup and environment |
| `Area/Docs` | `docs/manual/` and the generated manual, man page, and site |
| `Area/Tests` | `tests/` |
| `Area/CI` | `.github/workflows/` and repository automation |
| `Area/Integrations` | `integrations/` |
| `Area/Prompt & Theme` | `themes/` and prompt appearance |
| `Area/Components` | The opinionated-component system (C1-C6) |
| `Area/Scripts` | `scripts/` |
`Area/` is what makes the tracker searchable: it answers "what's still
outstanding in the docs pipeline?" in a way `Kind/` never can. Two edges
worth naming — `Area/Docs` covers the documentation *and its pipeline*, so
`README.md` and this file count even though they sit outside `docs/`; and
`Area/Components` is for the C1-C6 machinery itself, not for every function
that happens to carry a `# COMPONENT` header.
### `Compat/Breaking`
Applied to **any PR whose title carries `!` before the colon**, and to any
issue proposing a change that would. It travels with the `## ⚠️ Breaking
Change` section that such a PR must already include — see [Pull request
descriptions](#pull-request-descriptions).
### `Priority/` — exclusive, maintainer-applied
`Priority/Critical`, `Priority/High`, `Priority/Medium`, `Priority/Low`.
**No priority label means ordinary priority.** Labeling everything defeats
the point, so leave it off unless the item is genuinely more or less urgent
than the rest of the queue.
### `Reviewed/` — exclusive, maintainer-applied
`Reviewed/Confirmed` goes on a bug that has actually been reproduced —
that's the signal separating a report from a known defect.
`Reviewed/Duplicate`, `Reviewed/Invalid`, and `Reviewed/Won't Fix` accompany
closing an issue, always with a comment saying why; a close with only a
label on it is not an explanation.
### `Status/` — exclusive, maintainer-applied
`Status/Blocked`, `Status/Need More Info`, `Status/Abandoned`. These describe
the item's current state, so remove one as soon as it stops being true — a
stale `Status/Need More Info` on an issue that got its answer is worse than
no label, because it reads as still waiting.
### `good first issue` and `help wanted`
Invitations to contributors, applied by a maintainer. Both are deliberately
**unscoped**: they'd be a natural fit under `Status/`, but that group is
exclusive, and an issue is quite often both blocked on something *and* open
for someone to pick up. Keeping them outside the group lets them coexist
with a real status.
Use `good first issue` for work that is genuinely self-contained — a clear
acceptance criterion, one or two files, no need to understand the
opinionated-component system first.
### The GitHub mirror
The repo is mirrored to
[github.com/rootiest/fish-config](https://github.com/rootiest/fish-config),
and **the mirror carries the same labels, by the same names**. That isn't
cosmetic: GitHub reads the same `.github/ISSUE_TEMPLATE/` files, and a
`labels:` entry naming a label that doesn't exist on that side is silently
dropped rather than reported.
Mirroring copies files, not repository settings, so labels don't travel with
a push. **`.github/workflows/sync-labels.yml` closes that gap**: it runs
`scripts/sync-labels.py` on a daily schedule, and again whenever the script
itself changes, to make GitHub match Gitea. Manage labels here, in the Gitea
UI, and the mirror catches up within a day — or dispatch the workflow by
hand for it to happen now. Nothing needs doing on the GitHub side.
The sync creates what's missing and corrects color or description drift,
and it deletes an extra label on the mirror **only when no issue or PR there
carries it**; one that's in use is reported with its count and left for a
human to decide about. Run the script with `--dry-run` to see the plan
without changing anything, or `--self-test` to check its diff logic offline
— both are useful before editing it. Because labels are matched by name,
renaming one on Gitea reads as a delete plus a create: the new name appears
on the mirror, and the old one is pruned only if it's unused.
The workflow needs a GitHub token in this repo's Actions secrets as
`GH_MIRROR_TOKEN`, scoped to the mirror with **Issues: read and write**
(GitHub files labels under Issues) and **Pull requests: read** (so the
in-use check sees labels on PRs). The job fails with an explicit message if
it's missing rather than quietly doing nothing.
One behavioral difference to keep in mind: **GitHub has no exclusive
labels.** Gitea enforces one-at-a-time on `Priority/`, `Reviewed/`, and
`Status/` by swapping the old label out; on the mirror those are ordinary
labels and nothing stops two of a group coexisting, so there the one-of rule
holds by convention alone.
Issues and pull requests belong on the canonical Gitea repo — the template
chooser links there first, on both sides. The mirror's tracker stays open so
that a report which lands there anyway isn't lost, not because it's a second
supported front door.
## Commit Conventions
Commit subjects follow [Conventional Commits](https://www.conventionalcommits.org/):
```
type(scope): description
```
Types currently in use in this repo: `feat`, `fix`, `docs`, `test`, `chore`,
`perf`. The scope is usually the function, component, or subsystem touched
(e.g. `feat(help): ...`, `fix(scrub): ...`, `chore(docs): ...`). Look at
`git log` for recent examples before picking a type/scope for something
novel.
## Fish Coding Standards
### File header
Every hand-authored `.fish` file starts with:
```fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
```
(A few completion scripts adapted directly from a tool's own upstream
completions keep that tool's original attribution comment instead of this
header — match whichever convention the specific file already follows. New
files use the standard header above.)
### Public function documentation header
Every user-facing function gets a machine-parsed comment header directly
above its `function` line. This header is the single source of truth for
the generated manual (`docs/fish-config.md` / the man page / the docs
site) — see [Documentation Pipeline](#documentation-pipeline) for how it
gets consumed. The parser (`docs/manualtools.py`) recognizes these labels,
all optional except where noted:
| Label | Purpose |
|---|---|
| `CATEGORY` | **Required to appear in the manual at all** — see below. |
| `COMPONENT` | Only for functions gated by the [opinionated-component system](#opinionated-components). |
| `DEPENDENCIES` | Other functions this one calls that a reader may want to look up. |
| `SYNOPSIS` | One-line usage form. |
| `DESCRIPTION` | Prose description; can span multiple paragraphs. |
| `ARGUMENTS` | Flags/positional args, one per line. |
| `EXIT STATUS` | Exit codes and what they mean. |
| `RETURNS` | For functions used for their output/return value rather than exit status. |
| `EXAMPLE` | One or more realistic invocations. |
| `NOTES` | Anything else worth flagging (fallback behavior, caveats, gotchas). |
A full example (`functions/claude.fish`):
```fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CATEGORY
# 12-ai-and-developer-tools
#
# COMPONENT
# aliases/dev-tools
#
# DEPENDENCIES
# agents-init
#
# SYNOPSIS
# claude [ARGS...]
#
# DESCRIPTION
# Wrapper for the claude CLI that ensures the AGENTS/ sub-repository is
# initialized and any agent-made changes are committed before launch.
# ...
```
**`CATEGORY` is the opt-in gate for the manual:** a function with no
`# CATEGORY` line produces no manual entry at all — this is how bundled
plugin internals and prompt guts stay out of user-facing docs without an
exclusion list. `CATEGORY` must exactly match one of the existing
`docs/manual/05-functions/NN-*.md` stubs:
```
01-file-and-directory 08-terminal-management
02-navigation 09-clipboard
03-editors-and-viewers 10-network
04-git-and-version-control 11-pager-and-logging
05-package-management 12-ai-and-developer-tools
06-dependency-management 13-media-and-utilities
07-system-and-monitoring 14-miscellaneous
```
If your function genuinely doesn't fit any of these, add a new
`docs/manual/05-functions/NN-your-category.md` stub (with frontmatter
matching its siblings) rather than force-fitting it into an existing one.
### Private/internal helper functions
Functions named with a leading `_` (e.g. `_agents_init_ensure_gitignore`,
`__fish_config_op_enabled`) are excluded from the manual unconditionally,
regardless of whether they carry a `CATEGORY` line — so they generally don't
have one. They should still carry the standard file header, and a lighter
`SYNOPSIS`/`DESCRIPTION` comment is encouraged wherever the function's
purpose or calling convention isn't obvious from its body, matching the
convention already used across `__fish_config_op_*.fish` and similar files.
### Function declaration
Give every function a `--description`, since it's what shows up in `fish -c
'functions'`/completions and other introspection:
```fish
function my_function --description 'Short, imperative description'
```
### Colored `--help` output
Every function with a `-h`/`--help` flag uses this standardized color
palette (established across the codebase 2026-08-22):
```fish
set -l c_head (set_color --bold cyan) # section headers
set -l c_cmd (set_color --bold) # the command name itself (theme-adaptive)
set -l c_flag (set_color yellow) # flags
set -l c_arg (set_color cyan) # required-argument placeholders
set -l c_dim (set_color brblack) # optional-argument placeholders
set -l c_ok (set_color green) # success/positive status
set -l c_warn (set_color yellow) # warnings
set -l c_err (set_color red) # errors
set -l c_reset (set_color normal)
```
Not every function needs every variable — pull in only the ones your help
text actually uses. Structure the usage block as `Usage:`, then sections
for arguments/flags/examples as needed; see `functions/rand_string.fish` or
any recently-touched function for a full worked example.
## Opinionated Components
Some functionality in this config is classified into one of six toggleable
categories (C1C6: aliases, autoexec, overrides, integrations, logging,
greeting) plus sub-categories, so users can disable pieces of it via
`config-settings` or universal variables. If you're adding something that
shadows a builtin, runs at startup, overrides a key binding/environment
variable, or otherwise falls under an existing category, tag it with a
`# COMPONENT` header (`<category>/<subcategory>`, e.g.
`overrides/key-bindings`) and guard it with `__fish_config_op_enabled`. See
the [README's Minimal Mode section](README.md#minimal-mode) for the full
category list and toggle semantics, and
`AGENTS/specs/2026-08-17-opinionated-component-subcategories-design.md` for
the underlying design. Most new functions are *not* opinionated components
— only tag something if it fits an existing category; this isn't something
to force.
## Documentation Pipeline
`docs/manual/` is the single source of truth for the user manual, man page,
and documentation site. **Never hand-edit `docs/fish-config.md` or
`docs/fish-config.1`** — they're generated by `docs/build-manual.py` from
`docs/manual/**` plus every function's doc-header (see above), verified by
`docs/verify-manual.py`, and auto-committed by CI on push to `main`
(`chore(docs): regenerate manual, man page, and component registry`).
If you're changing something under `docs/manual/` directly, or adding a
function whose `CATEGORY`/`COMPONENT` header should surface new content, you
can build and check it locally before pushing:
```fish
python3 docs/build-manual.py --concat -o docs/fish-config.md
python3 docs/verify-manual.py
```
CI runs the same verification and regenerates the site/man page — a broken
manual won't get published, but running it locally saves a round trip.
Write doc-headers as plain text — no backticks. `-a/--all`,
`__fish_config_op_aliases` and `~/.config/fish/config.fish` are typed
bare, because the header is also read as-is by `config-help` and by
anyone opening the file. `docs/codespans.py` adds the inline code spans
when it renders, so the SSOT never carries them; see
`docs/site/README.md` for which shapes it recognises. That pass runs for
every output — the site, the man page and `config-help` — so a token is
typeset the same way wherever it is read.
Two rules apply to backticks you write under `docs/manual/` as well:
- **Never inside an indented block.** A four-space block is verbatim in
every renderer, so a backtick there is a literal character on the page
rather than markup.
- **Never wrapped across a line break.** Markdown accepts a span split
over two lines, but `config-help` pairs backticks one line at a time
and would show the halves literally. Reflow the sentence instead.
`docs/verify-manual.py` enforces both.
## Testing
```fish
fish tests/run-tests.fish
```
Runs before every push and gates the documentation build in CI. Two phases:
1. **Syntax lint** — every tracked `.fish` file (`config.fish`, `functions/`,
`conf.d/`, `completions/`, `integrations/`) is checked with `fish -n`.
2. **Sandboxed functional checks** — the config is copied into a throwaway
`HOME`/`XDG_CONFIG_HOME` sandbox (never this checkout itself, since it
doubles as a real `~/.config/fish`) and loaded as an isolated interactive
session. `tests/functional.fish` then runs against foundational behavior:
XDG/PATH/CDPATH setup, key bindings, abbreviations, core functions, the
opinionated-component registry, and more.
To add a new functional check, add a `test_*` function to
`tests/functional.fish` — it's picked up automatically by
`functional_test_main`, no registration needed. Return 0 on pass, non-zero
on fail; print a short diagnostic on failure.
## Secrets & Machine-Specific Config
**Nothing containing credentials, tokens, personal identifiers, or a
specific machine's paths belongs in this repo — not even in a PR.** That
kind of thing lives in each user's own private overlay
(`~/.config/.user-dots/fish/secrets.fish` and `local.fish`), which is
git-ignored by design. If you're writing something that needs a secret or a
machine-specific path, source it from there rather than hardcoding it.
Everything else — general-purpose, reusable across machines — belongs in
the tracked repo as normal. See [README's Personalization
section](README.md#personalization) and
[`docs/manual/07-customization.md`](docs/manual/07-customization.md) for the
full mechanics.
## License
This project is licensed under AGPL-3.0-or-later (see `LICENSE`). Every new
hand-authored file needs the SPDX header shown in [File
header](#file-header) above.
+42 -3
View File
@@ -15,6 +15,8 @@ abbreviation system for keyboard-driven workflows.
- [Installation](#installation)
- [Personalization](#personalization)
- [Minimal Mode](#minimal-mode)
- [Testing](#testing)
- [Contributing](#contributing)
- [Attribution](#attribution)
- [License](#license)
@@ -144,9 +146,12 @@ Contributing to the docs? There are two sources, split by content type:
above each function in `functions/*.fish`. Edit the function; the entry
and its site page are generated from the header.
- **Everything else** lives under `docs/manual/**`.
- **Testing, Contributing, Attribution, and License** are pulled straight
from this README (the sections below) rather than authored twice — edit
them here and the manual, man page, and site all pick up the change.
Never edit the generated `docs/fish-config.md` — it's rebuilt from both
sources and any hand-edits are discarded.
Never edit the generated `docs/fish-config.md` — it's rebuilt from all
three sources and any hand-edits are discarded.
To browse the docs from the terminal:
@@ -291,7 +296,7 @@ If you'd rather set them by hand, each category is controlled by a universal var
|---|---|
| `__fish_config_op_aliases` | Command shadows: `ls`→eza, `cat`→bat, `cd`→zoxide, `rm`→trash, `top`→btop, `edit`→multi-editor launcher, and friends; `grep`/`cp`/`mv`/`wget` flag injection |
| `__fish_config_op_autoexec` | Startup side-effects: Fisher bootstrap, theme apply, `paru`/`yay` wrapper generation, auto venv activation, WakaTime hook, auto-pull background fast-forward |
| `__fish_config_op_overrides` | Vi mode, `exit``smart_exit`, `$PAGER`/`$MANPAGER`/`$CDPATH`/`XDG`/`PATH`, bang-bang history expansion, autopair, puffer, Starship prompt, theme colors |
| `__fish_config_op_overrides` | Vi mode, `exit``smart_exit`, `$PAGER`/`$MANPAGER`/`$CDPATH`/`XDG`/`PATH`, `$DO_NOT_TRACK`/`$DISABLE_TELEMETRY`, bang-bang history expansion, autopair, puffer, Starship prompt, theme colors |
| `__fish_config_op_integrations` | Kitty/WezTerm window abbreviations, `done` notifications, `spwin`/`tab`/`split`, `hist`, `logs`, `upgrade`, WakaTime |
| `__fish_config_op_logging` | **Opt-in — off unless explicitly enabled.** Scrollback capture on exit, tmux `pipe-pane` pane logging, zellij `dump-screen` capture on exit, `paru`/`yay` AUR log wrappers, Kitty watcher capture (sentinel-file coordinated) |
| `__fish_config_op_greeting` | Per-session `fish_greeting` (suppresses distro greetings such as CachyOS fastfetch by overriding with an empty function); first-run welcome banner |
@@ -320,6 +325,40 @@ set -Ue __fish_config_op_greeting
Command shadows react immediately; bindings, prompt, and abbreviations take effect in new shells. With aliases disabled, `rm` deletes permanently again instead of trashing. See `help config opinionated` for the full component list.
Each category further sub-divides into two to six sub-categories with
their own `__fish_config_op_<category>_<subcategory>` toggles (e.g.
`__fish_config_op_aliases_filesystem`), following the exact same
truthy/falsy/unset cascade one level deeper. Run `config-settings` and
press Enter on a category row to browse and toggle its sub-categories, or
see the [Components Reference](https://fish.rootiest.fyi/08-components-reference/)
for the full sub-category list per category.
---
## Testing
```fish
fish tests/run-tests.fish
```
Runs before every push (and gates the [documentation build](.github/workflows/ci.yml) in CI, so a broken config can't get published): syntax-lints every `.fish` file, then loads the config in an isolated `HOME`/XDG sandbox — never this checkout itself, since it doubles as a real `~/.config/fish` — and runs functional checks against foundational behavior (XDG/PATH/CDPATH setup, key bindings, abbreviations, core functions, the opinionated-component registry, and more).
---
## Contributing
Interested in contributing? See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the
branching/PR workflow, commit conventions, fish coding standards, and the
docs/testing pipeline this repo follows.
**Preferred forge:** [git.rootiest.dev/rootiest/fish-config](https://git.rootiest.dev/rootiest/fish-config)
is the base repository. [github.com/rootiest/fish-config](https://github.com/rootiest/fish-config)
is a push-mirror of it — identical content, but one-way and read-only from a
contributor's perspective. Branches, forks, and merges made on the GitHub
side aren't fed back upstream, so they risk being silently overwritten by
the next mirror push. Until two-way sync exists, please fork, branch, and
open issues/PRs from the Gitea repository rather than the GitHub mirror.
---
## Attribution
+21
View File
@@ -0,0 +1,21 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Completions for agents-vault.
complete -c agents-vault -f
complete -c agents-vault -s h -l help -d 'Show help message'
complete -c agents-vault -l link -d "Ensure this project's memory link only"
complete -c agents-vault -l push -d 'Commit and push to the vault remote'
complete -c agents-vault -l restore -d 'Relink everything possible, report the rest'
complete -c agents-vault -l status -d 'Show entries, link health, remote, orphans'
# --adopt takes an existing vault slug, so offer the entries that are
# actually there; the vault may not exist yet, in which case this is empty.
# -A because a dot-led slug is legitimate (a relative-path remote keys as
# ..-mirror), and an entry that cannot be completed reads as one that is
# not there.
complete -c agents-vault -l adopt -r -a '(command ls -1A (_agents_vault_dir)/projects 2>/dev/null)' -d 'Bind this project to an existing vault entry'
complete -c agents-vault -l remote -r -d 'Set the vault remote URL'
complete -c agents-vault -s v -l verbose -d 'Print all per-step output (default)'
complete -c agents-vault -s q -l quiet -d 'Print one summary line only if changed'
complete -c agents-vault -s s -l silent -d 'Suppress all output; errors only'
+2
View File
@@ -0,0 +1,2 @@
complete --command play-media --short p --long player --description "Force a specific media player" --require-parameter
complete --command play-media --short h --long help --description "Print this help message"
+148
View File
@@ -0,0 +1,148 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# GENERATED FILE --- do not edit by hand.
# Regenerate with __fish_config_op_registry_rebuild after editing a
# # COMPONENT header, or automatically via docs/build-manual.py.
# Source: docs/generate_component_registry.py
#
# This file must be sourced before any other conf.d/*.fish file that
# calls the opinionated guard. That currently holds only because fish's
# glob-based conf.d loading happens to sort this filename first
# alphabetically among the guard-calling files -- do not rename it
# without preserving that ordering.
set -g __fish_config_op_registry_keys \
"__auto_source_fallback_venv:" \
"__fish_config_sync_logging:" \
"__fish_user_dots_link:" \
"_zellij_dump_log:" \
"abbr:abbr-integrations" \
"abbr:abbr-overrides" \
"agy:" \
"auto-pull:" \
"autopair:" \
"bash:" \
"bash_expands:" \
"cat:" \
"claude:" \
"config:cachyos-strip-aliases" \
"config:cachyos-strip-overrides" \
"config:cachyos-tricks" \
"config:cdpath" \
"config:exit-wiring" \
"config:greeting-stamp" \
"config:pager-editor-gpg" \
"config:path-setup" \
"config:privacy" \
"config:vi-mode" \
"done:" \
"du:" \
"edit:" \
"first_run:first-run-bootstrap" \
"first_run:first-run-greeting" \
"fish_right_prompt:" \
"help:" \
"hist:" \
"key_bindings:" \
"kitty-logging:" \
"kitty-watcher-reminder:" \
"less:" \
"logs:" \
"ls:" \
"mkdir:" \
"mv:" \
"paru-wrapper:paru-autoexec" \
"paru-wrapper:paru-logging" \
"ping:" \
"puffer:" \
"rg:" \
"rm:" \
"smart_exit:exit-plain" \
"smart_exit:logging-guard" \
"split:" \
"spwin:" \
"ssh:" \
"starship:" \
"tab:" \
"theme:" \
"tmux-logging:" \
"top:" \
"tricks:aliases-tricks" \
"tricks:tricks-bang" \
"tricks:tricks-manpager" \
"upgrade:" \
"wakatime:wakatime-autoexec" \
"wakatime:wakatime-hook" \
"yay-wrapper:yay-autoexec" \
"yay-wrapper:yay-logging" \
"yt-dlp:" \
"zoxide:"
set -g __fish_config_op_registry_values \
"autoexec/venv" \
"logging/terminal-capture" \
"autoexec/sync" \
"logging/multiplexer-capture" \
"integrations/terminal-abbrs" \
"overrides/key-bindings" \
"aliases/dev-tools" \
"autoexec/sync" \
"overrides/key-bindings" \
"aliases/shell-tools" \
"overrides/key-bindings" \
"aliases/filesystem" \
"aliases/dev-tools" \
"aliases/filesystem" \
"overrides/key-bindings" \
"overrides/environment" \
"overrides/environment" \
"overrides/key-bindings" \
"greeting/greeting-message" \
"overrides/environment" \
"overrides/environment" \
"overrides/privacy" \
"overrides/key-bindings" \
"integrations/notifications" \
"aliases/filesystem" \
"aliases/dev-tools" \
"autoexec/plugin-management" \
"greeting/first-run" \
"overrides/prompt" \
"aliases/shell-tools" \
"integrations/history-logs" \
"overrides/key-bindings" \
"logging/terminal-capture" \
"logging/terminal-capture" \
"aliases/shell-tools" \
"integrations/history-logs" \
"aliases/filesystem" \
"aliases/filesystem" \
"aliases/filesystem" \
"autoexec/pkg-wrappers" \
"logging/pkg-logs" \
"aliases/network" \
"overrides/key-bindings" \
"aliases/search" \
"aliases/filesystem" \
"overrides/key-bindings" \
"logging/terminal-capture" \
"integrations/window-mgmt" \
"integrations/window-mgmt" \
"aliases/network" \
"overrides/prompt" \
"integrations/window-mgmt" \
"overrides/prompt" \
"logging/multiplexer-capture" \
"aliases/monitor" \
"aliases/filesystem" \
"overrides/key-bindings" \
"overrides/environment" \
"integrations/pkg-upgrade" \
"autoexec/telemetry" \
"integrations/notifications" \
"autoexec/pkg-wrappers" \
"logging/pkg-logs" \
"aliases/network" \
"aliases/filesystem"
+7 -3
View File
@@ -8,6 +8,10 @@
# This file contains all the abbreviations for the terminal.
# It is sourced by Fish on startup.
# COMPONENT
# site abbr-integrations: integrations/terminal-abbrs
# site abbr-overrides: overrides/key-bindings
# Neovim
# @category Editors
# @desc nvim
@@ -155,7 +159,7 @@ abbr -a ag. agy .
# @desc exit
abbr -a /exit exit
# Window-management abbreviations are opinionated (C4 integrations)
if __fish_config_op_enabled __fish_config_op_integrations
if __fish_config_op_enabled (status basename) abbr-integrations
if test "$TERM" = xterm-kitty
# @category Terminal Windows, Tabs, and Panes
# @desc Close current pane/window
@@ -208,7 +212,7 @@ abbr -a speedtest-fast fast-cli
# Kitty/WezTerm window-management abbreviations are opinionated (C4
# integrations): they assume an active Kitty or WezTerm session.
if __fish_config_op_enabled __fish_config_op_integrations
if __fish_config_op_enabled (status basename) abbr-integrations
# Window Creation (OS Windows)
if test "$TERM" = xterm-kitty
# @category Terminal Windows, Tabs, and Panes
@@ -666,7 +670,7 @@ abbr -a url-open open-url
### History Expansions and Substitutions ###
# Bash-style history expansion is opinionated (C3 overrides), gated atomically
# with conf.d/tricks.fish, conf.d/puffer.fish, and functions/expand_*.fish.
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) abbr-overrides
# @category History Expansion
# @name !^
# @desc Expand to the first argument of the previous command
+4 -1
View File
@@ -14,8 +14,11 @@
# Manage the registry with: auto-pull add / remove / list / status
# C2 guard: when auto-execution is disabled, do not register the handler.
__fish_config_op_enabled __fish_config_op_autoexec; or exit
__fish_config_op_enabled (status basename); or exit
# COMPONENT
# autoexec/sync
#
# SYNOPSIS
# __auto_pull_on_pwd (event handler, --on-variable PWD)
#
+4 -1
View File
@@ -1,8 +1,11 @@
status is-interactive || exit
# COMPONENT
# overrides/key-bindings
# Local modification: opinionated guard (AGENTS.md Task #3). Bracket
# auto-pairing intercepts single-character input, classified as C3 overrides.
__fish_config_op_enabled __fish_config_op_overrides || exit
__fish_config_op_enabled (status basename) || exit
set --global autopair_left "(" "[" "{" '"' "'"
set --global autopair_right ")" "]" "}" '"' "'"
+9 -6
View File
@@ -1,13 +1,16 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# overrides/key-bindings
# Provides bash-style history expansion functions for abbreviations.
# These functions are gated by the C3 overrides switch.
# Execute expand_bang_all
function expand_bang_all --description 'Execute expand_bang_all'
# Opinionated guard (C3): no expansion when overrides are disabled.
__fish_config_op_enabled __fish_config_op_overrides; or return 1
__fish_config_op_enabled (status basename); or return 1
set -l token $argv[1]
if test -z "$token"; set token (commandline -t); end
@@ -23,7 +26,7 @@ end
# Execute expand_bang_caret
function expand_bang_caret --description 'Execute expand_bang_caret'
# Opinionated guard (C3): no expansion when overrides are disabled.
__fish_config_op_enabled __fish_config_op_overrides; or return 1
__fish_config_op_enabled (status basename); or return 1
# Split the last history item into a list
set -l tokens (string split -n ' ' -- $history[1])
@@ -36,7 +39,7 @@ end
# Execute expand_bang_minus_n
function expand_bang_minus_n --description 'Execute expand_bang_minus_n'
# Opinionated guard (C3): no expansion when overrides are disabled.
__fish_config_op_enabled __fish_config_op_overrides; or return 1
__fish_config_op_enabled (status basename); or return 1
set -l token $argv[1]
if test -z "$token"; set token (commandline -t); end
@@ -58,7 +61,7 @@ end
# Execute expand_bang_search
function expand_bang_search --description 'Execute expand_bang_search'
# Opinionated guard (C3): no expansion when overrides are disabled.
__fish_config_op_enabled __fish_config_op_overrides; or return 1
__fish_config_op_enabled (status basename); or return 1
set -l token $argv[1]
if test -z "$token"
@@ -84,7 +87,7 @@ end
# Execute expand_bang_string
function expand_bang_string --description 'Execute expand_bang_string'
# Opinionated guard (C3): no expansion when overrides are disabled.
__fish_config_op_enabled __fish_config_op_overrides; or return 1
__fish_config_op_enabled (status basename); or return 1
# Fish 4.x passes the matched token as argv[1]
set -l token $argv[1]
@@ -112,7 +115,7 @@ end
# Execute expand_typo_sub
function expand_typo_sub --description 'Execute expand_typo_sub'
# Opinionated guard (C3): no expansion when overrides are disabled.
__fish_config_op_enabled __fish_config_op_overrides; or return 1
__fish_config_op_enabled (status basename); or return 1
# In newer Fish, the matched token is often passed as $argv[1]
# if the abbr is set up correctly. We'll fallback to commandline just in case.
+4 -1
View File
@@ -19,6 +19,9 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# COMPONENT
# integrations/notifications
if not status is-interactive
exit
@@ -26,7 +29,7 @@ end
# Local modification: opinionated guard (AGENTS.md Task #3). Desktop
# notifications assume a graphical session, classified as C4 integrations.
__fish_config_op_enabled __fish_config_op_integrations; or exit
__fish_config_op_enabled (status basename); or exit
set -g __done_version 1.19.1
+6 -2
View File
@@ -7,6 +7,10 @@
#
# Runs exactly once on the first interactive fish session after install.
# To reset for testing, run: set -Ue __fish_config_first_run_complete
#
# COMPONENT
# site first-run-greeting: greeting/first-run
# site first-run-bootstrap: autoexec/plugin-management
# Exit early in non-interactive shells (scripts, completions, subshells)
if not status is-interactive
@@ -36,7 +40,7 @@ end
# Printing a first-run welcome banner is opinionated (C6 greeting). The
# first-run state variable is already set unconditionally above, so
# disabling the greeting never re-triggers this file.
if __fish_config_op_enabled __fish_config_op_greeting
if __fish_config_op_enabled (status basename) first-run-greeting
echo ""
echo " Welcome to your fish shell configuration!"
echo " Run 'help config' for offline documentation."
@@ -48,7 +52,7 @@ end
# Startup side-effects below (Fisher curl, fisher update, theme apply) are
# opinionated (C2 auto-execution). The first-run state variable is already
# set above either way, so disabling auto-exec never re-triggers this file.
if not __fish_config_op_enabled __fish_config_op_autoexec
if not __fish_config_op_enabled (status basename) first-run-bootstrap
return
end
+4 -1
View File
@@ -5,6 +5,9 @@
# │ help config wrapper │
# ╰──────────────────────────────────────────────────────────╯
#
# COMPONENT
# aliases/shell-tools
#
# SYNOPSIS
# help [topic] [sub-topic...]
# help config [section] [-w|--html] [-m|--man] [-h|--help]
@@ -50,7 +53,7 @@ end
# --- Wrapper Definition ---
function help --wraps help --description "Custom wrapper to intercept 'help config'"
# Opinionated guard (C1): fall back to the native fish help when disabled.
if not __fish_config_op_enabled __fish_config_op_aliases
if not __fish_config_op_enabled (status current-function)
__original_help $argv
return $status
end
+18 -3
View File
@@ -8,6 +8,9 @@
# This file defines custom key bindings for the Fish shell.
# It is sourced by Fish on startup.
# COMPONENT
# overrides/key-bindings
# ────────────────── Bind Prewious Path Head to Ctrl+G ─────────────────
# Bindings to insert the previous path head into the command line
# Behaves like `!$:h` does in bash
@@ -48,7 +51,11 @@ function fish_user_key_bindings
# Custom key chords are opinionated (C3 overrides); skip them entirely
# when overrides are disabled so stock bindings remain untouched.
__fish_config_op_enabled __fish_config_op_overrides; or return
# NOTE: (status basename), not (status current-function) -- this guard
# lives inside fish's own reserved fish_user_key_bindings function, whose
# name is not this file's identity; the registry key is this file's
# bare basename, key_bindings.
__fish_config_op_enabled (status basename); or return
# ───────────────────────────── Set Bindings ─────────────────────────────
#
@@ -58,7 +65,6 @@ function fish_user_key_bindings
bind ctrl-alt-u _replace_command_token
type -q qalc && bind ctrl-alt-= _qalc_eval
bind ctrl-enter _smart_execute
bind @@ __fzf_inline_picker
bind ctrl-right nextd-or-forward-word
bind \e\[1\;5C nextd-or-forward-word
@@ -70,8 +76,17 @@ function fish_user_key_bindings
bind --mode $mode ctrl-alt-u _replace_command_token
type -q qalc && bind --mode $mode ctrl-alt-= _qalc_eval
bind --mode $mode ctrl-enter _smart_execute
bind --mode $mode @@ __fzf_inline_picker
bind --mode $mode ctrl-right nextd-or-forward-word
bind --mode $mode \e\[1\;5C nextd-or-forward-word
end
# @ is Emacs/Vi-insert only. Emacs bindings and Vi's "default" (normal)
# mode share the same bind_mode name, so a mode-less `bind @` would also
# land in Vi normal mode, turning its current no-op @ into a self-insert.
# Register the mode-less form only when Vi bindings aren't the active
# base, and rely on the explicit insert-mode bind otherwise.
if test "$fish_key_bindings" != fish_vi_key_bindings
bind @ __fzf_inline_picker
end
bind --mode insert @ __fzf_inline_picker
end
+5 -1
View File
@@ -1,5 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# logging/terminal-capture
#
# C5 — Logging & Capture: a non-blocking, per-session reminder shown inside Kitty
# when the fish-config scrollback watcher is not yet set up. It never blocks the
@@ -10,7 +14,7 @@
status is-interactive; or exit
type -q kitty; or exit
set -q KITTY_WINDOW_ID; or exit
__fish_config_op_enabled __fish_config_op_logging; or exit
__fish_config_op_enabled (status basename); or exit
__fish_variable_check __fish_config_kitty_watcher_dismissed; and exit
__kitty_logging_has_watcher; and exit
+6 -2
View File
@@ -6,12 +6,16 @@
# bars are preserved, renders the captured animation to a clean static log
# (via scripts/clean_progress_log.py), and prunes old logs.
# COMPONENT
# site paru-autoexec: autoexec/pkg-wrappers
# site paru-logging: logging/pkg-logs
# Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec).
# Wrapper generation is also gated by C5 (Logging & Capture).
__fish_config_op_enabled __fish_config_op_autoexec; or return
__fish_config_op_enabled (status basename) paru-autoexec; or return
# C5 — Logging & Capture: remove generated wrapper and skip when logging is off
if not __fish_config_op_enabled __fish_config_op_logging
if not __fish_config_op_enabled (status basename) paru-logging
if test -f "$HOME/.local/bin/paru"
and grep -q "# paru-wrapper-version:" "$HOME/.local/bin/paru" 2>/dev/null
rm -f "$HOME/.local/bin/paru"
+4 -1
View File
@@ -1,9 +1,12 @@
status is-interactive || exit
# COMPONENT
# overrides/key-bindings
# Local modification: opinionated guard (AGENTS.md Task #3). Puffer's key
# intercepts are part of the bang-bang system, gated atomically under C3
# overrides with conf.d/tricks.fish, conf.d/abbr.fish, and expand_*.fish.
__fish_config_op_enabled __fish_config_op_overrides || exit
__fish_config_op_enabled (status basename) || exit
function _puffer_fish_key_bindings --on-variable fish_key_bindings
set -l modes
+5 -1
View File
@@ -1,12 +1,16 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# overrides/prompt
#
# Defines fish_prompt only when starship is installed.
# Without starship, fish's built-in prompt already emits OSC 133;A
# on the prompt line itself, so no wrapper is needed.
# Replacing the prompt is opinionated (C3 overrides)
__fish_config_op_enabled __fish_config_op_overrides; or return
__fish_config_op_enabled (status basename); or return
type -q starship; or return
+5 -1
View File
@@ -1,5 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# overrides/prompt
#
# ╭──────────────────────────────────────────────────────────╮
# │ Fish Theme │
@@ -9,7 +13,7 @@
# Forcing theme colors and $FZF_DEFAULT_OPTS is opinionated (C3 overrides).
# The FZF variable is universal, so clean up our Catppuccin value if it
# lingers from a session where overrides were still enabled.
if not __fish_config_op_enabled __fish_config_op_overrides
if not __fish_config_op_enabled (status basename)
if set -q FZF_DEFAULT_OPTS; and string match -q '*#1E1E2E*' -- "$FZF_DEFAULT_OPTS"
set --erase FZF_DEFAULT_OPTS
end
+5 -1
View File
@@ -1,12 +1,16 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# logging/multiplexer-capture
#
# C5 — Logging & Capture: starts a pipe-pane log for the current tmux pane
# when fish launches inside a tmux session. Each fish shell gets its own
# timestamped log file in SCROLLBACK_HISTORY_DIR (default: ~/.terminal_history).
# Naming: tmux_<session>-w<window>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
__fish_config_op_enabled __fish_config_op_logging; or exit
__fish_config_op_enabled (status basename); or exit
status is-interactive; or exit
type -q tmux; or exit
set -q TMUX; or exit
+9 -4
View File
@@ -7,6 +7,11 @@
# │ system aliases, and history/backup utilities │
# ╰──────────────────────────────────────────────────────────╯
# COMPONENT
# site aliases-tricks: aliases/filesystem
# site tricks-manpager: overrides/environment
# site tricks-bang: overrides/key-bindings
## Environment setup
# Apply .profile: use this to put fish compatible .profile stuff in
if test -f ~/.fish_profile
@@ -24,7 +29,7 @@ end
# Format man pages using bat (only if bat is installed)
# Overriding $MANPAGER is opinionated (C3 overrides)
if type -q bat; and __fish_config_op_enabled __fish_config_op_overrides
if type -q bat; and __fish_config_op_enabled (status basename) tricks-manpager
set -gx MANROFFOPT -c
set -gx MANPAGER "sh -c 'col -bx | bat -l man -p'"
end
@@ -37,7 +42,7 @@ set -gx __done_notification_urgency_level low
# Functions needed for !! and !$ https://github.com/oh-my-fish/plugin-bang-bang
# The bang-bang system is opinionated (C3 overrides) and is gated atomically
# here, in conf.d/abbr.fish, conf.d/puffer.fish, and functions/expand_*.fish.
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) tricks-bang
function __history_previous_command
switch (commandline -t)
case "!"
@@ -83,7 +88,7 @@ end
# Fish command history override to show timestamps
# Shadowing the history command is opinionated (C1 aliasing); when disabled,
# the function is never defined and fish's stock history behavior applies.
if __fish_config_op_enabled __fish_config_op_aliases
if __fish_config_op_enabled (status basename) aliases-tricks
function history
builtin history --show-time='%F %T '
end
@@ -123,7 +128,7 @@ alias .....='cd ../../../..'
alias ......='cd ../../../../..'
# Silent flag injection into POSIX tools is opinionated (C1 aliasing)
if __fish_config_op_enabled __fish_config_op_aliases
if __fish_config_op_enabled (status basename) aliases-tricks
# Tools & Core command color overrides
# @category Shell Aliases
# @desc dir --color=auto
+6 -2
View File
@@ -5,11 +5,15 @@
# see: https://github.com/ik11235/wakatime.fish
###
# COMPONENT
# site wakatime-autoexec: autoexec/telemetry
# site wakatime-hook: integrations/notifications
# Local modification: opinionated guard (AGENTS.md Task #3). WakaTime
# reporting is classified under both C2 auto-execution and C4 integrations;
# disabling either category skips registering the hook.
__fish_config_op_enabled __fish_config_op_autoexec; or exit
__fish_config_op_enabled __fish_config_op_integrations; or exit
__fish_config_op_enabled (status basename) wakatime-autoexec; or exit
__fish_config_op_enabled (status basename) wakatime-hook; or exit
function __register_wakatime_fish_before_exec -e fish_postexec
if set -q FISH_WAKATIME_DISABLED
+6 -2
View File
@@ -6,12 +6,16 @@
# bars are preserved, renders the captured animation to a clean static log
# (via scripts/clean_progress_log.py), and prunes old logs.
# COMPONENT
# site yay-autoexec: autoexec/pkg-wrappers
# site yay-logging: logging/pkg-logs
# Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec).
# Wrapper generation is also gated by C5 (Logging & Capture).
__fish_config_op_enabled __fish_config_op_autoexec; or return
__fish_config_op_enabled (status basename) yay-autoexec; or return
# C5 — Logging & Capture: remove generated wrapper and skip when logging is off
if not __fish_config_op_enabled __fish_config_op_logging
if not __fish_config_op_enabled (status basename) yay-logging
if test -f "$HOME/.local/bin/yay"
and grep -q "# yay-wrapper-version:" "$HOME/.local/bin/yay" 2>/dev/null
rm -f "$HOME/.local/bin/yay"
+4 -1
View File
@@ -3,6 +3,9 @@
# Adapted from icezyclon/zoxide.fish (MIT)
# Heavily customized for Fish 4.x compatibility and performance
# COMPONENT
# aliases/filesystem
if status is-interactive
if type -q zoxide
@@ -65,7 +68,7 @@ if status is-interactive
# Shadowing cd with zoxide is opinionated (C1 aliasing); z and zi
# remain available either way.
if __fish_config_op_enabled __fish_config_op_aliases
if __fish_config_op_enabled (status basename)
alias cd=z
end
+30 -10
View File
@@ -7,7 +7,7 @@
# ───────────────────── Opinionated component guards ─────────────────────
# Opinionated components (AGENTS.md Task #3) are wrapped in
# __fish_config_op_enabled <category> guards throughout this file and conf.d/.
# __fish_config_op_enabled <identity> [<site>] guards throughout this file and conf.d/.
# The helper always evaluates the master switch __fish_config_opinionated
# first (falsy disables everything), then the per-category opt-out variable:
# __fish_config_op_aliases C1 — command shadows / flag injection
@@ -21,12 +21,24 @@
# C5 is the one exception: it defaults to disabled and needs an explicit
# truthy value — set -U __fish_config_op_logging on
# COMPONENT
# site cachyos-tricks: overrides/environment
# site cachyos-strip-aliases: aliases/filesystem
# site cachyos-strip-overrides: overrides/key-bindings
# site privacy: overrides/privacy
# site pager-editor-gpg: overrides/environment
# site exit-wiring: overrides/key-bindings
# site path-setup: overrides/environment
# site cdpath: overrides/environment
# site vi-mode: overrides/key-bindings
# site greeting-stamp: greeting/greeting-message
# ──────────────────────── Source CachyOS configs ────────────────────────
if test -f /usr/share/cachyos-fish-config/cachyos-config.fish
source /usr/share/cachyos-fish-config/cachyos-config.fish
# Surgically overriding the distro config is opinionated (C3 overrides):
# skip it entirely when overrides are disabled, keeping CachyOS defaults.
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) cachyos-tricks
# Source our tricks over the cachyOS config
test -f "$__fish_config_dir/conf.d/tricks.fish"
and source "$__fish_config_dir/conf.d/tricks.fish"
@@ -41,7 +53,7 @@ if test -f /usr/share/cachyos-fish-config/cachyos-config.fish
# The distro config ships opinionated pieces of its own (it is the origin
# of tricks.fish); strip them when the matching category is disabled so
# the guards hold on CachyOS systems too.
if not __fish_config_op_enabled __fish_config_op_aliases
if not __fish_config_op_enabled (status basename) cachyos-strip-aliases
for _fname in grep fgrep egrep dir vdir wget
functions -q $_fname; and functions --erase $_fname
end
@@ -53,7 +65,7 @@ if test -f /usr/share/cachyos-fish-config/cachyos-config.fish
and source $__fish_data_dir/functions/$_fname.fish
end
end
if not __fish_config_op_enabled __fish_config_op_overrides
if not __fish_config_op_enabled (status basename) cachyos-strip-overrides
for _fname in __history_previous_command __history_previous_command_arguments
functions -q $_fname; and functions --erase $_fname
end
@@ -93,9 +105,17 @@ set -q NVIDIA_SETTINGS_RW_CONFIG_FILE; or set -gx NVIDIA_SETTINGS_RW_CONFIG_FILE
set -q CODEIUM_HOME; or set -gx CODEIUM_HOME "$XDG_CONFIG_HOME/codeium"
set -q WORDLIST; or set -gx WORDLIST "$XDG_CONFIG_HOME/hunspell_en_US"
# ───────────────────────── Privacy variables ────────────────────────────
# Global telemetry opt-out variables (C3 overrides: privacy)
# Various CLI tools, runtimes, and AI-agent tools respect these variables.
if __fish_config_op_enabled (status basename) privacy
set -gx DO_NOT_TRACK 1
set -gx DISABLE_TELEMETRY 1
end
# ─────────────────────────── Pager variables ────────────────────────────
# Overriding $PAGER, $EDITOR, and $GPG_TTY is opinionated (C3 overrides)
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) pager-editor-gpg
if type -q ov
set -gx PAGER ov
else if type -q less
@@ -136,7 +156,7 @@ and set -gx SCROLLBACK_HISTORY_MAX_FILES $__fish_scrollback_history_max_files
# Wire up a clean exit function that won't fire on background subshells
# Replacing the exit builtin is opinionated (C3 overrides); smart_exit also
# guards itself so a live toggle takes effect without restarting the shell.
if status is-interactive; and __fish_config_op_enabled __fish_config_op_overrides
if status is-interactive; and __fish_config_op_enabled (status basename) exit-wiring
function exit --description 'Safe interactive exit'
# If the smart_exit file exists in our function path, invoke it explicitly
if functions -q smart_exit
@@ -153,7 +173,7 @@ end
# the cargo bin directory is moved to the end of the PATH, which can help avoid conflicts
# with system-installed Rust tools while still allowing user-installed cargo binaries to be found.
# PATH setup is opinionated (C3 overrides)
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) path-setup
fish_add_path $HOME/.local/bin # Standard user-local executables (XDG spec)
fish_add_path $HOME/.local/share/../bin # Alternative/legacy path for local user binaries
fish_add_path $HOME/Applications # User-installed applications and standalone apps
@@ -176,7 +196,7 @@ end
# so if you have a directory named 'myproject' in the current directory,
# running 'cd myproject' will take you there instead of $HOME/projects/myproject.
# CDPATH injection is opinionated (C3 overrides)
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) cdpath
set -gx CDPATH . $HOME/projects $HOME
end
@@ -191,7 +211,7 @@ if status is-interactive
# This is optional but can improve the user experience for those who prefer Vi-style key bindings.
# Global Vi mode is opinionated (C3 overrides); without it fish keeps its
# default Emacs-style bindings.
if __fish_config_op_enabled __fish_config_op_overrides
if __fish_config_op_enabled (status basename) vi-mode
set -g fish_key_bindings fish_vi_key_bindings
end
@@ -251,7 +271,7 @@ if status is-interactive
# function that distro configs set (e.g., CachyOS defines it as fastfetch).
# This runs last inside the interactive block so our empty definition wins
# over whatever cachyos-config.fish or vendor conf.d installed.
if not __fish_config_op_enabled __fish_config_op_greeting
if not __fish_config_op_enabled (status basename) greeting-stamp
function fish_greeting
end
end
+151 -7
View File
@@ -8,19 +8,30 @@
"""
import argparse
import functools
import json
import re
import shutil
import sys
from pathlib import Path
import codespans
import manualtools as mt
import generate_component_registry
DOCS = Path(__file__).parent
MANUAL = DOCS / "manual"
FUNCTIONS = DOCS.parent / "functions"
COMPLETIONS = DOCS.parent / "completions"
README = DOCS.parent / "README.md"
REPO_BLOB_BASE = "https://git.rootiest.dev/rootiest/fish-config/src/branch/main/"
SLUG_DIR = "reference"
# File-tree branches whose real directory contents get listed inline on the
# Starlight site (never in the plain-text manual/man page, since only the
# --site path runs box-drawing trees through _as_file_tree).
EXPANDABLE_TREE_DIRS = {"functions/": FUNCTIONS, "completions/": COMPLETIONS}
def _is_function_page(path: Path, root: Path) -> bool:
"""True for a Section 5 category stub (not its index)."""
@@ -71,10 +82,104 @@ def _with_abbreviations(body: str, abbrs: dict[str, list[dict]]) -> str:
for cat, table in rendered_abbrs.items():
placeholder = f"<!-- GENERATED: {cat} -->"
body = body.replace(placeholder, table)
return body
TOC_PLACEHOLDER = "<!-- GENERATED: toc -->"
TOC_SKIP_STEMS = {"index"}
def _build_toc(root: Path) -> str:
"""Render the section list for docs/manual/00-table-of-contents.md.
Walks the same tree `build_concat` does, so it can never drift from the
man page's actual section order. `index.md` and the `00-*` front-matter
pages (Name, Synopsis, this page) sit before section 1 and are excluded,
same as any `man: false` page (currently only 404).
"""
lines: list[str] = []
n = 0
for path, depth in mt.walk(root):
rel = path.relative_to(root)
# len(rel.parts) == 1 means a root-level file, not a directory's own
# index page (e.g. 04-abbreviations/index.md), which must keep its
# own numbered line even though its stem is also "index".
if depth == 0 and len(rel.parts) == 1 and (rel.stem in TOC_SKIP_STEMS or rel.stem.startswith("00-")):
continue
fm, _ = mt.parse(path)
if not fm.get("man", True):
continue
title = fm.get("title", path.stem)
if depth == 0:
n += 1
lines.append(f" {n}. {title}")
else:
lines.append(f" - {title}")
return "\n".join(lines)
def _with_toc(body: str, root: Path) -> str:
"""Inject the `<!-- GENERATED: toc -->` placeholder with the built section list."""
return body.replace(TOC_PLACEHOLDER, _build_toc(root)) if TOC_PLACEHOLDER in body else body
README_LINK_RE = re.compile(r"\]\((?!https?://|#|mailto:)([^)]+)\)")
README_FENCE_RE = re.compile(r"```[^\n]*\n(.*?)```\n?", re.DOTALL)
README_PLACEHOLDER_RE = re.compile(r"<!-- README: (.+?) -->")
def _rewrite_repo_links(text: str) -> str:
"""Point a README-relative link (`CONTRIBUTING.md`, `LICENSE`) at its file on Gitea."""
return README_LINK_RE.sub(lambda m: f"]({REPO_BLOB_BASE}{m.group(1)})", text)
def _defence(text: str) -> str:
"""Rewind a README fenced code block into the manual's indented-block form.
`docs/manual` bodies are authored man-page style (4-space indent), never
fenced: `codespans`/pandoc pair backticks per line, and a fence line's
triple backtick throws that count off. README.md is ordinary markdown
and fences its examples, so an injected section is converted back.
"""
def repl(m: re.Match) -> str:
block = m.group(1).rstrip("\n")
return "\n".join(" " + line for line in block.split("\n")) + "\n"
return README_FENCE_RE.sub(repl, text)
@functools.lru_cache(maxsize=1)
def _readme_sections() -> dict[str, str]:
"""Split README.md into {H2 heading: body}, links rewritten to point at the repo.
Lets a manual stub pull one README section in verbatim via a
`<!-- README: <Heading> -->` placeholder, so the README stays the single
source of truth for sections that describe the repo itself rather than
the shell config (Testing, Contributing, Attribution, License).
"""
sections: dict[str, str] = {}
heading: str | None = None
lines: list[str] = []
for line in README.read_text().split("\n") + ["## "]:
if line.startswith("## "):
if heading is not None:
body = "\n".join(lines).strip()
if body.endswith("---"):
body = body[:-3].rstrip()
sections[heading] = _defence(_rewrite_repo_links(body))
heading = line[3:].strip()
lines = []
else:
lines.append(line)
return sections
def _with_readme(body: str) -> str:
"""Inject `<!-- README: Heading -->` placeholders with that README section's body."""
return README_PLACEHOLDER_RE.sub(lambda m: _readme_sections().get(m.group(1), ""), body)
def build_concat(root: Path) -> str:
"""Concatenate the manual into one ordered markdown document.
@@ -86,6 +191,16 @@ def build_concat(root: Path) -> str:
with no frontmatter fences and no Astro-visible frontmatter key. When
present, its contents are re-emitted byte-for-byte as the leading
`---`-fenced block, ahead of every heading.
Bodies go through `codespans` here so a token is typeset the same way
in every output: `tmux` and `local.fish` are wrapped on the site by
that pass, and without it the man page marked only what the SSOT
happened to backtick by hand. Section 5 is unaffected -- its entries
arrive as indented verbatim blocks, which `codespans` leaves alone and
pandoc already sets in a monospace font.
Only bodies are passed: the pandoc metadata block above is not prose
and must survive byte-for-byte.
"""
entries = build_entries(mt.parse_functions(FUNCTIONS))
chunks: list[str] = []
@@ -104,10 +219,13 @@ def build_concat(root: Path) -> str:
elif "04-abbreviations" in path.parts:
abbrs = mt.parse_abbreviations(DOCS.parent / "conf.d")
body = _with_abbreviations(body, abbrs)
body = _with_readme(body)
body = _with_toc(body, root)
if body:
body = re.sub(r"<LinkButton.*?</LinkButton>\n*", "", body, flags=re.DOTALL)
body = re.sub(r"<CardGrid.*?</CardGrid>\n*", "", body, flags=re.DOTALL)
body = re.sub(r"\[([^\]]+)\]\(/[^)]+\)", r"\1", body)
body = codespans.add_code_spans(body, _code_vocabulary())
chunks.append(mt.shift_headings(body, depth))
return "\n\n".join(chunks) + "\n"
@@ -188,7 +306,7 @@ SHELL_HEADS = frozenset(
if jobs kitty ls man math mkdir mv nvim npm pacman paru pip pip3 pkg printf
python python3 rm set shutdown source string sudo switch systemctl test time
tmux touch trash type wget wezterm while yay zellij zypper
fish_default_key_bindings fish_vi_key_bindings
fish_default_key_bindings fish_vi_key_bindings fish_config
""".split()
)
@@ -365,6 +483,11 @@ def _as_file_tree(para: list[str]) -> str | None:
depth = len(prefix.replace('\t', ' ')) // 4
indent = " " * (depth + 1)
out.append(f"{indent}- {name} {desc}".rstrip())
expand_dir = EXPANDABLE_TREE_DIRS.get(name)
if expand_dir is not None and expand_dir.is_dir():
child_indent = " " * (depth + 2)
for entry in sorted((p.name for p in expand_dir.iterdir() if p.is_file()), key=str.lower):
out.append(f"{child_indent}- {entry}")
out.append("</FileTree>")
return "\n".join(out)
@@ -393,7 +516,13 @@ def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str:
table = _as_ruled_table(para) or _as_table(para) or _as_file_tree(para)
if table is not None:
return table
return "\n".join(INDENT + line for line in para)
# MDX (used for any page that also carries an <Aside> or <FileTree>)
# has no indented-code-block syntax — a plain 4-space-indented block
# silently renders as flowed paragraph text there, collapsing every
# line break. A fenced block works in both MDX and plain Markdown, so
# it's the only fallback that's safe regardless of which one a given
# page ends up promoted to.
return "```text\n" + "\n".join(para) + "\n```"
def _prettify_block(block: list[str], entry_name: str | None) -> str:
@@ -466,12 +595,24 @@ def _as_aside(para: list[str]) -> str | None:
return f"<Aside {attrs}>\n{body}\n</Aside>"
@functools.lru_cache(maxsize=1)
def _code_vocabulary() -> codespans.Vocabulary:
"""The command names codespans may wrap, read from the repo once."""
return codespans.vocabulary(DOCS.parent)
def prettify(body: str, entry_name: str | None = None) -> str:
"""Rewrite a body's indented code blocks and labeled asides for the website.
Site-only: the man page and `config-help` keep reading the untouched
SSOT, where the indented form and the `LABEL:` text are exactly what
pandoc/`config-help` want.
The block and aside rewrites are site-only: the man page and
`config-help` keep reading the untouched SSOT, where the indented form
and the `LABEL:` text are exactly what pandoc/`config-help` want.
The inline code spans added last are not site-only. `-a/--all` and
`__fish_config_op_aliases` are authored bare so the `functions/*.fish`
headers stay readable as plain text, and the backticks every output
wants are put on here rather than in the SSOT -- `build_concat()` runs
the same pass for the man page and `config-help`.
"""
out: list[str] = []
block: list[str] = []
@@ -510,7 +651,7 @@ def prettify(body: str, entry_name: str | None = None) -> str:
while block and not block[-1].strip():
block.pop()
out.append(_prettify_block(block, entry_name))
return "\n".join(out)
return codespans.add_code_spans("\n".join(out), _code_vocabulary())
ENTRY_HEADS = {
@@ -706,6 +847,7 @@ def build_site(root: Path, out: Path) -> list[dict]:
if "04-abbreviations" in path.parts:
abbrs = mt.parse_abbreviations(DOCS.parent / "conf.d")
body = _with_abbreviations(body, abbrs)
body = _with_readme(body)
target.parent.mkdir(parents=True, exist_ok=True)
body = _inject_subheading_cards(body)
_write_prettified(target, _page_fm(fm), prettify(body))
@@ -840,6 +982,8 @@ def main() -> int:
if not (args.concat or args.site):
ap.error("nothing to do: pass --concat and/or --site")
generate_component_registry.main()
if args.site:
src = DOCS / "site" / "src"
out = src / "content" / "docs"
+546
View File
@@ -0,0 +1,546 @@
#!/usr/bin/env python3
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Wrap code-shaped tokens in inline code spans for the Starlight site.
Section 5 is generated from the `functions/*.fish` comment headers, which
are read as plain text by `config-help`, by `funcsave`, and by anyone
opening the source file. Backticks there would be noise, so the headers
are authored without them -- and the site inherited that, rendering
`-a/--all` and `__fish_config_op_aliases` as ordinary prose.
This module closes that gap at render time: it walks the markdown a page
is about to be written as, finds the tokens whose shape only ever means
"code" (flags, `$vars`, snake_case identifiers, paths, key chords, known
command names) and wraps each one in a code span. The SSOT is never
touched, so the man page and `config-help` keep the plain-text form.
Everything here is conservative by construction: leaving a token alone is
always safe and wrapping the wrong one is not, so every rule bails out the
moment it is unsure. The regions that must never be rewritten -- fenced
blocks, indented code blocks, existing code spans, link targets, URLs, JSX
attributes, `<FileTree>` bodies, headings -- are recognised first and
passed through.
"""
import functools
import re
from pathlib import Path
FENCE_RE = re.compile(r"^\s*(```|~~~)")
# ---------------------------------------------------------------------------
# Vocabulary
# ---------------------------------------------------------------------------
# Commands a reader expects to see typeset as code. This is the *wide* list:
# it decides that a table column holds command lines (see _command_columns),
# where position already proves the name is a command. Wrapping a bare
# mention in running prose is gated on the strict tier below.
STANDARD_COMMANDS = frozenset(
"""
apk apt awk basename bash bat bg bind brew builtin cargo cat cd chmod
chown chsh cmp column cp curl cut date dd delta df diff dig dirname dnf
docker dpkg du echo emacs emerge env eza exec exit export fastfetch fd
fdisk fg fgrep file find fisher flatpak fzf gh git grep gzip head help
hexdump host hostname id ifconfig install ip jq jobs journalctl kill
killall kitten kitty last less ln locale ls lsblk lsd lsof make man
micro mkdir more mount mpv mv nano nc neofetch neovim netstat nix nl
nohup npm nproc nvim od open openssl pacman paru paste pgrep ping pip
pip3 pkill pr printf ps pwd python python3 readlink realpath rg rm rmdir
rpm rsync scp sed seq sh shutdown sleep snap sort source ssh stat
strings su sudo sync systemctl tac tail tar tee test time tldr tmux
touch tr trash tree type udisksctl umount uname uniq unzip uv vdir vi
vim vlc wait wc wezterm wget which who whoami wl-copy wl-paste xargs
xbps-install xclip xdg-open xsel yay yum yt-dlp zellij zip zoxide zsh
zypper
abbr alias and argparse begin block break case command complete contains
continue count else emit end eval false for function funcsave functions
history if math not or random read return set set_color status string
switch true while
""".split()
)
# Names that also read as ordinary English (or as this manual's own prose)
# often enough that a bare mention is not evidence of a command. They still
# take part in command-line and list detection, where position disambiguates
# -- they just never get wrapped on their own.
AMBIGUOUS_COMMANDS = frozenset(
"""
abbr alias all and at basename bat begin bg bind block branch break case
cat cd cheat cleanup clone column command complete contains continue copy
count cut date dd df dir dirname do docker du duf dust echo edit else emit
end env eval exec exit export false fc fg file find fish for free function
functions git go head help hist history host hostname id if in install ip
jobs join key kill last less link list ln lock locale log logs look ls make
man math micro more mount mv next no not note od open or ov p page paste
pkg poke ports pr ps pwd random read real replay return rm run screen sed
search seq set sh show sleep sort source spark split stat status string
strings su switch sync tab tac tail tar tee test time top touch tr trash
tree true type uniq upgrade view vi wait watch wc which while who write
yes zip
builtin fast function vdir
""".split()
)
# Extensions that make a bare `name.ext` token unambiguously a filename.
PATH_EXTENSIONS = (
"fish md mdx json jsonc toml yml yaml py sh bash zsh lua conf cfg ini "
"txt log list service socket desktop css scss ts js astro nix rasi 1"
).split()
# English function words. A candidate command line containing one is prose.
STOPWORDS = frozenset(
"""
a an the this that these those it its is are was were be been being am
to of in into on at by for from with without within about across after
before during over under again then than so such as and or but nor if
when while where which who whom whose why how all any both each few more
most other some only own same too very can will just should now via per
also either neither every no not
""".split()
)
_CATALOG_ARRAY_RE = re.compile(
r"set\s+-g\s+_fdc_(?:bins|cargo|pm)\s+((?:[^\n]*\\\n)*[^\n]*)"
)
def dependency_names(repo: Path) -> set[str]:
"""Every tool name in the `fish-deps` catalog (`_fdc_*` arrays).
`functions/_fish_deps_catalog.fish` is this repo's dependency database;
reading it here means a tool added there starts rendering as code with
no second list to keep in sync.
"""
path = repo / "functions" / "_fish_deps_catalog.fish"
if not path.exists():
return set()
names: set[str] = set()
for m in _CATALOG_ARRAY_RE.finditer(path.read_text(encoding="utf-8")):
for token in m.group(1).replace("\\\n", " ").split():
token = token.strip("\"'")
if token and re.fullmatch(r"[\w.@+-]+", token):
names.add(token)
return names
def function_names(repo: Path) -> set[str]:
"""Public function names, from the `functions/` directory listing.
Underscore-prefixed internals are skipped only because the snake_case
rule already covers them, and covers them everywhere -- including the
ones that have no file of their own.
"""
directory = repo / "functions"
if not directory.is_dir():
return set()
return {p.stem for p in directory.glob("*.fish") if not p.stem.startswith("_")}
class Vocabulary:
"""The command names the rules recognise, in two tiers.
`full` is every name we know of, used where position already proves a
token is a command (a command-line table cell, an arrow chain, a
comma-separated run). `strict` is the subset safe to wrap on sight in
running prose: `zoxide` yes, `find` no.
"""
__slots__ = ("full", "strict")
def __init__(self, names: set[str]):
# `and`, `or`, `not`, `if` … are fish builtins, but as vocabulary
# entries they turn every conjunction into a command name and break
# list and command-line detection. They are never worth wrapping.
self.full = frozenset(names) - STOPWORDS
self.strict = frozenset(
n
for n in names
if n not in AMBIGUOUS_COMMANDS
and (len(n) >= 3 or any(c.isdigit() for c in n))
)
def __eq__(self, other):
return (
isinstance(other, Vocabulary)
and self.full == other.full
and self.strict == other.strict
)
def __hash__(self):
return hash((self.full, self.strict))
def vocabulary(repo: Path) -> Vocabulary:
"""Build the command vocabulary from the repo plus the standard list."""
return Vocabulary(
set(STANDARD_COMMANDS) | dependency_names(repo) | function_names(repo)
)
EMPTY_VOCABULARY = Vocabulary(set())
# ---------------------------------------------------------------------------
# Token grammar
# ---------------------------------------------------------------------------
# A token may not start inside a word, a path, a code span, a history
# expansion, or a hyphenated compound: `` `zoxide` ``-backed must not see
# `-backed` as a flag, `and/or` must not see `/or` as a path, and `!-N` must
# not see `-N` as one either.
BEFORE = r"(?<![\w`$/\\~.=+!-])"
# Ruling out a trailing `/` keeps a partially-recognised slash run
# (`grep/cp/mv/wget`, where only `grep` is in the vocabulary) from being
# wrapped one limb at a time.
AFTER = r"(?![\w`/])"
_SEG = r"[\w.@+-]+"
# The last segment of a path may not end in `.`, so a sentence-final full
# stop stays outside the span. A segment that is nothing but dots (`..`,
# `...`) is the exception: there the dots are the segment.
_LAST = r"(?:[\w.@+-]*[\w@+-]|\.+)"
_EXT = "|".join(PATH_EXTENSIONS)
# Key chords: `Ctrl-R`, `Ctrl+Alt+F`. Both separators appear in the manual.
_MODIFIER = r"(?:Ctrl|Alt|Shift|Super|Meta|Cmd|Opt)"
_KEY = (
r"(?:F\d{1,2}|Tab|Enter|Return|Space|Esc|Escape|Backspace|Delete|Insert"
r"|Home|End|Up|Down|Left|Right|PgUp|PgDn|[A-Za-z0-9])"
)
KEYBIND = rf"{_MODIFIER}(?:[+-]{_MODIFIER})*[+-]{_KEY}"
# `$EDITOR`, `${var}`, `$XDG_CONFIG_HOME/aichat/roles/cli.md`.
VAR = rf"\$\{{?[A-Za-z_]\w*\}}?(?:(?:/{_SEG})*/{_LAST})?"
PATH = (
rf"(?:~|\.{{1,2}})/(?:{_SEG}/)*(?:{_LAST})?" # ~/… ./… ../…
rf"|/(?:{_SEG}/)+(?:{_LAST})?" # /etc/sudoers.d/nofail-toggle
rf"|(?:{_SEG}/)+[\w@+-][\w.@+-]*\.(?:{_EXT})" # conf.d/abbr.fish
rf"|[\w@+-][\w.@+-]*\.(?:{_EXT})" # config.fish
rf"|(?:{_SEG}\.)+{_SEG}/" # conf.d/
)
# `-a`, `--dry-run`, `--color=auto`. A bare `--` (this manual's ASCII em
# dash) never matches: a letter has to follow. A single-hyphen flag is
# capped at five characters and may not contain a hyphen, so a hyphenated
# compound continued across a conjunction ("filesystem-inspection and
# -modification") is not mistaken for one.
FLAG = r"--[A-Za-z][\w-]*(?:=[\w.,:/@+-]+)?|-[A-Za-z][A-Za-z0-9]{0,4}(?:=[\w.,:/@+-]+)?"
# `XDG_CONFIG_HOME`, `NO_TMUX=1`. An underscore is required, so ordinary
# acronyms (`URL`, `AGPL`, `TCP`) are never touched.
ENVVAR = r"[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+(?:=[\w.,:/@+-]+)?"
# snake_case: `__fish_config_op_aliases`, `_fdc_bins`, `fish_greeting`,
# `prompt_pwd`, `expand_bang_*`. An internal underscore is required, which
# is also what keeps `_emphasised_` markdown out of the match.
IDENT = r"_{0,2}[a-z][a-z0-9]*(?:_(?:[a-z0-9]+|\*))+"
# Regions that are already code, or are markup rather than prose. `url`
# also covers `git@host:owner/repo.git` and `ssh://…`, whose scheme would
# otherwise be read as a bare command name.
PROTECTED = (
r"(?P<code>``+.+?``+|`[^`\n]*`)"
r"|(?P<link>\[[^\]\n]*\]\([^)\n]*\))"
r"|(?P<url>[A-Za-z][\w+.-]*://\S+|[\w.-]+@[\w.-]+(?::\S+)?)"
r"|(?P<tag></?[A-Za-z][^>\n]*?/?>)"
)
ARROW = r"(?:->|→|=>)"
# Shortest comma run that reads as a list of tools rather than as prose.
MIN_RUN_NAMES = 3
# `, and` must be tried before a bare `,` so the conjunction is a separator
# and not an item.
RUN_SPLIT = r",?\s+(?:and|or)\s+|,\s*"
RUN_SPLIT_RE = re.compile(RUN_SPLIT)
# A command name inside a chain is followed by `->`, so the usual "no
# trailing hyphen" guard has to make room for exactly that.
_CMD_END = r"(?!\w)(?!-(?!>))"
def _alternation(names) -> str:
"""Regex alternation over names, longest first so `rg` can't beat `rga`."""
if not names:
return r"(?!)"
return "|".join(re.escape(n) for n in sorted(names, key=lambda s: (-len(s), s)))
def _atom(vocab: Vocabulary) -> str:
cmd = rf"(?:{_alternation(vocab.strict)})(?![\w-])"
return rf"(?:{KEYBIND}|{VAR}|{PATH}|{FLAG}|{ENVVAR}|{IDENT}|{cmd})"
@functools.lru_cache(maxsize=4)
def _scanner(vocab: Vocabulary) -> re.Pattern:
"""The single pass over a line: protected regions plus wrappable tokens."""
full = rf"(?:{_alternation(vocab.full)})"
chain_link = rf"(?:{full}{_CMD_END}|{VAR})"
name = rf"{full}(?![\w-])"
return re.compile(
PROTECTED
# `ls->eza, cat->bat`: a shadow chain. Position makes even an
# ambiguous name unmistakably a command here.
+ rf"|(?P<chain>{BEFORE}{chain_link}(?:\s*{ARROW}\s*{chain_link})+{AFTER})"
# `cargo, starship, uv, zoxide`: a run of nothing but tool names.
+ rf"|(?P<run>{BEFORE}{name}(?:,\s*{name})+"
+ rf"(?:,?\s+(?:and|or)\s+{name})?{AFTER})"
# `-a/--all`: slash-joined atoms, each wrapped on its own.
+ rf"|(?P<group>{BEFORE}{_atom(vocab)}(?:/{_atom(vocab)})*{AFTER})"
)
@functools.lru_cache(maxsize=4)
def _atom_re(vocab: Vocabulary) -> re.Pattern:
return re.compile(_atom(vocab))
# ---------------------------------------------------------------------------
# Table cells that are whole command lines
# ---------------------------------------------------------------------------
# The abbreviation tables' second column is an expansion, not a sentence:
# `sudo -s`, `cd ../..`, `journalctl -p 3 -xb`. Wrapping only the flag would
# leave a bare `sudo` in front of a code span; the cell wants to be one span.
#
# Whether a column holds command lines is decided for the column as a whole
# -- one cell is far too little evidence, as `zoxide frecency-based
# navigation` (prose, in a column of prose) and `docker context ls` (a
# command, in a column of commands) open identically.
CELL_TOKEN_RE = re.compile(r"^[\w$~./=:;@+*?%'\"-]+$")
CELL_NAME_RE = re.compile(r"^[a-z][\w.+-]*$")
CELL_OPERATORS = frozenset((r"\|", "|", "&&", "||", ">", ">>", "<", ";"))
MAX_CELL_TOKENS = 8
COMMAND_COLUMN_RATIO = 0.7
MIN_COMMAND_COLUMN_ROWS = 3
def _cell_tokens(cell: str) -> list[str] | None:
"""Tokenise a cell that could be a command line, or None if it can't be."""
text = cell.strip()
if not text or any(c in text for c in "`<([)]"):
return None
tokens = text.split()
if not (1 <= len(tokens) <= MAX_CELL_TOKENS):
return None
for token in tokens:
if token in CELL_OPERATORS:
continue
if not CELL_TOKEN_RE.match(token):
return None
if token[:1].isupper() or token.lower() in STOPWORDS:
return None
return tokens
def _is_command_cell(cell: str, vocab: Vocabulary) -> bool:
"""True when a cell in a command column really is one command line."""
tokens = _cell_tokens(cell)
if tokens is None:
return False
return tokens[0] in vocab.full or bool(CELL_NAME_RE.match(tokens[0]))
def _opens_with_command(cell: str, vocab: Vocabulary) -> bool:
"""The per-cell evidence the column vote is counted from."""
tokens = _cell_tokens(cell)
return tokens is not None and tokens[0] in vocab.full
# ---------------------------------------------------------------------------
# Line classification
# ---------------------------------------------------------------------------
HEADING_RE = re.compile(r"^\s{0,3}#{1,6}\s")
TABLE_ROW_RE = re.compile(r"^\s*\|.*\|\s*$")
TABLE_RULE_RE = re.compile(r"^\s*\|[\s:|-]+\|\s*$")
IMPORT_RE = re.compile(r"^\s*import\s")
FILE_TREE_OPEN = "<FileTree"
FILE_TREE_CLOSE = "</FileTree>"
CELL_SPLIT_RE = re.compile(r"(?<!\\)\|")
# A four-space indent is this manual's code block. The site never sees one
# -- prettify() has already turned it into a fence by the time this module
# runs -- but build_concat() keeps the indented form, because that is what
# pandoc and `config-help` want, and its contents are code that must not be
# rewritten: the table of contents alone would otherwise have `ov`, `bat`,
# `less` and `cat` wrapped inside a code block.
INDENTED_CODE = " "
def _skip_line(line: str) -> bool:
"""True for a line that must be passed through untouched.
Headings are excluded because Starlight derives anchors -- and this
pipeline derives `LinkCard` hrefs -- from their text. A line opening
with `<` is component markup, whose attributes are JSX, not markdown.
"""
stripped = line.strip()
return bool(
not stripped
or HEADING_RE.match(line)
or IMPORT_RE.match(line)
or stripped.startswith("<")
or TABLE_RULE_RE.match(line)
)
def _row_cells(line: str) -> list[str]:
return CELL_SPLIT_RE.split(line)
def _command_columns(rows: list[str], vocab: Vocabulary) -> set[int]:
"""Which column indices of one table hold command lines rather than prose."""
votes: dict[int, list[int]] = {}
for line in rows:
if TABLE_RULE_RE.match(line):
continue
for index, cell in enumerate(_row_cells(line)):
if not cell.strip() or "`" in cell:
continue
votes.setdefault(index, []).append(_opens_with_command(cell, vocab))
return {
index
for index, seen in votes.items()
if len(seen) >= MIN_COMMAND_COLUMN_ROWS
and sum(seen) / len(seen) >= COMMAND_COLUMN_RATIO
}
# ---------------------------------------------------------------------------
# The pass
# ---------------------------------------------------------------------------
# Spans this pass creates are marked, not back-ticked, until the very end:
# adjacent ones are merged (`eza` `-l` `-a` -> `eza -l -a`), and only spans
# this pass created may take part in that.
MARK = "\x01"
MERGE_RE = re.compile(rf"{MARK} {MARK}")
def _mark(text: str) -> str:
return f"{MARK}{text}{MARK}"
def _wrap_atoms(text: str, atom_re: re.Pattern) -> str:
"""Mark each atom of a slash-joined group, keeping the separators.
Rescanning the group rather than capturing during the first match keeps
the grammar readable; the round-trip check makes that shortcut safe --
if the rescan disagrees with the original match, nothing is changed.
"""
wrapped = atom_re.sub(lambda m: _mark(m.group(0)), text)
if wrapped.replace(MARK, "") != text:
return text
return wrapped
def _wrap_split(text: str, separator: str) -> str:
"""Mark each item of a separated run, keeping the separators."""
parts = re.split(rf"({separator})", text)
return "".join(p if i % 2 else _mark(p) for i, p in enumerate(parts))
def _transform(text: str, scanner: re.Pattern, atom_re: re.Pattern, vocab: Vocabulary) -> str:
def repl(m: re.Match) -> str:
group = m.lastgroup
if group == "chain":
return _wrap_split(m.group(0), rf"\s*{ARROW}\s*")
if group == "run":
# A long run anchored by at least one unambiguous tool name is
# a list of commands; two names, one of them a word like
# `function`, is a sentence.
names = RUN_SPLIT_RE.split(m.group(0))
if len(names) < MIN_RUN_NAMES or not any(
n in vocab.strict for n in names
):
# Not a list after all -- hand the text back to the
# ordinary token rules rather than swallowing it.
return _wrap_atoms(m.group(0), atom_re)
return _wrap_split(m.group(0), RUN_SPLIT)
if group == "group":
return _wrap_atoms(m.group(0), atom_re)
return m.group(0)
return scanner.sub(repl, text)
def _transform_line(
line: str,
scanner: re.Pattern,
atom_re: re.Pattern,
vocab: Vocabulary,
command_columns: set[int],
) -> str:
if not command_columns:
return _transform(line, scanner, atom_re, vocab)
out = []
for index, cell in enumerate(_row_cells(line)):
if index in command_columns and _is_command_cell(cell, vocab):
body = cell.strip()
lead = cell[: len(cell) - len(cell.lstrip())]
trail = cell[len(cell.rstrip()) :]
out.append(f"{lead}{_mark(body)}{trail}")
else:
out.append(_transform(cell, scanner, atom_re, vocab))
return "|".join(out)
def _finish(line: str) -> str:
"""Merge abutting new spans, then turn the marks into backticks."""
return MERGE_RE.sub(" ", line).replace(MARK, "`")
def add_code_spans(text: str, vocab: Vocabulary = EMPTY_VOCABULARY) -> str:
"""Wrap code-shaped tokens in `text` in inline code spans.
`text` is a rendered page body (no frontmatter). Fenced blocks,
indented code blocks, `<FileTree>` bodies, headings, component markup,
existing code spans, link targets and URLs are left exactly as they
are.
"""
scanner = _scanner(vocab)
atom_re = _atom_re(vocab)
lines = text.split("\n")
eligible = [False] * len(lines)
in_fence = False
in_tree = False
for i, line in enumerate(lines):
if FENCE_RE.match(line):
in_fence = not in_fence
continue
if in_fence:
continue
if FILE_TREE_OPEN in line:
in_tree = True
if in_tree:
if FILE_TREE_CLOSE in line:
in_tree = False
continue
if line.startswith(INDENTED_CODE):
continue
eligible[i] = not _skip_line(line)
# Command columns are a property of a whole table, so the contiguous
# runs of table rows are resolved before any line is rewritten.
columns: list[set[int]] = [set() for _ in lines]
start = None
for i, line in enumerate(lines + [""]):
is_row = i < len(lines) and eligible[i] and TABLE_ROW_RE.match(line)
if is_row and start is None:
start = i
elif not is_row and start is not None:
found = _command_columns(lines[start:i], vocab)
for j in range(start, i):
columns[j] = found
start = None
return "\n".join(
_finish(_transform_line(line, scanner, atom_re, vocab, columns[i]))
if eligible[i]
else line
for i, line in enumerate(lines)
)
+1017 -327
View File
File diff suppressed because it is too large Load Diff
+109 -10
View File
@@ -27,6 +27,7 @@ scrollback=## Scrollback History
__fish_scrollback_history_dir=## Scrollback History
__fish_scrollback_history_max_files=## Scrollback History
scrollback-dir=## Scrollback History
misc-vars=## Other
# ── Section 2: PATH ───────────────────────────────────────────
path=# 2. PATH SETUP
@@ -34,7 +35,6 @@ path=# 2. PATH SETUP
# ── Section 3: Key Bindings ───────────────────────────────────
keybindings=# 3. KEY BINDINGS
bindings=# 3. KEY BINDINGS
key-bindings=# 3. KEY BINDINGS
keys=# 3. KEY BINDINGS
fzf-bindings=## FZF Bindings (bundled from PatrickF1/fzf.fish)
fzf=## FZF Bindings (bundled from PatrickF1/fzf.fish)
@@ -67,6 +67,8 @@ cat=### cat
copy=### copy
du=### du
dusize=### dusize
dirs=### lD
lsdirs=### lD
ls=### ls
lsr=### lsr
lss=### lss
@@ -76,6 +78,7 @@ ltr=### ltr
lx=### lx
mkdir=### mkdir
mkcd=### mkcd
mv=### mv
poke=### poke
rm=### rm
rg=### rg
@@ -110,6 +113,7 @@ parur=### parur
deps=## 5.6 Dependency Management
fish-deps=### fish-deps
check-deps=### check_fish_deps
fzf-update=### fzf-update
system=## 5.7 System and Monitoring
top=### top
swapstat=### swapstat
@@ -125,19 +129,33 @@ split=### split
spwin=### spwin
detach=### detach
bkg=### bkg
fish-mode-prompt=### fish_mode_prompt
fish-prompt=### fish_prompt
fish-right-prompt=### fish_right_prompt
right-prompt=### fish_right_prompt
jobrunner=### jobrunner
jr=### jr
ssh=### ssh
clipboard=## 5.9 Clipboard
copy-fn=### y
p=### p
paste=### paste
network=## 5.10 Network
fast=### fast
fast-cli=### fast-cli
speedtest=### fast-cli
gip=### gip
gip4=### gip4
ipv4=### gip4
gip6=### gip6
ipv6=### gip6
ping=### ping
qr=### qr
logging=## 5.11 Pager and Logging
logs=### logs
smart-exit=### smart_exit
sponge-filter-secrets=### sponge_filter_secrets
secret-filter=### sponge_filter_secrets
ai=## 5.12 AI and Developer Tools
antigravity-ide=### antigravity-ide
agy=### agy
@@ -152,28 +170,52 @@ devlogs=### agents-init
claude-cli=### claude
claude-docs=### claude-docs
claude-pr=### claude-pr
dops=### dops
docker-ps=### dops
qc=### qc
quick-chat=### qc
aichat=### qc
superpowers=### superpowers
media=## 5.13 Media and Utilities
dng2avif=### dng2avif
play-media=### play-media
video=### play-media
audio=### play-media
mpv=### play-media
vlc=### play-media
spark=### spark
steam-dl=### steam-dl
steam=### steam-dl
yt-dlp=### yt-dlp
miscfns=## 5.14 Miscellaneous
bash=### bash
bd-pull=### bd-pull
beads=### bd-pull
cffetch=### cffetch
fastfetch=### ffetch
neofetch=### cffetch
sysinfo=### cffetch
cheat=### cheat
config-help=### config-help
config-update=### config-update
config-settings=### config-settings
config-toggle=### config-toggle
toggle=### config-settings
dockup=### dockup
fetch=### cffetch
ffetch=### ffetch
fzf_configure_bindings=### fzf_configure_bindings
joplin=### joplin
ld=### ld
lazydocker=### ld
open-url=### open-url
url-open=### open-url
repo-open=### repo-open
open-repo=### repo-open
config-update=### config-update
config-settings=### config-settings
config-toggle=### config-settings
toggle=### config-settings
bash=### bash
cheat=### cheat
dockup=### dockup
joplin=### joplin
rand_string=### rand_string
random=### rand_string
password=### rand_string
randomword=### rand_string
replay=### replay
tmux=### tmux-clean
wake-lock=### wake-lock
@@ -208,6 +250,7 @@ pager-hierarchy=## Pager Hierarchy
shell-aliases=## 4.11 Shell Aliases
kitty-logging=### kitty-logging
watcher=### kitty-logging
kitty-scrollback=### Scrollback History
# ── Section 6: Dependency Catalog ────────────────────────────
catalog=# 6. DEPENDENCY CATALOG
@@ -215,6 +258,8 @@ deps-catalog=# 6. DEPENDENCY CATALOG
required=## Required
integrations=## Integrations
recommended=## Recommended
optional-deps=## Optional
terminal-emulators=## Terminal Emulators
install-methods=## Install Methods
# ── Section 7: Customization ──────────────────────────────────
@@ -231,19 +276,47 @@ minimal=## Opinionated Components (Minimal Mode)
minimal-mode=## Opinionated Components (Minimal Mode)
opt-out=## Opinionated Components (Minimal Mode)
toggles=## Opinionated Components (Minimal Mode)
agent-vault=## Agent Memory Vault
__fish_agent_vault_dir=## Agent Memory Vault
__fish_agent_vault_autopush=## Agent Memory Vault
component-reference=# 8. COMPONENTS REFERENCE
components=# 8. COMPONENTS REFERENCE
c0=## Per-function overrides: `C0`/`always`
always-tag=## Per-function overrides: `C0`/`always`
c1=## C1 — Command Shadows
command-shadows=## C1 — Command Shadows
aliases-detail=## C1 — Command Shadows
filesystem=### filesystem
network-shadow=### network
monitor=### monitor
shell-tools=### shell-tools
dev-tools=### dev-tools
c2=## C2 — Startup Side-Effects
autoexec=## C2 — Startup Side-Effects
startup=## C2 — Startup Side-Effects
plugin-management=### plugin-management
pkg-wrappers=### pkg-wrappers
venv-toggle=### venv
telemetry=### telemetry
sync=### sync
c3=## C3 — Key and Environment Overrides
overrides-detail=## C3 — Key and Environment Overrides
bang-bang=## C3 — Key and Environment Overrides
bindings-toggle=### key-bindings
environment=### environment
prompt-toggle=### prompt
privacy=### privacy
privacy-toggle=### privacy
do-not-track=### privacy
disable-telemetry=### privacy
c4=## C4 — Terminal and Tool Integration
integrations-detail=## C4 — Terminal and Tool Integration
terminal-abbrs=### terminal-abbrs
window-mgmt=### window-mgmt
window-management=### window-mgmt
notifications-toggle=### notifications
history-logs=### history-logs
pkg-upgrade=### pkg-upgrade
c5=## C5 — Logging and Capture
logging-detail=## C5 — Logging and Capture
logging-sentinel=## C5 — Logging and Capture
@@ -252,11 +325,17 @@ zellij-logging=## C5 — Logging and Capture
tmux-logging=## C5 — Logging and Capture
pipe-pane=## C5 — Logging and Capture
dump-screen=## C5 — Logging and Capture
terminal-capture=### terminal-capture
multiplexer-capture=### multiplexer-capture
pkg-logs=### pkg-logs
c6=## C6 — Greeting and First-Run UI
greeting=## C6 — Greeting and First-Run UI
first-run=### first-run
greeting-message=### greeting-message
# ── Prompt and Theme ──────────────────────────────────────────
prompt-theme=## Prompt and Theme
prompt=## Prompt and Theme
starship=### Starship
fallback-prompt=### Catppuccin Fallback Prompt
catppuccin-prompt=### Catppuccin Fallback Prompt
@@ -316,11 +395,31 @@ minimal-trouble=## What's with the C1-C6 stuff?
viewing=# 13. VIEWING THIS MANUAL
manual=# 13. VIEWING THIS MANUAL
ov=## In the terminal
man-page=## As a man page
manpage=## As a man page
jump=## In the terminal
html=## The documentation website
browser=## The documentation website
site=## The documentation website
source=## Reading the source directly
raw-source=## Reading the source directly
# ── Section 14: Testing ───────────────────────────────────────
testing=# 14. TESTING
tests=# 14. TESTING
# ── Section 15: Contributing ──────────────────────────────────
contributing=# 15. CONTRIBUTING
contribute=# 15. CONTRIBUTING
forge=# 15. CONTRIBUTING
# ── Section 16: Attribution ───────────────────────────────────
attribution=# 16. ATTRIBUTION
credits=# 16. ATTRIBUTION
# ── Section 17: License ───────────────────────────────────────
license=# 17. LICENSE
licensing=# 17. LICENSE
agpl=# 17. LICENSE
copyright=# 17. LICENSE
+807 -236
View File
File diff suppressed because it is too large Load Diff
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Generate the committed opinionated-component registry.
Walks every `# COMPONENT` header in functions/*.fish, conf.d/*.fish, and
config.fish and writes conf.d/__fish_config_op_registry.fish, the fish
data file __fish_config_op_registry_lookup reads at shell startup.
Run manually (via __fish_config_op_registry_rebuild) after editing a
# COMPONENT header, and automatically as a pre-step in build-manual.py
before the manual is built.
"""
import sys
from pathlib import Path
import manualtools as mt
DOCS = Path(__file__).parent
REPO = DOCS.parent
OUTPUT = REPO / "conf.d" / "__fish_config_op_registry.fish"
def collect_components() -> dict[str, list[str]]:
"""Gather every `# COMPONENT` header across the whole repo.
Concatenates raw component lines when the same identity appears in
more than one source (e.g. functions/auto-pull.fish and
conf.d/auto-pull.fish both self-identify as "auto-pull" at runtime,
since the guard can only ever look up the bare status
current-function/basename string) rather than letting one silently
overwrite the other.
"""
out: dict[str, list[str]] = {}
for source in (
mt.parse_components(REPO / "functions"),
mt.parse_components(REPO / "conf.d"),
mt.parse_component_file(REPO / "config.fish"),
):
for identity, lines in source.items():
out.setdefault(identity, []).extend(lines)
return out
def build_registry(components: dict[str, list[str]]) -> tuple[dict[str, list[str]], list[str]]:
"""Turn {identity: [raw COMPONENT lines]} into ({"identity:site": [tags]}, warnings).
A site with both always/on and always/off tagged is a contradiction:
both are stripped and a warning is emitted, but generation continues
-- any other real tag on that same site survives. A site whose
effective tag set is empty after stripping produces no registry entry
at all, which __fish_config_op_enabled already treats as always/on
(fail-open) at guard time -- see spec §4.5.
"""
registry: dict[str, list[str]] = {}
warnings: list[str] = []
for identity, raw_lines in components.items():
by_site: dict[str, list[str]] = {}
for site, tag in mt.parse_component_lines(raw_lines):
by_site.setdefault(site, []).append(tag)
for site, tags in by_site.items():
if "always/on" in tags and "always/off" in tags:
label = identity if not site else f"{identity}:{site}"
warnings.append(
f"{label}: both always/on and always/off tagged; ignoring both"
)
tags = [t for t in tags if t not in ("always/on", "always/off")]
if tags:
registry[f"{identity}:{site}"] = list(dict.fromkeys(tags))
return registry, warnings
def render(registry: dict[str, list[str]]) -> str:
keys = sorted(registry)
lines = [
"# Copyright (C) 2026 Rootiest",
"# SPDX-License-Identifier: AGPL-3.0-or-later",
"#",
"# GENERATED FILE --- do not edit by hand.",
"# Regenerate with __fish_config_op_registry_rebuild after editing a",
"# # COMPONENT header, or automatically via docs/build-manual.py.",
"# Source: docs/generate_component_registry.py",
"#",
"# This file must be sourced before any other conf.d/*.fish file that",
"# calls the opinionated guard. That currently holds only because fish's",
"# glob-based conf.d loading happens to sort this filename first",
"# alphabetically among the guard-calling files -- do not rename it",
"# without preserving that ordering.",
"",
]
if not keys:
lines.append("set -g __fish_config_op_registry_keys")
lines.append("set -g __fish_config_op_registry_values")
return "\n".join(lines) + "\n"
quoted_keys = [f'"{k}"' for k in keys]
lines.append("set -g __fish_config_op_registry_keys \\")
lines += [f" {k} \\" for k in quoted_keys[:-1]] + [f" {quoted_keys[-1]}"]
lines.append("")
values = ['"' + " ".join(registry[k]) + '"' for k in keys]
lines.append("set -g __fish_config_op_registry_values \\")
lines += [f" {v} \\" for v in values[:-1]] + [f" {values[-1]}"]
lines.append("")
return "\n".join(lines) + "\n"
def main() -> int:
components = collect_components()
registry, warnings = build_registry(components)
for w in warnings:
print(f" WARN {w}", file=sys.stderr)
OUTPUT.write_text(render(registry))
print(f"wrote {OUTPUT} ({len(registry)} entries)", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).parent))
raise SystemExit(main())
+1 -46
View File
@@ -7,51 +7,6 @@ sidebar:
order: 4
---
1. Configuration Variables
2. PATH Setup
3. Key Bindings
4. Abbreviations
4.1 Editors
4.2 Navigation and Listing
4.3 Git
4.4 Terminal Windows, Tabs, and Panes
4.5 Chezmoi
4.6 Docker
4.7 Systemctl
4.8 AI Assistants
4.9 History Expansion
4.10 Miscellaneous
4.11 Shell Aliases
5. Functions Reference
5.1 File and Directory
5.2 Navigation
5.3 Editors and Viewers
5.4 Git and Version Control
5.5 Package Management
5.6 Dependency Management
5.7 System and Monitoring
5.8 Terminal Management
5.9 Clipboard
5.10 Network
5.11 Pager and Logging
5.12 AI and Developer Tools
5.13 Media and Utilities
5.14 Miscellaneous
6. Dependency Catalog
7. Customization
8. Fisher Plugins
9. Installation
10. Personalization
11. Troubleshooting
11.1 Uninstalling and Reverting to Backup
11.2 Fish Version Requirement
11.3 Enable or Disable Session Logging
11.4 Change or Disable the Greeting
11.5 Secrets and Machine-Local Configuration
11.6 Tool Init Does Nothing (Return Sentinel)
11.7 Missing Dependencies
11.8 Vi Mode Keybindings
11.9 What's with the C1-C6 stuff?
12. Viewing This Manual
<!-- GENERATED: toc -->
---
+5 -3
View File
@@ -42,9 +42,11 @@ are active in Insert, Normal, and Visual modes unless noted.
pressing Enter a second time for certain fast-path
commands (speedtest-fast, etc.).
@@ FZF inline picker. Type @@ anywhere on the command
line to open an fzf picker and insert a selection
at the cursor position.
@@ FZF inline picker. Type @ twice anywhere on the
command line to open an fzf picker and replace the
@@ with the selection. The @@ must be typed as its
own token: "cat @@" triggers it, but "cat@@" does
not.
Ctrl+Right Accept autosuggestion one word/directory segment
at a time. (Restores Fish 3.x behavior by binding
+104 -30
View File
@@ -145,6 +145,40 @@ NOTE:
- Disabled integration commands (spwin, tab, split, hist, logs, upgrade) print an error naming the variable that disabled them.
- On CachyOS, the distro fish config's own aliases, history override, and bang-bang bindings are stripped per category as well.
### Sub-categories
Each of the six categories further sub-divides into two to six
sub-categories, each with its own `__fish_config_op_<category>_<subcategory>`
variable (e.g. `__fish_config_op_aliases_filesystem`). These follow the
exact same truthy/falsy/unset cascade one level deeper: an explicit
sub-category value overrides the master switch and the parent category's
setting, and an unset sub-category inherits from its parent category (which
in turn inherits from `__fish_config_opinionated`). Run config-settings and
press Enter on a category row to browse and toggle its sub-categories
interactively. See [Components Reference](/08-components-reference/) for the
full sub-category breakdown of every category.
## Agent Memory Vault
__fish_agent_vault_dir
Overrides the agent memory vault location. Defaults to
$XDG_DATA_HOME/agent-vault (or ~/.local/share/agent-vault).
__fish_agent_vault_autopush
When set to 1, agents-vault also pushes on wrapper launch. Defaults to
off: the vault commits locally on every launch and pushes from the
Claude Code SessionEnd hook or an explicit agents-vault --push. That
push is synchronous, so with autopush on the pull and the push are
each capped at 20 seconds; an explicit --push is left uncapped.
NOTE:
With autopush off and no SessionEnd hook installed, backups accumulate
locally and never reach the remote. Run agents-vault --status to check
how far ahead the vault is.
## Prompt and Theme
@@ -154,11 +188,14 @@ The primary prompt is Starship, initialized by conf.d/starship.fish.
Configure it via ~/.config/starship.toml.
conf.d/starship.fish defines a fish_prompt wrapper that only activates when
starship is in PATH. It emits OSC 133;A (prompt start) immediately before
Starship renders and OSC 133;B (input start) immediately after, placing both
markers on the prompt line itself. This allows ov to use them as sticky
section headers when browsing scrollback logs. Without Starship, fish's
built-in prompt handles these markers automatically.
starship is in PATH and C3 overrides are enabled (see Opinionated
Components above). It emits OSC 133;A (prompt start) immediately before
Starship renders and OSC 133;B (input start) immediately after, placing
both markers on the prompt line itself. This allows ov to use them as
sticky section headers when browsing scrollback logs. It also prints a
blank line before the prompt, skipped in private mode or on a freshly
cleared screen. Without Starship, fish's built-in prompt handles these
markers automatically.
### Catppuccin Fallback Prompt
@@ -166,47 +203,84 @@ When Starship is absent or C3 overrides are disabled, a built-in nim-style
two-line prompt activates from functions/fish_prompt.fish. No external
dependencies — fish builtins only.
Layout:
Layout (a dim job line appears between the two rows for each running
background job):
┬─[user@host:~/path] (main)
│ nvim notes.md
╰─>$
Elements:
user Yellow (Catppuccin Yellow); red if root
@host Blue (local) or Teal (SSH)
~/path prompt_pwd abbreviation (Catppuccin Text)
(main) Current git branch in Catppuccin Pink; omitted outside repos
─[V:name] Active Python venv basename; omitted when none
─[N/I/R/V] Vi-mode indicator when vi bindings are active
┬─ / ╰─> Connector lines: Catppuccin Green on success, Red on failure
Segment Meaning
────────────────────────────────────────────────────────────────
user Yellow (Catppuccin Yellow); red if root
@host Blue (local) or Teal (SSH)
~/path prompt_pwd abbreviation (Catppuccin Text)
─[N/I/R/V/O] Vi-mode indicator (Normal/Insert/Replace/Visual/Operator);
shown only when vi or hybrid key bindings are active
─[V:name] Active Python venv basename; omitted when none
(main) Current git branch in Catppuccin Pink, with ↑/↓
upstream-tracking arrows when applicable;
omitted outside repos
┬─ / ╰─> Connector lines: Catppuccin Green on success,
Red on failure
The right prompt (fish_right_prompt.fish) always renders, regardless of C3
state. On failure it shows a red ✘ and the exit code; on success it shows
only the dim timestamp. When starship is installed and C3 is enabled, the
active Docker context is also shown (if non-default):
The right prompt (fish_right_prompt.fish) always renders, independently of
which left prompt is active:
✘ 1 󰡨 myctx Fri Jun 12 00:51:21 2026 ← failed, starship+C3 active
✘ 1 Fri Jun 12 00:51:21 2026 ← failed, fallback prompt
Fri Jun 12 00:51:21 2026 ← success (no ✘)
Segment Shown when
────────────────────────────────────────────────────────────────
✘ <code> The previous command exited non-zero (red)
󰡨 <context> docker and starship are both installed, C3
overrides are enabled, and the active Docker
context is set and non-default
<timestamp> Always (dim, Catppuccin Overlay0)
The exit-status and Docker segments are independent — for example, right
after a failing command with a non-default Docker context active:
✘ 1 󰡨 myctx Fri Jun 12 00:51:21 2026
A successful command with the same Docker context shows the segment too:
󰡨 myctx Fri Jun 12 00:51:21 2026
And without Starship (or with C3 disabled, or Docker not installed), only
the exit-status prefix and timestamp ever appear:
✘ 1 Fri Jun 12 00:51:21 2026
### FZF
FZF is themed to Catppuccin Mocha via FZF_DEFAULT_OPTS set in
integrations/fzf.fish. The colors applied:
FZF is themed to Catppuccin Mocha via FZF_DEFAULT_OPTS, set in
conf.d/theme.fish (opinionated; disabled by `__fish_config_op_overrides`,
see Opinionated Components above). The colors applied:
Background: #1E1E2E (base) #313244 (surface0)
Foreground: #CDD6F4 (text)
Highlights: #F38BA8 (red) #CBA6F7 (mauve) #B4BEFE (lavender)
Hex Role Catppuccin name
────────────────────────────────────────────────────────
#1E1E2E Background Base
#313244 Highlighted background Surface0
#45475A Selected background Surface1
#CDD6F4 Foreground Text
#F38BA8 Highlight / header Red
#CBA6F7 Info / prompt Mauve
#B4BEFE Marker Lavender
#F5E0DC Spinner / pointer Rosewater
#6C7086 Border Overlay0
To customize, override FZF_DEFAULT_OPTS in local.fish.
To customize, override FZF_DEFAULT_OPTS in local.fish — it is sourced after
conf.d/theme.fish on every session, so a `set -Ux FZF_DEFAULT_OPTS ...`
there always wins.
### Catppuccin Mocha Syntax Highlighting
The Catppuccin Mocha theme ships with this config in themes/ and is applied
on first run via `conf.d/first_run.fish`. Colors are stored in fish_variables
(universal). To switch variants, install a different theme from themes/:
automatically on first run via `conf.d/first_run.fish` (gated by
`__fish_config_op_autoexec`; see Opinionated Components above). Colors are
stored in fish_variables (universal). Three other bundled variants are
available in themes/ — Latte, Frappé, and Macchiato. To switch:
fish_config theme save "Catppuccin Latte"
fish_config theme choose "Catppuccin Latte"
`---`
---
@@ -31,3 +31,35 @@ all of these commands.
When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files
are permanently deleted, not trashed. There is no intermediate safety net.
## Sub-categories
`__fish_config_op_aliases` sub-divides into six sub-categories, each with
its own `__fish_config_op_aliases_<slug>` toggle:
## filesystem
`ls`, `cat`, `cd`, `du`, `mkdir`, `rm`, `mv`, and `cd`/zoxide navigation --
the everyday filesystem-inspection and -modification shadows.
## search
`rg`, with its Kitty hyperlink formatting.
## network
`ping`, `ssh`, and `yt-dlp` -- shadows that talk to the network.
## monitor
`top` -> `btop`.
## shell-tools
`bash` (XDG bashrc + `$SHELL` reset), `less` (`$PAGER` fallback chain),
and the `help config` interception.
## dev-tools
`claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor
launcher), plus `agy`.
@@ -43,3 +43,28 @@ branches, or repos without a remote. The handler fires once per repo entry
(not on every sub-directory `cd`). The registry is machine-local at
`$__fish_user_dots_path/auto-pull.list` (defaults to `~/.config/.user-dots/fish/auto-pull.list`) and is never committed.
## Sub-categories
`__fish_config_op_autoexec` sub-divides into five sub-categories, each
with its own `__fish_config_op_autoexec_<slug>` toggle:
## plugin-management
Fisher bootstrap on first run.
## pkg-wrappers
`paru`/`yay` wrapper generation.
## venv
Automatic Python virtualenv activation.
## telemetry
The WakaTime hook's startup bootstrap.
## sync
Auto-pull background fast-forward, and the user-dots convenience symlink.
@@ -24,6 +24,8 @@ all of them.
Catppuccin colors 30+ fish_color_* variables set to Mocha palette
FZF_DEFAULT_OPTS FZF themed to Catppuccin Mocha colors
Right prompt fish_right_prompt: exit code (on failure) + dim timestamp; always rendered; Docker context added when starship+C3 active
DO_NOT_TRACK=1 Universal telemetry opt-out for tools and AI agents
DISABLE_TELEMETRY=1 Telemetry opt-out for telemetry-aware CLIs
The bang-bang system spans `key_bindings.fish`, `abbr.fish`, `puffer.fish`, and
six `expand_bang_*.fish` functions. All are gated together — disabling C3
@@ -33,3 +35,28 @@ When C3 is disabled, `exit` falls back to `builtin exit` with no scrollback
capture, no Kitty IPC, and no file I/O on exit. The scrollback capture block
is independently controlled by C5 (see below).
## Sub-categories
`__fish_config_op_overrides` sub-divides into four sub-categories, each
with its own `__fish_config_op_overrides_<slug>` toggle:
## key-bindings
Vi mode, autopair, puffer key intercepts, bang-bang history expansion,
and `smart_exit`'s plain-exit path.
## environment
`$PATH`, `$PAGER`/`$EDITOR`/`$GPG_TTY`, and `$CDPATH`.
## prompt
Starship, the right prompt, Catppuccin syntax/prompt colors, and FZF
theming (`$FZF_DEFAULT_OPTS`) -- all driven by the same guard as a single
unit, not independently toggleable from each other.
## privacy
`$DO_NOT_TRACK` and `$DISABLE_TELEMETRY` environment variables for
telemetry opt-out across CLI tools, runtimes, and AI agents.
@@ -22,3 +22,28 @@ Disabled integration commands (`spwin`, `tab`, `split`, `hist`, `logs`, `upgrade
a colored error to stderr naming the variable that disabled them rather than
silently failing.
## Sub-categories
`__fish_config_op_integrations` sub-divides into five sub-categories,
each with its own `__fish_config_op_integrations_<slug>` toggle:
## terminal-abbrs
The Kitty/WezTerm abbreviation set.
## window-mgmt
`spwin`, `tab`, `split`.
## notifications
`done`'s completion notifications, and the WakaTime activity hook.
## history-logs
`hist`, `logs`.
## pkg-upgrade
`upgrade`.
@@ -18,16 +18,16 @@ CAUTION: This configuration is capable of silently recording terminal output and
Component What it captures
───────────────────────────────────────────────────────────────────────────
Scrollback capture Terminal session output saved to:
`~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log`
~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log
tmux pane capture Continuous pane stream via pipe-pane, saved to:
`~/.terminal_history/tmux_<session>-w<win>-p<pane>_YYYY-MM-DD_HH-MM-SS.log`
~/.terminal_history/tmux_<session>-w<win>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
zellij pane capture Pane scrollback snapshot on shell exit, saved to:
`~/.terminal_history/zellij_<session>-p<pane>_YYYY-MM-DD_HH-MM-SS.log`
~/.terminal_history/zellij_<session>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
paru wrapper All paru/AUR output captured to:
`~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log`
~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log
yay wrapper All yay/AUR output captured to:
`~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log`
Kitty watcher `watcher.py` captures scrollback when Kitty closes
~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log
Kitty watcher watcher.py captures scrollback when Kitty closes
NOTE: **Turning off logging does not delete any existing logs.**
They remain in `$SCROLLBACK_HISTORY_DIR` (defaults to: `~/.terminal_history/`)
@@ -58,7 +58,7 @@ Ctrl-D, or a logout), because that is when the fish_exit handler runs. It does
NOT capture when you close a pane or quit zellij through zellij itself:
- Closing a pane signals the shell and tears the pane down concurrently, so
even if the handler runs, `dump-screen` may find the pane buffer already
even if the handler runs, dump-screen may find the pane buffer already
gone.
- Quitting zellij kills the zellij server, and `dump-screen` needs a live
server to read from — there is nothing left to snapshot.
@@ -93,7 +93,7 @@ start, so it appears without any action on your part.
Disabling `__fish_config_op_logging` (or leaving it unset):
1. Creates the sentinel immediately in every open shell.
2. Removes `~/.local/bin/paru` and `~/.local/bin/yay` logging wrappers;
bare `/usr/bin/paru` and `/usr/bin/yay` are used instead.
bare /usr/bin/paru and /usr/bin/yay are used instead.
3. Kitty's `watcher.py` reads the sentinel on each save attempt and
skips capture — no Kitty restart required.
4. smart_exit stops saving scrollback logs.
@@ -112,3 +112,22 @@ Note: C3 and C5 compose independently. C3 controls whether the smart_exit
wrapper is active at all; C5 controls only the scrollback-capture block
inside it. With C3 disabled, exit is plain builtin exit regardless of C5.
## Sub-categories
`__fish_config_op_logging` sub-divides into three sub-categories, each
with its own `__fish_config_op_logging_<slug>` toggle (all still opt-in
by default, inherited from C5's own opt-in behavior -- see §3 of the
design spec):
## terminal-capture
Kitty watcher scrollback capture, and `smart_exit`'s logging-guard path.
## multiplexer-capture
tmux `pipe-pane` and zellij `dump-screen` capture.
## pkg-logs
`paru`/`yay` AUR log wrappers.
@@ -13,3 +13,16 @@ When C6 is disabled, no greeting is printed by this config. Any greeting
set by the distro or other configs runs normally — this config simply does
not override it.
## Sub-categories
`__fish_config_op_greeting` sub-divides into two sub-categories, each
with its own `__fish_config_op_greeting_<slug>` toggle:
## first-run
The first-run welcome banner.
## greeting-message
The per-session `fish_greeting` override.
+17 -2
View File
@@ -12,9 +12,24 @@ category variable.
Category Description
──────────────────────────────────────────────────────────────────────────
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (`rm`, `cp`) to be safe by default
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (rm, cp) to be safe by default
C2 [Startup Side-Effects](/08-components-reference/02-c2-startup-side-effects/) — Bootstraps Fisher, generates wrappers, auto-activates venvs
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides `cd`, sets Vi mode, binds `<CR>` to `smart_enter`
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides cd, sets Vi mode, binds <CR> to smart_enter
C4 [Integrations](/08-components-reference/04-c4-terminal-and-tool-integration/) — Kitty/Wezterm integrations, starship hooks, fzf theme
C5 [Logging and Capture](/08-components-reference/05-c5-logging-and-capture/) — Session logs, command duration
C6 [Greeting & First-Run UI](/08-components-reference/06-c6-greeting-and-first-run-ui/) — Custom startup banner
Each category further sub-divides into two to six sub-categories (25 in
total) with their own `__fish_config_op_<category>_<subcategory>` toggles
-- see that category's page for its sub-category list.
## Per-function overrides: `C0`/`always`
Every guarded function or file can also carry a reserved `always/on` or
`always/off` tag in its `# COMPONENT` header, independent of every C1-C6
category and sub-category toggle and invisible to `config-settings`. An
`always/off` tag disables that function unconditionally; an `always/on`
tag enables it unconditionally, ignoring the state of every other tagged
sub-category. This is a per-function escape hatch for cases too granular
or too idiosyncratic to justify a taxonomy entry -- edit the header
directly and run `__fish_config_op_registry_rebuild` to apply the change.
+31 -12
View File
@@ -27,13 +27,18 @@ The following plugins are fully managed by Fisher. Their files are installed
into the repo directory by Fisher and are listed in `.gitignore` — do not
commit them. Fisher installs and updates them automatically.
- `jorgebucaran/fisher` — Plugin manager itself
- `meaningful-ooo/sponge` — Remove failed commands from history
- [`jorgebucaran/fisher`](https://github.com/jorgebucaran/fisher) — Plugin manager itself
- [`meaningful-ooo/sponge`](https://github.com/meaningful-ooo/sponge) — Remove failed commands from history
## Sponge History Filtering
Sponge removes failed commands from history and, via conf.d/sponge_privacy.fish,
also filters privacy-sensitive commands through three layers:
also filters privacy-sensitive commands through three layers. Detection is
heuristic — pattern- and variable-name-based — so this reduces the risk of a
credential landing in persistent history; it is not a guarantee that no
secret can ever reach it, and it is not a substitute for rotating a
credential that gets typed in plaintext. Treat it as a safety net, not a
vault.
Layer 1 — Static patterns (universal, persistent across sessions):
Commands matching any of these structural signatures are never recorded:
@@ -58,6 +63,20 @@ Layer 3 — Per-command filter (sponge_filter_secrets):
Catches credentials in variables exported after login, such as tokens
sourced from a project .env file mid-session.
A match is actively deleted from history, not stored and redacted. Sponge
queues a matched command on `fish_postexec` and purges anything past
`sponge_delay` entries on the very next `fish_prompt`, immediately forcing
a `history save`. With this config's (upstream) defaults, that means a
matched command is gone from disk within about one prompt cycle — it is
not left sitting in persistent history for the rest of the session.
This timing depends on `sponge_purge_only_on_exit` staying `false`, which
is sponge's own default and is not overridden here. Turning it on defers
all purging to the `fish_exit` event instead of the next prompt — and
because `fish_exit` does not fire on a killed or crashed session, a
matched command purged only on exit can survive indefinitely if the shell
never exits cleanly. Leave this setting off.
To add your own persistent patterns:
set -U -a sponge_regex_patterns 'your-regex-here'
@@ -85,11 +104,11 @@ releases. Installing them through Fisher would overwrite these customizations.
Bundled components and their upstream origins:
- `catppuccin/fish``themes/` + `conf.d/theme.fish`
- `PatrickF1/fzf.fish``functions/_fzf_*.fish` + `conf.d/fzf.fish`
- `franciscolourenco/done``conf.d/done.fish`
- `jorgebucaran/autopair.fish``functions/_autopair_*.fish` + `conf.d/autopair.fish`
- `nickeb96/puffer-fish``functions/_puffer_fish_*.fish` + `conf.d/puffer.fish`
- [`catppuccin/fish`](https://github.com/catppuccin/fish)`themes/` + `conf.d/theme.fish`
- [`PatrickF1/fzf.fish`](https://github.com/PatrickF1/fzf.fish)`functions/_fzf_*.fish` + `conf.d/fzf.fish`
- [`franciscolourenco/done`](https://github.com/franciscolourenco/done)`conf.d/done.fish`
- [`jorgebucaran/autopair.fish`](https://github.com/jorgebucaran/autopair.fish)`functions/_autopair_*.fish` + `conf.d/autopair.fish`
- [`nickeb96/puffer-fish`](https://github.com/nickeb96/puffer-fish)`functions/_puffer_fish_*.fish` + `conf.d/puffer.fish`
Do not run `fisher install` for these — it will overwrite the customized
versions. To update their behavior, edit the relevant bundled files directly.
@@ -98,10 +117,10 @@ versions. To update their behavior, edit the relevant bundled files directly.
The `fish_plugins` file at the config root:
- `jorgebucaran/fisher` — Plugin manager itself
- `meaningful-ooo/sponge` — Remove failed commands from history
- [`jorgebucaran/fisher`](https://github.com/jorgebucaran/fisher) — Plugin manager itself
- [`meaningful-ooo/sponge`](https://github.com/meaningful-ooo/sponge) — Remove failed commands from history
To update all Fisher-managed plugins, run `fisher update` or `fish-deps
update` which calls it as its first step.
To update all Fisher-managed plugins, run `fisher update` or
`fish-deps update` which calls it as its first step.
---
+9 -2
View File
@@ -208,9 +208,9 @@ This configuration groups its opinionated behaviors into six categories (C1C6
Category Description
──────────────────────────────────────────────────────────────────────────
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (`rm`, `cp`) to be safe by default
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (rm, cp) to be safe by default
C2 [Startup Side-Effects](/08-components-reference/02-c2-startup-side-effects/) — Bootstraps Fisher, generates wrappers, auto-activates venvs
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides `cd`, sets Vi mode, binds `<CR>` to `smart_enter`
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides cd, sets Vi mode, binds <CR> to smart_enter
C4 [Integrations](/08-components-reference/04-c4-terminal-and-tool-integration/) — Kitty/Wezterm integrations, starship hooks, fzf theme
C5 [Logging and Capture](/08-components-reference/05-c5-logging-and-capture/) — Session logs, command duration
C6 [Greeting & First-Run UI](/08-components-reference/06-c6-greeting-and-first-run-ui/) — Custom startup banner
@@ -237,6 +237,13 @@ Re-enable everything:
set -Ue __fish_config_opinionated
Each category also has two to six sub-categories (e.g.
`__fish_config_op_aliases_filesystem`) that can be checked, disabled, or
reset the same way — `set -U __fish_config_op_<category>_<subcategory> off`
and `set -Ue __fish_config_op_<category>_<subcategory>` work identically to
the category-level recipes above, just one level more granular. See
[Components Reference](/08-components-reference/) for the full list.
For an interactive alternative to setting these variables by hand, run `config-settings`.
---
+13
View File
@@ -0,0 +1,13 @@
---
title: Testing
manTitle: 14. TESTING
sidebar:
order: 18
helpKeywords:
- testing
- tests
- test-suite
- run-tests
---
<!-- README: Testing -->
+15
View File
@@ -0,0 +1,15 @@
---
title: Contributing
manTitle: 15. CONTRIBUTING
sidebar:
order: 19
helpKeywords:
- contributing
- contribute
- pull-request
- fork
- issues
- forge
---
<!-- README: Contributing -->
+12
View File
@@ -0,0 +1,12 @@
---
title: Attribution
manTitle: 16. ATTRIBUTION
sidebar:
order: 20
helpKeywords:
- attribution
- credits
- zoxide
---
<!-- README: Attribution -->
+13
View File
@@ -0,0 +1,13 @@
---
title: License
manTitle: 17. LICENSE
sidebar:
order: 21
helpKeywords:
- license
- licensing
- agpl
- copyright
---
<!-- README: License -->
+72 -5
View File
@@ -62,6 +62,7 @@ HEADER_LABEL = re.compile(r"^#\s+([A-Z][A-Z ]*[A-Z])\s*$")
FUNC_DEF = re.compile(r"^\s*function\s+(\S+)")
SECTIONS = (
"CATEGORY",
"COMPONENT",
"DEPENDENCIES",
"SYNOPSIS",
"DESCRIPTION",
@@ -109,6 +110,19 @@ def _trailing_blanks(lines: list[str]) -> int:
return n
def _block_identity(path: Path, lines: list[str], end: int, blocks_count: int) -> str:
"""Resolve a header block's associated name.
A file carrying exactly one header is associated with its own stem, so
a `function` nested inside a `type -q` guard still resolves. A file
with several headers walks forward to the next `function` definition.
"""
if blocks_count == 1:
return path.stem
after = (m.group(1) for ln in lines[end:] if (m := FUNC_DEF.match(ln)))
return next(after, path.stem)
def parse_functions(root: Path) -> dict[str, dict[str, list[str]]]:
"""Parse the comment header above every documented public function.
@@ -128,11 +142,7 @@ def parse_functions(root: Path) -> dict[str, dict[str, list[str]]]:
lines = path.read_text(encoding="utf-8").split("\n")
blocks = _header_blocks(lines)
for end, sections in blocks:
if len(blocks) == 1:
name = path.stem
else:
after = (m.group(1) for ln in lines[end:] if (m := FUNC_DEF.match(ln)))
name = next(after, path.stem)
name = _block_identity(path, lines, end, len(blocks))
if name.startswith("_") or "CATEGORY" not in sections:
continue
out[name] = {
@@ -185,6 +195,63 @@ def parse_abbreviations(root: Path) -> dict[str, list[dict]]:
return out
SITE_LINE_RE = re.compile(r"^site\s+(\S+):\s*(\S+)$")
def parse_component_lines(lines: list[str]) -> list[tuple[str, str]]:
"""Parse raw `# COMPONENT` body lines into (site, tag) pairs.
A line of the form `site <slug>: <tag>` scopes <tag> to that site; a
bare `<tag>` line belongs to the default (unnamed) site, keyed "".
Blank lines are skipped.
"""
out: list[tuple[str, str]] = []
for line in lines:
line = line.strip()
if not line:
continue
m = SITE_LINE_RE.match(line)
if m:
out.append((m.group(1), m.group(2)))
else:
out.append(("", line))
return out
def _parse_component_blocks(path: Path) -> dict[str, list[str]]:
"""Parse every `# COMPONENT` header block in one file.
Unlike parse_functions, there is no `# CATEGORY` gate and no
underscore exclusion: component classification applies to every
function/script, public or private, documented in the manual or not
-- the registry needs to see every guarded identity, not just the
ones that appear in the public function reference.
"""
lines = path.read_text(encoding="utf-8").split("\n")
blocks = _header_blocks(lines)
out: dict[str, list[str]] = {}
for end, sections in blocks:
if "COMPONENT" not in sections:
continue
name = _block_identity(path, lines, end, len(blocks))
body = sections["COMPONENT"]
out[name] = body[: len(body) - _trailing_blanks(body)]
return out
def parse_component_file(path: Path) -> dict[str, list[str]]:
"""Parse `# COMPONENT` header block(s) in one specific file (e.g. config.fish)."""
return _parse_component_blocks(path)
def parse_components(root: Path) -> dict[str, list[str]]:
"""Parse `# COMPONENT` header blocks across every `*.fish` file under root."""
out: dict[str, list[str]] = {}
for path in sorted(root.glob("*.fish")):
out.update(_parse_component_blocks(path))
return out
def _sort_key(entry: Path) -> tuple:
"""Order by sidebar.order when present, else by filename. Stable."""
target = entry / "index.md" if entry.is_dir() else entry
+3
View File
@@ -3,6 +3,9 @@ dist/
# generated types
.astro/
# starlight-plugin-icons safelist cache
.starlight-icons/
# dependencies
node_modules/
+47
View File
@@ -20,12 +20,59 @@ python3 docs/build-manual.py --site
`docs/verify-manual.py` validates both sources before you build; run it
first if you've touched a header or a manual page.
## Inline code spans
Function headers are read as plain text (by `config-help`, by `funcsave`,
by anyone opening the `.fish` file), so they're authored without backticks
`-a/--all`, not `` `-a`/`--all` ``. `docs/codespans.py` puts the
backticks on at render time, as the last step of `prettify()`.
`build_concat()` runs the same pass, so the man page and `config-help`
mark code the way the site does rather than only where the SSOT happened
to backtick something by hand. `config-help` then renders those spans
bold and drops the delimiters, since a terminal pager would otherwise
show them as literal punctuation.
It recognises flags, `$vars`, `SCREAMING_SNAKE` env vars, snake_case
identifiers (`__fish_config_op_aliases`, `fish_greeting`), paths and
filenames, key chords (`Ctrl-R`), shadow chains (`ls->eza`), runs of tool
names (`btop, dust, duf, …`), whole command lines in a table column of
command lines, and command names it knows — the `_fdc_*` catalog in
`functions/_fish_deps_catalog.fish`, the `functions/` directory listing,
and a standard-command list in the module.
Names that also read as English (`find`, `top`, `screen`) are listed in
`AMBIGUOUS_COMMANDS` and are never wrapped on sight; they still count
where position already proves they're a command. Add to that list rather
than removing a rule if a wrap ever reads wrong.
Fenced blocks, indented blocks, existing code spans, headings, link
targets, URLs, component markup, and `<FileTree>` bodies are never
touched. Leaving a token alone is always the safe outcome, so every rule
bails out when it isn't sure.
Indented blocks matter only to the concat — `prettify()` has already
fenced them by the time the site is rendered — but there they are the
table of contents and every section 5 entry, which must stay verbatim.
## llms.txt
The [`starlight-llms-txt`](https://www.npmjs.com/package/starlight-llms-txt)
plugin emits `llms.txt`, `llms-full.txt`, and `llms-small.txt` alongside the
built pages — no configuration needed, it just walks the generated content.
## Icons
[`starlight-plugin-icons`](https://docs.rettend.me/starlight-plugin-icons)
+ [UnoCSS](https://unocss.dev) (`uno.config.ts`) render icons from any
[Iconify](https://icones.js.org) set as `i-<collection>:<name>` classes.
The Gitea link in the header uses it (see
`src/components/starlight/SocialIcons.astro`) to show the real Gitea logo
instead of Starlight's generic `code-branch` icon. Sidebar (`sidebar:
true`) and codeblock (`codeblock: true`) icon support are wired up in
`astro.config.mjs` but unused so far — see the plugin docs for the
`icon:` sidebar syntax if you want to add them.
## Development
```fish title="local dev server"
+54 -39
View File
@@ -1,5 +1,6 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import UnoCSS from 'unocss/astro';
import Icons from 'starlight-plugin-icons';
import starlightLinksValidator from 'starlight-links-validator';
import starlightCatppuccin from '@catppuccin/starlight';
import starlightLlmsTxt from 'starlight-llms-txt';
@@ -9,45 +10,59 @@ export default defineConfig({
prerenderConflictBehavior: 'ignore',
site: 'https://fish.rootiest.fyi',
integrations: [
starlight({
title: 'Rootiest Fish Config',
description: 'Reference manual for the rootiest fish configuration.',
favicon: '/favicon.svg',
logo: {
src: './src/assets/logo.svg',
UnoCSS(),
...Icons({
sidebar: true,
codeblock: true,
extractSafelist: true,
starlight: {
title: 'Rootiest Fish Config',
description: 'Reference manual for the rootiest fish configuration.',
favicon: '/favicon.svg',
logo: {
src: './src/assets/logo.svg',
},
social: [
{
icon: 'code-branch',
label: 'Gitea',
href: 'https://git.rootiest.dev/rootiest/fish-config',
},
{
icon: 'github',
label: 'GitHub',
href: 'https://github.com/rootiest/fish-config',
},
],
components: {
SocialIcons: './src/components/starlight/SocialIcons.astro',
},
head: [
{
tag: 'script',
content: 'document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("starlight-file-tree").forEach(tree => { tree.querySelectorAll("details").forEach((d, i) => { if (i !== 0) d.removeAttribute("open"); }); }); });',
},
],
plugins: [
starlightLinksValidator(),
starlightCatppuccin({
dark: { flavor: "mocha", accent: "green" },
light: { flavor: "latte", accent: "sky" },
}),
starlightLlmsTxt(),
],
expressiveCode: {
// Shiki ships both Catppuccin flavours; Starlight picks by the
// reader's colour scheme, matching the palette in catppuccin.css.
themes: ['catppuccin-mocha', 'catppuccin-latte'],
styleOverrides: {
borderRadius: '0.4rem',
borderColor: 'var(--sl-color-gray-5)',
codeFontSize: '0.875rem',
},
},
sidebar,
},
social: [
{
icon: 'code-branch',
label: 'Gitea',
href: 'https://git.rootiest.dev/rootiest/fish-config',
},
],
head: [
{
tag: 'script',
content: 'document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("starlight-file-tree").forEach(tree => { tree.querySelectorAll("details").forEach((d, i) => { if (i !== 0) d.removeAttribute("open"); }); }); });',
},
],
plugins: [
starlightLinksValidator(),
starlightCatppuccin({
dark: { flavor: "mocha", accent: "green" },
light: { flavor: "latte", accent: "sky" },
}),
starlightLlmsTxt(),
],
expressiveCode: {
// Shiki ships both Catppuccin flavours; Starlight picks by the
// reader's colour scheme, matching the palette in catppuccin.css.
themes: ['catppuccin-mocha', 'catppuccin-latte'],
styleOverrides: {
borderRadius: '0.4rem',
borderColor: 'var(--sl-color-gray-5)',
codeFontSize: '0.875rem',
},
},
sidebar,
}),
],
});
+1389 -1
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -12,9 +12,13 @@
"dependencies": {
"@astrojs/starlight": "^0.41.4",
"@catppuccin/starlight": "^2.1.0",
"@iconify-json/pajamas": "^1.2.15",
"@unocss/astro": "^66.7.5",
"astro": "^7.0.2",
"sharp": "^0.35.3",
"starlight-links-validator": "^0.25.2",
"starlight-llms-txt": "^0.11.0"
"starlight-llms-txt": "^0.11.0",
"starlight-plugin-icons": "^1.1.6",
"unocss": "^66.7.5"
}
}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,50 @@
---
import config from 'virtual:starlight/user-config';
import { Icon } from '@astrojs/starlight/components';
// Maps a social link's `label` to a UnoCSS iconify icon class, letting a
// link use an icon outside Starlight's built-in set (e.g. a real Gitea
// logo instead of the generic `code-branch` icon).
const customIcons: Record<string, string> = {
Gitea: 'i-pajamas:gitea',
};
const links = config.social || [];
---
{
links.length > 0 && (
<>
{links.map(({ label, href, icon }) => {
const customIcon = customIcons[label];
return (
<a href={href} rel="me" class="sl-flex">
<span class="sr-only">{label}</span>
{customIcon ? <span class={`social-icon ${customIcon}`} aria-hidden="true" /> : <Icon name={icon} size="1.5em" />}
</a>
);
})}
</>
)
}
<style>
@layer starlight.core {
a {
color: var(--sl-color-text-accent);
padding: 0.5em;
margin: -0.5em;
}
a:hover {
color: var(--sl-color-white);
}
.social-icon {
/* !important: the i-pajamas:* class comes from UnoCSS, which emits
unlayered CSS. Unlayered rules always win over anything in a
@layer regardless of specificity or source order, so a plain
override here is silently ignored no matter how it's written. */
width: 1.5rem !important;
height: 1.5rem !important;
}
}
</style>
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'unocss';
import { presetStarlightIcons } from 'starlight-plugin-icons/uno';
export default defineConfig({
presets: [presetStarlightIcons()],
});
+658 -6
View File
@@ -9,6 +9,7 @@ import sys
import tempfile
from pathlib import Path
import codespans
import manualtools as mt
# docs/build-manual.py follows this repo's hyphenated CLI-script naming
@@ -130,6 +131,38 @@ def _parsed_functions() -> dict[str, dict[str, list[str]]]:
return mt.parse_functions(Path(__file__).parent.parent / "functions")
def _parsed_components() -> dict[str, list[str]]:
import generate_component_registry as gcr
return gcr.collect_components()
_C0_TAGS = {"always/on", "always/off"}
_TAXONOMY_FILES = {
"aliases": "01-c1-command-shadows.md",
"autoexec": "02-c2-startup-side-effects.md",
"overrides": "03-c3-key-and-environment-overrides.md",
"integrations": "04-c4-terminal-and-tool-integration.md",
"logging": "05-c5-logging-and-capture.md",
"greeting": "06-c6-greeting-and-first-run-ui.md",
}
def _load_taxonomy() -> dict[str, set[str]]:
"""{category: {sub-category slugs}}, parsed from `## <slug>` headings
in each category's docs/manual/08-components-reference/ file."""
ref_root = Path(__file__).parent / "manual" / "08-components-reference"
taxonomy: dict[str, set[str]] = {}
for category, filename in _TAXONOMY_FILES.items():
_, body = mt.parse(ref_root / filename)
taxonomy[category] = {
m.group(1)
for ln in body.split("\n")
if (m := re.match(r"^## ([a-z][a-z0-9-]*)$", ln))
}
return taxonomy
def test_every_categorised_function_produces_one_entry():
import build_manual
@@ -184,6 +217,36 @@ def test_dependencies_resolve():
assert not dangling, "unresolvable # DEPENDENCIES:\n " + "\n ".join(dangling)
def test_every_component_resolves_to_a_taxonomy_entry():
"""Every non-C0 # COMPONENT tag must resolve to a documented sub-category."""
taxonomy = _load_taxonomy()
unknown = []
for identity, raw_lines in _parsed_components().items():
for site, tag in mt.parse_component_lines(raw_lines):
if tag in _C0_TAGS:
continue
if "/" not in tag:
unknown.append(f"{identity}: malformed tag {tag!r}")
continue
category, subcat = tag.split("/", 1)
if category not in taxonomy or subcat not in taxonomy[category]:
unknown.append(f"{identity}: {tag}")
assert not unknown, "# COMPONENT tags with no taxonomy entry:\n " + "\n ".join(unknown)
def warn_c0_tags_never_combine_with_contradiction():
"""Warn -- never fail -- on always/on + always/off contradictions this
repo's own generator would warn about -- this is a direct repo-content
check, independent of running the generator, so CI surfaces them even
if someone forgets to regenerate. Warnings are non-fatal (spec §4.5);
this exists to print them prominently in CI output."""
from generate_component_registry import build_registry
_, warnings = build_registry(_parsed_components())
for w in warnings:
print(f" WARN {w}")
def warn_public_functions_without_category():
"""Warn — never fail — on a public function carrying no `# CATEGORY`.
@@ -205,6 +268,63 @@ def warn_public_functions_without_category():
print(" " + ", ".join(orphans))
# The guard's own supporting infrastructure: these files' bodies (function
# signature, SYNOPSIS/EXAMPLE prose) legitimately contain the literal text
# "__fish_config_op_enabled" without being a *caller* of the guard, so they
# are permanently exempt from warn_functions_without_component's substring
# check below.
_GUARD_INFRA_FILES = {
"__fish_config_op_enabled.fish",
"__fish_config_op_cascade.fish",
"__fish_config_op_registry_lookup.fish",
}
def warn_functions_without_component():
"""Warn -- never fail -- on a documented function calling the
opinionated guard but carrying no `# COMPONENT` section.
Mirrors warn_public_functions_without_category: a function that never
opted into the header convention at all (no # SYNOPSIS) is silently
out of scope, matching spec §4.5's fail-open tiering.
"""
repo = Path(__file__).parent.parent
components = _parsed_components()
orphans = []
for p in list((repo / "functions").glob("*.fish")) + list((repo / "conf.d").glob("*.fish")):
if p.name in _GUARD_INFRA_FILES:
continue
text = p.read_text(encoding="utf-8")
if "__fish_config_op_enabled" not in text or "# SYNOPSIS" not in text:
continue
if p.stem not in components:
orphans.append(str(p.relative_to(repo)))
if orphans:
print(f" WARN {len(orphans)} function(s) call the opinionated guard but lack # COMPONENT:")
print(" " + ", ".join(sorted(orphans)))
def warn_unused_taxonomy_entries():
"""Warn -- never fail -- on a documented sub-category with zero tagged functions."""
taxonomy = _load_taxonomy()
used: dict[str, set[str]] = {c: set() for c in taxonomy}
for raw_lines in _parsed_components().values():
for _site, tag in mt.parse_component_lines(raw_lines):
if tag in _C0_TAGS or "/" not in tag:
continue
category, subcat = tag.split("/", 1)
if category in used:
used[category].add(subcat)
unused = [
f"{category}/{subcat}"
for category, subcats in taxonomy.items()
for subcat in sorted(subcats - used[category])
]
if unused:
print(f" WARN {len(unused)} taxonomy entr{'y has' if len(unused) == 1 else 'ies have'} zero tagged functions:")
print(" " + ", ".join(unused))
def _without_section_5(text: str) -> str:
"""Drop `# 5. FUNCTIONS REFERENCE` through the start of section 6.
@@ -351,8 +471,8 @@ def test_prettify_splits_an_entry_block():
"option table was not converted to a markdown table"
)
assert (
"\nFalls back to /usr/bin/rm when trash is unavailable." in out
), "trailing prose stayed indented"
"\nFalls back to `/usr/bin/rm` when trash is unavailable." in out
), "trailing prose stayed indented (or lost its path code span)"
def test_as_table_converts_option_blocks():
@@ -507,19 +627,42 @@ def test_as_ruled_table_rejects_ambiguous_columns():
def test_prettify_leaves_reference_tables_alone():
"""Column-aligned blocks are data, not shell, and must not be fenced."""
"""Column-aligned blocks too small for a real table are data, not shell,
and must not get shell syntax highlighting but they still need SOME
fence, since indentation alone doesn't survive MDX (see
test_prettify_fallback_fences_instead_of_indenting)."""
import build_manual
table = " XDG_CONFIG_HOME ~/.config\n XDG_CACHE_HOME ~/.cache"
assert "```" not in build_manual.prettify(table), "a reference table got fenced"
out = build_manual.prettify(table)
assert "```fish" not in out, f"a reference table got shell-highlighted:\n{out}"
assert "```text" in out, f"a reference table lost its fence:\n{out}"
binds = " n / nv / neovim nvim\n e edit"
assert "```" not in build_manual.prettify(binds), "an abbreviation table got fenced"
out = build_manual.prettify(binds)
assert "```fish" not in out, f"an abbreviation table got shell-highlighted:\n{out}"
assert "```text" in out, f"an abbreviation table lost its fence:\n{out}"
shell = " set -U __fish_user_dots_path /path/to/dots"
assert "```fish" in build_manual.prettify(shell), "a shell block was not fenced"
def test_prettify_fallback_fences_instead_of_indenting():
"""The catch-all fallback must emit a fenced block, not bare indentation.
MDX (used for any page that also carries an <Aside> or <FileTree>) has
no indented-code-block syntax: a plain 4-space-indented block silently
renders as flowed paragraph text there, collapsing every line break. A
fenced block is the only fallback shape that's safe in both MDX and
plain Markdown.
"""
import build_manual
para = ["✘ 1 Fri Jun 12 00:51:21 2026 ← failed"]
out = build_manual._render_para(para, None, False)
assert out == "```text\n" + para[0] + "\n```", f"unexpected fallback output:\n{out}"
def test_prettify_titles_paths_and_commented_examples():
"""A bare file path or a leading '# in x.fish' comment become a title."""
import build_manual
@@ -686,6 +829,61 @@ def test_as_file_tree_accepts_deeper_trees():
), f"unexpected file tree output:\n{out}"
def test_as_file_tree_expands_functions_and_completions():
"""The functions/ and completions/ branches list real directory contents."""
import build_manual
with tempfile.TemporaryDirectory() as d:
root = Path(d)
functions_dir = root / "functions"
functions_dir.mkdir()
(functions_dir / "zeta.fish").write_text("")
(functions_dir / "Alpha.fish").write_text("")
completions_dir = root / "completions"
completions_dir.mkdir()
(completions_dir / "bd.fish").write_text("")
original = dict(build_manual.EXPANDABLE_TREE_DIRS)
build_manual.EXPANDABLE_TREE_DIRS["functions/"] = functions_dir
build_manual.EXPANDABLE_TREE_DIRS["completions/"] = completions_dir
try:
para = [
"~/.config/fish/",
"├── functions/ Custom functions, one per file",
"└── completions/ Tab completion scripts",
]
out = build_manual._as_file_tree(para)
finally:
build_manual.EXPANDABLE_TREE_DIRS.clear()
build_manual.EXPANDABLE_TREE_DIRS.update(original)
assert out == (
"<FileTree>\n"
"- ~/.config/fish/\n"
" - functions/ Custom functions, one per file\n"
" - Alpha.fish\n"
" - zeta.fish\n"
" - completions/ Tab completion scripts\n"
" - bd.fish\n"
"</FileTree>"
), f"unexpected file tree output:\n{out}"
def test_as_file_tree_leaves_unrelated_branches_unexpanded():
"""Only the mapped directory names get expanded; everything else is untouched."""
import build_manual
para = [
"$__fish_user_dots_path/",
"├── secrets.fish API keys, tokens, passwords, personal identifiers",
"└── local.fish Machine-specific paths, env vars, and sourcing secrets",
]
out = build_manual._as_file_tree(para)
assert "secrets.fish" in out and "local.fish" in out
assert len(out.splitlines()) == 5, f"unexpected expansion of unrelated branches:\n{out}"
def test_customization_notes_render_as_aside():
"""The real 07-customization NOTE paragraph converts to one intact <Aside>."""
import build_manual
@@ -698,7 +896,10 @@ def test_customization_notes_render_as_aside():
assert aside.count(" - ") == 4, f"expected exactly 4 bullets inside the aside:\n{aside}"
assert "- Command shadows (rm, cat, ls, ...) react immediately" in aside
assert "- With aliases disabled, rm falls back to bare `command rm`" in aside
assert "- Disabled integration commands (spwin, tab, split, hist, logs, upgrade)" in aside
assert (
"- Disabled integration commands "
"(`spwin`, `tab`, `split`, `hist`, `logs`, `upgrade`)" in aside
)
assert "- On CachyOS, the distro fish config's own aliases" in aside
@@ -827,6 +1028,454 @@ def test_site_avoids_reserved_dir():
)
def test_parse_component_lines_default_and_named_sites():
lines = [
"aliases/filesystem",
"site exit-plain: overrides/key-bindings",
"site logging-guard: logging/terminal-capture",
"",
" ",
]
got = mt.parse_component_lines(lines)
assert got == [
("", "aliases/filesystem"),
("exit-plain", "overrides/key-bindings"),
("logging-guard", "logging/terminal-capture"),
], f"unexpected parse: {got}"
def test_parse_components_includes_underscore_prefixed_and_uncategorised():
"""Unlike parse_functions, parse_components has no # CATEGORY gate and
no underscore exclusion -- every guarded identity must be visible."""
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "__private_helper.fish").write_text(
"# COMPONENT\n"
"# logging/terminal-capture\n"
"function __private_helper\n"
"end\n"
)
(root / "no_category.fish").write_text(
"# COMPONENT\n"
"# aliases/filesystem\n"
"#\n"
"# SYNOPSIS\n"
"# no_category\n"
"function no_category\n"
"end\n"
)
got = mt.parse_components(root)
assert got["__private_helper"] == ["logging/terminal-capture"]
assert got["no_category"] == ["aliases/filesystem"]
def test_parse_components_resolves_multi_header_file_to_function_name():
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "multi.fish").write_text(
"# COMPONENT\n"
"# aliases/filesystem\n"
"function first_fn\n"
"end\n"
"\n"
"# COMPONENT\n"
"# aliases/network\n"
"function second_fn\n"
"end\n"
)
got = mt.parse_components(root)
assert got == {
"first_fn": ["aliases/filesystem"],
"second_fn": ["aliases/network"],
}, f"unexpected resolution: {got}"
def test_parse_component_file_single_file():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "config.fish"
path.write_text(
"# COMPONENT\n"
"# site greeting-block: greeting/greeting-message\n"
)
got = mt.parse_component_file(path)
assert got == {"config": ["site greeting-block: greeting/greeting-message"]}
def test_build_registry_strips_on_off_contradiction_with_warning():
import generate_component_registry as gcr
components = {"contradictory_fn": ["always/on", "always/off", "aliases/filesystem"]}
registry, warnings = gcr.build_registry(components)
assert registry["contradictory_fn:"] == ["aliases/filesystem"], (
f"the non-contradictory tag should survive: {registry}"
)
assert len(warnings) == 1 and "contradictory_fn" in warnings[0]
def test_build_registry_drops_empty_effective_tag_sets():
import generate_component_registry as gcr
components = {"only_contradictory": ["always/on", "always/off"]}
registry, warnings = gcr.build_registry(components)
assert "only_contradictory:" not in registry, (
"a site stripped down to nothing must produce no registry entry "
"(fail-open: absence of an entry already means always/on at guard time)"
)
assert len(warnings) == 1
def test_build_registry_keeps_sites_independent():
import generate_component_registry as gcr
components = {
"smart_exit": [
"site exit-plain: overrides/key-bindings",
"site logging-guard: logging/terminal-capture",
]
}
registry, warnings = gcr.build_registry(components)
assert registry["smart_exit:exit-plain"] == ["overrides/key-bindings"]
assert registry["smart_exit:logging-guard"] == ["logging/terminal-capture"]
assert not warnings
def test_collect_components_merges_identity_collisions_across_sources():
"""functions/auto-pull.fish and conf.d/auto-pull.fish both self-identify
as "auto-pull" at runtime -- the guard only ever has the bare
status current-function/basename string to look up with -- so
collect_components must concatenate their raw COMPONENT lines
rather than letting conf.d's entry silently overwrite functions'."""
import generate_component_registry as gcr
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "functions").mkdir()
(root / "conf.d").mkdir()
(root / "functions" / "auto-pull.fish").write_text(
"# COMPONENT\n"
"# autoexec/sync\n"
"function auto-pull\n"
"end\n"
)
(root / "conf.d" / "auto-pull.fish").write_text(
"# COMPONENT\n"
"# autoexec/sync\n"
)
(root / "config.fish").write_text("")
orig_repo = gcr.REPO
try:
gcr.REPO = root
got = gcr.collect_components()
finally:
gcr.REPO = orig_repo
assert got["auto-pull"] == ["autoexec/sync", "autoexec/sync"], (
f"both sources' tags should survive the merge, not overwrite: {got}"
)
def test_render_registry_is_valid_fish_and_round_trips():
"""Sourcing render()'s output must leave the two arrays in the exact
shape __fish_config_op_registry_lookup expects -- checked via the real
lookup helper (functions/__fish_config_op_registry_lookup.fish, Task
2) rather than re-parsing the generated text by hand.
Written to a real temp file rather than piped through `source
/dev/stdin`: fish 3.7 (Ubuntu 24.04's packaged version, used in CI)
rejects `/dev/stdin` with "is not a file" when it's backed by a pipe,
even though fish 4.8 accepts it -- sourcing an actual file on disk is
the portable form across fish versions.
"""
import subprocess
import tempfile
import generate_component_registry as gcr
registry = {
"rm:": ["aliases/filesystem"],
"smart_exit:exit-plain": ["overrides/key-bindings"],
}
text = gcr.render(registry)
repo = Path(__file__).parent.parent
with tempfile.TemporaryDirectory() as d:
registry_path = Path(d) / "registry.fish"
registry_path.write_text(text)
proc = subprocess.run(
[
"fish", "-c",
f"source {repo}/functions/__fish_config_op_registry_lookup.fish; "
f"source {registry_path}; "
"__fish_config_op_registry_lookup rm ''; echo status=$status",
],
capture_output=True,
text=True,
)
assert "aliases/filesystem" in proc.stdout, f"unexpected output: {proc.stdout!r} {proc.stderr!r}"
assert "status=0" in proc.stdout, f"lookup did not report found: {proc.stdout!r}"
def test_build_manual_regenerates_registry_before_building():
"""docs/build-manual.py must regenerate the registry as a pre-step."""
import build_manual
assert hasattr(build_manual, "generate_component_registry"), (
"build-manual.py must import generate_component_registry so its "
"main() can be called as a pre-step before --site/--concat run"
)
def test_committed_registry_matches_headers():
"""The committed conf.d/__fish_config_op_registry.fish must match what
generate_component_registry.py would produce right now from the current
`# COMPONENT` headers -- otherwise CI has nothing catching drift."""
import generate_component_registry as gcr
registry, _ = gcr.build_registry(gcr.collect_components())
assert gcr.render(registry) == gcr.OUTPUT.read_text(), (
"conf.d/__fish_config_op_registry.fish is stale — run "
"__fish_config_op_registry_rebuild"
)
# ---------------------------------------------------------------------------
# codespans: inline code spans added at site-render time
# ---------------------------------------------------------------------------
_REPO = Path(__file__).parent.parent
def _spans(text: str) -> str:
return codespans.add_code_spans(text, codespans.vocabulary(_REPO))
def test_codespans_wraps_each_half_of_a_flag_pair():
"""`-a/--all` is the manual's usual way of naming a flag and its alias."""
got = _spans("Use -a/--all to include both, or -s/--stdout to print.")
assert got == "Use `-a`/`--all` to include both, or `-s`/`--stdout` to print.", got
def test_codespans_wraps_override_variables_and_snake_case():
got = _spans("Disabled via __fish_config_op_aliases; see _fdc_bins and fish_greeting.")
assert got == (
"Disabled via `__fish_config_op_aliases`; see `_fdc_bins` and `fish_greeting`."
), got
def test_codespans_wraps_paths_vars_env_and_key_chords():
cases = {
"Sourced from ~/.config/fish/config.fish.": (
"Sourced from `~/.config/fish/config.fish`."
),
"honoring $XDG_CONFIG_HOME/aichat/roles/cli.md.": (
"honoring `$XDG_CONFIG_HOME/aichat/roles/cli.md`."
),
"Launches with NO_TMUX=1 set.": "Launches with `NO_TMUX=1` set.",
"end the session with Ctrl-D or Ctrl+Alt+F.": (
"end the session with `Ctrl-D` or `Ctrl+Alt+F`."
),
}
for source, want in cases.items():
assert _spans(source) == want, f"{source!r} -> {_spans(source)!r}"
def test_codespans_leaves_existing_spans_and_fences_alone():
body = "\n".join(
[
"Already `--wrapped` here.",
"",
"```fish",
"rm -e --empty ~/.config/fish",
"```",
"",
"## --not-a-flag-heading",
"",
"<Aside type=\"note\" title=\"Note\">",
"See --verbose.",
"</Aside>",
]
)
got = _spans(body).split("\n")
assert got[0] == "Already `--wrapped` here.", got[0]
assert got[3] == "rm -e --empty ~/.config/fish", "a fenced line was rewritten"
assert got[6] == "## --not-a-flag-heading", "a heading was rewritten"
assert got[8].startswith("<Aside"), "component markup was rewritten"
assert got[9] == "See `--verbose`.", "aside body was not processed"
def test_codespans_leaves_file_tree_bodies_alone():
"""<FileTree> list items are filenames the component renders itself."""
body = "<FileTree>\n- ~/.config/fish/\n - config.fish\n</FileTree>"
assert _spans(body) == body
def test_codespans_leaves_links_and_urls_alone():
cases = [
"See [`fish-deps`](/reference/dependency-management/fish-deps/) for more.",
"Clone from ssh://git@host/owner/repo.git today.",
"Docs live at https://fish.rootiest.fyi/07-customization/ online.",
]
for source in cases:
assert _spans(source) == source, f"{source!r} -> {_spans(source)!r}"
def test_codespans_ignores_prose_that_only_looks_like_code():
"""Every one of these has bitten a looser version of the pattern set."""
cases = [
"the registry -- not the cache -- is rebuilt",
"the everyday filesystem-inspection and -modification shadows",
"expand !^, !*, !-N and !?str? abbreviations",
"grep/cp/mv/wget flag injection",
"resolved and/or rejected",
"a _really_ important caveat",
"the TCP and AGPL acronyms",
]
for source in cases:
assert _spans(source) == source, f"{source!r} -> {_spans(source)!r}"
def test_codespans_wraps_a_command_shadow_chain():
got = _spans("Falls back through ov -> bat -> man -> less -> cat.")
assert got == "Falls back through `ov` -> `bat` -> `man` -> `less` -> `cat`.", got
def test_codespans_wraps_long_runs_of_tool_names_only():
"""Two names, one of them an English word, is a sentence -- not a list."""
got = _spans("Supports paru, yay, pacman, apt, dnf, zypper, yum, brew, and pkg.")
assert got == (
"Supports `paru`, `yay`, `pacman`, `apt`, `dnf`, `zypper`, `yum`, "
"`brew`, and `pkg`."
), got
got = _spans("the custom rm function, trashy, or trash-cli")
assert got == "the custom rm function, `trashy`, or trash-cli", got
def test_codespans_wraps_whole_cells_only_in_command_columns():
"""A column of expansions becomes code; a column of prose stays prose."""
commands = "\n".join(
[
"| Abbreviation | Description |",
"|---|---|",
"| `..` | cd .. |",
"| `jctl` | journalctl -p 3 -xb |",
"| `sudu` | sudo -s |",
"| `kt` | kitty (Kitty only) |",
]
).split("\n")
got = _spans("\n".join(commands)).split("\n")
assert got[2] == "| `..` | `cd ..` |", got[2]
assert got[3] == "| `jctl` | `journalctl -p 3 -xb` |", got[3]
assert got[4] == "| `sudu` | `sudo -s` |", got[4]
assert got[5] == "| `kt` | `kitty` (Kitty only) |", got[5]
prose = "\n".join(
[
"| Command | Active behavior |",
"|---|---|",
"| `cd` | zoxide frecency-based navigation |",
"| `top` | btop resource monitor |",
"| `mkdir` | verbose path-tree display on creation |",
"| `history` | timestamps prepended to every entry |",
]
)
got = _spans(prose).split("\n")
assert got[2] == "| `cd` | `zoxide` frecency-based navigation |", got[2]
assert got[3] == "| `top` | `btop` resource monitor |", got[3]
def test_codespans_merges_abutting_new_spans():
"""One command line reads as one span, not as a row of adjacent ones."""
got = _spans("| `ls` | eza -l -a --icons --hyperlink | system ls |")
assert got == "| `ls` | `eza -l -a --icons --hyperlink` | system ls |", got
def test_codespans_vocabulary_comes_from_the_deps_catalog():
"""A tool added to _fish_deps_catalog.fish needs no second list."""
names = codespans.dependency_names(_REPO)
assert {"fzf", "zoxide", "prettyping"} <= names, sorted(names)[:20]
vocab = codespans.vocabulary(_REPO)
assert "zoxide" in vocab.strict, "an unambiguous tool name should be wrappable"
assert "find" not in vocab.strict, "an English word must not be wrappable on sight"
assert "find" in vocab.full, "…but it still counts as a command-line opener"
def test_codespans_leaves_indented_code_blocks_alone():
"""A four-space block is code, whatever it happens to contain.
The concat keeps the indented form pandoc wants, so unlike the site
this pass meets real indented blocks -- the table of contents among
them, which is nothing but a list of command names.
"""
body = "\n".join(
[
"Pick a viewer:",
"",
" 1. ov + bat section navigation",
" 2. less plain text with --jump",
"",
"Then run config-help.",
]
)
got = _spans(body).split("\n")
assert got[2] == " 1. ov + bat section navigation", got[2]
assert got[3] == " 2. less plain text with --jump", got[3]
assert got[5] == "Then run `config-help`.", "prose after the block was skipped"
def test_codespans_reach_the_man_page_pipeline():
"""Prose is marked identically wherever it is rendered.
`build_concat` runs the same pass `build_site` does, so a token the
site typesets as code is typeset as code in the man page and
`config-help` too, instead of only where the SSOT hand-wrote a
backtick.
"""
import build_manual
manual = Path(__file__).parent / "manual"
text = build_manual.build_concat(manual)
assert "`local.fish`" in text, "prose code spans never reached the concat"
assert "`tmux`" in text, "a vocabulary command was not wrapped in the concat"
def test_concat_code_spans_never_straddle_a_line():
"""`config-help` pairs backticks one line at a time.
Its `string replace` filters run per line, so a span split across a
line break -- ``run `fish-deps\\nupdate` `` -- leaves an unpaired
backtick the pager then shows literally. Markdown is happy to wrap
one, so nothing else catches this.
"""
import build_manual
text = build_manual.build_concat(Path(__file__).parent / "manual")
odd = [
(n, line)
for n, line in enumerate(text.split("\n"), 1)
if line.count("`") % 2
]
assert not odd, f"unpaired backtick, span wraps a line: {odd[:3]}"
def test_concat_section_five_stays_verbatim():
"""Section 5's entries are indented blocks, not prose.
They are generated from the `functions/*.fish` headers and pandoc sets
them verbatim, so a backtick there would be a literal character on the
page rather than markup.
"""
import build_manual
text = build_manual.build_concat(Path(__file__).parent / "manual")
body = text.split("\n# 5. ", 1)[1].split("\n# 6. ", 1)[0]
offenders = [
line for line in body.split("\n") if line.startswith(" ") and "`" in line
]
assert not offenders, f"backticks inside verbatim entries: {offenders[:3]}"
TESTS = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
@@ -840,6 +1489,9 @@ def main() -> int:
print(f" FAIL {t.__name__}: {e}", file=sys.stderr)
failed += 1
warn_public_functions_without_category()
warn_functions_without_component()
warn_unused_taxonomy_entries()
warn_c0_tags_never_combine_with_contradiction()
print(f"\n{len(TESTS) - failed}/{len(TESTS)} passed")
return 1 if failed else 0
+4 -1
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# autoexec/venv
#
# SYNOPSIS
# __auto_source_fallback_venv
#
@@ -16,7 +19,7 @@ function __auto_source_fallback_venv --on-variable PWD
status --is-command-substitution; and return
# Opinionated guard (C2): no automatic venv activation when disabled.
__fish_config_op_enabled __fish_config_op_autoexec; or return
__fish_config_op_enabled (status current-function); or return
# 1. Skip if direnv is already managing this directory
if set -q DIRENV_DIR; or test -e ".envrc"
+1 -1
View File
@@ -182,7 +182,7 @@ function __config_settings_draw
# ── Keybind hint ──────────────────────────────────────────────────────
# string pad is width-aware (arrows count as 1 column)
set -l hint " ↑↓/kj move ←→/hl set Tab page q quit"
set -l hint " ↑↓/kj move ←→/hl set Enter sub-cats Tab pg q quit"
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint) $c_reset
# ── Bottom border ─────────────────────────────────────────────────────
@@ -0,0 +1,151 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __config_settings_draw_subcat <cur_row> <cur_scope> <category_var>
#
# DESCRIPTION
# Renders the sub-category drill-down page for one C1-C6 category:
# the category's own toggle at the top (still meaningful as the cascade
# default for its sub-categories), then one row per sub-category from
# __config_settings_subcats, sized dynamically instead of the fixed
# 6-row layout __config_settings_draw uses for the category list.
# Follows the same width-tier and center-padding conventions as
# __config_settings_draw so the panel doesn't visibly jump between the
# two pages.
#
# Label and description fields are defensively truncated to their field
# width before padding (string pad only ever grows a string, never
# shrinks it) -- sub-category labels/descriptions are static data from
# __config_settings_subcats, not authored per width-tier the way
# __config_settings_draw's own category descriptions are, so a couple of
# them are longer than the narrower tiers' fields (e.g. "Notifications"
# is 13 chars against a 12-char label field; several descriptions run
# well past the 17-char field at the narrowest tier). Truncating keeps
# the box perfectly rectangular in every case instead of only in the
# cases the static text happens to fit.
#
# ARGUMENTS
# cur_row 0-based highlighted row (0 = the category's own toggle;
# 1..N = sub-category rows)
# cur_scope "universal" or "session"
# category_var One of the six __fish_config_op_<category> names
#
# EXIT STATUS
# 0 Always
#
# EXAMPLE
# __config_settings_draw_subcat 1 universal __fish_config_op_aliases
function __config_settings_draw_subcat
set -l cur_row $argv[1]
set -l cur_scope $argv[2]
set -l category_var $argv[3]
set -l c_ok (set_color green)
set -l c_err (set_color red)
set -l c_dim (set_color brblack)
set -l c_sel (set_color --bold magenta)
set -l c_head (set_color --bold cyan)
set -l c_reset (set_color normal)
set -l rows (__config_settings_subcats $category_var)
set -l n (count $rows)
# ── Width tier: matches __config_settings_draw's 6-col-per-side steps ──
set -l iw 50
if test "$COLUMNS" -ge 90
set iw 76
else if test "$COLUMNS" -ge 86
set iw 72
else if test "$COLUMNS" -ge 82
set iw 68
end
set -l HBR (string repeat -n $iw '─')
set -l p (string repeat -n (math --scale=0 "max(0, ($COLUMNS - ($iw + 2)) / 2)") ' ')
# Label field is 13 wide (one wider than __config_settings_draw's 12) --
# the longest real sub-category label ("Notifications") is 13 chars.
# Description field absorbs the difference so every row still totals
# iw+2, matching the surrounding box lines exactly.
set -l label_w 13
set -l desc_w (math $iw - 34)
set -l cat_label (string replace -r '^__fish_config_op_' '' -- $category_var)
# Scope indicator: toggling a row on this page writes -U (Universal,
# persistent) or -g (Session, this-shell-only) -- the title must say
# which, since it isn't otherwise visible anywhere on the page.
set -l scope_label Universal
test "$cur_scope" = session; and set scope_label Session
# Title layout is "┌─ Sub-categories: <label> (<scope>) ───┐"; the
# dash count must absorb every visible char added around cat_label so
# the line still totals iw+2, matching the surrounding box exactly --
# see the DESCRIPTION doc comment above for why this is hand-verified,
# not eyeballed.
set -l title_dashes (math $iw - (string length -- $cat_label) - (string length -- $scope_label) - 22)
printf '%s┌─%s Sub-categories: %s (%s)%s %s┐\n' \
$p $c_head "$cat_label" $scope_label "$c_reset" (string repeat -n (math "max(0, $title_dashes)") '─')
printf '%s│%s│\n' $p $HBR
# Row 0: the category's own toggle, still meaningful as the cascade
# default any DEFAULT-valued sub-category below falls back to.
set -l cat_val (__config_settings_get_val $category_var $cur_scope)
set -l cat_badge
switch $cat_val
case on
set cat_badge "$c_ok"" ON$c_reset"
case off
set cat_badge "$c_err""OFF $c_reset"
case '*'
set cat_badge "$c_dim""DEFAULT$c_reset"
end
set -l cat_curs " "
if test $cur_row -eq 0
set cat_curs "$c_sel$c_reset "
end
set -l cat_desc "cascade default"
if test $iw -ge 68
set cat_desc "default for all sub-cats below"
end
if test $iw -ge 72
set cat_desc "default for all sub-categories below"
end
printf '%s│ %s%s [ %s ] %s │\n' $p $cat_curs \
(string pad -r -w $label_w -- "(category)") $cat_badge \
(string pad -r -w $desc_w -- (string sub -l $desc_w -- $cat_desc))
printf '%s│ %s │\n' $p (string repeat -n (math $iw - 6) '─')
for i in (seq 1 $n)
set -l fields (string split -- \t $rows[$i])
set -l slug $fields[1]
set -l label $fields[2]
set -l desc $fields[3]
set -l subcat_var "$category_var"_(string replace -a -- '-' '_' $slug)
set -l val (__config_settings_get_val $subcat_var $cur_scope)
set -l badge
switch $val
case on
set badge "$c_ok"" ON$c_reset"
case off
set badge "$c_err""OFF $c_reset"
case '*'
set badge "$c_dim""DEFAULT$c_reset"
end
set -l curs " "
if test $i -eq $cur_row
set curs "$c_sel$c_reset "
end
set -l lpad (string pad -r -w $label_w -- (string sub -l $label_w -- $label))
set -l dpad (string pad -r -w $desc_w -- (string sub -l $desc_w -- $desc))
printf '%s│ %s%s [ %s ] %s │\n' $p $curs $lpad $badge $dpad
end
printf '%s│%s│\n' $p $HBR
set -l hint " ↑↓/kj move ←→/hl set Esc back q quit"
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint) $c_reset
printf '%s└%s┘\n' $p $HBR
end
+1 -1
View File
@@ -6,7 +6,7 @@
#
# DESCRIPTION
# Returns the current value of a named variable in the specified scope by
# parsing `set --show` output. Outputs "on", "off", or "DEFAULT" (when
# parsing set --show output. Outputs "on", "off", or "DEFAULT" (when
# the variable is not set in that scope). Scope "session" maps to "global"
# in fish's internal terminology.
#
+3 -3
View File
@@ -7,11 +7,11 @@
# DESCRIPTION
# Reads a single keypress directly from the controlling terminal in raw
# mode and echoes a normalized token naming the key. Bypasses fish's
# `read` builtin, whose interactive line editor swallows Tab and arrow
# keys (and prints a `read> ` prompt) — none of which is usable for a TUI.
# read builtin, whose interactive line editor swallows Tab and arrow
# keys (and prints a "read> " prompt) — none of which is usable for a TUI.
#
# The terminal is put into raw, no-echo mode with a 0.1s inter-byte timer
# (`stty raw -echo min 1 time 1`) so a multi-byte escape sequence (e.g.
# (stty raw -echo min 1 time 1) so a multi-byte escape sequence (e.g.
# an arrow key, ESC [ A) is captured in one read while a lone key returns
# promptly. Original terminal settings are always restored before return.
#
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __config_settings_subcats <category_variable>
#
# DESCRIPTION
# Prints the sub-category rows for one C1-C6 category, one per line as
# "<slug>\t<label>\t<description>". This is a fish-native copy of the
# taxonomy authored in docs/manual/08-components-reference/*.md (Task 9)
# -- kept here as a static table rather than parsed from markdown at
# draw time, since config-settings redraws on every keypress and every
# terminal resize.
#
# ARGUMENTS
# category_variable One of the six __fish_config_op_<category> names
#
# EXIT STATUS
# 0 Always
#
# RETURNS
# Tab-separated "<slug>\t<label>\t<description>" rows, one per line
#
# EXAMPLE
# __config_settings_subcats __fish_config_op_aliases
function __config_settings_subcats --description 'List the sub-categories for one opinionated-component category'
switch $argv[1]
case __fish_config_op_aliases
printf '%s\t%s\t%s\n' \
filesystem Filesystem "ls, cat, cd, du, mkdir, rm, mv, zoxide" \
search Search "rg" \
network Network "ping, ssh, yt-dlp" \
monitor Monitor "top" \
shell-tools Shell-tools "bash, less, help" \
dev-tools Dev-tools "claude, edit, agy"
case __fish_config_op_autoexec
printf '%s\t%s\t%s\n' \
plugin-management Plugins "Fisher bootstrap" \
pkg-wrappers Pkg-wrappers "paru/yay wrapper generation" \
venv Venv "Python auto-activation" \
telemetry Telemetry "WakaTime hook bootstrap" \
sync Sync "auto-pull, user-dots symlink"
case __fish_config_op_overrides
printf '%s\t%s\t%s\n' \
key-bindings Key-bindings "vi-mode, autopair, puffer, bang-bang" \
environment Environment "PATH, PAGER, EDITOR, CDPATH" \
prompt Prompt "Starship, right prompt, theme + FZF colors" \
privacy Privacy "DO_NOT_TRACK, DISABLE_TELEMETRY"
case __fish_config_op_integrations
printf '%s\t%s\t%s\n' \
terminal-abbrs Term-abbrs "Kitty/WezTerm abbreviations" \
window-mgmt Window-mgmt "spwin, tab, split" \
notifications Notifications "done, WakaTime hook" \
history-logs History-logs "hist, logs" \
pkg-upgrade Pkg-upgrade "upgrade"
case __fish_config_op_logging
printf '%s\t%s\t%s\n' \
terminal-capture Term-capture "Kitty watcher, smart_exit scrollback" \
multiplexer-capture Multiplexer "tmux, zellij" \
pkg-logs Pkg-logs "paru/yay AUR log wrappers"
case __fish_config_op_greeting
printf '%s\t%s\t%s\n' \
first-run First-run "welcome banner" \
greeting-message Greeting "fish_greeting override"
end
end
+63
View File
@@ -0,0 +1,63 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_config_op_cascade <category_variable> [<subcategory_variable>]
#
# DESCRIPTION
# Evaluates the opt-out cascade for one C1-C6 classification: an
# explicit truthy/falsy sub-category variable wins outright; otherwise
# falls back to the category variable; otherwise falls back to the
# master switch __fish_config_opinionated. A category in the opt-in
# list (currently just __fish_config_op_logging, C5) defaults to
# disabled when nothing in the chain is explicit, and the master switch
# cannot enable it -- this is data-driven here instead of a hardcoded
# string comparison, so any sub-category nested under an opt-in
# category inherits "off unless explicit" for free through the cascade,
# with no per-sub-category special-casing.
#
# ARGUMENTS
# category_variable Name (without $) of a C1-C6 category variable
# subcategory_variable Optional name (without $) of a sub-category
# variable nested under that category
#
# EXIT STATUS
# 0 Enabled
# 1 Disabled
#
# EXAMPLE
# __fish_config_op_cascade __fish_config_op_aliases
# __fish_config_op_cascade __fish_config_op_aliases __fish_config_op_aliases_filesystem
function __fish_config_op_cascade --description 'Evaluate the sub-category -> category -> master opt-out cascade'
set -l opt_in_categories __fish_config_op_logging
set -l chain $argv[1]
if test (count $argv) -ge 2 -a -n "$argv[2]"
set chain $argv[2] $argv[1]
end
for var_name in $chain
__fish_variable_check $var_name
set -l s $status
if test $s -eq 0
return 0
end
if test $s -eq 1
return 1
end
end
# Every variable in the chain was unset/unrecognized. chain[-1] is
# always the category variable (present whether or not a
# sub-category was given) -- opt-in categories default to disabled
# and the master switch cannot override that.
if contains -- $chain[-1] $opt_in_categories
return 1
end
__fish_variable_check __fish_config_opinionated
if test $status -eq 1
return 1
end
return 0
end
+43 -38
View File
@@ -2,63 +2,68 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_config_op_enabled <category_variable>
# __fish_config_op_enabled <identity> [<site>]
#
# DESCRIPTION
# Guard predicate for opinionated components (AGENTS.md Task #3).
# The category variable is evaluated first via __fish_variable_check:
# an explicit truthy value (1/true/yes/on/y) enables the component
# regardless of the master switch; an explicit falsy value
# (0/false/no/off/n) disables it regardless of the master switch.
# Only when the category variable is unset or unrecognized (status 2
# or 3) does the master switch __fish_config_opinionated apply: a
# falsy master disables every unset-category component at once.
# Unset master with unset category → enabled (active by default).
# Guard predicate for an opinionated component. <identity> is computed
# by the caller, never hand-typed as a category name: (status
# current-function) inside a function body, (status basename) at
# top-level conf.d/*.fish or config.fish code (fish has no API for a
# callee to introspect its own caller, so the caller must compute and
# pass its own identity -- see the spec's "A note on self-identifying").
# A trailing .fish is stripped so a status-basename identity and a
# status-current-function identity land in the same key space.
#
# One exception: __fish_config_op_logging (C5) is opt-in, because it
# writes terminal output to disk. Unset or unrecognized means disabled,
# and the master switch cannot enable it — only an explicit truthy
# value turns logging on.
# Looks up "<identity>:<site>" (site defaults to the empty/unnamed site)
# in the generated component registry. No registry entry (unclassified,
# or a doc header with no # COMPONENT section) resolves to enabled --
# the same fail-open default as an explicit always/on tag, so
# user-authored and third-party functions that never call this guard in
# the first place are unaffected, and one that somehow does is never
# silently broken by a missing header. A found always/off tag
# disables unconditionally; a found always/on tag enables
# unconditionally, short-circuiting before any other tagged
# sub-category is evaluated. Otherwise every tagged sub-category must
# pass the cascade (AND semantics).
#
# ARGUMENTS
# category_variable Name (without $) of the category opt-out variable:
# __fish_config_op_aliases, __fish_config_op_autoexec,
# __fish_config_op_overrides,
# __fish_config_op_integrations,
# __fish_config_op_logging, or
# __fish_config_op_greeting
# identity (status current-function) or (status basename)
# site Optional site slug (see # COMPONENT header grammar);
# omitted for the default/unnamed site
#
# EXIT STATUS
# 0 Component enabled (category explicitly truthy; or category unset and master not falsy — except C5 logging, which requires an explicit truthy value)
# 1 Component disabled (category explicitly falsy; or category unset and master falsy; or C5 logging unset; or no argument with falsy master)
# 0 Component enabled
# 1 Component disabled
#
# EXAMPLE
# if __fish_config_op_enabled __fish_config_op_aliases
# if not __fish_config_op_enabled (status current-function)
# alias grep='grep --color=auto'
# end
function __fish_config_op_enabled --description 'Check whether an opinionated component category is enabled'
__fish_variable_check $argv[1]
set -l cat_status $status
# if not __fish_config_op_enabled (status current-function) exit-plain
# builtin exit
# end
function __fish_config_op_enabled --description 'Guard for an opinionated component, identified by its own caller'
set -l identity (string replace -r '\.fish$' '' -- $argv[1])
set -l site $argv[2]
if test $cat_status -eq 0
set -l tags (__fish_config_op_registry_lookup $identity $site)
if test $status -ne 0
return 0
end
if test $cat_status -eq 1
if contains -- always/off $tags
return 1
end
# C5 logging is opt-in: it writes terminal output to disk, so an unset or
# unrecognized value means off — the master switch cannot enable it.
if test "$argv[1]" = __fish_config_op_logging
return 1
if contains -- always/on $tags
return 0
end
# Status 3 (garbage) defers to master — an unrecognized value is not an opt-out.
__fish_variable_check __fish_config_opinionated
if test $status -eq 1
return 1
for tag in $tags
set -l parts (string split -m 1 -- / $tag)
set -l category_var "__fish_config_op_$parts[1]"
set -l subcat_var "__fish_config_op_$parts[1]_"(string replace -a -- '-' '_' $parts[2])
__fish_config_op_cascade $category_var $subcat_var
or return 1
end
return 0
end
@@ -0,0 +1,38 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_config_op_registry_lookup <identity> <site>
#
# DESCRIPTION
# Looks up the "<identity>:<site>" key in the generated component
# registry ($__fish_config_op_registry_keys /
# $__fish_config_op_registry_values, sourced from
# conf.d/__fish_config_op_registry.fish at shell startup) and prints its
# tags, one per line. <site> is empty string for the default/unnamed
# site.
#
# ARGUMENTS
# identity Function name (status current-function) or file basename
# with any trailing .fish stripped (status basename)
# site Site slug, or empty string for the default site
#
# EXIT STATUS
# 0 Found: tags printed to stdout
# 1 Not found: nothing printed
#
# RETURNS
# Matching tags, one per line, printed to stdout
#
# EXAMPLE
# set -l tags (__fish_config_op_registry_lookup rm "")
# or return 0 # unclassified: caller treats this as always-on
function __fish_config_op_registry_lookup --description 'Look up the COMPONENT tags for an identity:site pair'
set -l key "$argv[1]:$argv[2]"
set -l idx (contains -i -- $key $__fish_config_op_registry_keys)
if test -z "$idx"
return 1
end
string split ' ' -- $__fish_config_op_registry_values[$idx]
return 0
end
@@ -0,0 +1,30 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_config_op_registry_rebuild
#
# DESCRIPTION
# Regenerates conf.d/__fish_config_op_registry.fish from every
# # COMPONENT header in functions/*.fish, conf.d/*.fish, and
# config.fish, then re-sources it into the current session so the
# change takes effect immediately. Run manually after editing a
# # COMPONENT header; never run automatically at shell startup --
# parsing every header on every new shell would be wasted work on
# every session that isn't actively editing a header.
#
# EXIT STATUS
# 0 Registry regenerated
# 1 python3 is not available, or generation failed
#
# EXAMPLE
# __fish_config_op_registry_rebuild
function __fish_config_op_registry_rebuild --description 'Regenerate the opinionated-component registry from # COMPONENT headers'
if not type -q python3
echo "__fish_config_op_registry_rebuild: python3 not found" >&2
return 1
end
python3 "$__fish_config_dir/docs/generate_component_registry.py"
or return 1
source "$__fish_config_dir/conf.d/__fish_config_op_registry.fish"
end
+4 -1
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# logging/terminal-capture
#
# SYNOPSIS
# __fish_config_sync_logging
#
@@ -29,7 +32,7 @@ function __fish_config_sync_logging --description 'Sync C5 logging state: sentin
set -l yay_wrapper "$HOME/.local/bin/yay"
set -l wrapper_version 1
if __fish_config_op_enabled __fish_config_op_logging
if __fish_config_op_enabled (status current-function)
# Logging enabled: remove sentinel and regenerate wrappers if binaries exist
rm -f $sentinel
+5 -2
View File
@@ -1,11 +1,14 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# autoexec/sync
#
# SYNOPSIS
# __fish_user_dots_link
#
# DESCRIPTION
# Manages the git-ignored `user-dots` convenience symlink in the fish config
# Manages the git-ignored user-dots convenience symlink in the fish config
# directory ($__fish_config_dir/user-dots), pointing it at the resolved
# $__fish_user_dots_path so the private overlay can be browsed from
# ~/.config/fish/.
@@ -41,7 +44,7 @@ function __fish_user_dots_link --description 'Manage the user-dots convenience s
end
# Enabled: creation is a C2 startup side-effect.
__fish_config_op_enabled __fish_config_op_autoexec; or return 0
__fish_config_op_enabled (status current-function); or return 0
test -d "$__fish_user_dots_path"; or return 0
if test -L "$link"
+36 -12
View File
@@ -5,23 +5,47 @@
# __fzf_inline_picker
#
# DESCRIPTION
# Opens an interactive fzf session and injects the selected item directly
# into the command line at the cursor position. Bound to @@ by default.
# Repaints the prompt after selection or cancellation.
# Bound to the @ key. Self-inserts @ normally; on a second consecutive @
# (detected by looking behind at the current token rather than making fish
# buffer ahead for a chord), opens an interactive fzf session listing both
# files and directories under the current directory, with a preview pane
# (bat-highlighted for text, image-rendered for pictures via
# _fzf_preview_file), and replaces the bare @ with the selected item.
# Cancelling leaves a literal @@ behind. Repaints the prompt after
# selection or cancellation.
#
# EXIT STATUS
# 0 Always; no-op if fzf is cancelled
# 0 Always
#
# EXAMPLE
# # Press @@ at the command prompt to open fzf and insert the selected item.
# # Press @ twice at the command prompt to open fzf and insert the
# # selected item in place of the second @, then keep typing to extend it
# # (e.g. a trailing /subdir).
function __fzf_inline_picker
# Open fzf and capture selection
set -l selection (fzf)
if test (commandline -t) = @
# Directly use fd binary to avoid output buffering delay caused by a fd
# alias, if any. Debian-based distros install fd as fdfind.
set -f fd_cmd (command -v fdfind || command -v fd || echo "fd")
set -f --append fd_cmd --color=always $fzf_fd_opts
if test -n "$selection"
# Injects text instantly at the cursor
commandline -i (string escape -- $selection)
set -l selection ($fd_cmd 2>/dev/null | _fzf_wrapper --ansi \
--height=90% \
--layout=reverse \
--border=rounded \
--border-label=' Insert Path ' \
--prompt='@@ -> ' \
--header='Enter: Insert Ctrl-/: Toggle Preview Esc: Cancel' \
--bind='ctrl-/:toggle-preview' \
--preview='_fzf_preview_file {}' \
--preview-window='right:50%:wrap:border-left')
if test -n "$selection"
commandline -t -- (string escape -- $selection)
else
commandline -t -- @@
end
commandline -f repaint
else
commandline -i @
end
# Refresh the line display
commandline -f repaint
end
+1 -1
View File
@@ -5,7 +5,7 @@
# __jobrunner_sessions [<tool>]
#
# DESCRIPTION
# Parses `tmux list-sessions` or `screen -ls` into machine-readable rows,
# Parses tmux list-sessions or screen -ls into machine-readable rows,
# one per active session: name, PID, state, and start time separated by tabs.
# Shared by jobrunner and its completions so both agree on what a session is
# named. Prints nothing when no sessions exist.
+1 -1
View File
@@ -6,7 +6,7 @@
#
# DESCRIPTION
# Succeeds (returns 0) when the top-level kitty.conf contains an active
# (non-commented) `watcher` directive — whether the fish-config managed one or
# (non-commented) watcher directive — whether the fish-config managed one or
# a user's own. Used to suppress the setup reminder and to inform status.
#
# EXIT STATUS
+2 -2
View File
@@ -6,9 +6,9 @@
#
# DESCRIPTION
# Appends any patterns not already covered by the project's .gitignore.
# Uses `git check-ignore` for accurate rule matching (catches wildcards
# Uses git check-ignore for accurate rule matching (catches wildcards
# and parent-dir globs). Falls back to a plain string search when the
# root is not a git repository. Leading `/` is stripped from each pattern
# root is not a git repository. Leading / is stripped from each pattern
# before the path-based check so root-anchored patterns (e.g. /AGENTS.md)
# are matched correctly.
#
@@ -0,0 +1,71 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_repo_ensure_symlink <link> <target>
#
# DESCRIPTION
# Idempotently makes <link> a symlink pointing at the directory <target>.
#
# Only directories are ever linked. The agent file-editing tools resolve a
# symlinked directory transparently but refuse to write through a
# symlinked file, so linking a file would silently break every later edit;
# a non-directory target is refused outright.
#
# A missing target is refused rather than linked, because a dangling
# memory/ symlink makes agent memory writes fail -- strictly worse than
# having no backup at all.
#
# When <link> is an existing real directory, its contents are copied into
# <target> without clobbering (cp -n) before the directory is replaced by
# the link, so adopting a populated live directory never overwrites the
# copy already in the vault.
#
# ARGUMENTS
# link Path that should become the symlink
# target Existing directory the link should point at
#
# EXIT STATUS
# 0 Link is correct (created, repinned, or already right)
# 1 Refused (non-directory target, missing target, non-directory link) or
# a copy, remove, or link operation failed
#
# RETURNS
# A single "→ ..." progress line on stdout when something changed;
# nothing at all when the link was already correct.
#
# EXAMPLE
# _agents_repo_ensure_symlink ~/.claude/projects/-home-u-proj/memory \
# ~/.local/share/agent-vault/projects/host-user-proj/claude/memory
function _agents_repo_ensure_symlink --argument-names link target
test -n "$link" -a -n "$target"; or return 1
if test -e "$target"; and not test -d "$target"
echo "_agents_repo_ensure_symlink: refusing non-directory target: $target" >&2
return 1
end
if not test -d "$target"
echo "_agents_repo_ensure_symlink: target does not exist: $target" >&2
return 1
end
if test -L "$link"
set -l cur (path resolve "$link")
set -l want (path resolve "$target")
test "$cur" = "$want"; and return 0
rm -f "$link"; or return 1
else if test -d "$link"
set -l contents (command ls -A "$link" 2>/dev/null)
if test (count $contents) -gt 0
command cp -rn "$link/." "$target/"; or return 1
end
rm -rf "$link"; or return 1
else if test -e "$link"
echo "_agents_repo_ensure_symlink: refusing to replace non-directory: $link" >&2
return 1
end
mkdir -p (path dirname "$link"); or return 1
ln -s "$target" "$link"; or return 1
echo "→ Linked "(path basename "$link")"$target"
end
@@ -2,31 +2,34 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_init_install_tools <agents_dir>
# _agents_repo_install_tools <repo_dir>
#
# DESCRIPTION
# Copies the canonical version-bump script and git hook shims from
# fish-config's scripts/agents-tools/ into <agents_dir>/.agents-tools/,
# refreshing them when the shipped `agents-tools-version:` marker is newer
# fish-config's scripts/agents-tools/ into <repo_dir>/.agents-tools/,
# refreshing them when the shipped agents-tools-version: marker is newer
# than the installed copy. Files are made executable. Idempotent: prints
# nothing when the installed tooling is already current, or a short summary
# line when it installed or updated the tooling.
# line when it installed or updated the tooling, naming <repo_dir>'s own
# basename rather than a hardcoded caller (e.g. "AGENTS/.agents-tools/" for
# agents-init, "agent-vault/.agents-tools/" for agents-vault). Shared by
# agents-init and agents-vault.
#
# ARGUMENTS
# agents_dir Absolute path to the AGENTS/ sub-repo root
# repo_dir Absolute path to the git repo root to install tooling into
#
# EXIT STATUS
# 0 Tooling is current or was installed/updated successfully
# 1 Canonical source missing or a copy failed
#
# EXAMPLE
# set -l msg (_agents_init_install_tools /path/to/AGENTS)
# set -l msg (_agents_repo_install_tools /path/to/AGENTS)
# test -n "$msg"; and echo $msg
function _agents_init_install_tools --argument-names agents_dir
test -n "$agents_dir"; or return 1
function _agents_repo_install_tools --argument-names repo_dir
test -n "$repo_dir"; or return 1
set -l src (path resolve (status dirname)/../scripts/agents-tools)
test -f "$src/version-bump"; or return 1
set -l dest "$agents_dir/.agents-tools"
set -l dest "$repo_dir/.agents-tools"
set -l want (command grep -m1 -oE 'agents-tools-version: *[0-9]+' "$src/version-bump" 2>/dev/null | command grep -oE '[0-9]+$')
set -l have ""
@@ -40,9 +43,10 @@ function _agents_init_install_tools --argument-names agents_dir
command cp "$src/hooks/prepare-commit-msg" "$dest/hooks/prepare-commit-msg"; or return 1
chmod +x "$dest/version-bump" "$dest/hooks/pre-commit" "$dest/hooks/prepare-commit-msg"; or return 1
set -l label (path basename -- "$repo_dir")
if test -z "$have"
echo "→ Installed AGENTS/.agents-tools/ (version-bump v$want)"
echo "→ Installed $label/.agents-tools/ (version-bump v$want)"
else
echo "→ Updated AGENTS/.agents-tools/ (v$have → v$want)"
echo "→ Updated $label/.agents-tools/ (v$have → v$want)"
end
end
+40
View File
@@ -0,0 +1,40 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_repo_local_slug <dir>
#
# DESCRIPTION
# Builds the path-derived fallback slug used when a project has no git
# remote: local-<sanitized-basename>-<8 hex of sha256(realpath)>. The
# basename is lowercased and every character outside [a-z0-9._-] is
# mapped to a dash, matching the sanitization the remote-URL branch of
# _agents_repo_slug applies to hostnames and paths.
#
# This is the single source of truth for that formula. It exists so the
# rule is written once: _agents_repo_slug's no-remote branch calls it to
# produce the slug, and agents-vault's slug-migration fallback (used when
# there is no live symlink yet to read the previous slug from) calls it
# to recompute the same candidate. Duplicating the formula in both places
# let them drift once before; this closes that gap for good.
#
# ARGUMENTS
# dir Absolute or relative path to the project directory
#
# EXIT STATUS
# 0 Slug printed
# 1 No directory argument given
#
# RETURNS
# The local-* slug, one line on stdout.
#
# EXAMPLE
# set -l slug (_agents_repo_local_slug /home/user/myproject)
function _agents_repo_local_slug --argument-names dir
test -n "$dir"; or return 1
set -l rp (path resolve "$dir")
set -l base (string lower -- (path basename "$rp") | string replace -ra '[^a-z0-9._-]' '-')
set -l digest (printf '%s' "$rp" | sha256sum | string split -f1 ' ')
printf 'local-%s-%s\n' "$base" (string sub -l 8 -- "$digest")
end
+70
View File
@@ -0,0 +1,70 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# DEPENDENCIES
# _agents_repo_local_slug
#
# SYNOPSIS
# _agents_repo_slug <dir>
#
# DESCRIPTION
# Derives the vault slug for a project directory. Prefers the normalized
# git remote URL so the same project keys identically from any clone on
# any machine; falls back to a path-derived key when no remote exists.
#
# Normalization strips the scheme, userinfo, and a numeric port, rewrites
# scp-form host:path to host/path, drops a trailing .git, lowercases, and
# maps every character outside [a-z0-9._-] to a dash. These all yield
# git.rootiest.dev-rootiest-fish-config:
#
# https://git.rootiest.dev/rootiest/fish-config.git
# git@git.rootiest.dev:rootiest/fish-config.git
# ssh://git@git.rootiest.dev:22/rootiest/fish-config.git
#
# With no remote the slug is local-<sanitized-basename>-<8 hex of sha256(realpath)>,
# where the basename is lowercased and mapped the same way as the remote form.
# That key is machine-dependent by construction and is best-effort only;
# agents-vault --adopt rebinds such an entry by hand.
#
# ARGUMENTS
# dir Absolute path to the project directory
#
# EXIT STATUS
# 0 Slug printed
# 1 No directory argument given
#
# RETURNS
# The slug, one line on stdout.
#
# EXAMPLE
# set -l slug (_agents_repo_slug /home/user/myproject)
function _agents_repo_slug --argument-names dir
test -n "$dir"; or return 1
set -l url (git -C "$dir" remote get-url origin 2>/dev/null)
if test -z "$url"
set -l remotes (git -C "$dir" remote 2>/dev/null)
if test (count $remotes) -gt 0
set url (git -C "$dir" remote get-url $remotes[1] 2>/dev/null)
end
end
if test -n "$url"
set -l s $url
# Order matters: the port must go before the scp-form rewrite, or
# ssh://host:22/a/b becomes host/22/a/b and diverges from the
# https slug for the same repository.
set s (string replace -r '^[A-Za-z][A-Za-z0-9+.-]*://' '' -- $s)
set s (string replace -r '^[^@/]+@' '' -- $s)
set s (string replace -r '^([^/:]+):[0-9]+/' '$1/' -- $s)
set s (string replace -r '^([^/:]+):' '$1/' -- $s)
set s (string replace -r '\.git$' '' -- $s)
set s (string replace -r '/+$' '' -- $s)
set s (string lower -- $s)
set s (string replace -ra '[^a-z0-9._-]' '-' -- $s)
printf '%s\n' $s
return 0
end
_agents_repo_local_slug "$dir"
end
+73
View File
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_repo_sync <dir> <message>
#
# DESCRIPTION
# Stages everything in <dir> and commits it with <message>. Shared by
# agents-init and agents-vault.
#
# It never touches the network, and that is the point rather than an
# omission. Both callers run on every agent launch, synchronously, ahead
# of the agent itself, and a fetch there blocks the launch for as long as
# an unreachable remote takes to time out and can prompt for credentials
# invisibly underneath a starting agent. Committing needs no remote at
# all -- only pushing does -- so the pull lives on agents-vault's push
# path, which is already opt-in for exactly this reason. An offline
# laptop therefore still gets a complete local backup, which is the whole
# point of keeping one.
#
# A rebase already in progress is refused rather than committed: the
# worktree then holds conflict markers, and recording those under a
# routine-looking message buries the conflict in the history instead of
# reporting it. The rebase is left exactly as it stands -- this function
# did not start it, so it is not this function's to abort -- and the
# caller says so.
#
# Commits are made with commit.gpgsign=false so a pinentry prompt can
# never block a shell or an agent launch. If a pre-commit or commit-msg
# hook rejects the commit (e.g. a secret scanner), that failure is
# surfaced too: nothing is committed and a diagnostic goes to stderr.
#
# ARGUMENTS
# dir Absolute path to the git repository
# message Commit subject used when there is something to commit
#
# EXIT STATUS
# 0 Committed, or nothing needed committing
# 1 <dir> is not a git repository, arguments were missing, or the commit
# itself failed (e.g. a pre-commit/commit-msg hook rejected it)
# 2 A rebase is in progress; nothing committed, nothing touched
#
# RETURNS
# A single "→ Committed (<sha>) <subject>" line on stdout when it
# commits; nothing when there was nothing to do.
#
# EXAMPLE
# _agents_repo_sync /path/to/AGENTS "chore: sync AGENTS repository"
function _agents_repo_sync --argument-names dir msg
test -n "$dir" -a -n "$msg"; or return 1
test -d "$dir/.git"; or return 1
# The guard above proved .git is a directory, so these are the same two
# paths `agents-vault --status` reports an unresolved rebase from.
if test -d "$dir/.git/rebase-merge"; or test -d "$dir/.git/rebase-apply"
echo "_agents_repo_sync: unresolved rebase in $dir; nothing committed" >&2
return 2
end
git -C "$dir" add -A 2>/dev/null
set -l status_out (git -C "$dir" status --porcelain 2>/dev/null)
test -n "$status_out"; or return 0
if git -C "$dir" -c commit.gpgsign=false commit -q -m "$msg" 2>/dev/null
set -l sha (git -C "$dir" rev-parse --short HEAD 2>/dev/null)
set -l subject (git -C "$dir" log -1 --pretty=%s 2>/dev/null)
echo "→ Committed ($sha) $subject"
return 0
else
echo "_agents_repo_sync: commit failed in $dir (hook rejected it?); nothing committed" >&2
return 1
end
end
+32
View File
@@ -0,0 +1,32 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_vault_dir
#
# DESCRIPTION
# Prints the agent memory vault root. Honors the universal variable
# __fish_agent_vault_dir when set, otherwise
# ${XDG_DATA_HOME:-$HOME/.local/share}/agent-vault.
#
# The vault holds agy state as well as Claude state, so it is not nested
# under either tool's directory; it is backed-up state rather than
# configuration, hence XDG_DATA_HOME rather than XDG_CONFIG_HOME.
#
# EXIT STATUS
# 0 Always
#
# RETURNS
# The vault root path, one line on stdout.
#
# EXAMPLE
# set -l vault (_agents_vault_dir)
function _agents_vault_dir
if set -q __fish_agent_vault_dir; and test -n "$__fish_agent_vault_dir"
printf '%s\n' "$__fish_agent_vault_dir"
return 0
end
set -l base $XDG_DATA_HOME
test -n "$base"; or set base "$HOME/.local/share"
printf '%s\n' "$base/agent-vault"
end
+9 -9
View File
@@ -18,13 +18,13 @@
# conveniences (e.g. backs one wrapper
# function) that only matter if you already
# use that specific tool. Skipped by
# `fish-deps install`/`sync` unless
# `--optional` (or `--all`) is passed.
# fish-deps install/sync unless
# --optional (or --all) is passed.
# term Terminal Emulators — GPU-accelerated terminal emulators
# (kitty, wezterm) that only matter if one
# of them is your actual terminal. Skipped
# by `fish-deps install`/`sync` unless
# `--terminals` (or `--all`) is passed.
# by fish-deps install/sync unless
# --terminals (or --all) is passed.
# int Integrations — opt-in third-party services requiring
# their own account/setup (wakatime,
# tailscale).
@@ -36,27 +36,27 @@ function _fish_deps_catalog
set -g _fdc_bins \
uv cargo fish starship fzf zoxide direnv paru yay \
wakatime tailscale \
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen mpv vlc
set -g _fdc_tiers \
rec rec req rec req rec rec rec rec \
int int \
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt opt opt
set -g _fdc_cargo \
"" "" "" starship "" zoxide "" "" "" \
"" "" \
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" ""
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" "" "" ""
set -g _fdc_pm \
uv cargo fish starship fzf zoxide direnv "" yay \
wakatime tailscale \
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen mpv vlc
set -g _fdc_special \
curl-uv rustup-installer git-cargo-fish curl-installer fzf-update "" "" paru-build yay-build \
wakatime-binary "" \
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" ""
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" "" "" ""
end
# SYNOPSIS
+2
View File
@@ -17,6 +17,8 @@ function _fzf_preview_file --description "Print a preview for the given file bas
if set --query fzf_preview_file_cmd
# need to escape quotes to make sure eval receives file_path as a single arg
eval "$fzf_preview_file_cmd '$file_path'"
else if command -q file; and string match -q 'image/*' -- (command file --brief --mime-type -- "$file_path" 2>/dev/null)
_fzf_preview_image "$file_path"
else
bat --style=numbers --color=always "$file_path"
end
+39
View File
@@ -0,0 +1,39 @@
# helper function for _fzf_preview_file
function _fzf_preview_image --description "Render an image preview using the best available tool for the current terminal."
set -f file_path $argv
set -l cols (set --query FZF_PREVIEW_COLUMNS; and echo $FZF_PREVIEW_COLUMNS; or echo 80)
set -l lines (set --query FZF_PREVIEW_LINES; and echo $FZF_PREVIEW_LINES; or echo 24)
set -l left (set --query FZF_PREVIEW_LEFT; and echo $FZF_PREVIEW_LEFT; or echo 0)
set -l top (set --query FZF_PREVIEW_TOP; and echo $FZF_PREVIEW_TOP; or echo 0)
# Terminals that implement the kitty graphics protocol (kitty itself,
# WezTerm, and Ghostty) can render via `kitten icat` even when it's not
# the active terminal, since `kitten` is just an escape-sequence emitter.
set -l kitty_capable 0
if set --query KITTY_WINDOW_ID
or test "$TERM" = xterm-kitty
or contains -- "$TERM_PROGRAM" WezTerm ghostty
or set --query GHOSTTY_RESOURCES_DIR
set kitty_capable 1
end
if test $kitty_capable -eq 1; and command -q kitten
# --place is measured from the top-left of the whole terminal, not the
# preview pane, so FZF_PREVIEW_LEFT/TOP (added to fzf specifically for
# this integration) are required to land the image in the right spot.
command kitten icat --clear --transfer-mode=memory --unicode-placeholder \
--stdin=no --place="$cols"x"$lines"@"$left"x"$top" -- "$file_path" 2>/dev/null
else if command -q chafa
command chafa --size="$cols"x"$lines" -- "$file_path"
else if command -q viu
command viu --width $cols -- "$file_path"
else if command -q timg
command timg -g "$cols"x"$lines" -- "$file_path"
else
set_color yellow
echo "No image preview tool available (install kitty, chafa, viu, or timg)."
set_color normal
command file --brief -- "$file_path"
end
end
+60
View File
@@ -0,0 +1,60 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _fzf_preview_media <file_path>
#
# DESCRIPTION
# Preview helper for play-media. Looks for a thumbnail already generated
# by a desktop file manager (Dolphin, Nautilus, GNOME Videos, ...) in the
# freedesktop thumbnail cache and renders it via _fzf_preview_image if
# found. Otherwise falls back to ffprobe-formatted metadata (duration,
# codec, resolution, tags) when ffprobe is installed, or plain file
# output as a last resort. Neither the thumbnail cache lookup nor ffprobe
# are tracked in fish-deps: both are best-effort, matching how the
# image-preview tool chain (kitten/chafa/viu/timg) is already handled.
#
# ARGUMENTS
# file_path Path to the audio/video file to preview
#
# EXIT STATUS
# 0 Always
#
# EXAMPLE
# _fzf_preview_media ./Videos/clip.mp4
function _fzf_preview_media
set -f file_path $argv
if not test -e "$file_path"
echo "$file_path doesn't exist." >&2
return 0
end
set -l abs_path (realpath -- "$file_path" 2>/dev/null)
# freedesktop.org thumbnail spec: md5 of the file:// URI names the
# cached thumbnail. Dolphin/Nautilus/GNOME Videos already populate this
# cache, so reuse it instead of generating a new thumbnail ourselves.
set -l uri "file://"(string escape --style=url -- "$abs_path")
set -l hash (echo -n "$uri" | md5sum | string split ' ')[1]
set -l cache_home (set --query XDG_CACHE_HOME; and echo $XDG_CACHE_HOME; or echo "$HOME/.cache")
for size in normal large x-large xx-large
set -l thumb "$cache_home/thumbnails/$size/$hash.png"
if test -f "$thumb"
_fzf_preview_image "$thumb"
return 0
end
end
if command -q ffprobe
set -l info (command ffprobe -v error -show_entries \
format=duration,bit_rate:format_tags=title,artist,album:stream=codec_name,codec_type,width,height \
-of default=noprint_wrappers=1 -- "$abs_path" 2>/dev/null)
if test -n "$info"
printf '%s\n' $info
return 0
end
end
command file --brief -- "$abs_path"
end
+2 -2
View File
@@ -11,9 +11,9 @@
# modification time, so the most recently written logs are kept — actively
# appended logs (e.g. a tmux pipe-pane stream) survive.
#
# Uses `command ls`/`command rm` to bypass the C1 shadows: the bare `ls` is
# Uses command ls/command rm to bypass the C1 shadows: the bare ls is
# the eza wrapper, which injects OSC-8 hyperlink escapes into paths, and the
# bare `rm` is the trash wrapper. The glob is expanded via `set` first so a
# bare rm is the trash wrapper. The glob is expanded via set first so a
# no-match (empty dir / first run) yields an empty list instead of a hard
# "No matches for wildcard" error.
#
+5 -2
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# COMPONENT
# logging/multiplexer-capture
#
# SYNOPSIS
# _zellij_dump_log
#
@@ -8,7 +11,7 @@
# Captures the current Zellij pane's scrollback to a timestamped log in
# SCROLLBACK_HISTORY_DIR (default ~/.terminal_history). Zellij has no live
# output-streaming facility like tmux's pipe-pane, so this performs a one-shot
# `zellij action dump-screen --full` — intended to run on shell exit. Old
# zellij action dump-screen --full — intended to run on shell exit. Old
# zellij_*.log files are pruned via _prune_terminal_logs to stay within
# SCROLLBACK_HISTORY_MAX_FILES.
#
@@ -23,7 +26,7 @@
# EXAMPLE
# _zellij_dump_log
function _zellij_dump_log --description 'Dump the current Zellij pane scrollback to a log file, with pruning'
__fish_config_op_enabled __fish_config_op_logging; or return 0
__fish_config_op_enabled (status current-function); or return 0
set -q ZELLIJ; or return 0
type -q zellij; or return 0
+63 -56
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# DEPENDENCIES
# _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
#
# SYNOPSIS
# agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
# [-q | --quiet] [-s | --silent] [-h | --help]
@@ -50,11 +53,16 @@
#
# With no flags, runs both --agents and --plugins setup; --agents re-runs
# only the AGENTS.md / symlink step and --plugins only the plans/specs/
# devlogs wiring step. Managed paths are added to .gitignore. The sub-repo
# is pulled first when it has an upstream, and at the end of every
# invocation any uncommitted changes inside it are auto-committed so
# agent-made edits are captured automatically. Fully idempotent: a second
# run produces no output and no new commits.
# devlogs wiring step. Managed paths are added to .gitignore. At the end
# of every invocation any uncommitted changes inside the sub-repo are
# auto-committed so agent-made edits are captured automatically. Fully
# idempotent: a second run produces no output and no new commits.
#
# The commit is local only. Nothing here fetches or pushes: the wrappers
# call this synchronously before starting an agent, and a network round
# trip there blocks the launch until an unreachable remote times out and
# can prompt for credentials with nobody watching. A sub-repo that has an
# upstream is pulled by hand, on the user's own schedule.
#
# Called automatically by the claude and agy wrappers on every invocation.
#
@@ -68,7 +76,8 @@
#
# EXIT STATUS
# 0 Setup completed successfully
# 1 Fatal error (git init failed, move failed, etc.)
# 1 Fatal error (git init failed, move failed, the AGENTS/ commit was
# rejected, or an unresolved rebase blocked it)
#
# EXAMPLE
# agents-init
@@ -77,7 +86,7 @@
# agents-init --quiet
function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec files and plugin dirs'
set -l c_head (set_color --bold cyan)
set -l c_cmd (set_color --bold white)
set -l c_cmd (set_color --bold)
set -l c_flag (set_color yellow)
set -l c_ok (set_color green)
set -l c_warn (set_color yellow)
@@ -165,7 +174,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
test $verbose -eq 1; and echo "$c_ok→ Created AGENTS/.version (1.0.0)$c_reset"
end
set -l _tools (_agents_init_install_tools "$agents_dir")
set -l _tools (_agents_repo_install_tools "$agents_dir")
if test -n "$_tools"
set changed 1
test $verbose -eq 1; and echo "$c_ok$_tools$c_reset"
@@ -259,38 +268,26 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
test $verbose -eq 1; and echo "$c_ok→ Linked AGENTS/CLAUDE.md → AGENTS/AGENTS.md$c_reset"
end
# ── Root symlink: AGENTS.md → AGENTS/AGENTS.md ───────────────────────
set -l _need_link 0
if not test -L "$root/AGENTS.md"
set _need_link 1
else if test (readlink "$root/AGENTS.md") != AGENTS/AGENTS.md
rm -f "$root/AGENTS.md"
set _need_link 1
end
if test $_need_link -eq 1
if not ln -s AGENTS/AGENTS.md "$root/AGENTS.md"
echo "$c_err""Error: could not create AGENTS.md symlink$c_reset" >&2
return 1
# Root symlinks point at files, not directories, so they cannot use
# _agents_repo_ensure_symlink (which is directory-only by design).
for pair in "AGENTS.md:AGENTS/AGENTS.md" "CLAUDE.md:AGENTS/CLAUDE.md"
set -l name (string split -f1 ':' -- $pair)
set -l want (string split -f2 ':' -- $pair)
set -l need 0
if not test -L "$root/$name"
set need 1
else if test (readlink "$root/$name") != "$want"
rm -f "$root/$name"
set need 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked AGENTS.md → AGENTS/AGENTS.md$c_reset"
end
# ── Root symlink: CLAUDE.md → AGENTS/CLAUDE.md ───────────────────────
set -l _need_link 0
if not test -L "$root/CLAUDE.md"
set _need_link 1
else if test (readlink "$root/CLAUDE.md") != AGENTS/CLAUDE.md
rm -f "$root/CLAUDE.md"
set _need_link 1
end
if test $_need_link -eq 1
if not ln -s AGENTS/CLAUDE.md "$root/CLAUDE.md"
echo "$c_err""Error: could not create CLAUDE.md symlink$c_reset" >&2
return 1
if test $need -eq 1
if not ln -s "$want" "$root/$name"
echo "$c_err""Error: could not create $name symlink$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked $name$want$c_reset"
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked CLAUDE.md → AGENTS/CLAUDE.md$c_reset"
end
# ── .gitignore ────────────────────────────────────────────────────────
@@ -461,24 +458,30 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
end
# ──────────────────────── Auto-commit AGENTS/ ────────────────────────────
# Pull first when an upstream is configured so the local .version reflects
# any remote bumps before we add to it (no-op for local-only repos).
if git -C "$agents_dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
git -C "$agents_dir" pull --rebase --autostash -q 2>/dev/null
end
git -C "$agents_dir" add -A 2>/dev/null
set -l status_out (git -C "$agents_dir" status --porcelain 2>/dev/null)
if test -n "$status_out"
set -l msg "chore: sync AGENTS repository"
test $did_init -eq 1; and set msg "chore: initialize AGENTS repository"
if git -C "$agents_dir" -c commit.gpgsign=false commit -q -m "$msg" 2>/dev/null
set changed 1
if test $verbose -eq 1
set -l sha (git -C "$agents_dir" rev-parse --short HEAD 2>/dev/null)
set -l realmsg (git -C "$agents_dir" log -1 --pretty=%s 2>/dev/null)
echo "$c_ok→ Committed AGENTS/ ($sha) $c_dim$realmsg$c_reset"
end
end
# Purely local: no fetch, no push. This function runs synchronously on
# every agent launch, and a network round trip there blocks the launch
# for as long as an unreachable remote takes to time out. Committing
# never needed one -- see _agents_repo_sync.
#
# Every way the commit can fail is an arm of its own. A sync that did
# not commit means agent-made edits were not captured, so it is a
# failure rather than a line to walk past -- and the missing `-ne 0`
# arm was not a cosmetic gap: fish resolves a branchless `if` to 0, so
# a hook-rejected commit fell straight through to a reported success.
set -l msg "chore: sync AGENTS repository"
test $did_init -eq 1; and set msg "chore: initialize AGENTS repository"
set -l sync_out (_agents_repo_sync "$agents_dir" "$msg")
set -l sync_rc $status
set -l failed 0
if test $sync_rc -eq 2
echo "$c_warn→ AGENTS/ has an unresolved rebase; nothing committed$c_reset" >&2
set failed 1
else if test $sync_rc -ne 0
echo "$c_err""Error: the AGENTS/ commit failed; nothing recorded$c_reset" >&2
set failed 1
else if test -n "$sync_out"
set changed 1
test $verbose -eq 1; and echo "$c_ok$sync_out$c_reset"
end
# Quiet summary: one line at the end, only if something actually changed
@@ -489,4 +492,8 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
echo "$c_ok→ Synced AGENTS scaffolding$c_reset"
end
end
# Explicit, because the branchless `if` above resolves to 0 and would
# otherwise be this function's exit status.
test $failed -eq 0
end
File diff suppressed because it is too large Load Diff
+14 -5
View File
@@ -4,8 +4,11 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# COMPONENT
# aliases/dev-tools
#
# DEPENDENCIES
# agents-init
# agents-init, agents-vault
#
# SYNOPSIS
# agy [ARGS...]
@@ -15,9 +18,14 @@
# sub-repository is initialized and any agent-made changes are committed
# before launch. Delegates all scaffold and commit logic to agents-init
# --quiet (full setup), which ensures AGENTS/ is scaffolded and CLAUDE.md
# is symlinked to AGENTS/AGENTS.md in the current project. Arguments are
# forwarded verbatim to the real agy binary, except for -r/--resume which
# are translated to -c/--continue.
# is symlinked to AGENTS/AGENTS.md in the current project.
#
# Also syncs the host-scoped agent memory vault (agents-vault). agy has
# no session-end hook, so its memory is captured on the next launch
# rather than at session end.
#
# Arguments are forwarded verbatim to the real agy binary, except for
# -r/--resume which are translated to -c/--continue.
#
# Opinionated component (C1): when disabled via __fish_config_op_aliases
# (or the __fish_config_opinionated master), the command is passed through
@@ -35,12 +43,13 @@
# agy -i "initial prompt"
# agy models
function agy --wraps=agy --description 'agy wrapper: auto-initializes AGENTS/ sub-repo before launch'
if not __fish_config_op_enabled __fish_config_op_aliases
if not __fish_config_op_enabled (status current-function)
command agy $argv
return $status
end
agents-init --quiet
agents-vault --quiet
for i in (seq (count $argv))
if test "$argv[$i]" = "-r"
+5 -2
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 04-git-and-version-control
#
# COMPONENT
# autoexec/sync
#
# SYNOPSIS
# auto-pull [list]
# auto-pull add [PATH]
@@ -40,7 +43,7 @@
# auto-pull remove qmk_firmware
function auto-pull --description 'Manage the auto-pull repository registry'
set -l c_head (set_color --bold cyan)
set -l c_cmd (set_color --bold white)
set -l c_cmd (set_color --bold)
set -l c_flag (set_color yellow)
set -l c_ok (set_color green)
set -l c_warn (set_color yellow)
@@ -136,7 +139,7 @@ function auto-pull --description 'Manage the auto-pull repository registry'
return 0
case status
if __fish_config_op_enabled __fish_config_op_autoexec
if __fish_config_op_enabled (status current-function)
echo "$c_ok""auto-pull: ENABLED$c_reset $c_dim(C2 auto-execution on)$c_reset"
else
echo "$c_warn""auto-pull: DISABLED$c_reset $c_dim(via __fish_config_op_autoexec)$c_reset"
+4 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# COMPONENT
# aliases/shell-tools
#
# SYNOPSIS
# bash [args...]
#
@@ -18,7 +21,7 @@
# bash
function bash --wraps='bash' --description 'bash switches to bash shell'
# Opinionated guard (C1): fall back to bare command bash when disabled.
if not __fish_config_op_enabled __fish_config_op_aliases
if not __fish_config_op_enabled (status current-function)
command bash $argv
return $status
end
+5 -1
View File
@@ -25,7 +25,11 @@
function bkg --description 'Execute bkg'
# Check if a command was provided as an argument.
if test -z "$argv[1]"
echo "Usage: bkg <command> [arguments...]"
set -l c_head (set_color --bold cyan)
set -l c_cmd (set_color --bold)
set -l c_arg (set_color cyan)
set -l c_reset (set_color normal)
echo "$c_head""Usage:$c_reset $c_cmd""bkg$c_reset $c_arg""<command> [arguments...]$c_reset"
return 1
end
+4 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# COMPONENT
# aliases/filesystem
#
# SYNOPSIS
# cat [args...]
#
@@ -21,7 +24,7 @@
# cat ~/projects/myapp
function cat --wraps='bat' --description 'Use bat for files, ls for directories, and raw cat for ANSI logs'
# Opinionated guard (C1): fall back to bare command cat when disabled.
if not __fish_config_op_enabled __fish_config_op_aliases
if not __fish_config_op_enabled (status current-function)
command cat $argv
return $status
end
+12 -2
View File
@@ -4,8 +4,11 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# COMPONENT
# aliases/dev-tools
#
# DEPENDENCIES
# agents-init
# agents-init, agents-vault
#
# SYNOPSIS
# claude [ARGS...]
@@ -16,6 +19,12 @@
# Delegates all scaffold and commit logic to agents-init --quiet (full
# setup), which ensures AGENTS/ is scaffolded and CLAUDE.md is symlinked
# to AGENTS/AGENTS.md in the current project.
#
# Also syncs the host-scoped agent memory vault (agents-vault), which
# tracks curated memory living outside the project tree. The vault
# commits on launch but does not push; pushing happens from the Claude
# Code SessionEnd hook or an explicit agents-vault --push.
#
# All arguments are forwarded verbatim to the real claude binary.
#
# Opinionated component (C1): when disabled via __fish_config_op_aliases
@@ -33,12 +42,13 @@
# claude --resume
# claude "Explain the recent changes"
function claude --wraps=claude --description 'claude wrapper: auto-links AGENTS.md as CLAUDE.md'
if not __fish_config_op_enabled __fish_config_op_aliases
if not __fish_config_op_enabled (status current-function)
command claude $argv
return $status
end
agents-init --quiet
agents-vault --quiet
command claude $argv
end

Some files were not shown because too many files have changed in this diff Show More