Compare commits

..
175 Commits
Author SHA1 Message Date
rootiest 679a703ab2 Merge pull request 'fix(ci): base64-encode the bot GPG key secret' (#147) from fix/ci-gpg-key-base64 into main 2026-09-10 04:00:24 +00:00
fishconfig-bot ecfcea5106 chore(docs): regenerate manual, man page, and component registry 2026-09-10 03:58:16 +00:00
rootiest 4da176d507 fix(ci): base64-encode the bot GPG key secret
Raw multi-line armored key piped through 'echo | gpg --import' came out
CRC-corrupted (Invalid keyring) on the first real run -- something in the
secret/env round-trip mangles embedded newlines. Regenerated the bot key
(old one is unrecoverable, secrets are write-only) and store it
base64-encoded, decoded with base64 -d before import.
2026-09-09 23:52:40 -04:00
rootiest cc9531f19f Merge pull request 'fix(agy): map -r/--resume with session id to --conversation' (#146) from fix/agy-resume-conversation-syntax into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m53s
CI / build-docs (push) Failing after 3m38s
Reviewed-on: #146
2026-09-10 03:37:58 +00:00
rootiest 4ab52329e1 fix(agy): map -r/--resume with session id to --conversation
agy uses different resume syntax than claude: -c/--continue always
resumes the most-recent session, while --conversation takes a
specific session id. Previously -r/--resume was blanket-mapped to
-c/--continue, breaking `agy --resume <id>` and `agy -r=<id>` style
invocations. Now bare -r/--resume (no id following) maps to
-c/--continue; -r/--resume given an id (via =id or a following
non-flag word) maps to --conversation(=id).
2026-09-09 23:24:29 -04:00
rootiest 2597b3ccd3 Merge pull request 'fix(ci): sign docs-regen commits with dedicated bot GPG key' (#145) from fix/ci-commit-signing into main 2026-09-10 02:48:51 +00:00
rootiest a751124b8b fix(ci): sign docs-regen commits with dedicated bot GPG key
actions@gitea was never a verified email on any Gitea account, so the
CI docs-regen commit (git commit && git push, done client-side in the
runner) could never show as verified regardless of server-side
[repository.signing] config -- Gitea only signs commits it generates
itself (merge button, web editor, wiki), never ones a client pushes.

Import a dedicated passphrase-less key for a new fishconfig-bot
account (verified email, no login) from the CI_GPG_PRIVATE_KEY repo
secret and sign with it instead.
2026-09-09 22:16:13 -04:00
rootiest 6732a71eb7 Merge pull request 'fix(git-clean): strip the '+' other-worktree marker too' (#144) from fix/clean-worktree-branch-marker into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m58s
CI / build-docs (push) Successful in 3m37s
2026-09-10 01:15:42 +00:00
rootiest e6c6d2533d fix(git-clean): strip the '+' other-worktree marker too
git branch -vv marks column 1 with '+' (not '*') for a branch checked
out in another linked worktree. Only '*' was stripped from $1, so a
gone branch shown with '+' left a bogus "+" entry in $gone_branches
that then failed to delete: error: branch '+' not found.

Add a regression case reproducing the '+'-marked gone-branch line via
the existing git-clean mock handler in tests/test-network-fish.fish.
2026-09-09 21:14:04 -04:00
Gitea Actions 6199caf0d0 chore(docs): regenerate manual, man page, and component registry 2026-09-10 01:11:47 +00:00
rootiest d93cb286ef Merge pull request 'feat(mkrep): add directory + git-repo scaffolding command' (#142) from feat/mkrep-command into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m49s
CI / build-docs (push) Successful in 3m35s
2026-09-10 01:01:37 +00:00
rootiest 3c621db727 Merge pull request 'test: expand test coverage across core functions, enforce strict fish_indent linting, and add network isolation harnesses' (#143) from test/network-edge-case-coverage into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m2s
CI / build-docs (push) Successful in 3m34s
2026-09-10 01:00:45 +00:00
rootiest 04edd6de89 test: add core utilities and security test suite 2026-09-09 20:50:21 -04:00
rootiest a1cf06624e fix(security): return full variable name in sponge_filter_secrets 2026-09-09 20:49:16 -04:00
rootiest c7d7cea8ae test: eliminate flakiness in rand_string casing test and improve history cleanup 2026-09-09 20:43:52 -04:00
rootiest ff4f7b3909 test: add string and commandline expansion test suite 2026-09-09 20:39:46 -04:00
rootiest b3082a3313 feat(mkrep): add directory + git-repo scaffolding command
mkcd's create-and-cd behavior plus git init, remote linking, and
optional remote creation via a user-configured command template
($MKREP_REMOTE_CMD, {name}/{user} placeholders).
2026-09-09 20:38:09 -04:00
rootiest d926a632d5 test: enforce fish_indent --check in test runner 2026-09-09 20:32:43 -04:00
rootiest e40d67df59 style: format fish files with fish_indent 2026-09-09 20:27:48 -04:00
rootiest 2dc978e719 fix(docs): align registry generator output with fish_indent 2026-09-09 20:27:21 -04:00
rootiest de6349f2dc test(net): add network isolation harnesses and failure edge case coverage 2026-09-09 19:51:17 -04:00
Gitea Actions b59318e957 chore(docs): regenerate manual, man page, and component registry 2026-09-09 19:59:39 +00:00
rootiest 90aaa6dcda Merge pull request 'feat(config-settings): replace the ANSI renderer with a curses front-end' (#141) from feat/config-settings-curses-tui into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m47s
CI / build-docs (push) Successful in 3m45s
Reviewed-on: #141
2026-09-09 19:54:11 +00:00
rootiest 9af0462d29 test(config-settings): cover the empty state dump refusal
The guard added in 208ad95 had no test. This is the failure worth
covering, because it is the one that does not announce itself: with an
empty dump every row renders as DEFAULT, indistinguishable from a config
where nothing is set, so the user toggles from a false baseline.

Reaching the guard needs a real terminal -- the isatty check sits in
front of it -- so the case runs fish under a pty via python3's stdlib
pty module, which this suite already depends on. An empty
__config_settings_state is shadowed in to fake the failure.

The pty reader's 15s deadline is load-bearing rather than defensive: if
the guard regresses, config-settings does not fail, it opens the TUI and
blocks on getch(), so an unbounded read would hang the suite instead of
failing it. Verified both ways -- passes with the guard, and with the
guard removed the deadline fires and the case fails with a legible
excerpt rather than a screenful of escape sequences.
2026-09-09 15:41:39 -04:00
rootiest 208ad95883 fix(config-settings): refuse to open on an empty state dump
An empty dump does not fail loudly: the TUI renders every row as
DEFAULT, which is indistinguishable from a config where nothing is set.
That is a wrong answer rather than a missing one -- the user would be
looking at ON rows reported as DEFAULT -- so the launcher now checks the
dump is non-empty and bails with a message instead. The taxonomy alone
guarantees output on any working checkout. Also guard a failed mktemp,
which would otherwise send the dump to /state and hand an empty path to
rm -rf.

README: mention that / searches sub-categories across every category.
2026-09-09 15:36:40 -04:00
rootiest 983434ffb4 docs(config-settings): drop backticks from the function header
test_concat_section_five_stays_verbatim forbids backticks inside
generated Section 5 entries -- function headers are rendered verbatim, so
inline code markup there breaks the man page.
2026-09-09 14:51:59 -04:00
rootiest 078d01666e feat(config-settings): replace the ANSI renderer with a curses front-end
config-settings is now a launcher for scripts/config-settings-tui.py,
drawn with Python's stdlib curses. The seven fish files that hand-rolled
the ANSI renderer are gone, along with the golden harness that had to pin
their byte-exact output.

The TUI is a child process, so it can neither read the session's global
variables nor write them. State goes in as a dump from the new
__config_settings_state; the edits come back as a fish script that
config-settings sources, which is what lets the Session page's `set -g`
land in the caller's shell instead of in a child that is about to exit.
Every edit is emitted as a call to __config_settings_apply or
__config_settings_set_value, so list splitting, the SCROLLBACK_HISTORY_*
export mirror and the shadow-warning suppression all stay in the fish
layer that already owned them.

The consequence, and the one behaviour change: edits are applied in one
batch on exit rather than on each keypress. The status bar shows a
pending count.

New: `/` filters the current page, and on the Universal and Session pages
it reaches into every category's sub-categories, listing hits as
"Category › Sub" so a sub-category can be toggled without drilling into
its parent first. Also a `?` help overlay, mouse selection, and a
drill-down page that leads with the category's own toggle.

Gone with the renderer: the four width tiers, the wrap-aware erase
arithmetic, the stty/dd/od raw key reader, the panel-height bookkeeping
and the hand-written redraw differ. curses owns all of it, and the alt
screen plus absolute addressing makes the desync class behind 608b022,
4210f3b, 93fc5e0 and 3c4f720 unreachable.

The sub-category taxonomy is NOT duplicated in Python: it travels in the
state dump, still sourced from __config_settings_subcats. The category,
Sponge and Paths row tables move into Python, consolidating the two
copies the fish renderers kept.

Dependency: python3 with curses. Stdlib on Arch, Fedora and a full
Debian/Ubuntu python3; python3-minimal alone lacks _curses. The launcher
checks for both and names what is missing. Called out in the README.

Verified: 416/416 assertions, plus a live end-to-end in a sandbox HOME
confirming the Universal page writes universal scope only (U1/G0) and the
Session page global scope only (U0/G1).
2026-09-09 14:50:26 -04:00
rootiest c5d2624a06 feat(config-settings): curses prototype front-end
Add scripts/config-settings-tui.py, a stdlib-curses prototype of the
config-settings interface, plus a gate that runs its --self-test.

The backend is stubbed: values live in an in-memory dict and nothing is
read from or written to fish variables. This is here to evaluate the
render engine and the interaction model before committing to a rewrite.

Why curses rather than more ANSI arithmetic:

  - No flicker, structurally. curses diffs its virtual screen against the
    physical one and emits only the changed cells, which is what
    __config_settings_diff_redraw.fish reimplements by hand.
  - Alternate screen plus absolute addressing. Stray output cannot desync
    the display, so the bug class behind 608b022 (fish's read prompt),
    4210f3b (a shadow warning on stderr), 93fc5e0 and 3c4f720 (line wrap
    breaking the erase height) cannot occur at all.
  - Resize is a repaint rather than wrap-factor arithmetic.
  - Overlays, panes, live filtering and mouse input cost a few lines each.

The layout departs from the current single panel: a page sidebar with a
live filter on the left, a scrolling detail pane on the right, a help
overlay on '?', and mouse selection. Sub-category drill-down, the
tri-state badges and the Sponge/Paths value rows all carry over.

Dependency note: python3 with the curses module. That is stdlib on Arch,
Fedora and a full Debian/Ubuntu python3; python3-minimal alone does not
carry _curses, so the test asserts the import.

The prototype is not wired into config-settings and nothing existing
changed.
2026-09-09 14:29:40 -04:00
Gitea Actions 69de224cbf chore(docs): regenerate manual, man page, and component registry 2026-09-09 04:49:54 +00:00
rootiest 8e4b719425 fix(docs): title distro code blocks and fix missed shell highlighting (#140)
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m19s
CI / build-docs (push) Successful in 3m46s
2026-09-09 04:43:50 +00:00
rootiest 8f48120ba9 Merge pull request 'docs: strip gitignored AGENTS/ and JOB-BRIEF-FINDINGS references from code comments' (#139) from docs/strip-agents-internal-refs into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m57s
CI / build-docs (push) Successful in 3m27s
2026-09-08 16:59:05 +00:00
rootiest 9d7cddf758 docs: strip gitignored AGENTS/ and JOB-BRIEF-FINDINGS references from code comments
Comments in tracked files pointed readers at AGENTS/specs/*.md,
AGENTS.md, and JOB-BRIEF-FINDINGS.md -- all gitignored, transient
dev notes not part of the base repo. Rewrote each to be self-contained
or point at tracked docs (docs/fish-config.md) instead.
2026-09-08 12:54:53 -04:00
Gitea Actions d4c40519e5 chore(docs): regenerate manual, man page, and component registry 2026-09-08 16:50:35 +00:00
rootiest fb454da3bc Merge pull request 'fix(docs): strip backticks from Section 5 verbatim EXIT STATUS/ARGUMENTS lines' (#138) from fix/docs-verbatim-backticks into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m17s
CI / build-docs (push) Successful in 3m56s
2026-09-08 16:44:10 +00:00
rootiest 91dabd565c fix(docs): strip backticks from Section 5 verbatim EXIT STATUS/ARGUMENTS lines
Section 5 (function manpages) is pandoc-rendered verbatim, so a backtick
there is a literal character on the page, not markup. Nine EXIT STATUS/
ARGUMENTS lines across check_fish_deps, lock, cleanup, claude-pr,
claude-docs, dops, ports, screensleep, and steam-dl quoted a command with
backticks; reworded to plain text matching each function's own DESCRIPTION
style.

Fixes docs/verify-manual.py's test_concat_section_five_stays_verbatim,
failing in CI since before this branch (pre-existing on main, confirmed
against the commit before PR #133 merged).
2026-09-08 12:42:30 -04:00
rootiest 453a8500af Merge pull request 'refactor(config-settings): shared frame renderer + line-diff redraw' (#133) from refactor/config-settings-renderer into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m1s
CI / build-docs (push) Failing after 1m6s
Reviewed-on: #133
2026-09-08 16:28:51 +00:00
rootiest afbc3e97d7 Merge remote-tracking branch 'origin/main' into refactor/config-settings-renderer
# Conflicts:
#	functions/__config_settings_draw.fish
#	functions/__config_settings_draw_subcat.fish
#	functions/__config_settings_draw_value.fish
#	tests/functional.fish
2026-09-08 12:19:31 -04:00
rootiest 3c4f720749 fix(config-settings): repair diff-redraw review findings
- Print the missing exit-repaint after the inline editor's
  __cs_dispatch_draw call (was silently changed to capture-only earlier
  in the branch; this call site was missed, blanking the panel on
  commit/cancel of an inline edit).
- Hoist (count $prev_edit_frame) out of a quoted math string in the
  inline editor's per-keystroke redraw -- fish doesn't expand a command
  substitution there, so math silently errored.
- Add a >= 52 column floor to both diff-path guards: below the
  narrowest tier's own 52-column box width, lines wrap and the diff
  path's one-physical-row-per-logical-line walk corrupts the display.
- Reword a stale test comment that described panel_h as mirroring a
  hand-set constant in __cs_dispatch_draw; it derives panel_h from real
  output now.
- Declare prev_edit_frame with -l alongside edit_frame instead of a
  bare set, matching the file's convention.
- Move prev_frame's declaration to its point of use instead of an
  empty top-level placeholder, matching old_h in the same block.
2026-09-08 04:37:27 -04:00
rootiest a0491b2bd1 feat(config-settings): diff redraw in the inline value editor 2026-09-08 04:21:07 -04:00
rootiest fae15b8d55 feat(config-settings): diff redraw in the main navigation loop 2026-09-08 04:17:32 -04:00
rootiest ab14837e39 refactor(config-settings): dispatch captures the frame instead of printing it 2026-09-08 04:13:52 -04:00
rootiest ece3e9dbfb feat(config-settings): add __config_settings_diff_redraw 2026-09-08 04:08:00 -04:00
rootiest 7b02348f64 test(config-settings): lock draw-function line counts to their panel heights 2026-09-08 03:57:26 -04:00
rootiest 4f62710061 Merge pull request 'chore(gitignore): ignore agy's .agents/ cache via shared AI-tool glob' (#137) from fix/gitignore-agy-agents-dir into main
Reviewed-on: #137
2026-09-08 07:02:37 +00:00
rootiest c77a52acd3 chore(gitignore): ignore agy's .agents/ cache via shared AI-tool glob
Root-cause fix in the shared boilerplate template (~/.config/.user-dots/git/gitignore),
same tier as .claude*/.gemini*/.antigrav*. Drops the manual one-off .agents
line; re-stamped this repo's boilerplate block to the new template hash.
2026-09-08 03:00:21 -04:00
rootiest 31b89bbac7 Merge pull request 'refactor(functions): consolidate 198 duplicate color declarations into one helper' (#131) from refactor/shared-color-palette into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m59s
CI / build-docs (push) Failing after 1m14s
2026-09-08 05:44:16 +00:00
rootiest b541323949 Merge remote-tracking branch 'origin/main' into refactor/shared-color-palette
# Conflicts:
#	tests/functional.fish
2026-09-08 01:43:01 -04:00
rootiest 6679d19a09 Merge pull request 'fix: split dops/docker, fix header doc gaps from Job 3 findings' (#135) from fix/dops-docker-and-header-gaps into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m2s
CI / build-docs (push) Failing after 1m18s
2026-09-08 05:40:55 +00:00
rootiest 6b2cc3aefc Merge remote-tracking branch 'origin/main' into fix/dops-docker-and-header-gaps 2026-09-08 01:39:44 -04:00
rootiest f2744a9a60 Merge pull request 'feat(help): render --help from function headers; stop 8 functions executing on --help' (#132) from feat/header-driven-help into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m9s
CI / build-docs (push) Failing after 3m59s
2026-09-08 05:39:23 +00:00
rootiest 2e82761b7d Merge remote-tracking branch 'origin/main' into feat/header-driven-help
# Conflicts:
#	tests/functional.fish
2026-09-08 01:38:24 -04:00
rootiest 001c5d3df7 Merge pull request 'fix: tricks.fish double-source and paru/yay wrapper duplication' (#136) from fix/tricks-double-source-and-paru-wrapper-dedup into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m46s
CI / build-docs (push) Failing after 4m22s
2026-09-08 05:31:52 +00:00
rootiest 8487f124c7 Merge remote-tracking branch 'origin/main' into fix/tricks-double-source-and-paru-wrapper-dedup 2026-09-08 01:30:43 -04:00
rootiest a994f9e91c Merge pull request 'perf(conf.d): cut shell startup latency by ~30%' (#130) from perf/startup-latency into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m49s
CI / build-docs (push) Failing after 3m48s
2026-09-08 05:30:05 +00:00
rootiest 43752aa268 Merge remote-tracking branch 'origin/main' into perf/startup-latency
# Conflicts:
#	tests/functional.fish
2026-09-08 01:28:27 -04:00
rootiest 0a2290640f Merge pull request 'test: unify the test harness, discover suites, fix a real hermeticity leak' (#134) from test/test-harness-overhaul into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m49s
CI / build-docs (push) Successful in 3m46s
2026-09-08 05:26:28 +00:00
rootiest 23bf649c5f fix(functions): dedupe conflicting paru/yay wrapper generators
__fish_config_sync_logging carried its own inferior copy of the
paru/yay wrapper generator (tee-based, no PTY, no progress-bar
rendering, hard-coded /usr/bin/paru|yay) alongside the canonical
version-6 generators in conf.d/paru-wrapper.fish and
conf.d/yay-wrapper.fish. Both wrote the same file with different
version markers and the same misattributed provenance comment, so
whichever ran last won and a subsequent C5 toggle would flip it back.
See startup-latency-JOB-BRIEF-FINDINGS.md §2.

__fish_config_sync_logging now delegates entirely to the canonical
generators instead of carrying a copy: they already resolve the real
binary via __fish_real_command (never /usr/bin-assumed) and
independently gate on their own C2/C5 keys, covering both the
enabled-regenerate and disabled-remove cases.

One behavior change falls out of delegating rather than special-casing
around it: a wrapper is no longer generated when C2 (paru-autoexec /
yay-autoexec) is disabled, even if C5 logging is on. The removed
sync-logging copy never checked C2, so it could reinstall a wrapper
the user had explicitly turned auto-exec off for.

Adds functions/_fish_source_scoped.fish: a small helper that runs
source inside its own function-call boundary. source itself runs in
the caller's scope, so a bare return inside a sourced conf.d guard
(both files have several) would otherwise unwind whatever function
called source directly -- verified with a minimal repro before relying
on it. Routing through this helper contains the return to just that
call, so calling paru's generator and then yay's actually reaches the
second call.

Manually verified end-to-end in an isolated HOME/XDG sandbox with a
stubbed paru/yay: enable generates both v6 wrappers, disable removes
both and drops the sentinel, re-enable regenerates them and clears the
sentinel.
2026-09-08 01:22:22 -04:00
rootiest f5c4016fa6 perf(conf.d): stop re-running expensive setup on tricks.fish's second source
tricks.fish is sourced twice per shell on CachyOS: once by the conf.d
autoload, once forced by config.fish to re-win over the distro's own
tricks.fish (measured 4.55ms for the second pass, see
startup-latency-JOB-BRIEF-FINDINGS.md §1). The bang-bang functions,
aliases, and history override further down need to re-run on both
passes since those are what re-assert over the distro config, but the
PATH/MANPAGER setup does not.

Gates fish_add_path and the type -q bat probe behind a once-per-session
global, guarding only that block. Everything else in the file, and the
coupling with config.fish's own cachyos-tricks guard, is unchanged.
2026-09-08 01:22:10 -04:00
rootiest dccf897c9f docs(functions): add missing EXIT STATUS header docs
Fills the gaps recorded in JOB-BRIEF-FINDINGS.md for the functions
converted to header-driven --help: 16 argument-less functions gain an
EXIT STATUS section (check_fish_deps, claude-docs, claude-pr, cleanup,
fast, fzf-update, gip, gip4, hist, limine-edit, lock, ports,
screensleep, steam-dl, swapstat, tmux-clean), and 8 functions that
already document ARGUMENTS gain EXIT STATUS too (lD, lsr, lss, lstree,
lt, ltr, lx, qr).

Not touched: gip6, ld, parur, sudo-toggle, upgrade, the JOB-BRIEF's
'missing ARGUMENTS' group. Re-checked their bodies against that claim
-- none of the five take a positional argument ($argv only appears
forwarded to the --help check), so an ARGUMENTS section would document
something that doesn't exist. All five already have EXIT STATUS.
2026-09-08 01:12:04 -04:00
rootiest 3aa462a2b8 style(functions): remove blank line in sponge_filter_secrets.fish header
The man-page comment header was separated from the function line by a
blank line -- the only file in functions/ with that gap. Cosmetic, no
runtime effect (manualtools and the help-flag renderer both tolerate
it by walking past a blank separator). See JOB-BRIEF-FINDINGS.md §2;
the renderer's tolerance for this case is left in place deliberately,
it is not dead code.
2026-09-08 01:11:56 -04:00
rootiest 23420b3235 fix(functions): split dops.fish into dops and docker
dops.fish defined `docker`, not `dops`. dops was never defined; docker
was only conditionally defined as a side effect of the failed dops
autoload, so its behavior could silently change mid-session. See
JOB-BRIEF-FINDINGS.md §1.

- functions/dops.fish now defines dops: a real enhanced `docker ps`
  listing (custom Names/Image/Status/Ports table), with its own --help.
- functions/docker.fish is a new file holding the ps-redirect wrapper,
  fixed to actually call dops (previously called the still-undefined
  dops from inside itself).
- Bare `docker` with no arguments no longer falls through an
  if-with-no-else (the fish false-zero, AGENTS.md standing gotcha #5)
  and does nothing; it now runs the real docker binary, which prints
  its own usage.
- tests/functional.fish: updated the now-stale comment explaining why
  the help-flag check resolves the real function name instead of the
  file stem.
2026-09-08 01:11:49 -04:00
rootiest a3853a93d8 test: run the config-settings render golden in the test runner
A byte-identity gate CI never runs will rot, and a rotted gate is worse
than no gate. Adds the render harness as Phase 4, in the same shape as the
vault suite: its own process, its own sandbox, no loaded config needed.

Kept to one self-contained block so it can be dropped or re-applied by
hand if the runner is restructured. The functional suite's 317/317 count
is untouched; the render cases report separately.
2026-09-07 20:13:08 -04:00
rootiest 328e30882e test: assert converted functions still emit colour in --help
Runtime escape-presence check over 24 user-facing functions.

Deliberately not a static grep for __fish_palette: measured on a broken
logs.fish whose --help block lost its declarations without gaining a call,
`fish -n` exited 0 and `grep -c __fish_palette` returned 1 while the help
output went 431 B -> 150 B with every escape stripped. The full suite was
green too. Only running the function and looking for an \e byte catches it;
that demonstration is recorded as a comment above the test.

Verified red as well as green -- deleting the __fish_palette call from
functions/mkcd.fish yields:

  --help lost its colour: mkcd
  FAIL  test_functions_keep_their_palette
  16/17 passed

fish_prompt.fish is excluded by name (its own hex palette); qc is excluded
because its --help shells out to aichat, which CI lacks.
2026-09-07 20:12:59 -04:00
rootiest 1f847bfce0 test(guards): cover always/on, always/off and AND via a synthetic registry
14 cases. The production registry has 65 entries all carrying exactly one
tag and no always/* anywhere, so these three documented semantics are
unreachable from real data -- the synthetic keys/values table is the only
way to reach them. Reasoning recorded at the fixture.

Also pins the degenerate no-slash tag and reaches C5 through
__fish_config_op_enabled, which is the path production code takes.
2026-09-07 20:12:22 -04:00
rootiest 7a6a7339dc test(guards): cover op_enabled against the real registry
5 cases: default-on, .fish suffix stripping, category off, subcategory
overriding it, and the documented fail-open on a missing entry.
2026-09-07 20:11:06 -04:00
rootiest 7d815b17e8 refactor: retire drifted palette names in the last five functions
c_rst/c_txt -> c_reset, c_ttl -> c_head, c_bold -> c_cmd, c_primary -> c_arg
across 67 interpolation sites (c_rst 53, c_primary 10, c_bold 2, c_ttl 1,
c_txt 1). Each maps to a variable holding identical bytes, so output is
unchanged. Unlike the 33 structural conversions this edits text that
renders, so it is gated on its own per-file byte-diff against main:

  ok  logs --help         (431 B out, 0 B err)
  ok  mkcd --help         (437 B out, 0 B err)
  ok  qc --help           (936 B out, 0 B err)
  ok  rand_string --help  (883 B out, 0 B err)
  ok  smart_exit --help   (265 B out, 0 B err)
  ok  mkcd                (437 B out, 0 B err)   <- no-args error path

c_accent is deliberately NOT merged into c_ok -- both are (set_color green)
but a role is a semantic slot, not a colour. See JOB-BRIEF-FINDINGS.md.

logs.fish needed two calls (the C4 guard block and the --help block, both at
8-space indentation); it is the only multi-run file in the repository and the
exact trap a depth-deduplicating converter falls into. logs.fish also declared
c_bold without ever interpolating it -- that declaration is simply deleted.
2026-09-07 20:10:43 -04:00
rootiest 2efbcfc53f test(guards): cover registry lookup, including identity:site pairing
5 cases. The wrong-site case pins that the key is the identity:site pair
rather than the identity alone.
2026-09-07 20:10:15 -04:00
rootiest f6ead99f48 feat(help): header-driven --help for development functions, plus the wiring guard
bd-pull, branch, check_fish_deps, claude-docs, claude-pr, fast, poke, qr
and parur take the standard call site. fish-deps instead routes -h/--help
to its existing __fish_deps_help, which previously answered only via the
unknown-subcommand path, printing "Unknown subcommand: --help" and
exiting 1.

This completes the 40 conversions and lands
test_every_user_facing_function_has_help -- acceptance criterion 6, the
check that fails when a new user-facing function ships without help. It
was developed red against the pre-conversion tree and reported exactly
the 40 names of the design's CONVERT table:

    bd-pull bkg branch check_fish_deps claude-docs claude-pr cleanup
    fast fc fish-deps fzf-update gip gip4 gip6 hist lD ld limine-edit
    lock lsr lss lstree lt ltr lx parur poke ports qr sbver screensleep
    split spwin steam-dl sudo-toggle swapstat tab tmux-clean upgrade
    wake-lock

each as "<name>: no -h/--help handling and not in $__help_exempt", with
the guard exiting 1. The count fell 40 -> 32 -> 24 -> 17 -> 10 -> 0
across the conversion commits. It is committed green so every commit on
this branch passes the suite.

$__help_exempt is the single machine-readable exemption record; a second
assertion fails if an exempt name stops being a published function, so a
rename or deletion upstream cannot silently exempt nothing.
2026-09-07 20:09:30 -04:00
rootiest 639d139799 test(guards): pin C5 logging as opt-in, including that the master cannot enable it
8 cases on the real __fish_config_op_logging name. AGENTS.md records this
as a deliberate special case agents keep trying to 'fix' out; three of
these cases exist specifically to turn that into a test failure.

Includes the three subcategory cases proving the opt-in is inherited
through chain[-1] rather than special-cased per subcategory.
2026-09-07 20:09:26 -04:00
rootiest d75bf4f1b4 refactor: use __fish_palette in small utility functions (2/2)
25 duplicated declarations replaced by 7 calls. Completes the 33 structural
conversions: 167 declarations removed, 33 calls added, and --structural
confirms every one of the 33 diffs is declaration removals plus inserted
calls only -- no rendering text touched.

functions/y.fish is the one file in the repo with no trailing newline on
main; its original file ending is preserved rather than normalised.
2026-09-07 20:08:45 -04:00
rootiest 2b1e99ab47 test(guards): cover the cascade's subcategory -> category -> master order
13 cases on invented variable names, so nothing ambient can perturb them:
default-on, explicit truthy/falsy, unrecognized-defers, subcategory
overriding the category in both directions, and the four cases that
together pin the master as an off switch only.
2026-09-07 20:07:37 -04:00
rootiest 3d826c5407 feat(help): header-driven --help for terminal and session functions
spwin, tab, hist, bkg, wake-lock and fc take the standard first-arg-only
call site, so `wake-lock rsync --help` still runs rsync.

split takes --help only: its own ARGUMENTS documents -h as --horizontal,
so a literal test replaces the renderer's own flag check. Verified that
`split -h` still selects a horizontal split and never prints help.
2026-09-07 20:07:25 -04:00
rootiest a8d564803b test(guards): cover __fish_variable_check's four exit codes
21 cases: a precondition asserting the fork's registry is loaded, then
truthy/falsy/unset/empty/unrecognized/no-argument/list.

Runs isolated, which is what lets these cases manipulate real guard
variable names without being able to reach the user's universal variables.
2026-09-07 20:06:42 -04:00
rootiest 3399149cba feat(help): header-driven --help for system-info functions
gip, gip4, gip6, ports, swapstat, sbver and steam-dl. All ignored $argv,
so --help previously ran the query or the launcher. sbver keeps its own
--brief flag: only argv[1] is inspected and --brief is not a help flag,
so the body still runs for it.
2026-09-07 20:06:23 -04:00
rootiest f3f6e6356b refactor: use __fish_palette in small utility functions (1/2)
37 duplicated declarations replaced by 8 calls. Output strings untouched;
byte-identical across 29 harness cases.
2026-09-07 20:06:20 -04:00
rootiest a824005893 refactor(config-settings): draw the value pages through the frame
Width tier, title border, boolean badge, cursor cell and row line now come
from __config_settings_frame. The type badges (PATH/INT/LIST/STR) stay
here -- they are this page's own vocabulary, used nowhere else -- and so
does the STR default arm, which covers a user-settable value rather than
being dead code.

Introduces the `shorten` fit policy: these fields hold arbitrary user
values and want an ellipsis, unlike the toggle page's per-tier text. The
inline editor keeps `pad`, because its field is already length-constrained
and carries a reverse-video caret whose escapes string shorten miscounts.

The panel stays exactly 16 lines -- chrome(6) + nrows + blanks -- so
config-settings.fish's panel_h and its erase are untouched.

Rendering unchanged: 377/377 render cases byte-identical, golden untouched.
Code lines 123 -> 98 (-25).
2026-09-07 20:06:14 -04:00
rootiest b0c6d7f7f5 refactor: use __fish_palette in system and package functions
44 duplicated declarations replaced by 8 calls. Output strings untouched.

fish-deps, upgrade and wake-lock have no --help path and are deliberately
NOT added to the harness case list -- upgrade and wake-lock have side
effects, and runtime coverage is not worth mutating state during tests.
They are covered by --structural, which proves mechanically that no
rendering line changed.
2026-09-07 20:05:23 -04:00
rootiest 71574b5030 feat(help): header-driven --help for listing presets
lD, lsr, lss, lstree, lt, ltr, lx and ld. None shadows a real binary, so
there is no disabled-fallback contract to honour, and `eza --help`
documented eza rather than the preset that is these functions' entire
content. First-arg-only interception leaves `lt -la --help` passing
through to eza untouched.
2026-09-07 20:05:15 -04:00
rootiest 53b353eb7f test: assert conf.d stays lazy in non-interactive shells
Spawns one non-interactive child against the sandboxed config and asserts on
what loaded -- abbreviations, key bindings, bang-expansions, the C5 event
handlers and the tailscale completion machinery must all be absent. Exit
codes name which guard regressed. No wall-clock assertions, so it cannot
flake.
2026-09-07 20:04:47 -04:00
rootiest ad9926861b refactor(config-settings): draw the sub-category page through the frame
Width tier, title border, badge, cursor cell and row line now come from
__config_settings_frame, and the hand-verified title dash count
(iw - L - S - 22) is derived from the segment's visible width instead.

Introduces the `cut` fit policy, which is this page's documented
divergence from the category list: its labels and descriptions are static
data from __config_settings_subcats rather than per-tier authored text,
and several exceed the narrower tiers' fields. `string pad` only ever
grows a string, so they are truncated before padding. That reason now
lives in two places -- the frame's NOTES and each call site -- and the
DESCRIPTION block stating it here is unchanged.

Drops label_w and desc_w, both left assigned and never read once the
frame derives the field width; the comment recording the 13-wide label
field and how the description field absorbs it stays.

Rendering unchanged: 377/377 render cases byte-identical, golden untouched.
Code lines 85 -> 49 (-36).
2026-09-07 20:04:44 -04:00
rootiest 29afe5c89f refactor: use __fish_palette in the config-settings TUI
27 duplicated declarations replaced by 5 calls. Output strings untouched.

Four of these five files have no --help path, so the byte-identity harness
cannot reach them. Covered instead by driving the real TUI under a pty on
both the baseline and converted trees with an identical paced keystroke
feed (down x3, Tab, Enter, Esc, Tab, down, q):

  9 redraws, 15068 bytes, byte-identical on both sides
  c_sel (ESC[35;1m) x9, c_hi (ESC[37;1m) x8, c_head (ESC[36;1m) x9
  all four draw helpers reached -- draw, pagetab, draw_subcat
  ("cascade default", "(category)"), draw_value ("Allow prev")
2026-09-07 20:04:26 -04:00
rootiest 037588ecf6 fix(help): stop eight functions executing on --help
These eight ignore $argv entirely, so passing --help ran the command
instead of describing it. The new check shadows every external binary
they reach with a recording stub on PATH and fails if anything is
invoked. Before the fix it reported, verbatim:

    cleanup --help EXECUTED: pacman -Qtdq
    fzf-update --help EXECUTED: git clone --depth 1 https://github.com/junegunn/fzf.git /tmp/.../.fzf
    limine-edit --help EXECUTED: sudoedit /boot/limine.conf sudo limine-enroll-config sudo limine-mkinitcpio sudo sbctl sign-all
    lock --help EXECUTED: loginctl lock-session
    screensleep --help EXECUTED: busctl --user call org.kde.kglobalaccel ... invokeShortcut s Turn Off Screen
    sudo-toggle --help EXECUTED: sudo stat -c %s /etc/sudoers.d/nofail-toggle sudo tee /etc/sudoers.d/nofail-toggle
    tmux-clean --help EXECUTED: tmux list-sessions -F #{session_name} #{session_attached}
    upgrade --help EXECUTED: paru -Syu --noconfirm

cleanup's log line is the read that precedes `sudo pacman -Rns $orphans`,
which the stub suppressed by returning no orphans; on a real machine with
orphans present the removal ran.

Each now answers --help from its own comment header. The call site is the
first statement of the body, above the C4 guard, so help stays reachable
when the component is disabled and nothing side-effecting runs first.
2026-09-07 20:04:02 -04:00
rootiest 0a9cfebcad refactor(tests): discover suites instead of naming them
run-tests.fish globs tests/test-*.fish and runs each in the mode the suite
declares in its own header. Isolated is the default and no typo can
promote a suite to in-session: detection is case-insensitive so a
near-miss is caught, the value comparison is exact.

In-session suites share one sandboxed session; isolated suites each get a
--no-config child with temp XDG dirs. Counts come back through a file so
output keeps streaming.

functional.fish becomes test-session.fish, its 15 predicates rewritten as
check calls. 332 assertions, unchanged per-phase.
2026-09-07 20:02:59 -04:00
rootiest 889f8e0768 refactor(config-settings): draw the category list through the frame
Width tier, title border, badge, cursor cell and row line now come from
__config_settings_frame. The per-tier description sets stay here -- they
are this page's data, authored to fit each tier's field exactly -- and are
selected by width rather than by re-testing $COLUMNS.

Rows pass the `pad` fit policy explicitly. That is a decision, not a
default: `cut` would be a byte-for-byte no-op on these strings today and
would silently discard the property that they are tuned to their field.

Drops c_hi, which was assigned and never read.

Rendering unchanged: 377/377 render cases byte-identical, golden untouched.
Code lines 114 -> 80 (-34).
2026-09-07 20:02:48 -04:00
rootiest e64e964666 refactor: use __fish_palette in agents and auto-pull functions
34 duplicated declarations replaced by 5 calls. Output strings untouched;
byte-identical across 29 harness cases.
2026-09-07 20:02:15 -04:00
rootiest bca79d3c5a fix(test): exclude __fish_palette.fish from the structural diff check
The helper is a new file, so its diff is entirely additions and can never
be "purely structural". It declares colours rather than rendering any, and
tests/functional.fish asserts its 12 roles directly.
2026-09-07 20:02:09 -04:00
rootiest d49d90a2de chore(docs): regenerate manual
docs/fish-config.1 is left for CI to regenerate: the local pandoc is 3.10.2
against CI's 3.1.3, so rebuilding it here emits ~4700 lines of formatter
churn unrelated to this change. The component registry rebuilt identically.
2026-09-07 20:01:24 -04:00
rootiest 2ac8e5c0d4 feat(config-settings): add __config_settings_frame shared renderer
The width tier, title-border arithmetic, ON/OFF/DEFAULT badge, cursor cell
and table row are currently hand-copied across the three draw functions.
This adds them once, with the two geometry identities derived rather than
hand-maintained: a row's chrome is a fixed 21 columns, so field_w is
iw - 21 - label_w (reproducing both iw-33 and iw-34), and a title border is
dashes = iw - visible(segment) - 1 (reproducing all three of iw-23,
iw-len-3 and iw-L-S-22).

The frame owns no page height. Every verb prints exactly one line or
fragment, so the fixed-16 category and value pages and the dynamic
7+n sub-category page keep their heights, and config-settings.fish's
erase is unaffected.

No caller yet, so rendering cannot move: the golden's existing page
section is byte-identical (verified with cmp -n over its previous size)
and the file only gains frame-verb cases appended after it.
2026-09-07 20:00:59 -04:00
rootiest 85c753aaa6 docs: point tailscale and cheat completions at completions/
Follows the relocation out of conf.d/. The completions/ branch of the file
tree is expanded from the real directory at build time, so it needs no
hand-written children. Generated docs are regenerated separately.
2026-09-07 19:59:53 -04:00
rootiest b424b26700 test(help): pin the renderer's degraded-path contract
A missing or label-less header must still print and exit 0. Returning 1
would hand control back to the caller's body, which for upgrade(1) is a
full system upgrade.

Both fixtures reach the renderer's bottom `count $out -eq 0` fallback,
not the unreadable-source branch. Mutating that fallback to `return 1`
turns the test red with:

    headerless executed its body despite --help
    malformed executed its body despite --help

The mutation was reverted before this commit.
2026-09-07 19:59:50 -04:00
rootiest d3028d7703 refactor(tests): extract the shared assertion core into tests/lib.fish
check/section/report and the counters move out of the vault suite
unchanged. report also writes its counts to $FISH_CONFIG_TEST_COUNTS so a
driver can aggregate without parsing stdout, and ends on an explicit
boolean per AGENTS.md item 5.

All 317 vault assertions and every fixture helper are untouched.
2026-09-07 19:59:23 -04:00
rootiest 52711a42a2 perf(conf.d): skip paru/yay wrapper generation in non-interactive shells
Neither file defines a function or sets a global; their only effect is
writing ~/.local/bin/<tool>, which every interactive session does anyway.
Combined 10.7 ms off every fish -c.
2026-09-07 19:58:10 -04:00
rootiest c998d1b8d7 perf(conf.d): skip C5 logging sync in non-interactive shells
__fish_config_sync_logging ran on every fish -c, mkdir+touching the C5
sentinel on disk from every subshell. Its consumers — the Kitty watcher and
the paru/yay wrappers — are interactive-context, and every interactive shell
still reconciles the state.
2026-09-07 19:56:45 -04:00
rootiest 5fa849fd81 perf(conf.d): skip wakatime hook in non-interactive shells
fish_postexec is emitted only by the interactive reader (verified), so the
handler could never fire in a script. No telemetry behaviour changes.
2026-09-07 15:00:57 -04:00
rootiest a31a46bdc2 feat: add __fish_palette shared output palette helper
Declared --no-scope-shadowing with a bare set, so the 12 colour roles land
in the caller's scope under the same short names the consuming functions
already interpolate. Keeping the names means the conversion never edits an
output string in 33 of the 38 consumers.

No consumer is converted yet; byte-identity harness still reports 29/29.
2026-09-07 15:00:32 -04:00
rootiest 8fc27fa9c2 fix(tests): route the runner's utilities through command
run-tests.fish executes under the config it tests, which shadows cp, rm
and cat. The real hazard is cp: the config aliases it to 'cp -i', which on
a non-empty destination reads EOF in a non-interactive runner, silently
skips the copy and exits 0 -- a sandbox missing config files, reported as
success.

rm -rf and cat were measured and behave correctly as-is (the rm wrapper
bails to command rm on any non-recursive flag, so -rf really deletes and
does not trash). Prefixed anyway: a test runner must not depend on the
configuration under test.
2026-09-07 15:00:10 -04:00
rootiest 2edea59ca3 perf(conf.d): skip auto-pull handler in non-interactive shells
The --on-variable PWD handler backgrounds a git fast-forward. A script that
cd's was firing it, which is also where AGENTS.md Task #4's credential
prompt could surface from a background job.
2026-09-07 15:00:01 -04:00
rootiest 5edfb5b720 perf(conf.d): skip theme colors in non-interactive shells
fish_color_* is consumed only by the syntax highlighter. The guard sits
below the existing cleanup branch so stale-FZF_DEFAULT_OPTS cleanup keeps
running where it does today; the FZF value itself is a persisted universal
and survives regardless.
2026-09-07 14:59:10 -04:00
rootiest 9077d9837e feat(help): add __fish_help_header runtime renderer
Parses a function's own man-page comment header at call time and prints
it as a help menu on stdout, so the documentation that already generates
Section 5 of the manual becomes reachable from the shell.

Reads the .fish source rather than the generated docs/fish-config.md, so
it cannot go stale between a header edit and a docs rebuild. Walks
backwards from the `function` line to collect the header, which resolves
multi-header files (fish-deps, gi, y) without reimplementing
manualtools._block_identity.

Returns 1 only when argv[1] is not a help flag; every other path prints
and returns 0. A return of 1 hands control back to the caller's body.

Nothing calls it yet.
2026-09-07 14:59:05 -04:00
rootiest dd672ded3b fix(tests): isolate the vault suite from the live config and universal variables
The suite ran under a plain `fish`, which loads the user's real
~/.config/fish and their universal variables -- this repo doubles as that
config. A test manipulating a guard variable could erase a real universal
variable out of the running shell.

Override XDG_CONFIG_HOME/XDG_DATA_HOME and pass --no-config. HOME stays
real on purpose: overriding it makes the suite's two hermeticity
assertions vacuous. Reasoning recorded at the call site.

Vault suite still 317/317 with byte-identical stderr.
2026-09-07 14:59:05 -04:00
rootiest 4d6b99fb28 perf(conf.d): skip starship.fish in non-interactive shells
Defines fish_prompt only. The guard precedes the op-guard and the type -q
PATH scan so both are skipped in scripts. Scripts fall back to the repo's
autoloadable functions/fish_prompt.fish, which nothing invokes anyway.
2026-09-07 14:58:18 -04:00
rootiest 51da3e9d83 test: add byte-identity harness for the shared palette
Compares rendered stdout and stderr of 29 colour-bearing cases between a
pristine baseline ref and the working tree, in isolated XDG_CONFIG_HOMEs
that carry the gitignored fish_variables so the opinionated guards resolve.

Two traps this harness exists to avoid, both of which silently produce a
meaningless comparison rather than an error:

  - `git archive main` omits fish_variables because it is untracked. Without
    it __fish_config_op_enabled is unresolvable and every guarded function
    short-circuits, so all cases render empty and trivially "match".
  - `qc --help` shells out to aichat and never reaches its colour path
    unless aichat is on PATH; the harness stubs it.
2026-09-07 14:57:07 -04:00
rootiest bb5b6b361a perf(conf.d): skip key_bindings.fish in non-interactive shells
fish_user_key_bindings is invoked only by the interactive reader.
2026-09-07 14:57:02 -04:00
rootiest 13d415db2b perf(conf.d): skip bash_expands.fish in non-interactive shells
The six expand_* functions are only ever reached through abbr --function,
which fires during interactive expansion.
2026-09-07 14:56:16 -04:00
rootiest c3a2b6fa0c perf(conf.d): skip abbr.fish in non-interactive shells
61 abbreviations were declared on every fish -c. Abbreviations expand only
in the line editor, so nothing outside an interactive session can use them.
2026-09-07 14:55:31 -04:00
rootiest 3d8d6a4468 refactor(conf.d): move cheat completions to completions/
Thirteen lines, every one a `complete -c cheat` registration, sourced on
every shell start from the wrong directory. Startup cost was already ~0
because its command substitutions are lazy, but completions/ is where fish
expects the file and the move is free.
2026-09-07 14:54:38 -04:00
rootiest fb21fa550a perf(completions): drop tailscale self-priming completion call
The Cobra-generated block ran `complete --do-complete "tailscale "` to flush
pre-existing completions before erasing them, which executed the tailscale
binary. From completions/ it has no job: fish autoloads only the first match
on $fish_complete_path and the repo's completions/ precedes the vendor dir,
so the vendor file is never sourced.

Verified byte-identical completion output across four probes with the vendor
file present. A comment at the deletion site records the reasoning.
2026-09-07 14:53:35 -04:00
rootiest 321b80f1f8 perf(conf.d): load tailscale completions lazily from completions/
conf.d/tailscale.fish is 252 lines of Cobra-generated completion that fish
sourced on every shell start, and its self-priming block executed the
tailscale binary to warm the completion cache. completions/ is the directory
fish autoloads on first <TAB>.

Measured: 19.2 ms off both interactive and non-interactive startup.
2026-09-07 07:32:03 -04:00
rootiest 574b235b6f test(config-settings): add byte-exact golden-output render harness
The three config-settings draw functions are hand-tuned layout code whose
field widths, dash counts and pad targets are arithmetic on the width tier.
Any refactor of them has to be byte-identical, and nothing until now could
prove that.

Renders all 356 page/width/scope/cursor-row combinations -- every page at
COLUMNS 100/88/84/70, both scopes, cursor on every row, plus the inline
editor -- and byte-compares against a committed baseline.

The golden holds raw output: set_color escapes and box drawing exactly as
emitted, plus the wrap-aware \e[<N>A\e[J erase config-settings.fish would
emit for each panel, and per case the declared panel height against the
measured line count. Nothing is normalized; the gate is cmp(1) and a
one-space change anywhere fails it. Proven by four deliberate mutations,
one per draw function plus a panel-height change, each caught and reverted.

Runs entirely inside a throwaway HOME/XDG_CONFIG_HOME sandbox: the fixtures
must be real universal variables, and this repo doubles as a live
~/.config/fish. fish --no-config cannot be used, as -N silently degrades
set -U to global scope. Every utility is called through `command`, since
the config itself shadows rm, cat and mkdir and aliases cp -i.
2026-09-07 07:28:45 -04:00
rootiest 8a2731d411 revert: untrack the spec
AGENTS/ is gitignored upstream; a tracked file there pollutes
git diff main..work, which is what the user reads when choosing
which job to merge. Spec stays on disk, browsable, out of history.
2026-09-07 07:22:51 -04:00
rootiest 21a02fd0e4 docs(specs): add header-driven --help design
Design for making the man-page-style comment header above each function
reachable at runtime via -h/--help, instead of hand-writing 40 more menus.

One renderer (__fish_help_header) parses the .fish source at call time and
prints the header to stdout; each call site is a single line. Audits all 79
published functions that lack help today into 35 shadow/pass-through exempt,
4 prompt-hook exempt, and 40 to convert, with a one-line reason each.

Corrects the baseline counts (109 published functions, 30 with menus, 79
without) and records two pre-existing defects found while auditing.

Note: AGENTS/ is gitignored upstream, so this file is force-added.
2026-09-07 03:06:30 -04:00
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 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
195 changed files with 14430 additions and 3080 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.
-->
- [ ]
- [ ]
+44 -3
View File
@@ -29,8 +29,15 @@ on:
- 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.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == '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
@@ -52,6 +59,7 @@ jobs:
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') &&
@@ -119,10 +127,43 @@ jobs:
--commit-dirty=true
- name: Commit generated docs
env:
BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }}
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea"
# actions@gitea was never a verified email on any account, so
# these commits could never show as signed/verified regardless
# of server config. fishconfig-bot is a dedicated Gitea account
# (verified email, no login) that owns this key; the private
# key lives only in the CI_GPG_PRIVATE_KEY repo secret.
#
# Secret is base64-encoded: a raw multi-line armored key piped
# through `echo "$VAR" | gpg --import` came out CRC-corrupted
# ("Invalid keyring") the first time this ran -- something in
# the secret/env round-trip mangles embedded newlines. Base64
# collapses it to one line immune to that.
export GNUPGHOME="$(mktemp -d)"
chmod 700 "$GNUPGHOME"
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
command -v gpg >/dev/null || sudo apt-get install -y --no-install-recommends gnupg
echo "$BOT_GPG_KEY" | base64 -d | gpg --batch --import
git config user.name "Gitea Actions Bot"
git config user.email "fishconfig-bot@git.rootiest.dev"
git config user.signingkey CAA082C2F3467E1F7217AD492075C120312D23F4
git config commit.gpgsign true
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
+3 -2
View File
@@ -1,6 +1,6 @@
# GitIgnore for Fish-config
# id: gitig-boilerplate-7f0528027e23f210196ae4583c408e5b
# id: gitig-boilerplate-a27b89403ff9f0bb3340ede87b42f93e
# ╭──────────────────────────────────────────────────────────╮
# │ GitIgnore Boilerplate Template │
# ╰──────────────────────────────────────────────────────────╯
@@ -93,8 +93,9 @@ functions/sponge_*.fish
[Aa][nN][Tt][Ii][Gg][Rr][Aa][Vv][Ii][Tt][Yy].[Mm][Dd]
.[Aa][Nn][Tt][Ii][Gg][Rr][Aa][Vv]*
# Matches AGENTS.md, .remember, etc.
# Matches AGENTS.md, .agents/ (agy), .remember, etc.
[Aa][Gg][Ee][Nn][Tt][Ss].[Mm][Dd]
.[Aa][Gg][Ee][Nn][Tt][Ss]*
.[Rr][Ee][Mm][Ee][Mm][Bb][Ee][Rr]
# ──────────────────────────────────────────────────────────────
+291 -5
View File
@@ -9,7 +9,9 @@ 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)
@@ -34,6 +36,79 @@ If you're touching anything under `docs/manual/`, you'll also want `pandoc`,
(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
@@ -48,6 +123,22 @@ assumes you *do* have push access (maintainers, regular contributors).
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,
@@ -64,6 +155,183 @@ assumes you *do* have push access (maintainers, regular contributors).
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/):
@@ -212,11 +480,9 @@ 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.
category list and toggle semantics. Most new functions are *not*
opinionated components — only tag something if it fits an existing
category; this isn't something to force.
## Documentation Pipeline
@@ -239,6 +505,26 @@ 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
+21 -6
View File
@@ -146,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:
@@ -183,6 +186,8 @@ Then open a new Fish shell — Fisher will be installed automatically on first l
A [chezmoi](https://www.chezmoi.io/) dotfile manager is also configured — secrets are kept in a private overlay directory (see [Personalization](#personalization)) and excluded from version control.
**External requirements.** Everything degrades gracefully if a tool is missing, with two exceptions worth calling out. `python3` is required by the documentation pipeline and by `config-settings`, whose TUI is drawn with the stdlib `curses` module. That module ships with CPython on Arch, Fedora and a full Debian/Ubuntu `python3`; `python3-minimal` alone does **not** carry `_curses`, so install the complete `python3` package there. `config-settings` checks for both and tells you which is missing rather than failing inside the renderer.
> [!IMPORTANT]
> `config.fish` ends with a `return` sentinel guard. Any lines appended **after** it by a tool's setup command will silently have no effect. Many tools (starship, zoxide, mise, etc.) offer a setup command that appends an `init | source` line to your `config.fish` — all integrations are managed through `conf.d/` files instead. If you add a new tool and its shell integration appears to do nothing, check whether its setup command appended an init line to the bottom of `config.fish` and create a `conf.d/<tool>.fish` file for it instead.
@@ -285,7 +290,7 @@ To opt out, set `__fish_user_dots_symlink` to a falsy value (or toggle **Dots li
Everything opinionated in this config — command shadows, startup side-effects, key and environment overrides, terminal integrations, and the first-run greeting — is active by default but can be switched off. Logging (C5) is the exception: it is **opt-in**, off until you explicitly enable it.
> **The easy way — `config-settings`:** Run `config-settings` for an interactive TUI that manages settings across four pages — **Universal** and **Session** (the opinionated category toggles below, persistent or per-shell), **Sponge** (history-scrubbing: delay, exit codes, purge-on-exit, and extra sensitive variable names), and **Paths** (scrollback log dir, max files, and the user-dots path) — without typing a single variable name. Navigate with the arrow keys (or `h`/`j`/`k`/`l`); toggle rows step OFF ← DEFAULT → ON, value rows edit inline with `Enter` and clear with `←`. `Tab`/`Shift-Tab` cycle pages and `q` quits. Changes apply instantly. The panel auto-sizes to your terminal width (four tiers from 52- to 78-wide with a 6-column margin), centers itself horizontally, and redraws within ~0.3 s of a resize.
> **The easy way — `config-settings`:** Run `config-settings` for an interactive TUI that manages settings across four pages — **Universal** and **Session** (the opinionated category toggles below, persistent or per-shell), **Sponge** (history-scrubbing: delay, exit codes, purge-on-exit, and extra sensitive variable names), and **Paths** (scrollback log dir, max files, and the user-dots path) — without typing a single variable name. Navigate with the arrow keys (or `h`/`j`/`k`/`l`); toggle rows step OFF ← DEFAULT → ON, value rows edit inline with `Enter` and reset with `←`. `Enter` on a category opens its sub-categories, `/` filters the page — reaching into sub-categories too, listed as `Category Sub` — `?` opens a help overlay, `Tab`/`Shift-Tab` cycle pages, and `q` applies your edits and quits. Edits are collected as you make them and written in one batch on exit; the status bar shows the pending count. The panel is drawn with Python's stdlib `curses`, so it resizes with the terminal and never flickers — see [Installation](#installation) for the `python3` requirement.
If you'd rather set them by hand, each category is controlled by a universal variable. Six category toggles and one master switch are available:
@@ -293,7 +298,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 |
@@ -326,8 +331,10 @@ 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/)
press Enter on a category row to browse and toggle its sub-categories
or press `/` and type, which searches sub-categories across every
category at once and lists the hits as `Category Sub`. Or see the
[Components Reference](https://fish.rootiest.fyi/08-components-reference/)
for the full sub-category list per category.
---
@@ -348,6 +355,14 @@ 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 -3
View File
@@ -108,7 +108,6 @@ function __bd_requires_order_preservation
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __bd_comp_results
# - Return false if file completion should be performed
@@ -215,7 +214,7 @@ end
# so we can properly delete any completions provided by another script.
# Only do this if the program can be found, or else fish may print some errors; besides,
# the existing completions will only be loaded if the program can be found.
if type -q "bd"
if type -q bd
# The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
@@ -226,7 +225,7 @@ end
complete -c bd -e
# this will get called after the two calls below and clear the $__bd_perform_completion_once_result global
complete -c bd -n '__bd_clear_perform_completion_once_result'
complete -c bd -n __bd_clear_perform_completion_once_result
# The call to __bd_prepare_completions will setup __bd_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
+22 -22
View File
@@ -1,14 +1,14 @@
# fish completion for copilot
# Generated by `copilot completion fish`. Do not edit by hand.
complete -c copilot -n '__fish_use_subcommand' -f -a 'login' -d 'Authenticate with Copilot'
complete -c copilot -n '__fish_use_subcommand' -f -a 'help' -d 'Display help information'
complete -c copilot -n '__fish_use_subcommand' -f -a 'init' -d 'Initialize Copilot instructions'
complete -c copilot -n '__fish_use_subcommand' -f -a 'update' -d 'Download the latest version'
complete -c copilot -n '__fish_use_subcommand' -f -a 'version' -d 'Display version information'
complete -c copilot -n '__fish_use_subcommand' -f -a 'plugin' -d 'Manage plugins'
complete -c copilot -n '__fish_use_subcommand' -f -a 'mcp' -d 'Manage MCP servers'
complete -c copilot -n '__fish_use_subcommand' -f -a 'completion' -d 'Generate a shell completion script'
complete -c copilot -n __fish_use_subcommand -f -a login -d 'Authenticate with Copilot'
complete -c copilot -n __fish_use_subcommand -f -a help -d 'Display help information'
complete -c copilot -n __fish_use_subcommand -f -a init -d 'Initialize Copilot instructions'
complete -c copilot -n __fish_use_subcommand -f -a update -d 'Download the latest version'
complete -c copilot -n __fish_use_subcommand -f -a version -d 'Display version information'
complete -c copilot -n __fish_use_subcommand -f -a plugin -d 'Manage plugins'
complete -c copilot -n __fish_use_subcommand -f -a mcp -d 'Manage MCP servers'
complete -c copilot -n __fish_use_subcommand -f -a completion -d 'Generate a shell completion script'
complete -c copilot -l version -s v -f -d 'show version information'
complete -c copilot -l interactive -s i -r -d 'Start interactive mode and automatically execute this prompt'
complete -c copilot -l prompt -s p -r -d 'Execute a prompt in non-interactive mode (exits after completion)'
@@ -75,22 +75,22 @@ complete -c copilot -l acp -f -d 'Start as Agent Client Protocol server'
complete -c copilot -l remote -f -d 'Enable remote control of your session from GitHub web and mobile'
complete -c copilot -l no-remote -f -d 'Disable remote control of your session from GitHub web and mobile'
complete -c copilot -n '__fish_seen_subcommand_from login' -l host -r -d 'GitHub host URL (default: https://github.com)'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'install' -d 'Install a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'uninstall' -d 'Uninstall a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'update' -d 'Update a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'list' -d 'List installed plugins'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'marketplace' -d 'Manage plugin marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a install -d 'Install a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a uninstall -d 'Uninstall a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a update -d 'Update a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a list -d 'List installed plugins'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a marketplace -d 'Manage plugin marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l all -f -d 'Update all installed plugins'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'add' -d 'Add a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'remove' -d 'Remove a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'list' -d 'List registered marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'browse' -d 'Browse plugins in a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'update' -d 'Update marketplace plugin catalogs'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a add -d 'Add a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a remove -d 'Remove a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a list -d 'List registered marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a browse -d 'Browse plugins in a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a update -d 'Update marketplace plugin catalogs'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace; and __fish_seen_subcommand_from remove' -l force -s f -f -d 'Force removal even if plugins are installed'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'list' -d 'List configured MCP servers'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'get' -d 'Show server details'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'add' -d 'Add an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'remove' -d 'Remove an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a list -d 'List configured MCP servers'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a get -d 'Show server details'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a add -d 'Add an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a remove -d 'Remove an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from list' -l json -f -d 'Output as JSON'
complete -c copilot -n '__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from get' -l json -f -d 'Output as JSON'
complete -c copilot -n '__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from get' -l show-secrets -f -d 'Show full environment variable and header values (masked by default)'
+44 -44
View File
@@ -24,15 +24,15 @@ function __fish_deadbranch_using_subcommand
contains -- $cmd[1] $argv
end
complete -c deadbranch -n "__fish_deadbranch_needs_command" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "list" -d 'List stale branches'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "clean" -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "config" -d 'Manage configuration'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "backup" -d 'Manage backups'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "stats" -d 'Show repository branch statistics'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "completions" -d 'Generate shell completion scripts'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n __fish_deadbranch_needs_command -s h -l help -d 'Print help'
complete -c deadbranch -n __fish_deadbranch_needs_command -s V -l version -d 'Print version'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a list -d 'List stale branches'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a clean -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a config -d 'Manage configuration'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a backup -d 'Manage backups'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a stats -d 'Show repository branch statistics'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a completions -d 'Generate shell completion scripts'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand list" -s d -l days -d 'Only show branches older than N days (default: from config or 30)' -r
complete -c deadbranch -n "__fish_deadbranch_using_subcommand list" -l local -d 'Only show local branches'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand list" -l remote -d 'Only show remote branches'
@@ -51,11 +51,11 @@ complete -c deadbranch -n "__fish_deadbranch_using_subcommand clean" -s h -l hel
complete -c deadbranch -n "__fish_deadbranch_using_subcommand clean" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "set" -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "show" -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "edit" -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "reset" -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a set -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a show -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a edit -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a reset -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from set" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from set" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from show" -s h -l help -d 'Print help'
@@ -64,18 +64,18 @@ complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from edit" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from reset" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from reset" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "set" -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "show" -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "edit" -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "reset" -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a set -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a show -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a edit -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a reset -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "list" -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "restore" -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "stats" -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "clean" -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a list -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a restore -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a stats -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a clean -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from list" -l repo -d 'Show backups for a specific repository by name' -r
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from list" -l current -d 'Only show backups for current repository'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
@@ -94,28 +94,28 @@ complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from clean" -s y -l yes -d 'Skip confirmation prompt'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from clean" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from clean" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "list" -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "restore" -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "stats" -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "clean" -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a list -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a restore -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a stats -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a clean -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand stats" -s d -l days -d 'Treat branches older than N days as stale (default: from config or 30)' -r
complete -c deadbranch -n "__fish_deadbranch_using_subcommand stats" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand stats" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand completions" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand completions" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "list" -d 'List stale branches'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "clean" -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "config" -d 'Manage configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "backup" -d 'Manage backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "stats" -d 'Show repository branch statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "completions" -d 'Generate shell completion scripts'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "set" -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "show" -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "edit" -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "reset" -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "list" -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "restore" -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "stats" -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "clean" -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a list -d 'List stale branches'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a clean -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a config -d 'Manage configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a backup -d 'Manage backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a stats -d 'Show repository branch statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a completions -d 'Generate shell completion scripts'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a set -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a show -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a edit -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a reset -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a list -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a restore -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a stats -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a clean -d 'Remove old backups, keeping the most recent ones'
+2 -3
View File
@@ -108,7 +108,6 @@ function __ov_requires_order_preservation
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __ov_comp_results
# - Return false if file completion should be performed
@@ -215,7 +214,7 @@ end
# so we can properly delete any completions provided by another script.
# Only do this if the program can be found, or else fish may print some errors; besides,
# the existing completions will only be loaded if the program can be found.
if type -q "ov"
if type -q ov
# The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
@@ -226,7 +225,7 @@ end
complete -c ov -e
# this will get called after the two calls below and clear the $__ov_perform_completion_once_result global
complete -c ov -n '__ov_clear_perform_completion_once_result'
complete -c ov -n __ov_clear_perform_completion_once_result
# The call to __ov_prepare_completions will setup __ov_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
@@ -125,7 +125,6 @@ function __tailscale_requires_order_preservation
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __tailscale_comp_results
# - Return false if file completion should be performed
@@ -228,22 +227,21 @@ function __tailscale_prepare_completions
return 0
end
# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
# so we can properly delete any completions provided by another script.
# Only do this if the program can be found, or else fish may print some errors; besides,
# the existing completions will only be loaded if the program can be found.
if type -q "tailscale"
# The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
complete --do-complete "tailscale " > /dev/null 2>&1
end
# REMOVED (2026-09-07): Cobra's self-priming block —
# if type -q "tailscale"; complete --do-complete "tailscale " >/dev/null 2>&1; end
# It existed to force any pre-existing tailscale completions to load so the
# `complete -c tailscale -e` below could erase them. From completions/ it has
# no job: fish autoloads only the FIRST match on $fish_complete_path, and
# $__fish_config_dir/completions precedes /usr/share/fish/vendor_completions.d,
# so the vendor file is never sourced and there is nothing to erase. It also
# executed the tailscale binary at startup. Verified: completion output is
# byte-identical with and without it.
# Remove any pre-existing completions for the program since we will be handling all of them.
complete -c tailscale -e
# this will get called after the two calls below and clear the $__tailscale_perform_completion_once_result global
complete -c tailscale -n '__tailscale_clear_perform_completion_once_result'
complete -c tailscale -n __tailscale_clear_perform_completion_once_result
# The call to __tailscale_prepare_completions will setup __tailscale_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
+66 -65
View File
@@ -34,6 +34,7 @@ set -g __fish_config_op_registry_keys \
"config:greeting-stamp" \
"config:pager-editor-gpg" \
"config:path-setup" \
"config:privacy" \
"config:vi-mode" \
"done:" \
"du:" \
@@ -79,68 +80,68 @@ set -g __fish_config_op_registry_keys \
"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/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"
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
+3
View File
@@ -12,6 +12,9 @@
# site abbr-integrations: integrations/terminal-abbrs
# site abbr-overrides: overrides/key-bindings
# Abbreviations only expand in the line editor; a script can never use one.
status is-interactive; or return
# Neovim
# @category Editors
# @desc nvim
+5
View File
@@ -13,6 +13,11 @@
#
# Manage the registry with: auto-pull add / remove / list / status
# Registers an --on-variable PWD handler that backgrounds a git fetch. In a
# script that cd's, that is both wasted work and AGENTS.md Task #4's
# credential-prompt hazard fired from a background job.
status is-interactive; or return
# C2 guard: when auto-execution is disabled, do not register the handler.
__fish_config_op_enabled (status basename); or exit
+10 -2
View File
@@ -7,13 +7,19 @@
# Provides bash-style history expansion functions for abbreviations.
# These functions are gated by the C3 overrides switch.
# The six expand_* functions are reachable only through abbr --function
# (conf.d/abbr.fish:677-697), i.e. only during interactive expansion.
status is-interactive; or return
# 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 (status basename); or return 1
set -l token $argv[1]
if test -z "$token"; set token (commandline -t); end
if test -z "$token"
set token (commandline -t)
end
set -l tokens (string split -n " " -- $history[1])
if test (count $tokens) -gt 1
@@ -42,7 +48,9 @@ function expand_bang_minus_n --description 'Execute expand_bang_minus_n'
__fish_config_op_enabled (status basename); or return 1
set -l token $argv[1]
if test -z "$token"; set token (commandline -t); end
if test -z "$token"
set token (commandline -t)
end
# Extract the number from the regex match
if string match -qr '!-(\d+)' -- "$token"
+4
View File
@@ -47,6 +47,10 @@
# This allows for rapid-fire math without leaving the current shell.
# ──────────────────────────────────────────────────────────────────────
# Defines only fish_user_key_bindings, which fish calls from the interactive
# reader and nowhere else.
status is-interactive; or return
function fish_user_key_bindings
# Custom key chords are opinionated (C3 overrides); skip them entirely
+6
View File
@@ -13,6 +13,12 @@
# solely in functions/ are never registered and their --on-variable triggers
# never fire.
# Calls __fish_config_sync_logging at every shell start, which mkdir+touches
# the C5 sentinel on disk. Its only consumers — the Kitty watcher and the
# paru/yay wrappers — are interactive-context; every interactive shell still
# refreshes it.
status is-interactive; or return
function __fish_config_logging_changed --on-variable __fish_config_op_logging \
--description 'C5 event handler: sync logging state when __fish_config_op_logging changes'
__fish_config_sync_logging
+9 -6
View File
@@ -10,6 +10,10 @@
# site paru-autoexec: autoexec/pkg-wrappers
# site paru-logging: logging/pkg-logs
# Defines nothing; its only effect is generating ~/.local/bin/paru, an
# idempotent write every interactive session already performs.
status is-interactive; or return
# 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 (status basename) paru-autoexec; or return
@@ -56,7 +60,7 @@ printf '%s\n' \
"cmd_str=\"$_paru_real\"" \
'for arg in "$@"; do' \
' cmd_str+=" $(printf '"'"'%q'"'"' "$arg")"' \
'done' \
done \
'script -q -e -c "$cmd_str" "$log_file"' \
'exit_code=$?' \
'' \
@@ -66,19 +70,18 @@ printf '%s\n' \
'cleaner="${XDG_CONFIG_HOME:-$HOME/.config}/fish/scripts/clean_progress_log.py"' \
'if command -v python3 >/dev/null 2>&1 && [[ -f "$cleaner" ]]; then' \
' python3 "$cleaner" < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \
'else' \
else \
' sed -i "/^Script \(started\|done\) on /d" "$log_file" 2>/dev/null || true' \
'fi' \
fi \
'' \
'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \
'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \
'excess=$(( ${#logs[@]} - max_files ))' \
'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \
' rm -f "${logs[$i]}"' \
'done' \
done \
'' \
'exit $exit_code' \
> $_paru_wrapper
'exit $exit_code' >$_paru_wrapper
chmod +x $_paru_wrapper
+4
View File
@@ -9,6 +9,10 @@
# Without starship, fish's built-in prompt already emits OSC 133;A
# on the prompt line itself, so no wrapper is needed.
# Defines fish_prompt; no script renders a prompt. Checked before the
# op-guard so the builtin short-circuits ahead of three function autoloads.
status is-interactive; or return
# Replacing the prompt is opinionated (C3 overrides)
__fish_config_op_enabled (status basename); or return
+5
View File
@@ -20,6 +20,11 @@ if not __fish_config_op_enabled (status basename)
return
end
# Below the cleanup block on purpose: that branch erases a stale universal
# FZF_DEFAULT_OPTS and must keep running wherever it runs today. Everything
# past here is fish_color_* for the syntax highlighter, interactive-only.
status is-interactive; or return
# ────────────────────── Syntax highlighting colors ──────────────────────
set --global fish_color_autosuggestion 6c7086
set --global fish_color_cancel f38ba8
+12
View File
@@ -18,6 +18,17 @@ if test -f ~/.fish_profile
source ~/.fish_profile
end
# This file is sourced twice per shell on CachyOS (once by the conf.d
# autoload, once forced by config.fish to re-win over the distro's own
# tricks.fish). The PATH/MANPAGER setup below doesn't need to repeat on
# the second pass -- only the functions/aliases further down do, since
# those are what re-assert over the distro config. Gate the expensive
# calls (fish_add_path, type -q bat) behind a once-per-session guard.
# ponytail: per-session guard, not per-value; if this file grows more
# expensive one-time setup, extend the same guard rather than adding more.
if not set -q __fish_config_tricks_env_applied
set -g __fish_config_tricks_env_applied 1
# Append unique directories to $PATH (fish_add_path handles duplicates automatically)
fish_add_path ~/.local/bin
fish_add_path ~/Applications/depot_tools
@@ -33,6 +44,7 @@ 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
end
# Set settings for https://github.com/franciscolourenco/done
set -gx __done_min_cmd_duration 10000
+8 -3
View File
@@ -9,6 +9,10 @@
# site wakatime-autoexec: autoexec/telemetry
# site wakatime-hook: integrations/notifications
# Registers a fish_postexec handler; that event is emitted only by the
# interactive reader, so the handler is dead weight in a script.
status is-interactive; or return
# 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.
@@ -24,7 +28,7 @@ function __register_wakatime_fish_before_exec -e fish_postexec
set exec_command_str (string split -f1 ' ' "$argv")
if test "$exec_command_str" = 'exit'
if test "$exec_command_str" = exit
return 0
end
@@ -45,8 +49,9 @@ function __register_wakatime_fish_before_exec -e fish_postexec
if git rev-parse --is-inside-work-tree &>/dev/null
set project (basename (git rev-parse --show-toplevel))
else
set project "Terminal"
set project Terminal
end
$wakatime_path --write --plugin "$PLUGIN_NAME/$PLUGIN_VERSION" --entity-type app --project "$project" --entity "$exec_command_str" &> /dev/null&; disown
$wakatime_path --write --plugin "$PLUGIN_NAME/$PLUGIN_VERSION" --entity-type app --project "$project" --entity "$exec_command_str" &>/dev/null &
disown
end
+9 -6
View File
@@ -10,6 +10,10 @@
# site yay-autoexec: autoexec/pkg-wrappers
# site yay-logging: logging/pkg-logs
# Defines nothing; its only effect is generating ~/.local/bin/yay, an
# idempotent write every interactive session already performs.
status is-interactive; or return
# 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 (status basename) yay-autoexec; or return
@@ -56,7 +60,7 @@ printf '%s\n' \
"cmd_str=\"$_yay_real\"" \
'for arg in "$@"; do' \
' cmd_str+=" $(printf '"'"'%q'"'"' "$arg")"' \
'done' \
done \
'script -q -e -c "$cmd_str" "$log_file"' \
'exit_code=$?' \
'' \
@@ -66,19 +70,18 @@ printf '%s\n' \
'cleaner="${XDG_CONFIG_HOME:-$HOME/.config}/fish/scripts/clean_progress_log.py"' \
'if command -v python3 >/dev/null 2>&1 && [[ -f "$cleaner" ]]; then' \
' python3 "$cleaner" < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \
'else' \
else \
' sed -i "/^Script \(started\|done\) on /d" "$log_file" 2>/dev/null || true' \
'fi' \
fi \
'' \
'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \
'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \
'excess=$(( ${#logs[@]} - max_files ))' \
'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \
' rm -f "${logs[$i]}"' \
'done' \
done \
'' \
'exit $exit_code' \
> $_yay_wrapper
'exit $exit_code' >$_yay_wrapper
chmod +x $_yay_wrapper
+9
View File
@@ -25,6 +25,7 @@
# 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
@@ -104,6 +105,14 @@ 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 (status basename) pager-editor-gpg
+179 -8
View File
@@ -8,12 +8,14 @@
"""
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
@@ -21,6 +23,8 @@ 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
@@ -82,6 +86,100 @@ def _with_abbreviations(body: str, abbrs: dict[str, list[dict]]) -> str:
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.
@@ -93,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] = []
@@ -111,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"
@@ -195,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()
)
@@ -223,7 +334,16 @@ def _is_prose(para: list[str]) -> bool:
def _is_shell(para: list[str], entry_name: str | None) -> bool:
"""True when every line of a paragraph looks like a shell command."""
"""True when every line of a paragraph looks like a shell command.
Recognised command names are `SHELL_HEADS` plus the same code
vocabulary `codespans` wraps in backticks (repo function names and
`fish-deps` catalog entries included) one shared list instead of a
second hand-maintained one that silently drifts, which is how
`fish-deps`/`config-settings`-style custom commands used to fall
through to an unhighlighted block.
"""
vocab = SHELL_HEADS | _code_vocabulary().full
name_re = (
re.compile(rf"(?<![\w-]){re.escape(entry_name)}(?![\w-])")
if entry_name
@@ -235,7 +355,7 @@ def _is_shell(para: list[str], entry_name: str | None) -> bool:
continue
if name_re and name_re.search(stripped):
continue
if stripped.split()[0].lstrip("$").rstrip(";") not in SHELL_HEADS:
if stripped.split()[0].lstrip("$").rstrip(";") not in vocab:
return False
return True
@@ -250,6 +370,30 @@ PATH_LINE_RE = re.compile(r"^[~$][\w./{}-]*\.\w+$")
# as a literal comment inside the code.
FILENAME_COMMENT_RE = re.compile(r"^#\s*(?:in\s+)?([$~\w./-]+\.\w+)\s*$")
# A leading comment that isn't a filename can still name what the block is
# about (e.g. "# Arch / AUR" heading a distro's install command) rather
# than explain a step ("# Turn it off:") — the trailing-punctuation and
# length checks in `_label_title` are what tell the two apart.
LABEL_COMMENT_RE = re.compile(r"^#\s*(.+)$")
def _label_title(line: str) -> str | None:
"""A short, label-shaped leading comment, promoted to a fence title.
Anything that reads as a sentence trailing `.`/`!`/`?`/`;`/`:`/`,`,
or just long is left as a literal comment instead: it's explaining a
step, not naming the block.
"""
m = LABEL_COMMENT_RE.match(line)
if not m:
return None
text = m.group(1).strip()
if not text or text[-1] in ".!?;:,":
return None
if len(text) > 48 or len(text.split()) > 8:
return None
return text
CELL_SPLIT = re.compile(r"\s{2,}")
# A rule line under a header row — the "Component Reference" tables'
@@ -394,18 +538,32 @@ def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str:
path = para[0].strip()
name = path.rsplit("/", 1)[-1]
return f'```fish title="{name}"\n{path}\n```'
# Checked even when `deeper`: a shell paragraph's own nested indentation
# (a for/if/while body) must not be mistaken for a table's alignment —
# `_is_shell` only looks at each line's first word, so it stays safe to
# try before falling through to the table/text fallbacks below.
if _is_shell(para, entry_name):
body = para
title = None
m = FILENAME_COMMENT_RE.match(para[0].strip())
if m:
title, body = m.group(1), para[1:]
else:
label = _label_title(para[0].strip())
if label:
title, body = label, para[1:]
info = f'fish title="{title}"' if title else "fish"
return f"```{info}\n" + "\n".join(body) + "\n```"
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:
@@ -478,12 +636,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] = []
@@ -522,7 +692,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 = {
@@ -718,6 +888,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))
+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)
)
+827 -260
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
+714 -219
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -99,10 +99,16 @@ def render(registry: dict[str, list[str]]) -> str:
lines += [f" {k} \\" for k in quoted_keys[:-1]] + [f" {quoted_keys[-1]}"]
lines.append("")
values = ['"' + " ".join(registry[k]) + '"' for k in keys]
values = [
(
'"' + " ".join(registry[k]) + '"'
if any(c in " ".join(registry[k]) for c in ' \t*?[]"\'\\$')
else " ".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"
+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 -->
---
+2 -1
View File
@@ -118,7 +118,8 @@ Every shell command is reported to WakaTime for time-tracking. Set
### Tailscale
Full tab completion for the `tailscale` CLI is provided via `conf.d/tailscale.fish`.
Full tab completion for the `tailscale` CLI is provided via
`completions/tailscale.fish`, autoloaded on the first `tailscale<TAB>`.
### Done Notifications
+87 -26
View File
@@ -159,6 +159,27 @@ 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
### Starship
@@ -167,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
@@ -179,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:
Segment Meaning
────────────────────────────────────────────────────────────────
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
─[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
─[N/I/R/V] Vi-mode indicator when vi bindings are active
┬─ / ╰─> Connector lines: Catppuccin Green on success, Red on failure
(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"
`---`
---
@@ -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
@@ -35,7 +37,7 @@ is independently controlled by C5 (see below).
## Sub-categories
`__fish_config_op_overrides` sub-divides into three sub-categories, each
`__fish_config_op_overrides` sub-divides into four sub-categories, each
with its own `__fish_config_op_overrides_<slug>` toggle:
## key-bindings
@@ -53,3 +55,8 @@ 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.
@@ -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.
+5 -5
View File
@@ -12,16 +12,16 @@ 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 (24 in
total) with their own `__fish_config_op_<category>_<subcategory>` toggles
-- see that category's page for its sub-category list.
Each category further sub-divides into several sub-categories, each with
its own `__fish_config_op_<category>_<subcategory>` toggle -- see that
category's page for its sub-category list.
## Per-function overrides: `C0`/`always`
+2 -2
View File
@@ -120,7 +120,7 @@ The `fish_plugins` file at the config root:
- [`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.
---
+3 -3
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,7 +237,7 @@ Re-enable everything:
set -Ue __fish_config_opinionated
Each category also has two to six sub-categories (e.g.
Each category can also have several 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
+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 -->
+1 -3
View File
@@ -35,7 +35,6 @@ The configuration uses a structured file tree:
├── conf.d/
│ ├── abbr.fish All abbreviations
│ ├── autopair.fish Auto-pair brackets and quotes
│ ├── cheat.fish cheat.sh tab completions
│ ├── done.fish Desktop notifications for long commands
│ ├── first_run.fish One-time init: Fisher bootstrap, theme
│ ├── key_bindings.fish Custom key bindings and Vi mode
@@ -47,14 +46,13 @@ The configuration uses a structured file tree:
│ ├── zellij-logging.fish C5 fish_exit handler for zellij
│ ├── sponge_privacy.fish Sponge privacy patterns
│ ├── starship.fish fish_prompt shell-integration markers
│ ├── tailscale.fish Tailscale CLI tab completions
│ ├── theme.fish Catppuccin syntax highlight colors
│ ├── tricks.fish PATH, bang-bang helpers, bat man pages
│ ├── wakatime.fish WakaTime shell hook
│ ├── yay-wrapper.fish Auto-generates yay logging wrapper
│ └── zoxide.fish Zoxide z/zi integration; overrides cd
├── functions/ Custom functions, one per file
├── completions/ Tab completion scripts
├── completions/ Tab completion scripts, autoloaded on demand
├── integrations/
│ └── fzf.fish FZF Catppuccin theme and key bindings
├── scripts/
+35
View File
@@ -20,6 +20,41 @@ 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)
+5
View File
@@ -28,6 +28,11 @@ export default defineConfig({
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',
+5 -1
View File
@@ -2,7 +2,11 @@
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Accent{color:#ff004c}
.ColorScheme-Text{color:#ff004c}
</style>
</defs>
<path fill="currentColor" d="M1595.4 0c2.1 3.3 7.6 2 11 2 184 1.9 408.1 39.6 576.4 115.6 21.9 9.9 41.4 24.3 62.6 35.4 7.5 3.9 31.4 10.3 32.1 18.1.3 3.1-1.4 6.6-4.1 8-4.9 2.5-41.8-13-50.4-15.6-43.5-13.4-89.2-27.2-133.1-38.9-37.5-10-79-23.5-117.6-16.7-6.4 15 20.6 16.7 24.2 22.1s2.5 6.9-1.8 5.9c-25.1-5.8-61.6-32.7-87.9-44.1-181.7-79.1-436.4-32-621.8 18.9-62.2 17.1-134.7 38.6-193.5 64.5-6.5 2.8-26.5 11.6-30.8 15.2-5.8 4.7-6.3 15.3 0 19.6s21.8 1.6 29.8 2.2c60.4 4.1 109.5-1.3 169.9 14.1 32.2 8.2 75.3 25 99 49 13.2 13.4-9.9 11.2-18.8 10.8-26.4-1.1-53.4-6.9-79.9-8.1-359.5-15.3-709.4 225.7-883.4 528.7-39.7 69.1-86 168.1-104.7 245.3-2.5 10.2-4.8 30.5 11.9 28.1 4.1-.6 27.2-26.6 32.1-32 19-21 35.6-44.5 54.9-65.1 49.2-52.5 133-106.5 200.7-131.3 9.8-3.6 21.1-7.4 29.3 1.5 1.2 5.4-65.6 70-75 80.8-150.4 171.5-255.6 462.1-269.1 688.9-2 33.9-3.9 58.7-7.7 92.3-5 45.3.9 103.6 6 149.5 15.1 137.1 41.9 256.6 107.4 378.5 31.1 57.9 71.4 117.9 123.4 158.6.2-44.3-9.3-88.7-13-133-11.8-142 20.4-314.7 69.5-448.5.6-1.7-.2-6.2 3.5-4.4 8.6 4.1 6.2 25.5 7 33 5.2 43.5 4.9 86.4 8 130 21.2 296.4 113.7 587.3 363.3 766.6 117.3 84.3 282 143.6 422.6 175.4 48.2 10.9 110.9 26.2 159.3 30.7 9.9.9 28.4 2.4 16.8-10.8s-51.6-26.9-68.1-35.9c-81.7-45-140-102.4-203.9-168.1-5.7-5.8-36.5-28.3-30-36.9 82.5 57.3 163.1 113.7 258.4 148.5 234.9 85.7 522.2 75 731-70.1 81.2-56.5 139.1-110 198.5-189.5 11.7-15.6 43.6-53 17-66.9-81.7 3.4-160.6-6.1-238-32-32.1-10.8-72.2-25.3-102.9-39.1s-40.9-19.2-27.1-20.9c80.7 2.5 170.3-9.8 242.3-47.6 13-6.8 58-35.1 63.1-46.8 8.7-20-12.2-13.7-21.5-13.5-67 1.2-135.7 14-202 12-87.1-2.7-190.5-19.5-271-53-96.7-40.3-177.4-108.6-256.2-175.9-4.2-6.2 4.1-14.8 11-13 36.7 14.2 75.8 21.2 114.5 27.7 86.4 14.3 178 24.9 264.3 5 5-1.2 29.6-6.3 30.4-9.7-8.6-15.9-21.4-20.4-37.9-24-81.9-18-164.5-27.7-246.9-49.1-154.8-40.2-344-122.2-426-267.9-5.7-10.2-18.2-35.6 5.6-23.6 14.8 7.5 37.4 29 54.7 39.3 117.7 70.4 312.2 80.7 444.7 55.4 13-2.5 64.8-9.1 41-29.1s-56.3-29.3-71.7-36.3c-179.8-82-393.1-100.3-578.4-165.6-233-82.1-413.3-209.7-545.8-420.2-26.4-41.9-48-87.4-65.2-133.8 10.7.2 11 12 16 19 14.5 20.1 69.2 63.3 91 81 281.1 227.1 683.7 237.1 1020.9 333 80.4 22.9 187.3 55.5 261.4 92.6 16.9 8.4 32 20.5 48.6 29.4 58.2 31 104.9 55.4 158.7 95.3 35.2 26.1 64.9 60.7 106.9 75.1 97.7 33.4 169.4 16.3 265.5 24.5 37.3 3.2 11.7 26.7 3.2 45.3-48.7 106.4-25.7 198.6 32.9 294.6 38.9 63.7 125.6 144.1 206.9 129.2 74.9-13.7 115.4-158 88.3-220.5s-28-33.5-46.2-25.4c-26.8 11.9-51.8 78.2-53.2 106.8-1.8 3-5.6 5.6-9.1 3.1-2.1-3-3.2-6.7-3.7-10.3-6.5-46.6 1.8-85.2 3.9-129.7.9-19.5-2.9-24.7-23.1-23.1-14 1.1-49.1 22.9-50.4.7 32.1-101.4 57-212.9 120.9-300.1 57.4-78.4 218.2-105.9 308.9-139 40.5-14.8 81.1-31.5 120.2-49.8 7.4-3.4 34.8-19.9 39.4-18.9s4.4 3.2 5.9 5.2v8c-26.1 37.3-53.7 70.8-76.3 110.6-52.8 92.8-89.8 196.9-140.6 291.4-35.6 66.1-51.1 76.5-59 157-31.1 314.5-173.9 594.9-419.1 794.8-212.4 173.2-477.4 270.8-750 298l-133 10c-35.9-.9-72.1 1.2-108 0-397.6-12.9-799.2-153.9-1100.9-414C4.9 2277.1-145.4 1511.2 147.7 877.2 251.2 653.3 437.2 437.1 640 297.5 932.6 96.1 1223.4 8.1 1579.4 0z" class="ColorScheme-Accent"/>
<path fill="currentColor" class="ColorScheme-Accent" d="M1595.4 0c2.1 3.3 7.6 2 11 2 184 1.9 408.1 39.6 576.4 115.6 21.9 9.9 41.4 24.3 62.6 35.4 7.5 3.9 31.4 10.3 32.1 18.1.3 3.1-1.4 6.6-4.1 8-4.9 2.5-41.8-13-50.4-15.6-43.5-13.4-89.2-27.2-133.1-38.9-37.5-10-79-23.5-117.6-16.7-6.4 15 20.6 16.7 24.2 22.1s2.5 6.9-1.8 5.9c-25.1-5.8-61.6-32.7-87.9-44.1-181.7-79.1-436.4-32-621.8 18.9-62.2 17.1-134.7 38.6-193.5 64.5-6.5 2.8-26.5 11.6-30.8 15.2-5.8 4.7-6.3 15.3 0 19.6s21.8 1.6 29.8 2.2c60.4 4.1 109.5-1.3 169.9 14.1 32.2 8.2 75.3 25 99 49 13.2 13.4-9.9 11.2-18.8 10.8-26.4-1.1-53.4-6.9-79.9-8.1-359.5-15.3-709.4 225.7-883.4 528.7-39.7 69.1-86 168.1-104.7 245.3-2.5 10.2-4.8 30.5 11.9 28.1 4.1-.6 27.2-26.6 32.1-32 19-21 35.6-44.5 54.9-65.1 49.2-52.5 133-106.5 200.7-131.3 9.8-3.6 21.1-7.4 29.3 1.5 1.2 5.4-65.6 70-75 80.8-150.4 171.5-255.6 462.1-269.1 688.9-2 33.9-3.9 58.7-7.7 92.3-5 45.3.9 103.6 6 149.5 15.1 137.1 41.9 256.6 107.4 378.5 31.1 57.9 71.4 117.9 123.4 158.6.2-44.3-9.3-88.7-13-133-11.8-142 20.4-314.7 69.5-448.5.6-1.7-.2-6.2 3.5-4.4 8.6 4.1 6.2 25.5 7 33 5.2 43.5 4.9 86.4 8 130 21.2 296.4 113.7 587.3 363.3 766.6 117.3 84.3 282 143.6 422.6 175.4 48.2 10.9 110.9 26.2 159.3 30.7 9.9.9 28.4 2.4 16.8-10.8s-51.6-26.9-68.1-35.9c-81.7-45-140-102.4-203.9-168.1-5.7-5.8-36.5-28.3-30-36.9 82.5 57.3 163.1 113.7 258.4 148.5 234.9 85.7 522.2 75 731-70.1 81.2-56.5 139.1-110 198.5-189.5 11.7-15.6 43.6-53 17-66.9-81.7 3.4-160.6-6.1-238-32-32.1-10.8-72.2-25.3-102.9-39.1s-40.9-19.2-27.1-20.9c80.7 2.5 170.3-9.8 242.3-47.6 13-6.8 58-35.1 63.1-46.8 8.7-20-12.2-13.7-21.5-13.5-67 1.2-135.7 14-202 12-87.1-2.7-190.5-19.5-271-53-96.7-40.3-177.4-108.6-256.2-175.9-4.2-6.2 4.1-14.8 11-13 36.7 14.2 75.8 21.2 114.5 27.7 86.4 14.3 178 24.9 264.3 5 5-1.2 29.6-6.3 30.4-9.7-8.6-15.9-21.4-20.4-37.9-24-81.9-18-164.5-27.7-246.9-49.1-154.8-40.2-344-122.2-426-267.9-5.7-10.2-18.2-35.6 5.6-23.6 14.8 7.5 37.4 29 54.7 39.3 117.7 70.4 312.2 80.7 444.7 55.4 13-2.5 64.8-9.1 41-29.1s-56.3-29.3-71.7-36.3c-179.8-82-393.1-100.3-578.4-165.6-233-82.1-413.3-209.7-545.8-420.2-26.4-41.9-48-87.4-65.2-133.8 10.7.2 11 12 16 19 14.5 20.1 69.2 63.3 91 81 281.1 227.1 683.7 237.1 1020.9 333 80.4 22.9 187.3 55.5 261.4 92.6 16.9 8.4 32 20.5 48.6 29.4 58.2 31 104.9 55.4 158.7 95.3 35.2 26.1 64.9 60.7 106.9 75.1 97.7 33.4 169.4 16.3 265.5 24.5 37.3 3.2 11.7 26.7 3.2 45.3-48.7 106.4-25.7 198.6 32.9 294.6 38.9 63.7 125.6 144.1 206.9 129.2 74.9-13.7 115.4-158 88.3-220.5s-28-33.5-46.2-25.4c-26.8 11.9-51.8 78.2-53.2 106.8-1.8 3-5.6 5.6-9.1 3.1-2.1-3-3.2-6.7-3.7-10.3-6.5-46.6 1.8-85.2 3.9-129.7.9-19.5-2.9-24.7-23.1-23.1-14 1.1-49.1 22.9-50.4.7 32.1-101.4 57-212.9 120.9-300.1 57.4-78.4 218.2-105.9 308.9-139 40.5-14.8 81.1-31.5 120.2-49.8 7.4-3.4 34.8-19.9 39.4-18.9s4.4 3.2 5.9 5.2v8c-26.1 37.3-53.7 70.8-76.3 110.6-52.8 92.8-89.8 196.9-140.6 291.4-35.6 66.1-51.1 76.5-59 157-31.1 314.5-173.9 594.9-419.1 794.8-212.4 173.2-477.4 270.8-750 298l-133 10c-35.9-.9-72.1 1.2-108 0-397.6-12.9-799.2-153.9-1100.9-414C4.9 2277.1-145.4 1511.2 147.7 877.2 251.2 653.3 437.2 437.1 640 297.5 932.6 96.1 1223.4 8.1 1579.4 0z"/>
<g transform="translate(907.9791817150754, 326.150186646207) scale(105.39115723844282, 105.39115723844282)">
<path fill="currentColor" class="ColorScheme-Text" d="M9.366 2.085C9.773 2.973 10 3.96 10 5s-.227 2.027-.634 2.915C12.104 7.567 14 6.215 14 5s-1.896-2.567-4.634-2.915zM15 2.577c1.052-1.184 2.851-2.074 5-2.42v9.685c-2.149-.345-3.948-1.235-5-2.42C13.635 8.96 11.012 10 8 10c-4.418 0-8-2.239-8-5s3.582-5 8-5c3.012 0 5.635 1.04 7 2.577zM18 7V3c-1.15.23-2 1.038-2 2s.85 1.77 2 2zM2 5c0 1.27 2.077 2.696 5.032 2.958A4.973 4.973 0 0 0 8 5a4.973 4.973 0 0 0-.968-2.958C4.077 2.304 2 3.73 2 5zm3 1a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -20,7 +20,7 @@ const links = config.social || [];
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} />}
{customIcon ? <span class={`social-icon ${customIcon}`} aria-hidden="true" /> : <Icon name={icon} size="1.5em" />}
</a>
);
})}
@@ -39,8 +39,12 @@ const links = config.social || [];
color: var(--sl-color-white);
}
.social-icon {
width: 1.5rem;
height: 1.5rem;
/* !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>
+307 -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
@@ -470,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():
@@ -626,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
@@ -657,6 +681,43 @@ def test_prettify_titles_paths_and_commented_examples():
)
def test_prettify_titles_label_comments_but_not_explanations():
"""A short leading comment titles a shell block; a sentence stays a comment."""
import build_manual
distro = "\n".join([" # Arch / AUR", " pacman -S fish"])
out = build_manual.prettify(distro)
assert '```fish title="Arch / AUR"\npacman -S fish\n```' in out, (
f"a distro label comment was not promoted to the fence title:\n{out}"
)
explanation = "\n".join(
[" # Turn it off:", " set -U __fish_config_op_logging off"]
)
out = build_manual.prettify(explanation)
assert 'title=' not in out and "# Turn it off:" in out, (
f"a sentence-shaped comment was wrongly promoted to a title:\n{out}"
)
def test_prettify_highlights_nested_shell_and_custom_commands():
"""A for-loop's indented body and a repo-only command still get shell highlighting."""
import build_manual
loop = "\n".join(
[" for v in (set -Un | string match 'x*')", " set -Ue $v", " end"]
)
out = build_manual.prettify(loop)
assert out.startswith("```fish\n") and "```text" not in out, (
f"a nested for-loop body lost shell highlighting:\n{out}"
)
out = build_manual.prettify(" fish-deps sync")
assert out == '```fish\nfish-deps sync\n```', (
f"a repo function name was not recognised as a shell command:\n{out}"
)
def test_as_aside_converts_a_single_line_label():
"""A `LABEL: text` line becomes a titled <Aside> with the label's type."""
import build_manual
@@ -872,7 +933,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
@@ -1212,6 +1276,243 @@ def test_committed_registry_matches_headers():
)
# ---------------------------------------------------------------------------
# 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_")]
-190
View File
@@ -1,190 +0,0 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __config_settings_draw <cur_row> <cur_scope> <var1> ... <var7>
#
# DESCRIPTION
# Renders the 16-line config-settings TUI panel to stdout. Panel width and
# horizontal position are chosen automatically from $COLUMNS each call,
# so a terminal resize takes effect on the next keypress without any
# extra bookkeeping. Four width tiers with a 6-col buffer per side:
#
# COLUMNS ≥ 9078-wide (IW=76, desc=43 chars)
# COLUMNS ≥ 8674-wide (IW=72, desc=39 chars)
# COLUMNS ≥ 8270-wide (IW=68, desc=35 chars)
# COLUMNS < 8252-wide (IW=50, desc=17 chars) ← default
#
# The box is horizontally centered via a left-padding prefix on every
# output line. \e[16A\e[J erases by line count so the horizontal offset
# does not interfere with the redraw loop.
#
# ARGUMENTS
# cur_row 06, the currently highlighted row
# cur_scope "universal" or "session"
# var1var7 Variable names for rows 06 (6 categories + master)
#
# EXIT STATUS
# 0 Always
#
# EXAMPLE
# __config_settings_draw 0 universal \
# __fish_config_op_aliases __fish_config_op_autoexec \
# __fish_config_op_overrides __fish_config_op_integrations \
# __fish_config_op_logging __fish_config_op_greeting \
# __fish_config_opinionated
function __config_settings_draw
set -l cur_row $argv[1]
set -l cur_scope $argv[2]
set -l vars $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_hi (set_color --bold white)
set -l c_head (set_color --bold cyan)
set -l c_reset (set_color normal)
set -l labels Aliases Auto-exec Overrides Integrations Logging Greeting Master
# ── Width tier: 6-col buffer per side before stepping up ──────────────
# IW = inner width (chars between │ │); desc field = IW - 33.
# All four layouts are exactly 16 lines tall — panel_h in caller stays 16.
set -l iw 50
set -l descs \
"cmd shadows" \
startup \
"keys/env/prompt" \
"terminal coupling" \
scrollback \
fish_greeting \
"disable all"
if test "$COLUMNS" -ge 90
set iw 76
set descs \
"shadows: ls→eza, cat→bat, cd→z, rm→trash" \
"Fisher bootstrap, themes, py-venv activate" \
"vi-mode, bang-bang, PAGER, CDPATH, starship" \
"Kitty/WezTerm tab/split fns, notifications" \
"scrollback capture & paru/yay AUR wrappers" \
"fish_greeting & first-run welcome banner" \
"master off-switch: overrides all categories"
else if test "$COLUMNS" -ge 86
set iw 72
set descs \
"ls→eza, cat→bat, cd→zoxide, rm→trash" \
"Fisher bootstrap, themes, py-venv auto" \
"vi-mode, bang-bang, PAGER, starship" \
"Kitty/WezTerm fns, done notifications" \
"scrollback capture & paru/yay wrappers" \
"fish_greeting: first-run welcome banner" \
"master off-switch for all categories"
else if test "$COLUMNS" -ge 82
set iw 68
set descs \
"ls→eza, cat→bat, cd→z, rm→trash" \
"Fisher, themes, py-venv activate" \
"vi-mode, bang-bang, PAGER, starship" \
"Kitty/WezTerm, done notifications" \
"scrollback & paru/yay log wrappers" \
"fish_greeting & first-run banner" \
"master disable for all categories"
end
set -l HBR (string repeat -n $iw '─')
# ── Center padding ────────────────────────────────────────────────────
# ponytail: floor division — left margin may be 1 col less than right if gap is odd
set -l p (string repeat -n (math --scale=0 "max(0, ($COLUMNS - ($iw + 2)) / 2)") ' ')
# ── Top border ────────────────────────────────────────────────────────
# ┌─ Opinionated Settings (iw-23)×─ ┐ total = iw+2
printf '%s┌─%s Opinionated Settings %s┐\n' \
$p $c_head $c_reset(string repeat -n (math $iw - 23) '─')
# ── Page-tab header ───────────────────────────────────────────────────
set -l active_idx 0
if test $cur_scope = session
set active_idx 1
end
printf '%s│%s│\n' $p (__config_settings_pagetab $active_idx $iw)
# ── Top divider ───────────────────────────────────────────────────────
printf '%s│%s│\n' $p $HBR
# ── Category rows 05 ─────────────────────────────────────────────────
for i in (seq 0 5)
set -l idx (math $i + 1)
set -l var $vars[$idx]
set -l label $labels[$idx]
set -l desc $descs[$idx]
set -l val (__config_settings_get_val $var $cur_scope)
# Badge: 7 visible chars, coloured
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
# Cursor: 2 visible chars
set -l curs " "
if test $i -eq $cur_row
set curs "$c_sel$c_reset "
end
# Label padded to 12, desc padded to (iw-33), right margin 3
set -l lpad (string pad -r -w 12 -- $label)
set -l dpad (string pad -r -w (math $iw - 33) -- $desc)
printf '%s│ %s%s [ %s ] %s │\n' $p $curs $lpad $badge $dpad
end
# ── Separator before Master ───────────────────────────────────────────
printf '%s│ %s │\n' $p (string repeat -n (math $iw - 6) '─')
# ── Master row (index 6) ──────────────────────────────────────────────
set -l val (__config_settings_get_val $vars[7] $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 $cur_row -eq 6
set curs "$c_sel$c_reset "
end
printf '%s│ %s%s [ %s ] %s │\n' \
$p $curs \
(string pad -r -w 12 -- Master) \
$badge \
(string pad -r -w (math $iw - 33) -- $descs[7])
# ── Filler (Dots Path moved to the Paths page) ────────────────────────
printf '%s│ %s%s│\n' $p \
"$c_dim→ Tab for Sponge & Path settings$c_reset" \
(string repeat -n (math $iw - 34) ' ')
printf '%s│%s│\n' $p (string repeat -n $iw ' ')
# ── Bottom divider ────────────────────────────────────────────────────
printf '%s│%s│\n' $p $HBR
# ── Keybind hint ──────────────────────────────────────────────────────
# string pad is width-aware (arrows count as 1 column)
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 ─────────────────────────────────────────────────────
printf '%s└%s┘\n' $p $HBR
end
@@ -1,151 +0,0 @@
# 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
-184
View File
@@ -1,184 +0,0 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __config_settings_draw_value <cur_row> <page>
#
# DESCRIPTION
# Renders a config-settings value page (Sponge or Paths) as exactly 16
# lines, matching the box geometry of the opinionated toggle page so the
# caller's wrap-aware erase (panel_h=16) is unchanged. Each row shows a
# label, a badge, and the variable's current value (or its default hint).
# Toggle-type rows (the two sponge booleans) reuse the ON/OFF/DEFAULT badge;
# value rows (path/int/list/string) show a type badge and the live value.
#
# Page width follows the same $COLUMNS tiers as the toggle page for a
# consistent look; exact width is not required for the erase (the erase
# over-clears to end of screen using the 78-col worst case).
#
# ARGUMENTS
# cur_row 0-based highlighted row within the page
# page "sponge" or "paths"
#
# EXIT STATUS
# 0 Always
#
# EXAMPLE
# __config_settings_draw_value 0 sponge
function __config_settings_draw_value
set -l cur_row $argv[1]
set -l page $argv[2]
# Inline-edit state: when argv[3] is "edit", the cur_row field renders the
# live input buffer (argv[4]) with a caret instead of its stored value.
set -l edit_mode $argv[3]
set -l edit_buf $argv[4]
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)
# ── Page row metadata (parallel lists) ────────────────────────────────
set -l title
set -l vars
set -l labels
set -l types
set -l hints # default hint shown when unset
set -l active_idx
if test $page = sponge
set title "Sponge Settings"
set active_idx 2
set vars sponge_delay sponge_purge_only_on_exit sponge_allow_previously_successful sponge_successful_exit_codes __fish_sponge_extra_sensitive
set labels Delay "Purge@exit" "Allow prev" "OK codes" "Extra secret"
set types int bool bool list list
set hints 2 false true 0 "(none)"
else
set title "Path Settings"
set active_idx 3
set vars __fish_scrollback_history_dir __fish_scrollback_history_max_files __fish_user_dots_path __fish_user_dots_symlink
set labels "Log dir" "Log max" "Dots path" "Dots link"
set types path int path bool
set hints "~/.terminal_history" 100 "(default)" on
end
set -l nrows (count $vars)
# ── Width tier (same thresholds as the toggle page) ───────────────────
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)") ' ')
# ── Line 1: top border with title ─────────────────────────────────────
set -l title_dashes (math $iw - (string length -- $title) - 3)
printf '%s┌─%s %s %s┐\n' \
$p $c_head "$title$c_reset" (string repeat -n $title_dashes '─')
# ── Line 2: page-tab header ───────────────────────────────────────────
printf '%s│%s│\n' $p (__config_settings_pagetab $active_idx $iw)
# ── Line 3: divider ───────────────────────────────────────────────────
printf '%s│%s│\n' $p $HBR
# ── Value rows ────────────────────────────────────────────────────────
for i in (seq 0 (math $nrows - 1))
set -l idx (math $i + 1)
set -l var $vars[$idx]
set -l label $labels[$idx]
set -l type $types[$idx]
set -l hint $hints[$idx]
# Badge (7 visible cols) + value field
set -l badge
set -l field
if test $type = bool
# Booleans store true/false (sponge convention); unset = DEFAULT.
set -l val (__config_settings_get_raw $var)
switch $val
case true
set badge "$c_ok"" ON$c_reset"
case false
set badge "$c_err""OFF $c_reset"
case '*'
set badge "$c_dim""DEFAULT$c_reset"
end
set field "default: $hint"
else
set -l raw (__config_settings_get_raw $var)
if test "$raw" = DEFAULT
set badge "$c_dim""DEFAULT$c_reset"
set field "$hint"
else
switch $type
case path
set badge "$c_ok"" PATH $c_reset"
case int
set badge "$c_ok"" INT $c_reset"
case list
set badge "$c_ok"" LIST $c_reset"
case '*'
set badge "$c_ok"" STR $c_reset"
end
set field "$raw"
end
end
# Inline edit: render the active row's field as the live buffer with a
# block caret, tail-anchored so the caret stays visible as text grows.
if test "$edit_mode" = edit -a $i -eq $cur_row
set -l fw (math $iw - 33)
set -l avail (math $fw - 1)
set -l shown "$edit_buf"
set -l blen (string length -- "$edit_buf")
if test $blen -gt $avail
set shown (string sub -s (math $blen - $avail + 1) -- "$edit_buf")
end
set badge "$c_head"" EDIT $c_reset"
set field "$shown"(set_color --reverse)" "(set_color normal)
end
set -l curs " "
if test $i -eq $cur_row
set curs "$c_sel$c_reset "
end
set -l fw (math $iw - 33)
set -l lpad (string pad -r -w 12 -- $label)
# The edit field is already length-constrained and contains a reverse
# caret; running it through `string shorten` miscounts the escapes, so
# pad it directly. Non-edit fields still shorten to add an ellipsis.
set -l fpad
if test "$edit_mode" = edit -a $i -eq $cur_row
set fpad (string pad -r -w $fw -- "$field")
else
set fpad (string pad -r -w $fw -- (string shorten -m $fw -- "$field"))
end
printf '%s│ %s%s [ %s ] %s │\n' $p $curs $lpad $badge $fpad
end
# ── Pad blank rows so chrome(6) + nrows + blanks = 16 ─────────────────
set -l blanks (math 10 - $nrows)
for i in (seq 1 $blanks)
printf '%s│%s│\n' $p (string repeat -n $iw ' ')
end
# ── Bottom divider ────────────────────────────────────────────────────
printf '%s│%s│\n' $p $HBR
# ── Hint line (changes while editing) ─────────────────────────────────
set -l hint_line " ↑↓ move Enter edit ←/h clear Tab page q quit"
if test "$edit_mode" = edit
set hint_line " type value Enter save Esc cancel ⌫ delete"
end
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint_line) $c_reset
# ── Bottom border ─────────────────────────────────────────────────────
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.
#
-46
View File
@@ -1,46 +0,0 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __config_settings_pagetab <active_idx> <iw>
#
# DESCRIPTION
# Renders the four-page tab strip used as the header line of every
# config-settings page. Pages: 0 Universal, 1 Session, 2 Sponge, 3 Paths.
# The active page is marked with a filled bullet and bold text; the others
# with a hollow bullet. The returned string is padded to exactly <iw>
# printable columns (the caller adds the │ │ border and center offset).
#
# ARGUMENTS
# active_idx 03, the active page index
# iw inner width in columns to pad the strip to
#
# EXIT STATUS
# 0 Always
#
# RETURNS
# The rendered tab strip, printed to stdout (no trailing newline beyond printf's)
#
# EXAMPLE
# set strip (__config_settings_pagetab 2 76)
function __config_settings_pagetab
set -l active $argv[1]
set -l iw $argv[2]
set -l c_hi (set_color --bold white)
set -l c_reset (set_color normal)
set -l names Universal Session Sponge Paths
set -l strip ' '
for i in (seq 0 3)
set -l idx (math $i + 1)
if test $i -eq $active
set strip "$strip$c_hi$names[$idx]$c_reset "
else
set strip "$strip$names[$idx] "
end
end
# string pad is width-aware: color escapes count as 0 columns, the bullets
# and letters as their printable width.
string pad -r -w $iw -- $strip
end
-97
View File
@@ -1,97 +0,0 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __config_settings_read_key
#
# 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.
#
# 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.
# an arrow key, ESC [ A) is captured in one read while a lone key returns
# promptly. Original terminal settings are always restored before return.
#
# In raw mode Ctrl-C does not raise SIGINT; it arrives as byte 3 (ETX),
# which is reported as the token "quit".
#
# ARGUMENTS
# (none)
#
# EXIT STATUS
# 0 A key was read
# 1 The terminal could not be put into raw mode (stdin is not a TTY)
#
# RETURNS
# One token, printed to stdout:
# up down left right arrow keys
# space tab backtab enter escape backspace
# quit Ctrl-C (byte 3) in raw mode
# <char> any other single printable character
# "" nothing decodable was read
#
# EXAMPLE
# set -l key (__config_settings_read_key)
# or return # not a TTY — bail
# switch $key
# case up; echo "moved up"
# case space; echo "toggled"
# end
function __config_settings_read_key
# Snapshot current terminal settings; failure means stdin is not a TTY.
set -l saved (stty -g </dev/tty 2>/dev/null)
or return 1
# Raw, no-echo. min 0 / time 3: return after 0.3s even with no bytes (poll
# interval for resize detection), or immediately when any bytes arrive.
# Escape sequences (e.g. arrow keys) arrive fast enough to land in one read.
stty raw -echo min 0 time 3 </dev/tty 2>/dev/null
# One read() of up to 3 bytes — covers ESC [ A style sequences. od emits
# the bytes as space-separated decimal codes.
set -l codes (dd if=/dev/tty bs=3 count=1 2>/dev/null \
| od -An -tu1 2>/dev/null | string trim | string split -n ' ')
# Restore the terminal before doing anything else.
stty $saved </dev/tty 2>/dev/null
switch (string join ' ' $codes)
case '27 91 65'
echo up
case '27 91 66'
echo down
case '27 91 67'
echo right
case '27 91 68'
echo left
case '27 91 90'
echo backtab
case 27
echo escape
case 9
echo tab
case 32
echo space
case 10 13
echo enter
case 8 127
echo backspace
case 3
echo quit
case ''
echo ''
case '*'
# Single printable byte → emit its character; ignore stray
# multi-byte sequences we do not recognise. The two-step octal
# form avoids fish mangling a one-shot '\\%03o' format string.
if test (count $codes) -eq 1
set -l oct (printf '%03o' $codes[1])
printf '%b\n' "\\$oct"
else
echo ''
end
end
end
+76
View File
@@ -0,0 +1,76 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# DEPENDENCIES
# __config_settings_get_val, __config_settings_get_raw,
# __config_settings_subcats
#
# SYNOPSIS
# __config_settings_state
#
# DESCRIPTION
# Dumps everything scripts/config-settings-tui.py needs to render the
# settings TUI: the current value of every opinionated-component,
# sponge, scrollback and user-dots variable, plus the sub-category
# taxonomy from __config_settings_subcats.
#
# Two record types, separated by RS (0x1e), fields separated by US
# (0x1f). Both are ASCII control characters, so no value these variables
# can hold needs escaping on the way through:
#
# var<US><scope><US><name><US><value>
# sub<US><category_var><US><slug><US><label><US><description>
#
# Only variables that are actually set are emitted; the TUI renders an
# absent variable as DEFAULT. That is what keeps the variable list out of
# this function -- names are discovered with `set --names` and filtered by
# prefix, so a new sub-category needs no edit here, only in
# __config_settings_subcats.
#
# Toggle variables are dumped once per scope (universal and session), since
# the TUI edits those scopes independently. Sponge and Paths variables are
# universal-only and dumped as the value the running shell resolves,
# space-joined for list variables.
#
# EXIT STATUS
# 0 Always
#
# RETURNS
# The RS/US-separated state dump, printed to stdout
#
# EXAMPLE
# __config_settings_state | string split \x1e
function __config_settings_state --description 'Dump config-settings state for the curses TUI'
# ── Variable values ───────────────────────────────────────────────────
for name in (set --names)
switch $name
case '__fish_config_op_registry_*'
# conf.d data table, not a setting -- shares the op_ prefix.
continue
case '__fish_config_op_*' __fish_config_opinionated
# Toggles: both scopes, independently editable.
for scope in universal session
set -l val (__config_settings_get_val $name $scope)
test "$val" = DEFAULT; and continue
printf '%s\x1f%s\x1f%s\x1f%s\x1e' var $scope $name $val
end
case 'sponge_*' __fish_sponge_extra_sensitive \
'__fish_scrollback_history_*' __fish_user_dots_path \
__fish_user_dots_symlink
# Value rows: universal-only, list variables space-joined.
set -l val (__config_settings_get_raw $name)
test "$val" = DEFAULT; and continue
printf '%s\x1f%s\x1f%s\x1f%s\x1e' var universal $name $val
end
end
# ── Sub-category taxonomy ─────────────────────────────────────────────
for cvar in __fish_config_op_aliases __fish_config_op_autoexec \
__fish_config_op_overrides __fish_config_op_integrations \
__fish_config_op_logging __fish_config_op_greeting
for row in (__config_settings_subcats $cvar)
set -l f (string split -- \t $row)
printf '%s\x1f%s\x1f%s\x1f%s\x1f%s\x1e' sub $cvar $f[1] $f[2] $f[3]
end
end
end
+5 -4
View File
@@ -28,9 +28,9 @@ function __config_settings_subcats --description 'List the sub-categories for on
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" \
search Search rg \
network Network "ping, ssh, yt-dlp" \
monitor Monitor "top" \
monitor Monitor top \
shell-tools Shell-tools "bash, less, help" \
dev-tools Dev-tools "claude, edit, agy"
case __fish_config_op_autoexec
@@ -44,14 +44,15 @@ function __config_settings_subcats --description 'List the sub-categories for on
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"
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"
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" \
+3 -3
View File
@@ -17,11 +17,11 @@
# 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
# 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
# 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).
+19 -65
View File
@@ -28,84 +28,38 @@ function __fish_config_sync_logging --description 'Sync C5 logging state: sentin
set config_home "$HOME/.config"
end
set -l sentinel "$config_home/fish/.logging_disabled"
set -l paru_wrapper "$HOME/.local/bin/paru"
set -l yay_wrapper "$HOME/.local/bin/yay"
set -l wrapper_version 1
if __fish_config_op_enabled (status current-function)
# Logging enabled: remove sentinel and regenerate wrappers if binaries exist
# Logging enabled: remove sentinel
rm -f $sentinel
# Restart tmux pipe-pane for the current pane if inside tmux
_tmux_pipe_log
if test -x /usr/bin/paru
mkdir -p (dirname $paru_wrapper)
printf '%s\n' \
'#!/usr/bin/env bash' \
"# paru-wrapper-version: $wrapper_version" \
'# Auto-generated by conf.d/paru-wrapper.fish — do not edit by hand.' \
'# Tees paru output to a timestamped log file and prunes old ones.' \
'set -o pipefail' \
'' \
'log_dir="${SCROLLBACK_HISTORY_DIR:-$HOME/.terminal_history}"' \
'mkdir -p "$log_dir"' \
'log_file="$log_dir/paru_$(date +%Y-%m-%d_%H-%M-%S).log"' \
'' \
'/usr/bin/paru "$@" 2>&1 | tee "$log_file"' \
'' \
'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \
'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \
'excess=$(( ${#logs[@]} - max_files ))' \
'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \
' rm -f "${logs[$i]}"' \
'done' \
>$paru_wrapper
chmod +x $paru_wrapper
end
if test -x /usr/bin/yay
mkdir -p (dirname $yay_wrapper)
printf '%s\n' \
'#!/usr/bin/env bash' \
"# yay-wrapper-version: $wrapper_version" \
'# Auto-generated by conf.d/yay-wrapper.fish — do not edit by hand.' \
'# Tees yay output to a timestamped log file and prunes old ones.' \
'set -o pipefail' \
'' \
'log_dir="${SCROLLBACK_HISTORY_DIR:-$HOME/.terminal_history}"' \
'mkdir -p "$log_dir"' \
'log_file="$log_dir/yay_$(date +%Y-%m-%d_%H-%M-%S).log"' \
'' \
'/usr/bin/yay "$@" 2>&1 | tee "$log_file"' \
'' \
'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \
'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \
'excess=$(( ${#logs[@]} - max_files ))' \
'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \
' rm -f "${logs[$i]}"' \
'done' \
>$yay_wrapper
chmod +x $yay_wrapper
end
else
# Logging disabled: create sentinel and remove any generated wrappers
# Logging disabled: create sentinel
mkdir -p (dirname $sentinel)
touch $sentinel
if test -f $paru_wrapper
and grep -q "# paru-wrapper-version:" $paru_wrapper 2>/dev/null
rm -f $paru_wrapper
end
if test -f $yay_wrapper
and grep -q "# yay-wrapper-version:" $yay_wrapper 2>/dev/null
rm -f $yay_wrapper
end
# Stop tmux pipe-pane for the current pane if inside tmux
if set -q TMUX
tmux pipe-pane 2>/dev/null
end
end
# Delegate paru/yay wrapper (re)generation and removal to the canonical
# generators. They resolve the real binary via __fish_real_command
# (never /usr/bin-assumed) and independently gate on their own C2/C5
# keys, so sourcing them here covers both the enabled-regenerate and
# disabled-remove cases without duplicating that logic. Previously this
# function carried its own inferior copy (tee instead of a PTY, no
# progress-bar rendering, hard-coded /usr/bin/paru|yay), which fought
# the canonical generator for the wrapper file on every version-marker
# mismatch.
#
# Routed through _fish_source_scoped: both files `return` early on
# several guard checks, and a sourced `return` exits the *calling*
# function, which would otherwise abort this function and skip
# whichever of paru/yay hadn't run yet.
_fish_source_scoped "$__fish_config_dir/conf.d/paru-wrapper.fish"
_fish_source_scoped "$__fish_config_dir/conf.d/yay-wrapper.fish"
end
+141
View File
@@ -0,0 +1,141 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_help_header <name> [args...]
#
# DESCRIPTION
# Prints <name>'s man-page comment header as a help menu on stdout.
# Intended as the first statement of a user-facing function's body:
#
# __fish_help_header (status current-function) $argv; and return 0
#
# Returns 1 -- printing nothing -- ONLY when args[1] is not a help flag.
# Every other outcome, including an unreadable or headerless source
# file, prints something and returns 0. That asymmetry is load-bearing:
# a return of 1 means "run the real body", and the real body of upgrade
# is `paru -Syu --noconfirm`. A parse failure must never return 1.
#
# Only args[1] is inspected, never the whole list. wake-lock, bkg,
# split and spwin take a command to run as their arguments, so
# scanning all of $argv would make `wake-lock rsync --help` print
# wake-lock's own help instead of running rsync.
#
# The header is read from the caller's source at call time rather than
# from the generated manual, so it cannot go stale between a header
# edit and a docs rebuild.
#
# ARGUMENTS
# name The calling function's name, from (status current-function)
# args... The caller's $argv, forwarded verbatim
#
# EXIT STATUS
# 0 Help was printed, including the degraded fallback
# 1 args[1] is not -h/--help; the caller should carry on
#
# EXAMPLE
# __fish_help_header (status current-function) $argv; and return 0
#
# NOTES
# Section labels are those of the manual SSOT parser in
# docs/manualtools.py. CATEGORY, COMPONENT and DEPENDENCIES are build
# metadata and are suppressed; SYNOPSIS renders as USAGE and EXAMPLE as
# EXAMPLES.
function __fish_help_header --argument-names name
# First argument only -- see DESCRIPTION.
contains -- "$argv[2]" -h --help; or return 1
set -l c_ttl (set_color --bold)
set -l c_sec (set_color --bold brblue)
set -l c_rst (set_color normal)
set -l miss " No documentation header found. Try: help config $name"
set -l file (functions -D -- $name 2>/dev/null)
if not test -f "$file"
# Quoted: set_color yields an EMPTY LIST under TERM=dumb, and an
# unquoted empty list in a concatenation annihilates the whole
# word -- the title line would silently vanish wherever colour is
# off, which is exactly where a test would be reading it.
echo "$c_ttl$name$c_rst"
echo $miss
return 0
end
# Collect the contiguous comment run directly above `function <name>`,
# walking backwards. This resolves multi-header files (fish-deps, gi,
# y) without reimplementing manualtools._block_identity, and is more
# accurate at runtime: in dops.fish it finds the header above
# `function docker` rather than attributing it to the file stem.
# One blank separator line is tolerated -- sponge_filter_secrets.fish
# is the only file that has one. Do not remove this tolerance as dead
# code.
set -l lines (string split \n -- (command cat $file))
set -l pat '^\s*function\s+'(string escape --style=regex -- $name)'(\s|$)'
set -l start 0
for i in (seq (count $lines))
if string match -qr -- $pat $lines[$i]
set start $i
break
end
end
set -l header
if test $start -gt 1
set -l j (math $start - 1)
if test -z (string trim -- "$lines[$j]")
set j (math $j - 1)
end
while test $j -ge 1; and string match -q '#*' -- $lines[$j]
set -p header $lines[$j]
set j (math $j - 1)
end
end
# Render. Comment lines before the first `# LABEL` -- the copyright
# preamble -- carry no label and are dropped, matching
# manualtools._header_blocks.
set -l skip CATEGORY COMPONENT DEPENDENCIES
set -l label ""
set -l out
for line in $header
set -l m (string match -r -- '^#\s+([A-Z][A-Z ]*[A-Z])\s*$' $line)
if set -q m[2]
set label $m[2]
contains -- $label $skip; and continue
set -l shown (string replace SYNOPSIS USAGE -- $label)
set shown (string replace EXAMPLE EXAMPLES -- $shown)
# One blank line before a heading, never two: the header's own
# `#` separator has usually already emitted one.
if set -q out[1]; and test -n (string trim -- "$out[-1]")
set -a out ""
end
set -a out "$c_sec$shown$c_rst"
continue
end
test -n "$label"; or continue
contains -- $label $skip; and continue
set -l body (string sub -s 2 -- $line)
if string match -q ' *' -- $body
set -a out " "(string sub -s 4 -- $body)
else
set -a out (string trim -- $body)
end
end
# Trim the trailing blank separator, mirroring
# manualtools._trailing_blanks.
while set -q out[-1]; and test -z (string trim -- "$out[-1]")
set -e out[-1]
end
echo "$c_ttl$name$c_rst"
if test (count $out) -eq 0
echo $miss
else
# out[1] is always a heading -- a body line cannot precede the
# first label -- so this blank is never doubled.
echo ""
printf '%s\n' $out
end
return 0
end
+63
View File
@@ -0,0 +1,63 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_palette
#
# DESCRIPTION
# Defines the shared terminal-output colour roles used by the
# user-facing functions in this configuration. Declared
# --no-scope-shadowing and using a bare `set`, so the variables are
# created in the CALLER's scope -- a consumer just calls it and then
# interpolates $c_head, $c_err and friends exactly as it did when the
# declarations were inline.
#
# Call it where the local declarations used to sit, once per contiguous
# block that needs the palette. set_color runs at call time, so the
# values track $TERM exactly as inline declarations did. (Measured:
# set_color output is identical across every TERM tested except
# TERM=dumb, which yields empty strings, and is unaffected by whether
# stdout is a tty or a pipe.)
#
# A role is a semantic slot, not a colour. c_flag and c_warn are both
# yellow but stay separate, as do c_ok and c_accent (both green) --
# merging either pair would foreclose ever restyling one without the
# other. c_accent is the command name in logs and smart_exit, which
# style it green where the rest of the config styles it bold.
#
# ARGUMENTS
# none
#
# EXIT STATUS
# 0 always
#
# EXAMPLE
# function mytool
# __fish_palette
# echo "$c_head""Usage:$c_reset $c_cmd""mytool$c_reset"
# end
#
# NOTES
# Calling this at top level (outside any function) creates GLOBAL
# variables. Every consumer calls it from inside a function, where the
# variables stay function-local and do not leak.
#
# functions/fish_prompt.fish deliberately does NOT use this palette. Its
# c_* values are Catppuccin hex strings passed as ARGUMENTS to set_color
# (`set_color --bold $c_green`), not captured escape sequences -- colour
# inputs rather than rendered output, a different concern.
function __fish_palette --no-scope-shadowing --description 'Define the shared output colour palette in the caller scope'
set c_reset (set_color normal)
set c_head (set_color --bold cyan)
set c_cmd (set_color --bold)
set c_arg (set_color cyan)
set c_flag (set_color yellow)
set c_warn (set_color yellow)
set c_err (set_color red)
set c_ok (set_color green)
set c_accent (set_color green)
set c_dim (set_color brblack)
set c_sel (set_color --bold magenta)
set c_hi (set_color --bold white)
end
+1 -1
View File
@@ -8,7 +8,7 @@
# __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/.
+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
+3 -4
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.
#
@@ -31,8 +31,7 @@
# EXAMPLE
# _agents_init_ensure_gitignore /home/user/myproject "agents-init" "AGENTS/" "/AGENTS.md"
function _agents_init_ensure_gitignore
set -l c_ok (set_color green)
set -l c_reset (set_color normal)
__fish_palette
if test (count $argv) -lt 3
echo (set_color red)"_agents_init_ensure_gitignore: requires <root> <label> <pattern>..."(set_color normal) >&2
@@ -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
+4 -4
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).
+13 -5
View File
@@ -75,7 +75,7 @@ function _fish_deps_install
set -l _major (fish --version 2>&1 | string match -r 'version (\d+)')[2]
if test -n "$_major"; and test "$_major" -lt 4
set needs_install 1
set upgrade_label "Upgrade"
set upgrade_label Upgrade
end
end
@@ -193,7 +193,9 @@ function _fish_deps_install
echo " Available methods:"
set -l m 1
for lbl in $method_labels
set_color brblack; echo -n " $m) "; set_color normal
set_color brblack
echo -n " $m) "
set_color normal
echo $lbl
set m (math $m + 1)
end
@@ -202,7 +204,9 @@ function _fish_deps_install
set chosen_method $methods[$_choice]
end
else
set_color brblack; echo " "(string lower $upgrade_label)"ing via $method_labels[1]"; set_color normal
set_color brblack
echo " "(string lower $upgrade_label)"ing via $method_labels[1]"
set_color normal
end
# Execute chosen method
@@ -329,14 +333,18 @@ function _fish_deps_install
if test $status -eq 0
set installed_any 1
set_color green; echo " $bin "(string lower $upgrade_label)"ed."; set_color normal
set_color green
echo " $bin "(string lower $upgrade_label)"ed."
set_color normal
if test "$bin" = fish
set_color yellow
echo " Fish upgraded — restart your shell to use the new version."
set_color normal
end
else
set_color red; echo " $bin "(string lower $upgrade_label)" failed."; set_color normal
set_color red
echo " $bin "(string lower $upgrade_label)" failed."
set_color normal
end
end
set i (math $i + 1)
+33 -11
View File
@@ -23,35 +23,57 @@ function _fish_deps_status
set -l _major (fish --version 2>&1 | string match -r 'version (\d+)')[2]
if test -n "$_major"; and test "$_major" -lt 4
set -l _ver (fish --version 2>&1 | string replace 'fish, ' '')
set_color yellow; echo -n " ⚠ "; set_color normal
set_color yellow
echo -n " ⚠ "
set_color normal
echo -n "$bin "
set_color brblack; echo "($_ver — upgrade to 4.0+ required)"; set_color normal
set_color brblack
echo "($_ver — upgrade to 4.0+ required)"
set_color normal
return
end
end
set_color green; echo -n " ✓ "; set_color normal
set_color green
echo -n " ✓ "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Found at "(__fish_real_command $bin)")"; set_color normal
set_color brblack
echo "(Found at "(__fish_real_command $bin)")"
set_color normal
else if test "$tier" = req
set_color red; echo -n " ✗ "; set_color normal
set_color red
echo -n " ✗ "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Not installed)"; set_color normal
set_color brblack
echo "(Not installed)"
set_color normal
else if test "$tier" = rec
set_color yellow; echo -n " ⚠ "; set_color normal
set_color yellow
echo -n " ⚠ "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Not installed)"; set_color normal
set_color brblack
echo "(Not installed)"
set_color normal
else
# opt / term / int: absence is expected and not alarming
set_color brblack; echo -n " "; set_color normal
set_color brblack
echo -n " "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Not installed)"; set_color normal
set_color brblack
echo "(Not installed)"
set_color normal
end
end
for tier_label in "Required Dependencies:req" "Recommended Dependencies:rec" "Optional Dependencies:opt" "Terminal Emulators:term" "Integrations:int"
set -l label (string split : $tier_label)[1]
set -l tier (string split : $tier_label)[2]
set_color cyan; echo $label; set_color normal
set_color cyan
echo $label
set_color normal
set -l i 1
for bin in $_fdc_bins
if test "$_fdc_tiers[$i]" = $tier
+8 -4
View File
@@ -101,10 +101,14 @@ function _fish_deps_update
echo "Updating $bin..."
set -l _arch (uname -m)
switch $_arch
case x86_64; set _arch amd64
case aarch64 arm64; set _arch arm64
case armv7l; set _arch arm
case '*'; set _arch amd64
case x86_64
set _arch amd64
case aarch64 arm64
set _arch arm64
case armv7l
set _arch arm
case '*'
set _arch amd64
end
set -l _zip "wakatime-cli-linux-$_arch.zip"
set -l _bin_src "wakatime-cli-linux-$_arch"
+26
View File
@@ -0,0 +1,26 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _fish_source_scoped <file>
#
# DESCRIPTION
# Sources <file> inside its own function-call boundary. `source` runs
# in the caller's own scope, so a bare `return` in a sourced file --
# used by several conf.d guards as an early exit -- would otherwise
# unwind whatever function called `source` directly, not just the
# sourced file. Calling through this helper contains it to here.
#
# ARGUMENTS
# file Path to the fish script to source
#
# EXIT STATUS
# 0 File does not exist (nothing to do)
# Exit status of the sourced file otherwise
#
# EXAMPLE
# _fish_source_scoped $__fish_config_dir/conf.d/paru-wrapper.fish
function _fish_source_scoped --argument-names file
test -f $file; or return 0
source $file
end
+1 -1
View File
@@ -9,7 +9,7 @@
# 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`
# 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.
+20
View File
@@ -0,0 +1,20 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _mkrep_say <silent> <message>
#
# DESCRIPTION
# Prints <message> unless <silent> is 1. Used by mkrep for its default
# per-step summary lines, which -s/--silent must suppress entirely.
#
# ARGUMENTS
# silent 1 to suppress output, 0 to print
# message Text to print (echo -e, so escapes/colour codes render)
#
# EXIT STATUS
# 0 always
function _mkrep_say --argument-names silent msg
test "$silent" = 1; and return 0
echo -e $msg
end
+22
View File
@@ -0,0 +1,22 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _mkrep_verbose <silent> <verbose> <message>
#
# DESCRIPTION
# Prints <message> only when <verbose> is 1 and <silent> is 0. Used by
# mkrep for the extra step-by-step tracing -v/--verbose adds.
#
# ARGUMENTS
# silent 1 to suppress output, 0 to allow it
# verbose 1 to print, 0 to stay quiet
# message Text to print (echo -e, so escapes/colour codes render)
#
# EXIT STATUS
# 0 always
function _mkrep_verbose --argument-names silent verbose msg
test "$silent" = 1; and return 0
test "$verbose" = 1; or return 0
echo -e $msg
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.
#
-1
View File
@@ -7,4 +7,3 @@ function _puffer_fish_expand_bang
commandline --insert '!'
end
end
+1 -1
View File
@@ -11,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.
#
+57 -57
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
@@ -76,14 +85,7 @@
# agents-init --plugins
# 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)
set -l c_flag (set_color yellow)
set -l c_ok (set_color green)
set -l c_warn (set_color yellow)
set -l c_dim (set_color brblack)
set -l c_err (set_color red)
set -l c_reset (set_color normal)
__fish_palette
argparse h/help a/agents p/plugins v/verbose q/quiet s/silent -- $argv
or return 1
@@ -165,7 +167,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 +261,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
# 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
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
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 AGENTS.md → AGENTS/AGENTS.md$c_reset"
test $verbose -eq 1; and echo "$c_ok→ Linked $name$want$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
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked CLAUDE.md → AGENTS/CLAUDE.md$c_reset"
end
# ── .gitignore ────────────────────────────────────────────────────────
@@ -461,24 +451,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"
# 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"
if git -C "$agents_dir" -c commit.gpgsign=false commit -q -m "$msg" 2>/dev/null
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
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
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 +485,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
+33 -11
View File
@@ -8,7 +8,7 @@
# aliases/dev-tools
#
# DEPENDENCIES
# agents-init
# agents-init, agents-vault
#
# SYNOPSIS
# agy [ARGS...]
@@ -18,16 +18,26 @@
# 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 use different syntax in agy than claude: bare
# -r/--resume (no session id following) translate to -c/--continue
# (resume most-recent session); -r/--resume given a session id (via
# =id or a following bare word) translate to --conversation(=id)
# (open that specific session).
#
# Opinionated component (C1): when disabled via __fish_config_op_aliases
# (or the __fish_config_opinionated master), the command is passed through
# to the real agy binary unchanged.
#
# ARGUMENTS
# ARGS Arguments forwarded to the underlying agy binary (-r translates to -c)
# ARGS Arguments forwarded to the underlying agy binary (-r/--resume
# translate to -c/--continue or --conversation, see DESCRIPTION)
#
# EXIT STATUS
# Exit status of the underlying agy binary
@@ -35,6 +45,7 @@
# EXAMPLE
# agy
# agy --resume
# agy --resume=5fffb251-2cd6-4cfe-8dac-b5e913a86db6
# agy -i "initial prompt"
# agy models
function agy --wraps=agy --description 'agy wrapper: auto-initializes AGENTS/ sub-repo before launch'
@@ -44,14 +55,25 @@ function agy --wraps=agy --description 'agy wrapper: auto-initializes AGENTS/ su
end
agents-init --quiet
agents-vault --quiet
for i in (seq (count $argv))
if test "$argv[$i]" = "-r"
set argv[$i] "-c"
else if test "$argv[$i]" = "--resume"
set argv[$i] "--continue"
else if string match -q -- "--resume=*" "$argv[$i]"
set argv[$i] (string replace -- "--resume=" "--continue=" "$argv[$i]")
switch "$argv[$i]"
case -r --resume
# Session id given as next bare word (not a flag) -> --conversation.
# Nothing follows, or next word is a flag -> resume most-recent (-c/--continue).
set -l next (math $i + 1)
if test $next -le (count $argv); and not string match -q -- '-*' "$argv[$next]"
set argv[$i] --conversation
else if test "$argv[$i]" = -r
set argv[$i] -c
else
set argv[$i] --continue
end
case '-r=*'
set argv[$i] (string replace -- '-r=' '--conversation=' "$argv[$i]")
case '--resume=*'
set argv[$i] (string replace -- '--resume=' '--conversation=' "$argv[$i]")
end
end
+1 -8
View File
@@ -42,14 +42,7 @@
# auto-pull list
# 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)
set -l c_flag (set_color yellow)
set -l c_ok (set_color green)
set -l c_warn (set_color yellow)
set -l c_err (set_color red)
set -l c_dim (set_color brblack)
set -l c_reset (set_color normal)
__fish_palette
set -q __fish_config_dir; or set -l __fish_config_dir $XDG_CONFIG_HOME/fish
set -q __fish_user_dots_path; or set -l __fish_user_dots_path "$XDG_CONFIG_HOME/.user-dots/fish"
+14 -3
View File
@@ -23,11 +23,22 @@
# bd-pull myuser/myproject
# bd-pull rootiest/fish-config
function bd-pull --description 'Pull new Gitea issues into local Beads and link them'
if not set -q argv[1]; echo "Need repo owner/name"; return 1; end
if not set -q GITEA_TOKEN; echo "\$GITEA_TOKEN not set"; return 1; end
__fish_help_header (status current-function) $argv; and return 0
if not set -q argv[1]
echo "Need repo owner/name"
return 1
end
if not set -q GITEA_TOKEN
echo "\$GITEA_TOKEN not set"
return 1
end
set -l REPO $argv[1]
if not set -q GITEA_URL; echo "\$GITEA_URL not set"; return 1; end
if not set -q GITEA_URL
echo "\$GITEA_URL not set"
return 1
end
set -l IMPORT_COUNT 0
echo (set_color blue)"📡 Checking Gitea: $REPO..."(set_color normal)
+3 -4
View File
@@ -23,12 +23,11 @@
# EXAMPLE
# bkg firefox
function bkg --description 'Execute bkg'
__fish_help_header (status current-function) $argv; and return 0
# Check if a command was provided as an argument.
if test -z "$argv[1]"
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)
__fish_palette
echo "$c_head""Usage:$c_reset $c_cmd""bkg$c_reset $c_arg""<command> [arguments...]$c_reset"
return 1
end
+2
View File
@@ -21,6 +21,8 @@
# EXAMPLE
# branch feature/new-ui
function branch --description 'Switch to or create a git branch'
__fish_help_header (status current-function) $argv; and return 0
if not git rev-parse --is-inside-work-tree >/dev/null 2>&1
echo "Not a git repo."
return 1
+5
View File
@@ -11,8 +11,13 @@
# Backwards-compatibility wrapper that delegates to fish-deps status to
# report which fish shell dependencies are installed or missing.
#
# EXIT STATUS
# Exit status of fish-deps status
#
# EXAMPLE
# check_fish_deps
function check_fish_deps --description 'Check all fish-related dependencies'
__fish_help_header (status current-function) $argv; and return 0
fish-deps status
end
+5
View File
@@ -12,8 +12,13 @@
# README.md, ensuring all features and examples are accurate and pruning
# obsolete content.
#
# EXIT STATUS
# Exit status of the claude invocation
#
# EXAMPLE
# claude-docs
function claude-docs --description 'Claude-code: Sync README with recent changes'
__fish_help_header (status current-function) $argv; and return 0
claude "Analyze the recent changes and update the README.md to ensure all features, setup instructions, and examples are 100% accurate. Prune any obsolete information."
end
+5
View File
@@ -12,8 +12,13 @@
# branch, write a Conventional Commit, run verification, push, and open a
# pull request with a manual verification checklist.
#
# EXIT STATUS
# Exit status of the claude invocation
#
# EXAMPLE
# claude-pr
function claude-pr --description 'Claude-code: New branch, commit, push, and PR'
__fish_help_header (status current-function) $argv; and return 0
claude "Act as a senior engineer. Execute this sequence: 1. Create a new git branch (kebab-case). 2. Stage changes and write a Conventional Commit message. 3. Self-verify the changes by running relevant build/test commands or linting. 4. Push to remote. 5. Create a PR to 'main' including a summary of changes and a 'Manual Verification' section containing a Markdown checklist (- [ ]) of specific, bite-sized steps required to manually verify the functionality."
end
+8 -1
View File
@@ -8,7 +8,7 @@
# aliases/dev-tools
#
# DEPENDENCIES
# agents-init
# agents-init, agents-vault
#
# SYNOPSIS
# claude [ARGS...]
@@ -19,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
@@ -42,6 +48,7 @@ function claude --wraps=claude --description 'claude wrapper: auto-links AGENTS.
end
agents-init --quiet
agents-vault --quiet
command claude $argv
end
+6
View File
@@ -11,9 +11,15 @@
# Identifies and removes Arch Linux orphan packages using pacman. Logs
# package names and versions to ~/.removed_orphans before removal.
#
# EXIT STATUS
# 0 No orphans found, or orphans removed successfully
# Nonzero sudo pacman -Rns failed
#
# EXAMPLE
# cleanup
function cleanup --description 'Log orphans to ~/.removed_orphans and remove them'
__fish_help_header (status current-function) $argv; and return 0
set -l orphans (pacman -Qtdq)
if test -n "$orphans"
echo "📝 Logging orphans to ~/.removed_orphans..."
+69 -13
View File
@@ -22,7 +22,7 @@
# in the default browser via xdg-open — deep links to a section aren't
# supported there, so if a keyword is given a note points you to the site's
# search box instead. Pass --man / -m to open the compiled man page
# (docs/fish-config.1) via `man -l`; if a section keyword is given, the
# (docs/fish-config.1) via man -l; if a section keyword is given, the
# pager opens at the nearest match. Pass --help or -h for usage and the
# navigation key reference.
#
@@ -52,9 +52,9 @@
# config-help pkg --man
#
# NOTES
# The preferred invocation is `help config [...]` — this function is
# The preferred invocation is help config [...] — this function is
# registered as a handler in the help wrapper so that syntax works
# transparently. Direct `config-help` calls are also valid.
# transparently. Direct config-help calls are also valid.
function config-help --description 'Open the offline fish shell configuration manual'
set -l doc_file "$__fish_config_dir/docs/fish-config.md"
set -l idx_file "$__fish_config_dir/docs/fish-config.index"
@@ -77,6 +77,27 @@ function config-help --description 'Open the offline fish shell configuration ma
if test -n "$section_kw"
set -l norm_kw (string lower -- $section_kw | string replace -ra '[^a-z0-9]' '')
# Singular/plural variants of the keyword. The heading scan below
# matches a keyword that is *contained in* a heading, so a plural
# never reaches a singular heading on its own: `customization`
# finds "7. CUSTOMIZATION", `customizations` finds nothing. The
# keyword as typed is always tried first and alone; these only run
# when it matched nothing at all.
set -l kw_variants $norm_kw
if test -n "$norm_kw"
if string match -qr 's$' -- $norm_kw
if string match -qr 'ies$' -- $norm_kw
set -a kw_variants (string replace -r 'ies$' y -- $norm_kw)
end
if string match -qr 'es$' -- $norm_kw
set -a kw_variants (string replace -r 'es$' '' -- $norm_kw)
end
set -a kw_variants (string replace -r 's$' '' -- $norm_kw)
else
set -a kw_variants "$norm_kw"s "$norm_kw"es
end
end
# 1. Index lookup (keyword aliases)
if test -f "$idx_file"
while read -l idxline
@@ -84,7 +105,7 @@ function config-help --description 'Open the offline fish shell configuration ma
set -l kv (string split -m 1 '=' -- $idxline)
test (count $kv) -lt 2; and continue
set -l k (string lower -- $kv[1] | string replace -ra '[^a-z0-9]' '')
if test "$k" = "$norm_kw"
if contains -- $k $kw_variants
set found_text $kv[2]
break
end
@@ -92,16 +113,25 @@ function config-help --description 'Open the offline fish shell configuration ma
end
# 2. Normalized heading scan fallback
# Each variant is tried against every heading before the next one
# is considered, so a loose plural never beats an exact hit that
# appears further down the document.
if test -z "$found_text"; and test -f "$doc_file"
for entry in (grep -n "^#" "$doc_file")
set -l heading_lines (grep -n "^#" "$doc_file")
for kw in $kw_variants
for entry in $heading_lines
set -l parts (string split -m 1 ':' -- $entry)
set -l text $parts[2]
set -l norm_text (string lower -- $text | string replace -ra '[^a-z0-9]' '')
if string match -q "*$norm_kw*" $norm_text
if string match -q "*$kw*" $norm_text
set found_text $text
break
end
end
if test -n "$found_text"
break
end
end
end
end
@@ -247,6 +277,22 @@ function config-help --description 'Open the offline fish shell configuration ma
set -l nav_hint \
" \033[2mNAVIGATION: [ Space=next section ^=prev Alt+u=sections /=search q=quit ]\033[0m"
# ── Inline code spans ────────────────────────────────────────
# The document carries backticks for pandoc and the docs site, but
# nothing in this chain consumes them: bat dims the delimiters and
# leaves the content the same colour as the surrounding prose, so
# they arrive as literal punctuation. Render each span bold instead.
#
# Two forms are matched. After bat, every backtick carries its own
# SGR sequence, and a fence survives because it puts three of them
# inside one sequence. On raw Markdown a fence survives because it
# offers no non-backtick content to capture. Both substitutions are
# line-preserving, so the tail-slice below still lands on the
# requested section.
set -l span_ansi '\e\[[0-9;]*m`\e\[0m(.*?)\e\[[0-9;]*m`\e\[0m'
set -l span_raw '`([^`]+)`'
set -l span_bold (printf '\e[1m$1\e[0m')
# ── Viewer fallback chain ────────────────────────────────────
# When jumping to a section, slice the file from start_line so ov
# opens with that section at the top without needing --pattern.
@@ -261,12 +307,14 @@ function config-help --description 'Open the offline fish shell configuration ma
begin
printf "$nav_hint\n"
bat --color=always --style=plain --language=markdown "$doc_file" \
| tail -n +$start_line
| tail -n +$start_line \
| string replace -ra $span_ansi $span_bold
end | ov $ov_args
else
begin
printf "$nav_hint\n"
bat --color=always --style=plain --language=markdown "$doc_file"
bat --color=always --style=plain --language=markdown "$doc_file" \
| string replace -ra $span_ansi $span_bold
end | ov $ov_args
end
@@ -279,12 +327,13 @@ function config-help --description 'Open the offline fish shell configuration ma
if test $start_line -gt 1
begin
printf "$nav_hint\n"
tail -n +$start_line "$doc_file"
tail -n +$start_line "$doc_file" \
| string replace -ra $span_raw $span_bold
end | ov $ov_args
else
begin
printf "$nav_hint\n"
cat "$doc_file"
string replace -ra $span_raw $span_bold <"$doc_file"
end | ov $ov_args
end
@@ -295,16 +344,23 @@ function config-help --description 'Open the offline fish shell configuration ma
echo "note: bat pager — use / to search for your section" >&2
set_color normal
end
bat --language=markdown --paging=always "$doc_file"
# bat owns the pager here, so the spans are flattened on the way
# in rather than styled on the way out — bat would escape any
# SGR sequence handed to it as input.
string replace -ra $span_raw '$1' <"$doc_file" \
| bat --language=markdown --paging=always
# Pre-compiled man page (generated by CI after merge).
# pandoc consumed the backticks when it built this, so there is
# nothing to strip.
else if test -f "$man_file"
man -l "$man_file"
else if type -q less
less +"$start_line" "$doc_file"
string replace -ra $span_raw $span_bold <"$doc_file" \
| less -R +"$start_line"
else
cat "$doc_file"
string replace -ra $span_raw $span_bold <"$doc_file"
end
end

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