Compare commits
138
Commits
c17f6663db
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e66c81ca | ||
|
|
c009bbf257 | ||
|
|
e6d3fd80b3
|
||
|
|
54375a9530
|
||
|
|
2ee2806e01
|
||
|
|
23cae4bd10
|
||
|
|
8c21d34943
|
||
|
|
4912c4052f
|
||
|
|
c5bbbf06e3 | ||
|
|
76afcd5e13 | ||
|
|
28a88a9bdf | ||
|
|
d278a47b32 | ||
|
|
dc3236fddb | ||
|
|
73d41da07d
|
||
|
|
75fbfa1e16
|
||
|
|
3c967c0cee | ||
|
|
f9d400699f
|
||
|
|
d9b56790c5
|
||
|
|
16ea31289d
|
||
|
|
2ad5bf75d2
|
||
|
|
11f4551fa5
|
||
|
|
090779ae5d
|
||
|
|
45f0fb9688
|
||
|
|
4c7334c46e
|
||
|
|
1c9cedb8f3
|
||
|
|
a018d7997c
|
||
|
|
b7ff4e0981
|
||
|
|
2d8db42b12
|
||
|
|
19126316a7
|
||
|
|
2c185f7e23
|
||
|
|
b0585d00ad
|
||
|
|
b5d2c9ba87
|
||
|
|
51543cb7ca
|
||
|
|
3fd9476fbc
|
||
|
|
1dc0e5293d
|
||
|
|
335fdff433 | ||
|
|
a78c6a604d | ||
|
|
aed37d5a75 | ||
|
|
cff4f7b414 | ||
|
|
877c973e87 | ||
|
|
ce3c44a053 | ||
|
|
af764903e7 | ||
|
|
fcb9e0c468 | ||
|
|
1dc86b9ff0 | ||
|
|
ea8b13b38a | ||
|
|
2f49960149 | ||
|
|
f52bfaa55e | ||
|
|
dfb353af09 | ||
|
|
5328529e18 | ||
|
|
81d26e095a | ||
|
|
6cf690e637 | ||
|
|
f7a9ff9d23 | ||
|
|
577ad993ea | ||
|
|
f0de5378fe | ||
|
|
b01124f99d | ||
|
|
2ce8bebf29 | ||
|
|
618ce00f9e | ||
|
|
1aef0ffca8 | ||
|
|
a89a5576a3 | ||
|
|
b754709f02 | ||
|
|
f7cfad559c | ||
|
|
e97b3ee6ab | ||
|
|
d44dbd3564 | ||
|
|
2aad1aca98 | ||
|
|
9969f43ba3 | ||
|
|
82d78d19f5 | ||
|
|
38924d5c3c | ||
|
|
057d9913b9 | ||
|
|
62167a439e | ||
|
|
f4c4922f0e | ||
|
|
857faebeba | ||
|
|
b1a0c6c488 | ||
|
|
500dd8a735 | ||
|
|
9e74cb8f0a | ||
|
|
4006a2f855 | ||
|
|
8b9d0ac1f4 | ||
|
|
51fc669c02 | ||
|
|
c25bb3b105 | ||
|
|
4d7ad64c3a | ||
|
|
3c27e52ada | ||
|
|
70b5868e3e | ||
|
|
c47b51cd6e | ||
|
|
c4e225b007 | ||
|
|
f7598e5e23 | ||
|
|
a5cc8cbd36 | ||
|
|
616596726d | ||
|
|
7466188d35 | ||
|
|
2608a6cd72 | ||
|
|
642c981e4b | ||
|
|
451097d384 | ||
|
|
1ec69309cf | ||
|
|
27f780e733 | ||
|
|
9368a4864c | ||
|
|
6689e69ece | ||
|
|
5f55438581 | ||
|
|
0ef99e4874 | ||
|
|
df929c5335 | ||
|
|
6ef0859e43 | ||
|
|
01f4e376ae | ||
|
|
31d04a2fe0 | ||
|
|
8866236bda | ||
|
|
f7b207aaf4 | ||
|
|
1cb6f37b5b | ||
|
|
a8468f3402 | ||
|
|
040c98c0a7 | ||
|
|
3c0bba1737 | ||
|
|
489e3cc7de | ||
|
|
7ad3b90503 | ||
|
|
db21efee0c | ||
|
|
207540c093 | ||
|
|
6b2d3bf491 | ||
|
|
c3a2e35465 | ||
|
|
e02919de21 | ||
|
|
516b2ba26c | ||
|
|
a981fa7f0a | ||
|
|
760b8e68fd | ||
|
|
04fd832974 | ||
|
|
77c68558b2 | ||
|
|
0554a3dddc | ||
|
|
fbb6b6e740 | ||
|
|
2cf61b0590 | ||
|
|
d748289c11 | ||
|
|
d2e07effc1 | ||
|
|
9d6923f225 | ||
|
|
c0628dbd4c | ||
|
|
31b47ddfc5 | ||
|
|
9642cd69db | ||
|
|
7fa5ca2568 | ||
|
|
94a4fbe45f | ||
|
|
04a02d089f | ||
|
|
3ef83d1747 | ||
|
|
f865ad766d | ||
|
|
f32e0d0ce5 | ||
|
|
8a937124d3 | ||
|
|
d8feaf7143 | ||
|
|
22dcd31c05 | ||
|
|
cbce46b63f | ||
|
|
8ab69f53ed |
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
-->
|
||||
@@ -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.
|
||||
-->
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
@@ -1,82 +0,0 @@
|
||||
name: Generate documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/manual/**"
|
||||
- "docs/build-manual.py"
|
||||
- "docs/manualtools.py"
|
||||
- "docs/verify-manual.py"
|
||||
- "docs/site/**"
|
||||
- "functions/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
runs-on: racknerd-mini
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y pandoc python3-yaml
|
||||
|
||||
- name: Generate concatenated markdown
|
||||
run: python3 docs/build-manual.py --concat -o docs/fish-config.md
|
||||
|
||||
# Regeneration MUST run before verification: verify-manual.py's
|
||||
# test_concat_roundtrips_original compares a freshly-built concat
|
||||
# against docs/fish-config.md on disk. Before this step ran, that
|
||||
# file was still the stale pre-push copy, so any ordinary edit under
|
||||
# docs/manual/** failed the round-trip check before anything was
|
||||
# regenerated. Do not reorder this back — verification still gates
|
||||
# pandoc and the auto-commit below, it just no longer requires a
|
||||
# contributor to hand-sync the generated file before pushing.
|
||||
- name: Verify manual integrity
|
||||
run: python3 docs/verify-manual.py
|
||||
|
||||
- name: Compile man page
|
||||
run: |
|
||||
pandoc --standalone \
|
||||
--from markdown \
|
||||
--to man \
|
||||
docs/fish-config.md \
|
||||
-o docs/fish-config.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Build documentation site
|
||||
run: |
|
||||
python3 docs/build-manual.py --site
|
||||
cd docs/site
|
||||
npm ci
|
||||
npx astro build
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
run: |
|
||||
cd docs/site
|
||||
npx --yes wrangler pages deploy dist/ \
|
||||
--project-name=fish-config-docs \
|
||||
--branch=main \
|
||||
--commit-dirty=true
|
||||
|
||||
- name: Commit generated docs
|
||||
run: |
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@gitea"
|
||||
git add docs/fish-config.md docs/fish-config.1
|
||||
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
||||
git commit -m "chore(docs): regenerate manual and man page"
|
||||
git push
|
||||
@@ -0,0 +1,148 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/manual/**"
|
||||
- "docs/build-manual.py"
|
||||
- "docs/manualtools.py"
|
||||
- "docs/verify-manual.py"
|
||||
- "docs/site/**"
|
||||
- "functions/**"
|
||||
- "conf.d/**"
|
||||
- "config.fish"
|
||||
- "completions/**"
|
||||
- "integrations/**"
|
||||
- "tests/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
job:
|
||||
description: "Job to run"
|
||||
required: false
|
||||
default: all
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- test
|
||||
- build-docs
|
||||
|
||||
jobs:
|
||||
# This workflow file is mirrored to GitHub as-is, but the runner label
|
||||
# below (racknerd-mini) only exists on the Gitea instance -- on GitHub
|
||||
# the job just sits queued forever with no matching runner, so the
|
||||
# mirror never gets a completed status. Gate the real jobs to Gitea and
|
||||
# let the github-mirror job below stand in on GitHub instead.
|
||||
test:
|
||||
if: |
|
||||
github.server_url != 'https://github.com' &&
|
||||
(github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'test')
|
||||
runs-on: racknerd-mini
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Install fish
|
||||
run: |
|
||||
sudo apt-get -o Acquire::Retries=3 update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:fish-shell/release-4
|
||||
sudo apt-get -o Acquire::Retries=3 update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish
|
||||
|
||||
- name: Run fish config tests
|
||||
run: fish tests/run-tests.fish
|
||||
|
||||
build-docs:
|
||||
needs: test
|
||||
if: |
|
||||
github.server_url != 'https://github.com' &&
|
||||
always() &&
|
||||
(github.event.inputs.job == 'build-docs' ||
|
||||
((github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all') &&
|
||||
needs.test.result == 'success'))
|
||||
runs-on: racknerd-mini
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get -o Acquire::Retries=3 update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y software-properties-common
|
||||
sudo add-apt-repository -y ppa:fish-shell/release-4
|
||||
sudo apt-get -o Acquire::Retries=3 update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish
|
||||
|
||||
- name: Generate concatenated markdown
|
||||
run: python3 docs/build-manual.py --concat -o docs/fish-config.md
|
||||
|
||||
# Regeneration MUST run before verification: verify-manual.py's
|
||||
# test_concat_roundtrips_original compares a freshly-built concat
|
||||
# against docs/fish-config.md on disk. Before this step ran, that
|
||||
# file was still the stale pre-push copy, so any ordinary edit under
|
||||
# docs/manual/** failed the round-trip check before anything was
|
||||
# regenerated. Do not reorder this back — verification still gates
|
||||
# pandoc and the auto-commit below, it just no longer requires a
|
||||
# contributor to hand-sync the generated file before pushing.
|
||||
- name: Verify manual integrity
|
||||
run: python3 docs/verify-manual.py
|
||||
|
||||
- name: Compile man page
|
||||
run: |
|
||||
pandoc --standalone \
|
||||
--from markdown \
|
||||
--to man \
|
||||
docs/fish-config.md \
|
||||
-o docs/fish-config.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Generate site content
|
||||
run: python3 docs/build-manual.py --site
|
||||
|
||||
- name: Build documentation site
|
||||
working-directory: docs/site
|
||||
run: |
|
||||
npm ci
|
||||
npx astro build
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
working-directory: docs/site
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
run: |
|
||||
npx --yes wrangler pages deploy dist/ \
|
||||
--project-name=fish-config-docs \
|
||||
--branch=main \
|
||||
--commit-dirty=true
|
||||
|
||||
- name: Commit generated docs
|
||||
run: |
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@gitea"
|
||||
git add docs/fish-config.md docs/fish-config.1 conf.d/__fish_config_op_registry.fish
|
||||
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
||||
git commit -m "chore(docs): regenerate manual, man page, and component registry"
|
||||
git push
|
||||
|
||||
# Stand-in for the GitHub mirror so the commit gets a completed status
|
||||
# instead of the real jobs above sitting queued forever for a
|
||||
# self-hosted runner that only exists on the Gitea instance.
|
||||
github-mirror:
|
||||
if: github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Note that CI runs on Gitea
|
||||
run: |
|
||||
echo "This repository mirrors from Gitea (git.rootiest.dev), where CI actually runs."
|
||||
echo "See the commit's status on the Gitea instance for the real test/build-docs results."
|
||||
@@ -0,0 +1,71 @@
|
||||
name: Sync labels to mirror
|
||||
|
||||
# Labels do not travel with a mirror push -- mirroring copies files, not
|
||||
# repository settings -- but they matter on the GitHub side anyway, because
|
||||
# GitHub reads the same .github/ISSUE_TEMPLATE/ files and silently drops a
|
||||
# labels: entry naming a label it does not have. Gitea is the source of
|
||||
# truth; this makes the mirror match.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 06:00 UTC daily. Label churn is rare, so a slower cadence than this
|
||||
# would leave the mirror wrong for most of a working day after an edit.
|
||||
- cron: "0 6 * * *"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
# Exercise the sync as soon as its own logic changes, rather than
|
||||
# waiting for the next scheduled run to find out it is broken.
|
||||
- "scripts/sync-labels.py"
|
||||
- ".github/workflows/sync-labels.yml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "Report the plan without changing anything"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
sync-labels:
|
||||
# This file is mirrored to GitHub as-is. The runner label below only
|
||||
# exists on the Gitea instance, so on GitHub the job would sit queued
|
||||
# forever against a runner that will never pick it up -- the same
|
||||
# problem the github-mirror stand-in in ci.yml exists to solve. A
|
||||
# skipped job costs nothing and produces no stuck status.
|
||||
if: github.server_url != 'https://github.com'
|
||||
runs-on: racknerd-mini
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Install Python
|
||||
run: |
|
||||
sudo apt-get -o Acquire::Retries=3 update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install \
|
||||
--no-install-recommends -y python3
|
||||
|
||||
# Cheap, offline, and no token needed. Catches a broken diff before
|
||||
# anything is allowed to mutate labels on the mirror.
|
||||
- name: Check the diff logic
|
||||
run: python3 scripts/sync-labels.py --self-test
|
||||
|
||||
- name: Sync labels
|
||||
env:
|
||||
GH_MIRROR_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$GH_MIRROR_TOKEN" ]; then
|
||||
echo "::error::GH_MIRROR_TOKEN is not set in this repository's Actions secrets."
|
||||
echo "Create a fine-grained GitHub token scoped to rootiest/fish-config with"
|
||||
echo "Issues: read and write, plus Pull requests: read, and add it as"
|
||||
echo "GH_MIRROR_TOKEN under Settings -> Actions -> Secrets."
|
||||
exit 1
|
||||
fi
|
||||
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||
python3 scripts/sync-labels.py --dry-run
|
||||
else
|
||||
python3 scripts/sync-labels.py
|
||||
fi
|
||||
+570
@@ -0,0 +1,570 @@
|
||||
# Contributing to fish-config
|
||||
|
||||
This is a personal dotfiles repo, but it's run with practices meant to scale
|
||||
to a team of contributors, not just one person's solo habits. This document
|
||||
formalizes those practices so they live somewhere durable instead of only in
|
||||
commit history and conversation memory. It will grow as the project does —
|
||||
treat it as a living document, not a final word.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Issues](#issues)
|
||||
- [Branching & Pull Requests](#branching--pull-requests)
|
||||
- [Labels](#labels)
|
||||
- [Commit Conventions](#commit-conventions)
|
||||
- [Fish Coding Standards](#fish-coding-standards)
|
||||
- [Opinionated Components](#opinionated-components)
|
||||
- [Documentation Pipeline](#documentation-pipeline)
|
||||
- [Testing](#testing)
|
||||
- [Secrets & Machine-Specific Config](#secrets--machine-specific-config)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
You'll need [fish 4.x](https://fishshell.com/). Clone the repo and run the
|
||||
test suite to confirm your environment is sane:
|
||||
|
||||
```fish
|
||||
fish tests/run-tests.fish
|
||||
```
|
||||
|
||||
If you're touching anything under `docs/manual/`, you'll also want `pandoc`,
|
||||
`python3-yaml`, and Node 24+ to exercise the full doc-build pipeline locally
|
||||
(see [Documentation Pipeline](#documentation-pipeline)) — otherwise CI will
|
||||
catch problems on push.
|
||||
|
||||
## Issues
|
||||
|
||||
Issues live on the Gitea repo. Three templates cover the common cases, each
|
||||
pre-applying its `Kind/` label; blank issues stay enabled for everything else
|
||||
— a chore, a refactor, a question, a tracking issue.
|
||||
|
||||
| Template | Format | Use it for | Applies |
|
||||
|---|---|---|---|
|
||||
| **Bug report** | web form | Something is broken or behaves unexpectedly | `Kind/Bug` |
|
||||
| **Feature or enhancement request** | markdown | New functionality, or an improvement to what exists | `Kind/Feature` |
|
||||
| **Documentation issue** | markdown | The manual, man page, `config-help`, or docs site is wrong, missing, or unclear | `Kind/Documentation` |
|
||||
|
||||
They live in `.github/ISSUE_TEMPLATE/`, next to the PR template, so the
|
||||
GitHub mirror offers the same set. The bug report is a Gitea *issue form* —
|
||||
a real web form with required fields — because a bug report missing its
|
||||
version, reproduction, or full error text can't be acted on, and a form
|
||||
refuses to submit without them. The other two are markdown templates in the
|
||||
same comment-guided style as `.github/PULL_REQUEST_TEMPLATE.md`, since what
|
||||
they ask for is open-ended prose that structure would only get in the way of.
|
||||
|
||||
GitHub reads these same files on the mirror, and its schema differs from
|
||||
Gitea's in two places, so both are pinned to the spelling that works on both
|
||||
and each file says so in a comment: the chooser config must be `config.yml`
|
||||
(GitHub ignores `config.yaml`), and `bug.yml` declares `description:` rather
|
||||
than `about:` (GitHub requires it; Gitea accepts it as an alias). The two
|
||||
markdown templates keep `about:`, which is correct for their format on both.
|
||||
|
||||
### Issue titles
|
||||
|
||||
**Issue titles are plain descriptions of the problem, not Conventional
|
||||
Commits subjects.**
|
||||
|
||||
```text
|
||||
mv clobbers a symlink when the target exists ← yes
|
||||
fix(mv): prompt before replacing an existing symlink ← no
|
||||
```
|
||||
|
||||
An issue states a problem; a commit states a change. The type and scope that
|
||||
`fix(mv):` would carry are already on the issue as its `Kind/` and `Area/`
|
||||
labels, and the conventional subject belongs on the PR that closes it, where
|
||||
it becomes the commit message. Writing the fix into the title also presumes
|
||||
one, which is the wrong end to start from for anything still being diagnosed.
|
||||
|
||||
### What an issue owes
|
||||
|
||||
- **A bug** needs a reproduction someone else can paste and run, starting
|
||||
from a fresh shell, plus the complete error output. A stale function
|
||||
definition in a long-lived session is the most common false alarm, so
|
||||
confirm it survives `exec fish` first. `Status/Need More Info` is where
|
||||
reports without a reproduction end up.
|
||||
- **A feature** needs `## Acceptance criteria` — the checkbox list of what
|
||||
must be true for the issue to close. It is the issue-side counterpart to a
|
||||
PR's `## Verification`: a definition of done agreed before the work starts
|
||||
rather than argued about after, and the PR's checks usually grow out of it.
|
||||
- **A docs issue** needs to name the `docs/manual/**` source, not just the
|
||||
page where the problem showed up. `docs/fish-config.md` and
|
||||
`docs/fish-config.1` are generated, and a fix applied there is overwritten
|
||||
by the next CI run — see [Documentation
|
||||
Pipeline](#documentation-pipeline).
|
||||
|
||||
### Triage
|
||||
|
||||
Reporters aren't expected to label anything. Contributors without push access
|
||||
can't, and the templates apply the `Kind/` label by themselves; the rest is
|
||||
the maintainer's job when the issue is triaged — add the `Area/` label (the
|
||||
bug form's **Area** dropdown is how a reporter tells you, since no forge can
|
||||
map a form field to a label), set a `Priority/` if it isn't ordinary, and
|
||||
apply `Reviewed/Confirmed` once a bug actually reproduces. See
|
||||
[Labels](#labels).
|
||||
|
||||
When a PR resolves an issue it closes it with a trailing `Closes #N` line —
|
||||
see [Pull request descriptions](#pull-request-descriptions).
|
||||
|
||||
## Branching & Pull Requests
|
||||
|
||||
**If you don't have push access to this repo**, fork it and open your PR
|
||||
from a branch on your fork back to `main` here — everything below about
|
||||
branch naming and commit hygiene still applies, it just happens on your
|
||||
fork instead of a branch of this repo directly. The rest of this section
|
||||
assumes you *do* have push access (maintainers, regular contributors).
|
||||
|
||||
- **Branch off `main` before starting work.** Don't accumulate uncommitted
|
||||
changes directly on `main`. (If you already started editing before
|
||||
branching, that's fine — create the branch now, before your first commit;
|
||||
branching doesn't touch the working tree.)
|
||||
- **Merge target is `main`, via PR.** Contributors open the PR; the repo
|
||||
owner merges it. Don't merge your own PR.
|
||||
- **Label every PR.** At minimum one `Kind/` and one `Area/`, same as an
|
||||
issue — see [Labels](#labels). If you can't set labels, say what the
|
||||
change is in the description and a maintainer applies them.
|
||||
- **Don't merge until the `## Verification` checklist is fully checked.**
|
||||
Unchecked boxes are outstanding manual checks, not decoration. See
|
||||
[Pull request descriptions](#pull-request-descriptions) below.
|
||||
- **Prefix in-progress PRs with `WIP:`.** If the branch still has code
|
||||
changes coming, open it as `WIP: type(scope): description`. Gitea
|
||||
recognizes the prefix, flags the PR as a draft, and refuses to merge it
|
||||
until the prefix is removed; drop it once the branch is complete.
|
||||
|
||||
`WIP:` signals **more changes are coming** — not "done but unverified".
|
||||
A finished branch waiting on manual checks is an ordinary PR whose
|
||||
`## Verification` boxes aren't all ticked yet; that's already the merge
|
||||
gate above and doesn't need the prefix. The two are independent: a PR
|
||||
can be WIP with everything ticked, or complete with checks outstanding.
|
||||
- **Keep feature branches focused.** If you stumble onto something unrelated
|
||||
to your current task while working (a pre-existing bug, a stray cleanup),
|
||||
don't fold it into the same commit or PR. Handle it with one of these,
|
||||
in order of preference:
|
||||
1. **Separate branch, separate PR, merged to `main` independently.** The
|
||||
default for anything that doesn't overlap the code your current branch
|
||||
touches. The two PRs review and merge independently, in either order.
|
||||
2. **Separate branch and PR, then sync `main` back into your feature
|
||||
branch** once it merges. Use this only when the unrelated fix actually
|
||||
touches the same file/function as your feature branch, or your feature
|
||||
branch depends on the corrected behavior to work or test correctly.
|
||||
3. **Commit directly to `main`.** Reserved for changes too small to
|
||||
justify a branch, or genuinely urgent fixes. **Always ask for explicit
|
||||
approval before doing this** — there's no standing exception, no matter
|
||||
how trivial the change looks.
|
||||
|
||||
### Pull request descriptions
|
||||
|
||||
Fill in `.github/PULL_REQUEST_TEMPLATE.md` — Gitea pre-loads it into the
|
||||
description box when you open a PR. Every PR carries, in this order:
|
||||
|
||||
- **`## Summary`** — what changed and why, as a short paragraph, 2-5
|
||||
bullets, or both. Name concrete paths and identifiers in backticks, and
|
||||
explain the reasoning rather than restating the diff.
|
||||
- **Optional `##` sections** — add what the change actually needs
|
||||
(`Root cause`, `Why`, `How it works`, `Behavior`, `Docs`, `Notes`,
|
||||
`Scope note`, `Opinionated guard (C1-C6)`), and skip them entirely for a
|
||||
straightforward change. A breaking change (title ending in `!` before
|
||||
the colon) must include `## ⚠️ Breaking Change` with the migration path.
|
||||
- **`## Verification`** — always last. Every check this change needs, as a
|
||||
checkbox list, each with the exact command and its expected result.
|
||||
A **checked** box means verified, whether programmatically (test suite,
|
||||
linter, docs verifier, CI) or by hand; check those off before opening the
|
||||
PR. An **unchecked** box is an outstanding manual check the reviewer
|
||||
still has to perform — leave anything you couldn't verify yourself
|
||||
unchecked rather than dropping it.
|
||||
|
||||
**This list is the merge gate: a PR isn't merged until every box is
|
||||
checked.** Only list checks that can actually be resolved — one nobody
|
||||
can run blocks the PR indefinitely. Put genuinely unverifiable caveats,
|
||||
assumptions, and known limitations in `## Notes` instead, where they
|
||||
inform the review without gating it.
|
||||
|
||||
When a PR resolves a tracked issue, close it with a trailing `Closes #42`
|
||||
line at the end of `## Summary` — not at the very bottom of the body, since
|
||||
`## Verification` is always last. `Fixes #N` and `Resolves #N` behave
|
||||
identically. Repeat the keyword for each issue (`Closes #42, closes #43`); a
|
||||
bare `#43` is only a link and won't close anything. To point at a related
|
||||
issue that should stay open, drop the keyword and use `Refs #42`. Leave the
|
||||
line out entirely when no issue is involved.
|
||||
|
||||
## Labels
|
||||
|
||||
**Every issue and every pull request carries exactly one `Kind/` label and at
|
||||
least one `Area/` label.** Everything else is optional, and most of it is
|
||||
applied by a maintainer at triage rather than by whoever opened the thing.
|
||||
|
||||
Labels are scoped: the `Group/Name` form renders as a two-tone chip in Gitea,
|
||||
and for the three *exclusive* groups below Gitea enforces one-at-a-time by
|
||||
swapping the old label out when you apply a new one.
|
||||
|
||||
### `Kind/` — what this is
|
||||
|
||||
Required, and by convention exactly one. Gitea doesn't enforce one-of here,
|
||||
so pick the dominant character of the change instead of stacking two.
|
||||
|
||||
| Label | For |
|
||||
|---|---|
|
||||
| `Kind/Bug` | Something is not working |
|
||||
| `Kind/Feature` | New functionality |
|
||||
| `Kind/Enhancement` | Improves functionality that already exists |
|
||||
| `Kind/Documentation` | Documentation changes |
|
||||
| `Kind/Testing` | The test suite itself |
|
||||
| `Kind/Refactor` | Restructures code without changing behavior |
|
||||
| `Kind/Chore` | Tooling, dependencies, housekeeping |
|
||||
| `Kind/Performance` | Makes existing behavior faster or lighter |
|
||||
| `Kind/Security` | A security issue |
|
||||
|
||||
These deliberately mirror the Conventional Commits types in [Commit
|
||||
Conventions](#commit-conventions), so a PR's label and its title agree:
|
||||
`fix` → `Kind/Bug`, `feat` → `Kind/Feature` or `Kind/Enhancement`, `docs` →
|
||||
`Kind/Documentation`, `test` → `Kind/Testing`, `refactor` →
|
||||
`Kind/Refactor`, `chore` → `Kind/Chore`, `perf` → `Kind/Performance`.
|
||||
|
||||
### `Area/` — what it touches
|
||||
|
||||
Required, and non-exclusive on purpose: a change that adds a function, its
|
||||
completions, and a manual entry gets all three.
|
||||
|
||||
| Label | Covers |
|
||||
|---|---|
|
||||
| `Area/Functions` | `functions/` |
|
||||
| `Area/Completions` | `completions/` |
|
||||
| `Area/Config` | `config.fish`, `conf.d/` — startup and environment |
|
||||
| `Area/Docs` | `docs/manual/` and the generated manual, man page, and site |
|
||||
| `Area/Tests` | `tests/` |
|
||||
| `Area/CI` | `.github/workflows/` and repository automation |
|
||||
| `Area/Integrations` | `integrations/` |
|
||||
| `Area/Prompt & Theme` | `themes/` and prompt appearance |
|
||||
| `Area/Components` | The opinionated-component system (C1-C6) |
|
||||
| `Area/Scripts` | `scripts/` |
|
||||
|
||||
`Area/` is what makes the tracker searchable: it answers "what's still
|
||||
outstanding in the docs pipeline?" in a way `Kind/` never can. Two edges
|
||||
worth naming — `Area/Docs` covers the documentation *and its pipeline*, so
|
||||
`README.md` and this file count even though they sit outside `docs/`; and
|
||||
`Area/Components` is for the C1-C6 machinery itself, not for every function
|
||||
that happens to carry a `# COMPONENT` header.
|
||||
|
||||
### `Compat/Breaking`
|
||||
|
||||
Applied to **any PR whose title carries `!` before the colon**, and to any
|
||||
issue proposing a change that would. It travels with the `## ⚠️ Breaking
|
||||
Change` section that such a PR must already include — see [Pull request
|
||||
descriptions](#pull-request-descriptions).
|
||||
|
||||
### `Priority/` — exclusive, maintainer-applied
|
||||
|
||||
`Priority/Critical`, `Priority/High`, `Priority/Medium`, `Priority/Low`.
|
||||
|
||||
**No priority label means ordinary priority.** Labeling everything defeats
|
||||
the point, so leave it off unless the item is genuinely more or less urgent
|
||||
than the rest of the queue.
|
||||
|
||||
### `Reviewed/` — exclusive, maintainer-applied
|
||||
|
||||
`Reviewed/Confirmed` goes on a bug that has actually been reproduced —
|
||||
that's the signal separating a report from a known defect.
|
||||
`Reviewed/Duplicate`, `Reviewed/Invalid`, and `Reviewed/Won't Fix` accompany
|
||||
closing an issue, always with a comment saying why; a close with only a
|
||||
label on it is not an explanation.
|
||||
|
||||
### `Status/` — exclusive, maintainer-applied
|
||||
|
||||
`Status/Blocked`, `Status/Need More Info`, `Status/Abandoned`. These describe
|
||||
the item's current state, so remove one as soon as it stops being true — a
|
||||
stale `Status/Need More Info` on an issue that got its answer is worse than
|
||||
no label, because it reads as still waiting.
|
||||
|
||||
### `good first issue` and `help wanted`
|
||||
|
||||
Invitations to contributors, applied by a maintainer. Both are deliberately
|
||||
**unscoped**: they'd be a natural fit under `Status/`, but that group is
|
||||
exclusive, and an issue is quite often both blocked on something *and* open
|
||||
for someone to pick up. Keeping them outside the group lets them coexist
|
||||
with a real status.
|
||||
|
||||
Use `good first issue` for work that is genuinely self-contained — a clear
|
||||
acceptance criterion, one or two files, no need to understand the
|
||||
opinionated-component system first.
|
||||
|
||||
### The GitHub mirror
|
||||
|
||||
The repo is mirrored to
|
||||
[github.com/rootiest/fish-config](https://github.com/rootiest/fish-config),
|
||||
and **the mirror carries the same labels, by the same names**. That isn't
|
||||
cosmetic: GitHub reads the same `.github/ISSUE_TEMPLATE/` files, and a
|
||||
`labels:` entry naming a label that doesn't exist on that side is silently
|
||||
dropped rather than reported.
|
||||
|
||||
Mirroring copies files, not repository settings, so labels don't travel with
|
||||
a push. **`.github/workflows/sync-labels.yml` closes that gap**: it runs
|
||||
`scripts/sync-labels.py` on a daily schedule, and again whenever the script
|
||||
itself changes, to make GitHub match Gitea. Manage labels here, in the Gitea
|
||||
UI, and the mirror catches up within a day — or dispatch the workflow by
|
||||
hand for it to happen now. Nothing needs doing on the GitHub side.
|
||||
|
||||
The sync creates what's missing and corrects color or description drift,
|
||||
and it deletes an extra label on the mirror **only when no issue or PR there
|
||||
carries it**; one that's in use is reported with its count and left for a
|
||||
human to decide about. Run the script with `--dry-run` to see the plan
|
||||
without changing anything, or `--self-test` to check its diff logic offline
|
||||
— both are useful before editing it. Because labels are matched by name,
|
||||
renaming one on Gitea reads as a delete plus a create: the new name appears
|
||||
on the mirror, and the old one is pruned only if it's unused.
|
||||
|
||||
The workflow needs a GitHub token in this repo's Actions secrets as
|
||||
`GH_MIRROR_TOKEN`, scoped to the mirror with **Issues: read and write**
|
||||
(GitHub files labels under Issues) and **Pull requests: read** (so the
|
||||
in-use check sees labels on PRs). The job fails with an explicit message if
|
||||
it's missing rather than quietly doing nothing.
|
||||
|
||||
One behavioral difference to keep in mind: **GitHub has no exclusive
|
||||
labels.** Gitea enforces one-at-a-time on `Priority/`, `Reviewed/`, and
|
||||
`Status/` by swapping the old label out; on the mirror those are ordinary
|
||||
labels and nothing stops two of a group coexisting, so there the one-of rule
|
||||
holds by convention alone.
|
||||
|
||||
Issues and pull requests belong on the canonical Gitea repo — the template
|
||||
chooser links there first, on both sides. The mirror's tracker stays open so
|
||||
that a report which lands there anyway isn't lost, not because it's a second
|
||||
supported front door.
|
||||
|
||||
## Commit Conventions
|
||||
|
||||
Commit subjects follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
```
|
||||
|
||||
Types currently in use in this repo: `feat`, `fix`, `docs`, `test`, `chore`,
|
||||
`perf`. The scope is usually the function, component, or subsystem touched
|
||||
(e.g. `feat(help): ...`, `fix(scrub): ...`, `chore(docs): ...`). Look at
|
||||
`git log` for recent examples before picking a type/scope for something
|
||||
novel.
|
||||
|
||||
## Fish Coding Standards
|
||||
|
||||
### File header
|
||||
|
||||
Every hand-authored `.fish` file starts with:
|
||||
|
||||
```fish
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
```
|
||||
|
||||
(A few completion scripts adapted directly from a tool's own upstream
|
||||
completions keep that tool's original attribution comment instead of this
|
||||
header — match whichever convention the specific file already follows. New
|
||||
files use the standard header above.)
|
||||
|
||||
### Public function documentation header
|
||||
|
||||
Every user-facing function gets a machine-parsed comment header directly
|
||||
above its `function` line. This header is the single source of truth for
|
||||
the generated manual (`docs/fish-config.md` / the man page / the docs
|
||||
site) — see [Documentation Pipeline](#documentation-pipeline) for how it
|
||||
gets consumed. The parser (`docs/manualtools.py`) recognizes these labels,
|
||||
all optional except where noted:
|
||||
|
||||
| Label | Purpose |
|
||||
|---|---|
|
||||
| `CATEGORY` | **Required to appear in the manual at all** — see below. |
|
||||
| `COMPONENT` | Only for functions gated by the [opinionated-component system](#opinionated-components). |
|
||||
| `DEPENDENCIES` | Other functions this one calls that a reader may want to look up. |
|
||||
| `SYNOPSIS` | One-line usage form. |
|
||||
| `DESCRIPTION` | Prose description; can span multiple paragraphs. |
|
||||
| `ARGUMENTS` | Flags/positional args, one per line. |
|
||||
| `EXIT STATUS` | Exit codes and what they mean. |
|
||||
| `RETURNS` | For functions used for their output/return value rather than exit status. |
|
||||
| `EXAMPLE` | One or more realistic invocations. |
|
||||
| `NOTES` | Anything else worth flagging (fallback behavior, caveats, gotchas). |
|
||||
|
||||
A full example (`functions/claude.fish`):
|
||||
|
||||
```fish
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# CATEGORY
|
||||
# 12-ai-and-developer-tools
|
||||
#
|
||||
# COMPONENT
|
||||
# aliases/dev-tools
|
||||
#
|
||||
# DEPENDENCIES
|
||||
# agents-init
|
||||
#
|
||||
# SYNOPSIS
|
||||
# claude [ARGS...]
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Wrapper for the claude CLI that ensures the AGENTS/ sub-repository is
|
||||
# initialized and any agent-made changes are committed before launch.
|
||||
# ...
|
||||
```
|
||||
|
||||
**`CATEGORY` is the opt-in gate for the manual:** a function with no
|
||||
`# CATEGORY` line produces no manual entry at all — this is how bundled
|
||||
plugin internals and prompt guts stay out of user-facing docs without an
|
||||
exclusion list. `CATEGORY` must exactly match one of the existing
|
||||
`docs/manual/05-functions/NN-*.md` stubs:
|
||||
|
||||
```
|
||||
01-file-and-directory 08-terminal-management
|
||||
02-navigation 09-clipboard
|
||||
03-editors-and-viewers 10-network
|
||||
04-git-and-version-control 11-pager-and-logging
|
||||
05-package-management 12-ai-and-developer-tools
|
||||
06-dependency-management 13-media-and-utilities
|
||||
07-system-and-monitoring 14-miscellaneous
|
||||
```
|
||||
|
||||
If your function genuinely doesn't fit any of these, add a new
|
||||
`docs/manual/05-functions/NN-your-category.md` stub (with frontmatter
|
||||
matching its siblings) rather than force-fitting it into an existing one.
|
||||
|
||||
### Private/internal helper functions
|
||||
|
||||
Functions named with a leading `_` (e.g. `_agents_init_ensure_gitignore`,
|
||||
`__fish_config_op_enabled`) are excluded from the manual unconditionally,
|
||||
regardless of whether they carry a `CATEGORY` line — so they generally don't
|
||||
have one. They should still carry the standard file header, and a lighter
|
||||
`SYNOPSIS`/`DESCRIPTION` comment is encouraged wherever the function's
|
||||
purpose or calling convention isn't obvious from its body, matching the
|
||||
convention already used across `__fish_config_op_*.fish` and similar files.
|
||||
|
||||
### Function declaration
|
||||
|
||||
Give every function a `--description`, since it's what shows up in `fish -c
|
||||
'functions'`/completions and other introspection:
|
||||
|
||||
```fish
|
||||
function my_function --description 'Short, imperative description'
|
||||
```
|
||||
|
||||
### Colored `--help` output
|
||||
|
||||
Every function with a `-h`/`--help` flag uses this standardized color
|
||||
palette (established across the codebase 2026-08-22):
|
||||
|
||||
```fish
|
||||
set -l c_head (set_color --bold cyan) # section headers
|
||||
set -l c_cmd (set_color --bold) # the command name itself (theme-adaptive)
|
||||
set -l c_flag (set_color yellow) # flags
|
||||
set -l c_arg (set_color cyan) # required-argument placeholders
|
||||
set -l c_dim (set_color brblack) # optional-argument placeholders
|
||||
set -l c_ok (set_color green) # success/positive status
|
||||
set -l c_warn (set_color yellow) # warnings
|
||||
set -l c_err (set_color red) # errors
|
||||
set -l c_reset (set_color normal)
|
||||
```
|
||||
|
||||
Not every function needs every variable — pull in only the ones your help
|
||||
text actually uses. Structure the usage block as `Usage:`, then sections
|
||||
for arguments/flags/examples as needed; see `functions/rand_string.fish` or
|
||||
any recently-touched function for a full worked example.
|
||||
|
||||
## Opinionated Components
|
||||
|
||||
Some functionality in this config is classified into one of six toggleable
|
||||
categories (C1–C6: aliases, autoexec, overrides, integrations, logging,
|
||||
greeting) plus sub-categories, so users can disable pieces of it via
|
||||
`config-settings` or universal variables. If you're adding something that
|
||||
shadows a builtin, runs at startup, overrides a key binding/environment
|
||||
variable, or otherwise falls under an existing category, tag it with a
|
||||
`# COMPONENT` header (`<category>/<subcategory>`, e.g.
|
||||
`overrides/key-bindings`) and guard it with `__fish_config_op_enabled`. See
|
||||
the [README's Minimal Mode section](README.md#minimal-mode) for the full
|
||||
category list and toggle semantics, and
|
||||
`AGENTS/specs/2026-08-17-opinionated-component-subcategories-design.md` for
|
||||
the underlying design. Most new functions are *not* opinionated components
|
||||
— only tag something if it fits an existing category; this isn't something
|
||||
to force.
|
||||
|
||||
## Documentation Pipeline
|
||||
|
||||
`docs/manual/` is the single source of truth for the user manual, man page,
|
||||
and documentation site. **Never hand-edit `docs/fish-config.md` or
|
||||
`docs/fish-config.1`** — they're generated by `docs/build-manual.py` from
|
||||
`docs/manual/**` plus every function's doc-header (see above), verified by
|
||||
`docs/verify-manual.py`, and auto-committed by CI on push to `main`
|
||||
(`chore(docs): regenerate manual, man page, and component registry`).
|
||||
|
||||
If you're changing something under `docs/manual/` directly, or adding a
|
||||
function whose `CATEGORY`/`COMPONENT` header should surface new content, you
|
||||
can build and check it locally before pushing:
|
||||
|
||||
```fish
|
||||
python3 docs/build-manual.py --concat -o docs/fish-config.md
|
||||
python3 docs/verify-manual.py
|
||||
```
|
||||
|
||||
CI runs the same verification and regenerates the site/man page — a broken
|
||||
manual won't get published, but running it locally saves a round trip.
|
||||
|
||||
Write doc-headers as plain text — no backticks. `-a/--all`,
|
||||
`__fish_config_op_aliases` and `~/.config/fish/config.fish` are typed
|
||||
bare, because the header is also read as-is by `config-help` and by
|
||||
anyone opening the file. `docs/codespans.py` adds the inline code spans
|
||||
when it renders, so the SSOT never carries them; see
|
||||
`docs/site/README.md` for which shapes it recognises. That pass runs for
|
||||
every output — the site, the man page and `config-help` — so a token is
|
||||
typeset the same way wherever it is read.
|
||||
|
||||
Two rules apply to backticks you write under `docs/manual/` as well:
|
||||
|
||||
- **Never inside an indented block.** A four-space block is verbatim in
|
||||
every renderer, so a backtick there is a literal character on the page
|
||||
rather than markup.
|
||||
- **Never wrapped across a line break.** Markdown accepts a span split
|
||||
over two lines, but `config-help` pairs backticks one line at a time
|
||||
and would show the halves literally. Reflow the sentence instead.
|
||||
|
||||
`docs/verify-manual.py` enforces both.
|
||||
|
||||
## Testing
|
||||
|
||||
```fish
|
||||
fish tests/run-tests.fish
|
||||
```
|
||||
|
||||
Runs before every push and gates the documentation build in CI. Two phases:
|
||||
|
||||
1. **Syntax lint** — every tracked `.fish` file (`config.fish`, `functions/`,
|
||||
`conf.d/`, `completions/`, `integrations/`) is checked with `fish -n`.
|
||||
2. **Sandboxed functional checks** — the config is copied into a throwaway
|
||||
`HOME`/`XDG_CONFIG_HOME` sandbox (never this checkout itself, since it
|
||||
doubles as a real `~/.config/fish`) and loaded as an isolated interactive
|
||||
session. `tests/functional.fish` then runs against foundational behavior:
|
||||
XDG/PATH/CDPATH setup, key bindings, abbreviations, core functions, the
|
||||
opinionated-component registry, and more.
|
||||
|
||||
To add a new functional check, add a `test_*` function to
|
||||
`tests/functional.fish` — it's picked up automatically by
|
||||
`functional_test_main`, no registration needed. Return 0 on pass, non-zero
|
||||
on fail; print a short diagnostic on failure.
|
||||
|
||||
## Secrets & Machine-Specific Config
|
||||
|
||||
**Nothing containing credentials, tokens, personal identifiers, or a
|
||||
specific machine's paths belongs in this repo — not even in a PR.** That
|
||||
kind of thing lives in each user's own private overlay
|
||||
(`~/.config/.user-dots/fish/secrets.fish` and `local.fish`), which is
|
||||
git-ignored by design. If you're writing something that needs a secret or a
|
||||
machine-specific path, source it from there rather than hardcoding it.
|
||||
Everything else — general-purpose, reusable across machines — belongs in
|
||||
the tracked repo as normal. See [README's Personalization
|
||||
section](README.md#personalization) and
|
||||
[`docs/manual/07-customization.md`](docs/manual/07-customization.md) for the
|
||||
full mechanics.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under AGPL-3.0-or-later (see `LICENSE`). Every new
|
||||
hand-authored file needs the SPDX header shown in [File
|
||||
header](#file-header) above.
|
||||
@@ -15,6 +15,8 @@ abbreviation system for keyboard-driven workflows.
|
||||
- [Installation](#installation)
|
||||
- [Personalization](#personalization)
|
||||
- [Minimal Mode](#minimal-mode)
|
||||
- [Testing](#testing)
|
||||
- [Contributing](#contributing)
|
||||
- [Attribution](#attribution)
|
||||
- [License](#license)
|
||||
|
||||
@@ -144,9 +146,12 @@ Contributing to the docs? There are two sources, split by content type:
|
||||
above each function in `functions/*.fish`. Edit the function; the entry
|
||||
and its site page are generated from the header.
|
||||
- **Everything else** lives under `docs/manual/**`.
|
||||
- **Testing, Contributing, Attribution, and License** are pulled straight
|
||||
from this README (the sections below) rather than authored twice — edit
|
||||
them here and the manual, man page, and site all pick up the change.
|
||||
|
||||
Never edit the generated `docs/fish-config.md` — it's rebuilt from both
|
||||
sources and any hand-edits are discarded.
|
||||
Never edit the generated `docs/fish-config.md` — it's rebuilt from all
|
||||
three sources and any hand-edits are discarded.
|
||||
|
||||
To browse the docs from the terminal:
|
||||
|
||||
@@ -291,7 +296,7 @@ If you'd rather set them by hand, each category is controlled by a universal var
|
||||
|---|---|
|
||||
| `__fish_config_op_aliases` | Command shadows: `ls`→eza, `cat`→bat, `cd`→zoxide, `rm`→trash, `top`→btop, `edit`→multi-editor launcher, and friends; `grep`/`cp`/`mv`/`wget` flag injection |
|
||||
| `__fish_config_op_autoexec` | Startup side-effects: Fisher bootstrap, theme apply, `paru`/`yay` wrapper generation, auto venv activation, WakaTime hook, auto-pull background fast-forward |
|
||||
| `__fish_config_op_overrides` | Vi mode, `exit`→`smart_exit`, `$PAGER`/`$MANPAGER`/`$CDPATH`/`XDG`/`PATH`, bang-bang history expansion, autopair, puffer, Starship prompt, theme colors |
|
||||
| `__fish_config_op_overrides` | Vi mode, `exit`→`smart_exit`, `$PAGER`/`$MANPAGER`/`$CDPATH`/`XDG`/`PATH`, `$DO_NOT_TRACK`/`$DISABLE_TELEMETRY`, bang-bang history expansion, autopair, puffer, Starship prompt, theme colors |
|
||||
| `__fish_config_op_integrations` | Kitty/WezTerm window abbreviations, `done` notifications, `spwin`/`tab`/`split`, `hist`, `logs`, `upgrade`, WakaTime |
|
||||
| `__fish_config_op_logging` | **Opt-in — off unless explicitly enabled.** Scrollback capture on exit, tmux `pipe-pane` pane logging, zellij `dump-screen` capture on exit, `paru`/`yay` AUR log wrappers, Kitty watcher capture (sentinel-file coordinated) |
|
||||
| `__fish_config_op_greeting` | Per-session `fish_greeting` (suppresses distro greetings such as CachyOS fastfetch by overriding with an empty function); first-run welcome banner |
|
||||
@@ -320,6 +325,40 @@ set -Ue __fish_config_op_greeting
|
||||
|
||||
Command shadows react immediately; bindings, prompt, and abbreviations take effect in new shells. With aliases disabled, `rm` deletes permanently again instead of trashing. See `help config opinionated` for the full component list.
|
||||
|
||||
Each category further sub-divides into two to six sub-categories with
|
||||
their own `__fish_config_op_<category>_<subcategory>` toggles (e.g.
|
||||
`__fish_config_op_aliases_filesystem`), following the exact same
|
||||
truthy/falsy/unset cascade one level deeper. Run `config-settings` and
|
||||
press Enter on a category row to browse and toggle its sub-categories, or
|
||||
see the [Components Reference](https://fish.rootiest.fyi/08-components-reference/)
|
||||
for the full sub-category list per category.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```fish
|
||||
fish tests/run-tests.fish
|
||||
```
|
||||
|
||||
Runs before every push (and gates the [documentation build](.github/workflows/ci.yml) in CI, so a broken config can't get published): syntax-lints every `.fish` file, then loads the config in an isolated `HOME`/XDG sandbox — never this checkout itself, since it doubles as a real `~/.config/fish` — and runs functional checks against foundational behavior (XDG/PATH/CDPATH setup, key bindings, abbreviations, core functions, the opinionated-component registry, and more).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Interested in contributing? See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the
|
||||
branching/PR workflow, commit conventions, fish coding standards, and the
|
||||
docs/testing pipeline this repo follows.
|
||||
|
||||
**Preferred forge:** [git.rootiest.dev/rootiest/fish-config](https://git.rootiest.dev/rootiest/fish-config)
|
||||
is the base repository. [github.com/rootiest/fish-config](https://github.com/rootiest/fish-config)
|
||||
is a push-mirror of it — identical content, but one-way and read-only from a
|
||||
contributor's perspective. Branches, forks, and merges made on the GitHub
|
||||
side aren't fed back upstream, so they risk being silently overwritten by
|
||||
the next mirror push. Until two-way sync exists, please fork, branch, and
|
||||
open issues/PRs from the Gitea repository rather than the GitHub mirror.
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
@@ -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'
|
||||
@@ -0,0 +1,2 @@
|
||||
complete --command play-media --short p --long player --description "Force a specific media player" --require-parameter
|
||||
complete --command play-media --short h --long help --description "Print this help message"
|
||||
@@ -0,0 +1,148 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# GENERATED FILE --- do not edit by hand.
|
||||
# Regenerate with __fish_config_op_registry_rebuild after editing a
|
||||
# # COMPONENT header, or automatically via docs/build-manual.py.
|
||||
# Source: docs/generate_component_registry.py
|
||||
#
|
||||
# This file must be sourced before any other conf.d/*.fish file that
|
||||
# calls the opinionated guard. That currently holds only because fish's
|
||||
# glob-based conf.d loading happens to sort this filename first
|
||||
# alphabetically among the guard-calling files -- do not rename it
|
||||
# without preserving that ordering.
|
||||
|
||||
set -g __fish_config_op_registry_keys \
|
||||
"__auto_source_fallback_venv:" \
|
||||
"__fish_config_sync_logging:" \
|
||||
"__fish_user_dots_link:" \
|
||||
"_zellij_dump_log:" \
|
||||
"abbr:abbr-integrations" \
|
||||
"abbr:abbr-overrides" \
|
||||
"agy:" \
|
||||
"auto-pull:" \
|
||||
"autopair:" \
|
||||
"bash:" \
|
||||
"bash_expands:" \
|
||||
"cat:" \
|
||||
"claude:" \
|
||||
"config:cachyos-strip-aliases" \
|
||||
"config:cachyos-strip-overrides" \
|
||||
"config:cachyos-tricks" \
|
||||
"config:cdpath" \
|
||||
"config:exit-wiring" \
|
||||
"config:greeting-stamp" \
|
||||
"config:pager-editor-gpg" \
|
||||
"config:path-setup" \
|
||||
"config:privacy" \
|
||||
"config:vi-mode" \
|
||||
"done:" \
|
||||
"du:" \
|
||||
"edit:" \
|
||||
"first_run:first-run-bootstrap" \
|
||||
"first_run:first-run-greeting" \
|
||||
"fish_right_prompt:" \
|
||||
"help:" \
|
||||
"hist:" \
|
||||
"key_bindings:" \
|
||||
"kitty-logging:" \
|
||||
"kitty-watcher-reminder:" \
|
||||
"less:" \
|
||||
"logs:" \
|
||||
"ls:" \
|
||||
"mkdir:" \
|
||||
"mv:" \
|
||||
"paru-wrapper:paru-autoexec" \
|
||||
"paru-wrapper:paru-logging" \
|
||||
"ping:" \
|
||||
"puffer:" \
|
||||
"rg:" \
|
||||
"rm:" \
|
||||
"smart_exit:exit-plain" \
|
||||
"smart_exit:logging-guard" \
|
||||
"split:" \
|
||||
"spwin:" \
|
||||
"ssh:" \
|
||||
"starship:" \
|
||||
"tab:" \
|
||||
"theme:" \
|
||||
"tmux-logging:" \
|
||||
"top:" \
|
||||
"tricks:aliases-tricks" \
|
||||
"tricks:tricks-bang" \
|
||||
"tricks:tricks-manpager" \
|
||||
"upgrade:" \
|
||||
"wakatime:wakatime-autoexec" \
|
||||
"wakatime:wakatime-hook" \
|
||||
"yay-wrapper:yay-autoexec" \
|
||||
"yay-wrapper:yay-logging" \
|
||||
"yt-dlp:" \
|
||||
"zoxide:"
|
||||
|
||||
set -g __fish_config_op_registry_values \
|
||||
"autoexec/venv" \
|
||||
"logging/terminal-capture" \
|
||||
"autoexec/sync" \
|
||||
"logging/multiplexer-capture" \
|
||||
"integrations/terminal-abbrs" \
|
||||
"overrides/key-bindings" \
|
||||
"aliases/dev-tools" \
|
||||
"autoexec/sync" \
|
||||
"overrides/key-bindings" \
|
||||
"aliases/shell-tools" \
|
||||
"overrides/key-bindings" \
|
||||
"aliases/filesystem" \
|
||||
"aliases/dev-tools" \
|
||||
"aliases/filesystem" \
|
||||
"overrides/key-bindings" \
|
||||
"overrides/environment" \
|
||||
"overrides/environment" \
|
||||
"overrides/key-bindings" \
|
||||
"greeting/greeting-message" \
|
||||
"overrides/environment" \
|
||||
"overrides/environment" \
|
||||
"overrides/privacy" \
|
||||
"overrides/key-bindings" \
|
||||
"integrations/notifications" \
|
||||
"aliases/filesystem" \
|
||||
"aliases/dev-tools" \
|
||||
"autoexec/plugin-management" \
|
||||
"greeting/first-run" \
|
||||
"overrides/prompt" \
|
||||
"aliases/shell-tools" \
|
||||
"integrations/history-logs" \
|
||||
"overrides/key-bindings" \
|
||||
"logging/terminal-capture" \
|
||||
"logging/terminal-capture" \
|
||||
"aliases/shell-tools" \
|
||||
"integrations/history-logs" \
|
||||
"aliases/filesystem" \
|
||||
"aliases/filesystem" \
|
||||
"aliases/filesystem" \
|
||||
"autoexec/pkg-wrappers" \
|
||||
"logging/pkg-logs" \
|
||||
"aliases/network" \
|
||||
"overrides/key-bindings" \
|
||||
"aliases/search" \
|
||||
"aliases/filesystem" \
|
||||
"overrides/key-bindings" \
|
||||
"logging/terminal-capture" \
|
||||
"integrations/window-mgmt" \
|
||||
"integrations/window-mgmt" \
|
||||
"aliases/network" \
|
||||
"overrides/prompt" \
|
||||
"integrations/window-mgmt" \
|
||||
"overrides/prompt" \
|
||||
"logging/multiplexer-capture" \
|
||||
"aliases/monitor" \
|
||||
"aliases/filesystem" \
|
||||
"overrides/key-bindings" \
|
||||
"overrides/environment" \
|
||||
"integrations/pkg-upgrade" \
|
||||
"autoexec/telemetry" \
|
||||
"integrations/notifications" \
|
||||
"autoexec/pkg-wrappers" \
|
||||
"logging/pkg-logs" \
|
||||
"aliases/network" \
|
||||
"aliases/filesystem"
|
||||
|
||||
+7
-3
@@ -8,6 +8,10 @@
|
||||
# This file contains all the abbreviations for the terminal.
|
||||
# It is sourced by Fish on startup.
|
||||
|
||||
# COMPONENT
|
||||
# site abbr-integrations: integrations/terminal-abbrs
|
||||
# site abbr-overrides: overrides/key-bindings
|
||||
|
||||
# Neovim
|
||||
# @category Editors
|
||||
# @desc nvim
|
||||
@@ -155,7 +159,7 @@ abbr -a ag. agy .
|
||||
# @desc exit
|
||||
abbr -a /exit exit
|
||||
# Window-management abbreviations are opinionated (C4 integrations)
|
||||
if __fish_config_op_enabled __fish_config_op_integrations
|
||||
if __fish_config_op_enabled (status basename) abbr-integrations
|
||||
if test "$TERM" = xterm-kitty
|
||||
# @category Terminal Windows, Tabs, and Panes
|
||||
# @desc Close current pane/window
|
||||
@@ -208,7 +212,7 @@ abbr -a speedtest-fast fast-cli
|
||||
|
||||
# Kitty/WezTerm window-management abbreviations are opinionated (C4
|
||||
# integrations): they assume an active Kitty or WezTerm session.
|
||||
if __fish_config_op_enabled __fish_config_op_integrations
|
||||
if __fish_config_op_enabled (status basename) abbr-integrations
|
||||
# Window Creation (OS Windows)
|
||||
if test "$TERM" = xterm-kitty
|
||||
# @category Terminal Windows, Tabs, and Panes
|
||||
@@ -666,7 +670,7 @@ abbr -a url-open open-url
|
||||
### History Expansions and Substitutions ###
|
||||
# Bash-style history expansion is opinionated (C3 overrides), gated atomically
|
||||
# with conf.d/tricks.fish, conf.d/puffer.fish, and functions/expand_*.fish.
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) abbr-overrides
|
||||
# @category History Expansion
|
||||
# @name !^
|
||||
# @desc Expand to the first argument of the previous command
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
# Manage the registry with: auto-pull add / remove / list / status
|
||||
|
||||
# C2 guard: when auto-execution is disabled, do not register the handler.
|
||||
__fish_config_op_enabled __fish_config_op_autoexec; or exit
|
||||
__fish_config_op_enabled (status basename); or exit
|
||||
|
||||
# COMPONENT
|
||||
# autoexec/sync
|
||||
#
|
||||
# SYNOPSIS
|
||||
# __auto_pull_on_pwd (event handler, --on-variable PWD)
|
||||
#
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
status is-interactive || exit
|
||||
|
||||
# COMPONENT
|
||||
# overrides/key-bindings
|
||||
|
||||
# Local modification: opinionated guard (AGENTS.md Task #3). Bracket
|
||||
# auto-pairing intercepts single-character input, classified as C3 overrides.
|
||||
__fish_config_op_enabled __fish_config_op_overrides || exit
|
||||
__fish_config_op_enabled (status basename) || exit
|
||||
|
||||
set --global autopair_left "(" "[" "{" '"' "'"
|
||||
set --global autopair_right ")" "]" "}" '"' "'"
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# overrides/key-bindings
|
||||
|
||||
# Provides bash-style history expansion functions for abbreviations.
|
||||
# These functions are gated by the C3 overrides switch.
|
||||
|
||||
# Execute expand_bang_all
|
||||
function expand_bang_all --description 'Execute expand_bang_all'
|
||||
# Opinionated guard (C3): no expansion when overrides are disabled.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return 1
|
||||
__fish_config_op_enabled (status basename); or return 1
|
||||
|
||||
set -l token $argv[1]
|
||||
if test -z "$token"; set token (commandline -t); end
|
||||
@@ -23,7 +26,7 @@ end
|
||||
# Execute expand_bang_caret
|
||||
function expand_bang_caret --description 'Execute expand_bang_caret'
|
||||
# Opinionated guard (C3): no expansion when overrides are disabled.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return 1
|
||||
__fish_config_op_enabled (status basename); or return 1
|
||||
|
||||
# Split the last history item into a list
|
||||
set -l tokens (string split -n ' ' -- $history[1])
|
||||
@@ -36,7 +39,7 @@ end
|
||||
# Execute expand_bang_minus_n
|
||||
function expand_bang_minus_n --description 'Execute expand_bang_minus_n'
|
||||
# Opinionated guard (C3): no expansion when overrides are disabled.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return 1
|
||||
__fish_config_op_enabled (status basename); or return 1
|
||||
|
||||
set -l token $argv[1]
|
||||
if test -z "$token"; set token (commandline -t); end
|
||||
@@ -58,7 +61,7 @@ end
|
||||
# Execute expand_bang_search
|
||||
function expand_bang_search --description 'Execute expand_bang_search'
|
||||
# Opinionated guard (C3): no expansion when overrides are disabled.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return 1
|
||||
__fish_config_op_enabled (status basename); or return 1
|
||||
|
||||
set -l token $argv[1]
|
||||
if test -z "$token"
|
||||
@@ -84,7 +87,7 @@ end
|
||||
# Execute expand_bang_string
|
||||
function expand_bang_string --description 'Execute expand_bang_string'
|
||||
# Opinionated guard (C3): no expansion when overrides are disabled.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return 1
|
||||
__fish_config_op_enabled (status basename); or return 1
|
||||
|
||||
# Fish 4.x passes the matched token as argv[1]
|
||||
set -l token $argv[1]
|
||||
@@ -112,7 +115,7 @@ end
|
||||
# Execute expand_typo_sub
|
||||
function expand_typo_sub --description 'Execute expand_typo_sub'
|
||||
# Opinionated guard (C3): no expansion when overrides are disabled.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return 1
|
||||
__fish_config_op_enabled (status basename); or return 1
|
||||
|
||||
# In newer Fish, the matched token is often passed as $argv[1]
|
||||
# if the abbr is set up correctly. We'll fallback to commandline just in case.
|
||||
|
||||
+4
-1
@@ -19,6 +19,9 @@
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# COMPONENT
|
||||
# integrations/notifications
|
||||
|
||||
if not status is-interactive
|
||||
exit
|
||||
@@ -26,7 +29,7 @@ end
|
||||
|
||||
# Local modification: opinionated guard (AGENTS.md Task #3). Desktop
|
||||
# notifications assume a graphical session, classified as C4 integrations.
|
||||
__fish_config_op_enabled __fish_config_op_integrations; or exit
|
||||
__fish_config_op_enabled (status basename); or exit
|
||||
|
||||
set -g __done_version 1.19.1
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
#
|
||||
# Runs exactly once on the first interactive fish session after install.
|
||||
# To reset for testing, run: set -Ue __fish_config_first_run_complete
|
||||
#
|
||||
# COMPONENT
|
||||
# site first-run-greeting: greeting/first-run
|
||||
# site first-run-bootstrap: autoexec/plugin-management
|
||||
|
||||
# Exit early in non-interactive shells (scripts, completions, subshells)
|
||||
if not status is-interactive
|
||||
@@ -36,7 +40,7 @@ end
|
||||
# Printing a first-run welcome banner is opinionated (C6 greeting). The
|
||||
# first-run state variable is already set unconditionally above, so
|
||||
# disabling the greeting never re-triggers this file.
|
||||
if __fish_config_op_enabled __fish_config_op_greeting
|
||||
if __fish_config_op_enabled (status basename) first-run-greeting
|
||||
echo ""
|
||||
echo " Welcome to your fish shell configuration!"
|
||||
echo " Run 'help config' for offline documentation."
|
||||
@@ -48,7 +52,7 @@ end
|
||||
# Startup side-effects below (Fisher curl, fisher update, theme apply) are
|
||||
# opinionated (C2 auto-execution). The first-run state variable is already
|
||||
# set above either way, so disabling auto-exec never re-triggers this file.
|
||||
if not __fish_config_op_enabled __fish_config_op_autoexec
|
||||
if not __fish_config_op_enabled (status basename) first-run-bootstrap
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
+4
-1
@@ -5,6 +5,9 @@
|
||||
# │ help config wrapper │
|
||||
# ╰──────────────────────────────────────────────────────────╯
|
||||
#
|
||||
# COMPONENT
|
||||
# aliases/shell-tools
|
||||
#
|
||||
# SYNOPSIS
|
||||
# help [topic] [sub-topic...]
|
||||
# help config [section] [-w|--html] [-m|--man] [-h|--help]
|
||||
@@ -50,7 +53,7 @@ end
|
||||
# --- Wrapper Definition ---
|
||||
function help --wraps help --description "Custom wrapper to intercept 'help config'"
|
||||
# Opinionated guard (C1): fall back to the native fish help when disabled.
|
||||
if not __fish_config_op_enabled __fish_config_op_aliases
|
||||
if not __fish_config_op_enabled (status current-function)
|
||||
__original_help $argv
|
||||
return $status
|
||||
end
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
# This file defines custom key bindings for the Fish shell.
|
||||
# It is sourced by Fish on startup.
|
||||
|
||||
# COMPONENT
|
||||
# overrides/key-bindings
|
||||
|
||||
# ────────────────── Bind Prewious Path Head to Ctrl+G ─────────────────
|
||||
# Bindings to insert the previous path head into the command line
|
||||
# Behaves like `!$:h` does in bash
|
||||
@@ -48,7 +51,11 @@ function fish_user_key_bindings
|
||||
|
||||
# Custom key chords are opinionated (C3 overrides); skip them entirely
|
||||
# when overrides are disabled so stock bindings remain untouched.
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return
|
||||
# NOTE: (status basename), not (status current-function) -- this guard
|
||||
# lives inside fish's own reserved fish_user_key_bindings function, whose
|
||||
# name is not this file's identity; the registry key is this file's
|
||||
# bare basename, key_bindings.
|
||||
__fish_config_op_enabled (status basename); or return
|
||||
|
||||
# ───────────────────────────── Set Bindings ─────────────────────────────
|
||||
#
|
||||
@@ -58,7 +65,6 @@ function fish_user_key_bindings
|
||||
bind ctrl-alt-u _replace_command_token
|
||||
type -q qalc && bind ctrl-alt-= _qalc_eval
|
||||
bind ctrl-enter _smart_execute
|
||||
bind @@ __fzf_inline_picker
|
||||
bind ctrl-right nextd-or-forward-word
|
||||
bind \e\[1\;5C nextd-or-forward-word
|
||||
|
||||
@@ -70,8 +76,17 @@ function fish_user_key_bindings
|
||||
bind --mode $mode ctrl-alt-u _replace_command_token
|
||||
type -q qalc && bind --mode $mode ctrl-alt-= _qalc_eval
|
||||
bind --mode $mode ctrl-enter _smart_execute
|
||||
bind --mode $mode @@ __fzf_inline_picker
|
||||
bind --mode $mode ctrl-right nextd-or-forward-word
|
||||
bind --mode $mode \e\[1\;5C nextd-or-forward-word
|
||||
end
|
||||
|
||||
# @ is Emacs/Vi-insert only. Emacs bindings and Vi's "default" (normal)
|
||||
# mode share the same bind_mode name, so a mode-less `bind @` would also
|
||||
# land in Vi normal mode, turning its current no-op @ into a self-insert.
|
||||
# Register the mode-less form only when Vi bindings aren't the active
|
||||
# base, and rely on the explicit insert-mode bind otherwise.
|
||||
if test "$fish_key_bindings" != fish_vi_key_bindings
|
||||
bind @ __fzf_inline_picker
|
||||
end
|
||||
bind --mode insert @ __fzf_inline_picker
|
||||
end
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# logging/terminal-capture
|
||||
|
||||
#
|
||||
# C5 — Logging & Capture: a non-blocking, per-session reminder shown inside Kitty
|
||||
# when the fish-config scrollback watcher is not yet set up. It never blocks the
|
||||
@@ -10,7 +14,7 @@
|
||||
status is-interactive; or exit
|
||||
type -q kitty; or exit
|
||||
set -q KITTY_WINDOW_ID; or exit
|
||||
__fish_config_op_enabled __fish_config_op_logging; or exit
|
||||
__fish_config_op_enabled (status basename); or exit
|
||||
__fish_variable_check __fish_config_kitty_watcher_dismissed; and exit
|
||||
__kitty_logging_has_watcher; and exit
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
# bars are preserved, renders the captured animation to a clean static log
|
||||
# (via scripts/clean_progress_log.py), and prunes old logs.
|
||||
|
||||
# COMPONENT
|
||||
# site paru-autoexec: autoexec/pkg-wrappers
|
||||
# site paru-logging: logging/pkg-logs
|
||||
|
||||
# Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec).
|
||||
# Wrapper generation is also gated by C5 (Logging & Capture).
|
||||
__fish_config_op_enabled __fish_config_op_autoexec; or return
|
||||
__fish_config_op_enabled (status basename) paru-autoexec; or return
|
||||
|
||||
# C5 — Logging & Capture: remove generated wrapper and skip when logging is off
|
||||
if not __fish_config_op_enabled __fish_config_op_logging
|
||||
if not __fish_config_op_enabled (status basename) paru-logging
|
||||
if test -f "$HOME/.local/bin/paru"
|
||||
and grep -q "# paru-wrapper-version:" "$HOME/.local/bin/paru" 2>/dev/null
|
||||
rm -f "$HOME/.local/bin/paru"
|
||||
|
||||
+4
-1
@@ -1,9 +1,12 @@
|
||||
status is-interactive || exit
|
||||
|
||||
# COMPONENT
|
||||
# overrides/key-bindings
|
||||
|
||||
# Local modification: opinionated guard (AGENTS.md Task #3). Puffer's key
|
||||
# intercepts are part of the bang-bang system, gated atomically under C3
|
||||
# overrides with conf.d/tricks.fish, conf.d/abbr.fish, and expand_*.fish.
|
||||
__fish_config_op_enabled __fish_config_op_overrides || exit
|
||||
__fish_config_op_enabled (status basename) || exit
|
||||
|
||||
function _puffer_fish_key_bindings --on-variable fish_key_bindings
|
||||
set -l modes
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# overrides/prompt
|
||||
|
||||
#
|
||||
# Defines fish_prompt only when starship is installed.
|
||||
# Without starship, fish's built-in prompt already emits OSC 133;A
|
||||
# on the prompt line itself, so no wrapper is needed.
|
||||
|
||||
# Replacing the prompt is opinionated (C3 overrides)
|
||||
__fish_config_op_enabled __fish_config_op_overrides; or return
|
||||
__fish_config_op_enabled (status basename); or return
|
||||
|
||||
type -q starship; or return
|
||||
|
||||
|
||||
+5
-1
@@ -1,5 +1,9 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# overrides/prompt
|
||||
|
||||
#
|
||||
# ╭──────────────────────────────────────────────────────────╮
|
||||
# │ Fish Theme │
|
||||
@@ -9,7 +13,7 @@
|
||||
# Forcing theme colors and $FZF_DEFAULT_OPTS is opinionated (C3 overrides).
|
||||
# The FZF variable is universal, so clean up our Catppuccin value if it
|
||||
# lingers from a session where overrides were still enabled.
|
||||
if not __fish_config_op_enabled __fish_config_op_overrides
|
||||
if not __fish_config_op_enabled (status basename)
|
||||
if set -q FZF_DEFAULT_OPTS; and string match -q '*#1E1E2E*' -- "$FZF_DEFAULT_OPTS"
|
||||
set --erase FZF_DEFAULT_OPTS
|
||||
end
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# logging/multiplexer-capture
|
||||
|
||||
#
|
||||
# C5 — Logging & Capture: starts a pipe-pane log for the current tmux pane
|
||||
# when fish launches inside a tmux session. Each fish shell gets its own
|
||||
# timestamped log file in SCROLLBACK_HISTORY_DIR (default: ~/.terminal_history).
|
||||
# Naming: tmux_<session>-w<window>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
|
||||
|
||||
__fish_config_op_enabled __fish_config_op_logging; or exit
|
||||
__fish_config_op_enabled (status basename); or exit
|
||||
status is-interactive; or exit
|
||||
type -q tmux; or exit
|
||||
set -q TMUX; or exit
|
||||
|
||||
+9
-4
@@ -7,6 +7,11 @@
|
||||
# │ system aliases, and history/backup utilities │
|
||||
# ╰──────────────────────────────────────────────────────────╯
|
||||
|
||||
# COMPONENT
|
||||
# site aliases-tricks: aliases/filesystem
|
||||
# site tricks-manpager: overrides/environment
|
||||
# site tricks-bang: overrides/key-bindings
|
||||
|
||||
## Environment setup
|
||||
# Apply .profile: use this to put fish compatible .profile stuff in
|
||||
if test -f ~/.fish_profile
|
||||
@@ -24,7 +29,7 @@ end
|
||||
|
||||
# Format man pages using bat (only if bat is installed)
|
||||
# Overriding $MANPAGER is opinionated (C3 overrides)
|
||||
if type -q bat; and __fish_config_op_enabled __fish_config_op_overrides
|
||||
if type -q bat; and __fish_config_op_enabled (status basename) tricks-manpager
|
||||
set -gx MANROFFOPT -c
|
||||
set -gx MANPAGER "sh -c 'col -bx | bat -l man -p'"
|
||||
end
|
||||
@@ -37,7 +42,7 @@ set -gx __done_notification_urgency_level low
|
||||
# Functions needed for !! and !$ https://github.com/oh-my-fish/plugin-bang-bang
|
||||
# The bang-bang system is opinionated (C3 overrides) and is gated atomically
|
||||
# here, in conf.d/abbr.fish, conf.d/puffer.fish, and functions/expand_*.fish.
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) tricks-bang
|
||||
function __history_previous_command
|
||||
switch (commandline -t)
|
||||
case "!"
|
||||
@@ -83,7 +88,7 @@ end
|
||||
# Fish command history override to show timestamps
|
||||
# Shadowing the history command is opinionated (C1 aliasing); when disabled,
|
||||
# the function is never defined and fish's stock history behavior applies.
|
||||
if __fish_config_op_enabled __fish_config_op_aliases
|
||||
if __fish_config_op_enabled (status basename) aliases-tricks
|
||||
function history
|
||||
builtin history --show-time='%F %T '
|
||||
end
|
||||
@@ -123,7 +128,7 @@ alias .....='cd ../../../..'
|
||||
alias ......='cd ../../../../..'
|
||||
|
||||
# Silent flag injection into POSIX tools is opinionated (C1 aliasing)
|
||||
if __fish_config_op_enabled __fish_config_op_aliases
|
||||
if __fish_config_op_enabled (status basename) aliases-tricks
|
||||
# Tools & Core command color overrides
|
||||
# @category Shell Aliases
|
||||
# @desc dir --color=auto
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
# see: https://github.com/ik11235/wakatime.fish
|
||||
###
|
||||
|
||||
# COMPONENT
|
||||
# site wakatime-autoexec: autoexec/telemetry
|
||||
# site wakatime-hook: integrations/notifications
|
||||
|
||||
# Local modification: opinionated guard (AGENTS.md Task #3). WakaTime
|
||||
# reporting is classified under both C2 auto-execution and C4 integrations;
|
||||
# disabling either category skips registering the hook.
|
||||
__fish_config_op_enabled __fish_config_op_autoexec; or exit
|
||||
__fish_config_op_enabled __fish_config_op_integrations; or exit
|
||||
__fish_config_op_enabled (status basename) wakatime-autoexec; or exit
|
||||
__fish_config_op_enabled (status basename) wakatime-hook; or exit
|
||||
|
||||
function __register_wakatime_fish_before_exec -e fish_postexec
|
||||
if set -q FISH_WAKATIME_DISABLED
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
# bars are preserved, renders the captured animation to a clean static log
|
||||
# (via scripts/clean_progress_log.py), and prunes old logs.
|
||||
|
||||
# COMPONENT
|
||||
# site yay-autoexec: autoexec/pkg-wrappers
|
||||
# site yay-logging: logging/pkg-logs
|
||||
|
||||
# Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec).
|
||||
# Wrapper generation is also gated by C5 (Logging & Capture).
|
||||
__fish_config_op_enabled __fish_config_op_autoexec; or return
|
||||
__fish_config_op_enabled (status basename) yay-autoexec; or return
|
||||
|
||||
# C5 — Logging & Capture: remove generated wrapper and skip when logging is off
|
||||
if not __fish_config_op_enabled __fish_config_op_logging
|
||||
if not __fish_config_op_enabled (status basename) yay-logging
|
||||
if test -f "$HOME/.local/bin/yay"
|
||||
and grep -q "# yay-wrapper-version:" "$HOME/.local/bin/yay" 2>/dev/null
|
||||
rm -f "$HOME/.local/bin/yay"
|
||||
|
||||
+4
-1
@@ -3,6 +3,9 @@
|
||||
# Adapted from icezyclon/zoxide.fish (MIT)
|
||||
# Heavily customized for Fish 4.x compatibility and performance
|
||||
|
||||
# COMPONENT
|
||||
# aliases/filesystem
|
||||
|
||||
if status is-interactive
|
||||
|
||||
if type -q zoxide
|
||||
@@ -65,7 +68,7 @@ if status is-interactive
|
||||
|
||||
# Shadowing cd with zoxide is opinionated (C1 aliasing); z and zi
|
||||
# remain available either way.
|
||||
if __fish_config_op_enabled __fish_config_op_aliases
|
||||
if __fish_config_op_enabled (status basename)
|
||||
alias cd=z
|
||||
end
|
||||
|
||||
|
||||
+30
-10
@@ -7,7 +7,7 @@
|
||||
|
||||
# ───────────────────── Opinionated component guards ─────────────────────
|
||||
# Opinionated components (AGENTS.md Task #3) are wrapped in
|
||||
# __fish_config_op_enabled <category> guards throughout this file and conf.d/.
|
||||
# __fish_config_op_enabled <identity> [<site>] guards throughout this file and conf.d/.
|
||||
# The helper always evaluates the master switch __fish_config_opinionated
|
||||
# first (falsy disables everything), then the per-category opt-out variable:
|
||||
# __fish_config_op_aliases C1 — command shadows / flag injection
|
||||
@@ -21,12 +21,24 @@
|
||||
# C5 is the one exception: it defaults to disabled and needs an explicit
|
||||
# truthy value — set -U __fish_config_op_logging on
|
||||
|
||||
# COMPONENT
|
||||
# site cachyos-tricks: overrides/environment
|
||||
# site cachyos-strip-aliases: aliases/filesystem
|
||||
# site cachyos-strip-overrides: overrides/key-bindings
|
||||
# site privacy: overrides/privacy
|
||||
# site pager-editor-gpg: overrides/environment
|
||||
# site exit-wiring: overrides/key-bindings
|
||||
# site path-setup: overrides/environment
|
||||
# site cdpath: overrides/environment
|
||||
# site vi-mode: overrides/key-bindings
|
||||
# site greeting-stamp: greeting/greeting-message
|
||||
|
||||
# ──────────────────────── Source CachyOS configs ────────────────────────
|
||||
if test -f /usr/share/cachyos-fish-config/cachyos-config.fish
|
||||
source /usr/share/cachyos-fish-config/cachyos-config.fish
|
||||
# Surgically overriding the distro config is opinionated (C3 overrides):
|
||||
# skip it entirely when overrides are disabled, keeping CachyOS defaults.
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) cachyos-tricks
|
||||
# Source our tricks over the cachyOS config
|
||||
test -f "$__fish_config_dir/conf.d/tricks.fish"
|
||||
and source "$__fish_config_dir/conf.d/tricks.fish"
|
||||
@@ -41,7 +53,7 @@ if test -f /usr/share/cachyos-fish-config/cachyos-config.fish
|
||||
# The distro config ships opinionated pieces of its own (it is the origin
|
||||
# of tricks.fish); strip them when the matching category is disabled so
|
||||
# the guards hold on CachyOS systems too.
|
||||
if not __fish_config_op_enabled __fish_config_op_aliases
|
||||
if not __fish_config_op_enabled (status basename) cachyos-strip-aliases
|
||||
for _fname in grep fgrep egrep dir vdir wget
|
||||
functions -q $_fname; and functions --erase $_fname
|
||||
end
|
||||
@@ -53,7 +65,7 @@ if test -f /usr/share/cachyos-fish-config/cachyos-config.fish
|
||||
and source $__fish_data_dir/functions/$_fname.fish
|
||||
end
|
||||
end
|
||||
if not __fish_config_op_enabled __fish_config_op_overrides
|
||||
if not __fish_config_op_enabled (status basename) cachyos-strip-overrides
|
||||
for _fname in __history_previous_command __history_previous_command_arguments
|
||||
functions -q $_fname; and functions --erase $_fname
|
||||
end
|
||||
@@ -93,9 +105,17 @@ set -q NVIDIA_SETTINGS_RW_CONFIG_FILE; or set -gx NVIDIA_SETTINGS_RW_CONFIG_FILE
|
||||
set -q CODEIUM_HOME; or set -gx CODEIUM_HOME "$XDG_CONFIG_HOME/codeium"
|
||||
set -q WORDLIST; or set -gx WORDLIST "$XDG_CONFIG_HOME/hunspell_en_US"
|
||||
|
||||
# ───────────────────────── Privacy variables ────────────────────────────
|
||||
# Global telemetry opt-out variables (C3 overrides: privacy)
|
||||
# Various CLI tools, runtimes, and AI-agent tools respect these variables.
|
||||
if __fish_config_op_enabled (status basename) privacy
|
||||
set -gx DO_NOT_TRACK 1
|
||||
set -gx DISABLE_TELEMETRY 1
|
||||
end
|
||||
|
||||
# ─────────────────────────── Pager variables ────────────────────────────
|
||||
# Overriding $PAGER, $EDITOR, and $GPG_TTY is opinionated (C3 overrides)
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) pager-editor-gpg
|
||||
if type -q ov
|
||||
set -gx PAGER ov
|
||||
else if type -q less
|
||||
@@ -136,7 +156,7 @@ and set -gx SCROLLBACK_HISTORY_MAX_FILES $__fish_scrollback_history_max_files
|
||||
# Wire up a clean exit function that won't fire on background subshells
|
||||
# Replacing the exit builtin is opinionated (C3 overrides); smart_exit also
|
||||
# guards itself so a live toggle takes effect without restarting the shell.
|
||||
if status is-interactive; and __fish_config_op_enabled __fish_config_op_overrides
|
||||
if status is-interactive; and __fish_config_op_enabled (status basename) exit-wiring
|
||||
function exit --description 'Safe interactive exit'
|
||||
# If the smart_exit file exists in our function path, invoke it explicitly
|
||||
if functions -q smart_exit
|
||||
@@ -153,7 +173,7 @@ end
|
||||
# the cargo bin directory is moved to the end of the PATH, which can help avoid conflicts
|
||||
# with system-installed Rust tools while still allowing user-installed cargo binaries to be found.
|
||||
# PATH setup is opinionated (C3 overrides)
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) path-setup
|
||||
fish_add_path $HOME/.local/bin # Standard user-local executables (XDG spec)
|
||||
fish_add_path $HOME/.local/share/../bin # Alternative/legacy path for local user binaries
|
||||
fish_add_path $HOME/Applications # User-installed applications and standalone apps
|
||||
@@ -176,7 +196,7 @@ end
|
||||
# so if you have a directory named 'myproject' in the current directory,
|
||||
# running 'cd myproject' will take you there instead of $HOME/projects/myproject.
|
||||
# CDPATH injection is opinionated (C3 overrides)
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) cdpath
|
||||
set -gx CDPATH . $HOME/projects $HOME
|
||||
end
|
||||
|
||||
@@ -191,7 +211,7 @@ if status is-interactive
|
||||
# This is optional but can improve the user experience for those who prefer Vi-style key bindings.
|
||||
# Global Vi mode is opinionated (C3 overrides); without it fish keeps its
|
||||
# default Emacs-style bindings.
|
||||
if __fish_config_op_enabled __fish_config_op_overrides
|
||||
if __fish_config_op_enabled (status basename) vi-mode
|
||||
set -g fish_key_bindings fish_vi_key_bindings
|
||||
end
|
||||
|
||||
@@ -251,7 +271,7 @@ if status is-interactive
|
||||
# function that distro configs set (e.g., CachyOS defines it as fastfetch).
|
||||
# This runs last inside the interactive block so our empty definition wins
|
||||
# over whatever cachyos-config.fish or vendor conf.d installed.
|
||||
if not __fish_config_op_enabled __fish_config_op_greeting
|
||||
if not __fish_config_op_enabled (status basename) greeting-stamp
|
||||
function fish_greeting
|
||||
end
|
||||
end
|
||||
|
||||
+151
-7
@@ -8,19 +8,30 @@
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import codespans
|
||||
import manualtools as mt
|
||||
import generate_component_registry
|
||||
|
||||
DOCS = Path(__file__).parent
|
||||
MANUAL = DOCS / "manual"
|
||||
FUNCTIONS = DOCS.parent / "functions"
|
||||
COMPLETIONS = DOCS.parent / "completions"
|
||||
README = DOCS.parent / "README.md"
|
||||
REPO_BLOB_BASE = "https://git.rootiest.dev/rootiest/fish-config/src/branch/main/"
|
||||
SLUG_DIR = "reference"
|
||||
|
||||
# File-tree branches whose real directory contents get listed inline on the
|
||||
# Starlight site (never in the plain-text manual/man page, since only the
|
||||
# --site path runs box-drawing trees through _as_file_tree).
|
||||
EXPANDABLE_TREE_DIRS = {"functions/": FUNCTIONS, "completions/": COMPLETIONS}
|
||||
|
||||
|
||||
def _is_function_page(path: Path, root: Path) -> bool:
|
||||
"""True for a Section 5 category stub (not its index)."""
|
||||
@@ -71,10 +82,104 @@ def _with_abbreviations(body: str, abbrs: dict[str, list[dict]]) -> str:
|
||||
for cat, table in rendered_abbrs.items():
|
||||
placeholder = f"<!-- GENERATED: {cat} -->"
|
||||
body = body.replace(placeholder, table)
|
||||
|
||||
|
||||
return body
|
||||
|
||||
|
||||
TOC_PLACEHOLDER = "<!-- GENERATED: toc -->"
|
||||
TOC_SKIP_STEMS = {"index"}
|
||||
|
||||
|
||||
def _build_toc(root: Path) -> str:
|
||||
"""Render the section list for docs/manual/00-table-of-contents.md.
|
||||
|
||||
Walks the same tree `build_concat` does, so it can never drift from the
|
||||
man page's actual section order. `index.md` and the `00-*` front-matter
|
||||
pages (Name, Synopsis, this page) sit before section 1 and are excluded,
|
||||
same as any `man: false` page (currently only 404).
|
||||
"""
|
||||
lines: list[str] = []
|
||||
n = 0
|
||||
for path, depth in mt.walk(root):
|
||||
rel = path.relative_to(root)
|
||||
# len(rel.parts) == 1 means a root-level file, not a directory's own
|
||||
# index page (e.g. 04-abbreviations/index.md), which must keep its
|
||||
# own numbered line even though its stem is also "index".
|
||||
if depth == 0 and len(rel.parts) == 1 and (rel.stem in TOC_SKIP_STEMS or rel.stem.startswith("00-")):
|
||||
continue
|
||||
fm, _ = mt.parse(path)
|
||||
if not fm.get("man", True):
|
||||
continue
|
||||
title = fm.get("title", path.stem)
|
||||
if depth == 0:
|
||||
n += 1
|
||||
lines.append(f" {n}. {title}")
|
||||
else:
|
||||
lines.append(f" - {title}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _with_toc(body: str, root: Path) -> str:
|
||||
"""Inject the `<!-- GENERATED: toc -->` placeholder with the built section list."""
|
||||
return body.replace(TOC_PLACEHOLDER, _build_toc(root)) if TOC_PLACEHOLDER in body else body
|
||||
|
||||
|
||||
README_LINK_RE = re.compile(r"\]\((?!https?://|#|mailto:)([^)]+)\)")
|
||||
README_FENCE_RE = re.compile(r"```[^\n]*\n(.*?)```\n?", re.DOTALL)
|
||||
README_PLACEHOLDER_RE = re.compile(r"<!-- README: (.+?) -->")
|
||||
|
||||
|
||||
def _rewrite_repo_links(text: str) -> str:
|
||||
"""Point a README-relative link (`CONTRIBUTING.md`, `LICENSE`) at its file on Gitea."""
|
||||
return README_LINK_RE.sub(lambda m: f"]({REPO_BLOB_BASE}{m.group(1)})", text)
|
||||
|
||||
|
||||
def _defence(text: str) -> str:
|
||||
"""Rewind a README fenced code block into the manual's indented-block form.
|
||||
|
||||
`docs/manual` bodies are authored man-page style (4-space indent), never
|
||||
fenced: `codespans`/pandoc pair backticks per line, and a fence line's
|
||||
triple backtick throws that count off. README.md is ordinary markdown
|
||||
and fences its examples, so an injected section is converted back.
|
||||
"""
|
||||
def repl(m: re.Match) -> str:
|
||||
block = m.group(1).rstrip("\n")
|
||||
return "\n".join(" " + line for line in block.split("\n")) + "\n"
|
||||
|
||||
return README_FENCE_RE.sub(repl, text)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _readme_sections() -> dict[str, str]:
|
||||
"""Split README.md into {H2 heading: body}, links rewritten to point at the repo.
|
||||
|
||||
Lets a manual stub pull one README section in verbatim via a
|
||||
`<!-- README: <Heading> -->` placeholder, so the README stays the single
|
||||
source of truth for sections that describe the repo itself rather than
|
||||
the shell config (Testing, Contributing, Attribution, License).
|
||||
"""
|
||||
sections: dict[str, str] = {}
|
||||
heading: str | None = None
|
||||
lines: list[str] = []
|
||||
for line in README.read_text().split("\n") + ["## "]:
|
||||
if line.startswith("## "):
|
||||
if heading is not None:
|
||||
body = "\n".join(lines).strip()
|
||||
if body.endswith("---"):
|
||||
body = body[:-3].rstrip()
|
||||
sections[heading] = _defence(_rewrite_repo_links(body))
|
||||
heading = line[3:].strip()
|
||||
lines = []
|
||||
else:
|
||||
lines.append(line)
|
||||
return sections
|
||||
|
||||
|
||||
def _with_readme(body: str) -> str:
|
||||
"""Inject `<!-- README: Heading -->` placeholders with that README section's body."""
|
||||
return README_PLACEHOLDER_RE.sub(lambda m: _readme_sections().get(m.group(1), ""), body)
|
||||
|
||||
|
||||
def build_concat(root: Path) -> str:
|
||||
"""Concatenate the manual into one ordered markdown document.
|
||||
|
||||
@@ -86,6 +191,16 @@ def build_concat(root: Path) -> str:
|
||||
with no frontmatter fences and no Astro-visible frontmatter key. When
|
||||
present, its contents are re-emitted byte-for-byte as the leading
|
||||
`---`-fenced block, ahead of every heading.
|
||||
|
||||
Bodies go through `codespans` here so a token is typeset the same way
|
||||
in every output: `tmux` and `local.fish` are wrapped on the site by
|
||||
that pass, and without it the man page marked only what the SSOT
|
||||
happened to backtick by hand. Section 5 is unaffected -- its entries
|
||||
arrive as indented verbatim blocks, which `codespans` leaves alone and
|
||||
pandoc already sets in a monospace font.
|
||||
|
||||
Only bodies are passed: the pandoc metadata block above is not prose
|
||||
and must survive byte-for-byte.
|
||||
"""
|
||||
entries = build_entries(mt.parse_functions(FUNCTIONS))
|
||||
chunks: list[str] = []
|
||||
@@ -104,10 +219,13 @@ def build_concat(root: Path) -> str:
|
||||
elif "04-abbreviations" in path.parts:
|
||||
abbrs = mt.parse_abbreviations(DOCS.parent / "conf.d")
|
||||
body = _with_abbreviations(body, abbrs)
|
||||
body = _with_readme(body)
|
||||
body = _with_toc(body, root)
|
||||
if body:
|
||||
body = re.sub(r"<LinkButton.*?</LinkButton>\n*", "", body, flags=re.DOTALL)
|
||||
body = re.sub(r"<CardGrid.*?</CardGrid>\n*", "", body, flags=re.DOTALL)
|
||||
body = re.sub(r"\[([^\]]+)\]\(/[^)]+\)", r"\1", body)
|
||||
body = codespans.add_code_spans(body, _code_vocabulary())
|
||||
chunks.append(mt.shift_headings(body, depth))
|
||||
return "\n\n".join(chunks) + "\n"
|
||||
|
||||
@@ -188,7 +306,7 @@ SHELL_HEADS = frozenset(
|
||||
if jobs kitty ls man math mkdir mv nvim npm pacman paru pip pip3 pkg printf
|
||||
python python3 rm set shutdown source string sudo switch systemctl test time
|
||||
tmux touch trash type wget wezterm while yay zellij zypper
|
||||
fish_default_key_bindings fish_vi_key_bindings
|
||||
fish_default_key_bindings fish_vi_key_bindings fish_config
|
||||
""".split()
|
||||
)
|
||||
|
||||
@@ -365,6 +483,11 @@ def _as_file_tree(para: list[str]) -> str | None:
|
||||
depth = len(prefix.replace('\t', ' ')) // 4
|
||||
indent = " " * (depth + 1)
|
||||
out.append(f"{indent}- {name} {desc}".rstrip())
|
||||
expand_dir = EXPANDABLE_TREE_DIRS.get(name)
|
||||
if expand_dir is not None and expand_dir.is_dir():
|
||||
child_indent = " " * (depth + 2)
|
||||
for entry in sorted((p.name for p in expand_dir.iterdir() if p.is_file()), key=str.lower):
|
||||
out.append(f"{child_indent}- {entry}")
|
||||
out.append("</FileTree>")
|
||||
return "\n".join(out)
|
||||
|
||||
@@ -393,7 +516,13 @@ def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str:
|
||||
table = _as_ruled_table(para) or _as_table(para) or _as_file_tree(para)
|
||||
if table is not None:
|
||||
return table
|
||||
return "\n".join(INDENT + line for line in para)
|
||||
# MDX (used for any page that also carries an <Aside> or <FileTree>)
|
||||
# has no indented-code-block syntax — a plain 4-space-indented block
|
||||
# silently renders as flowed paragraph text there, collapsing every
|
||||
# line break. A fenced block works in both MDX and plain Markdown, so
|
||||
# it's the only fallback that's safe regardless of which one a given
|
||||
# page ends up promoted to.
|
||||
return "```text\n" + "\n".join(para) + "\n```"
|
||||
|
||||
|
||||
def _prettify_block(block: list[str], entry_name: str | None) -> str:
|
||||
@@ -466,12 +595,24 @@ def _as_aside(para: list[str]) -> str | None:
|
||||
return f"<Aside {attrs}>\n{body}\n</Aside>"
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _code_vocabulary() -> codespans.Vocabulary:
|
||||
"""The command names codespans may wrap, read from the repo once."""
|
||||
return codespans.vocabulary(DOCS.parent)
|
||||
|
||||
|
||||
def prettify(body: str, entry_name: str | None = None) -> str:
|
||||
"""Rewrite a body's indented code blocks and labeled asides for the website.
|
||||
|
||||
Site-only: the man page and `config-help` keep reading the untouched
|
||||
SSOT, where the indented form and the `LABEL:` text are exactly what
|
||||
pandoc/`config-help` want.
|
||||
The block and aside rewrites are site-only: the man page and
|
||||
`config-help` keep reading the untouched SSOT, where the indented form
|
||||
and the `LABEL:` text are exactly what pandoc/`config-help` want.
|
||||
|
||||
The inline code spans added last are not site-only. `-a/--all` and
|
||||
`__fish_config_op_aliases` are authored bare so the `functions/*.fish`
|
||||
headers stay readable as plain text, and the backticks every output
|
||||
wants are put on here rather than in the SSOT -- `build_concat()` runs
|
||||
the same pass for the man page and `config-help`.
|
||||
"""
|
||||
out: list[str] = []
|
||||
block: list[str] = []
|
||||
@@ -510,7 +651,7 @@ def prettify(body: str, entry_name: str | None = None) -> str:
|
||||
while block and not block[-1].strip():
|
||||
block.pop()
|
||||
out.append(_prettify_block(block, entry_name))
|
||||
return "\n".join(out)
|
||||
return codespans.add_code_spans("\n".join(out), _code_vocabulary())
|
||||
|
||||
|
||||
ENTRY_HEADS = {
|
||||
@@ -706,6 +847,7 @@ def build_site(root: Path, out: Path) -> list[dict]:
|
||||
if "04-abbreviations" in path.parts:
|
||||
abbrs = mt.parse_abbreviations(DOCS.parent / "conf.d")
|
||||
body = _with_abbreviations(body, abbrs)
|
||||
body = _with_readme(body)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
body = _inject_subheading_cards(body)
|
||||
_write_prettified(target, _page_fm(fm), prettify(body))
|
||||
@@ -840,6 +982,8 @@ def main() -> int:
|
||||
if not (args.concat or args.site):
|
||||
ap.error("nothing to do: pass --concat and/or --site")
|
||||
|
||||
generate_component_registry.main()
|
||||
|
||||
if args.site:
|
||||
src = DOCS / "site" / "src"
|
||||
out = src / "content" / "docs"
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Wrap code-shaped tokens in inline code spans for the Starlight site.
|
||||
|
||||
Section 5 is generated from the `functions/*.fish` comment headers, which
|
||||
are read as plain text by `config-help`, by `funcsave`, and by anyone
|
||||
opening the source file. Backticks there would be noise, so the headers
|
||||
are authored without them -- and the site inherited that, rendering
|
||||
`-a/--all` and `__fish_config_op_aliases` as ordinary prose.
|
||||
|
||||
This module closes that gap at render time: it walks the markdown a page
|
||||
is about to be written as, finds the tokens whose shape only ever means
|
||||
"code" (flags, `$vars`, snake_case identifiers, paths, key chords, known
|
||||
command names) and wraps each one in a code span. The SSOT is never
|
||||
touched, so the man page and `config-help` keep the plain-text form.
|
||||
|
||||
Everything here is conservative by construction: leaving a token alone is
|
||||
always safe and wrapping the wrong one is not, so every rule bails out the
|
||||
moment it is unsure. The regions that must never be rewritten -- fenced
|
||||
blocks, indented code blocks, existing code spans, link targets, URLs, JSX
|
||||
attributes, `<FileTree>` bodies, headings -- are recognised first and
|
||||
passed through.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FENCE_RE = re.compile(r"^\s*(```|~~~)")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vocabulary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Commands a reader expects to see typeset as code. This is the *wide* list:
|
||||
# it decides that a table column holds command lines (see _command_columns),
|
||||
# where position already proves the name is a command. Wrapping a bare
|
||||
# mention in running prose is gated on the strict tier below.
|
||||
STANDARD_COMMANDS = frozenset(
|
||||
"""
|
||||
apk apt awk basename bash bat bg bind brew builtin cargo cat cd chmod
|
||||
chown chsh cmp column cp curl cut date dd delta df diff dig dirname dnf
|
||||
docker dpkg du echo emacs emerge env eza exec exit export fastfetch fd
|
||||
fdisk fg fgrep file find fisher flatpak fzf gh git grep gzip head help
|
||||
hexdump host hostname id ifconfig install ip jq jobs journalctl kill
|
||||
killall kitten kitty last less ln locale ls lsblk lsd lsof make man
|
||||
micro mkdir more mount mpv mv nano nc neofetch neovim netstat nix nl
|
||||
nohup npm nproc nvim od open openssl pacman paru paste pgrep ping pip
|
||||
pip3 pkill pr printf ps pwd python python3 readlink realpath rg rm rmdir
|
||||
rpm rsync scp sed seq sh shutdown sleep snap sort source ssh stat
|
||||
strings su sudo sync systemctl tac tail tar tee test time tldr tmux
|
||||
touch tr trash tree type udisksctl umount uname uniq unzip uv vdir vi
|
||||
vim vlc wait wc wezterm wget which who whoami wl-copy wl-paste xargs
|
||||
xbps-install xclip xdg-open xsel yay yum yt-dlp zellij zip zoxide zsh
|
||||
zypper
|
||||
abbr alias and argparse begin block break case command complete contains
|
||||
continue count else emit end eval false for function funcsave functions
|
||||
history if math not or random read return set set_color status string
|
||||
switch true while
|
||||
""".split()
|
||||
)
|
||||
|
||||
# Names that also read as ordinary English (or as this manual's own prose)
|
||||
# often enough that a bare mention is not evidence of a command. They still
|
||||
# take part in command-line and list detection, where position disambiguates
|
||||
# -- they just never get wrapped on their own.
|
||||
AMBIGUOUS_COMMANDS = frozenset(
|
||||
"""
|
||||
abbr alias all and at basename bat begin bg bind block branch break case
|
||||
cat cd cheat cleanup clone column command complete contains continue copy
|
||||
count cut date dd df dir dirname do docker du duf dust echo edit else emit
|
||||
end env eval exec exit export false fc fg file find fish for free function
|
||||
functions git go head help hist history host hostname id if in install ip
|
||||
jobs join key kill last less link list ln lock locale log logs look ls make
|
||||
man math micro more mount mv next no not note od open or ov p page paste
|
||||
pkg poke ports pr ps pwd random read real replay return rm run screen sed
|
||||
search seq set sh show sleep sort source spark split stat status string
|
||||
strings su switch sync tab tac tail tar tee test time top touch tr trash
|
||||
tree true type uniq upgrade view vi wait watch wc which while who write
|
||||
yes zip
|
||||
builtin fast function vdir
|
||||
""".split()
|
||||
)
|
||||
|
||||
# Extensions that make a bare `name.ext` token unambiguously a filename.
|
||||
PATH_EXTENSIONS = (
|
||||
"fish md mdx json jsonc toml yml yaml py sh bash zsh lua conf cfg ini "
|
||||
"txt log list service socket desktop css scss ts js astro nix rasi 1"
|
||||
).split()
|
||||
|
||||
# English function words. A candidate command line containing one is prose.
|
||||
STOPWORDS = frozenset(
|
||||
"""
|
||||
a an the this that these those it its is are was were be been being am
|
||||
to of in into on at by for from with without within about across after
|
||||
before during over under again then than so such as and or but nor if
|
||||
when while where which who whom whose why how all any both each few more
|
||||
most other some only own same too very can will just should now via per
|
||||
also either neither every no not
|
||||
""".split()
|
||||
)
|
||||
|
||||
_CATALOG_ARRAY_RE = re.compile(
|
||||
r"set\s+-g\s+_fdc_(?:bins|cargo|pm)\s+((?:[^\n]*\\\n)*[^\n]*)"
|
||||
)
|
||||
|
||||
|
||||
def dependency_names(repo: Path) -> set[str]:
|
||||
"""Every tool name in the `fish-deps` catalog (`_fdc_*` arrays).
|
||||
|
||||
`functions/_fish_deps_catalog.fish` is this repo's dependency database;
|
||||
reading it here means a tool added there starts rendering as code with
|
||||
no second list to keep in sync.
|
||||
"""
|
||||
path = repo / "functions" / "_fish_deps_catalog.fish"
|
||||
if not path.exists():
|
||||
return set()
|
||||
names: set[str] = set()
|
||||
for m in _CATALOG_ARRAY_RE.finditer(path.read_text(encoding="utf-8")):
|
||||
for token in m.group(1).replace("\\\n", " ").split():
|
||||
token = token.strip("\"'")
|
||||
if token and re.fullmatch(r"[\w.@+-]+", token):
|
||||
names.add(token)
|
||||
return names
|
||||
|
||||
|
||||
def function_names(repo: Path) -> set[str]:
|
||||
"""Public function names, from the `functions/` directory listing.
|
||||
|
||||
Underscore-prefixed internals are skipped only because the snake_case
|
||||
rule already covers them, and covers them everywhere -- including the
|
||||
ones that have no file of their own.
|
||||
"""
|
||||
directory = repo / "functions"
|
||||
if not directory.is_dir():
|
||||
return set()
|
||||
return {p.stem for p in directory.glob("*.fish") if not p.stem.startswith("_")}
|
||||
|
||||
|
||||
class Vocabulary:
|
||||
"""The command names the rules recognise, in two tiers.
|
||||
|
||||
`full` is every name we know of, used where position already proves a
|
||||
token is a command (a command-line table cell, an arrow chain, a
|
||||
comma-separated run). `strict` is the subset safe to wrap on sight in
|
||||
running prose: `zoxide` yes, `find` no.
|
||||
"""
|
||||
|
||||
__slots__ = ("full", "strict")
|
||||
|
||||
def __init__(self, names: set[str]):
|
||||
# `and`, `or`, `not`, `if` … are fish builtins, but as vocabulary
|
||||
# entries they turn every conjunction into a command name and break
|
||||
# list and command-line detection. They are never worth wrapping.
|
||||
self.full = frozenset(names) - STOPWORDS
|
||||
self.strict = frozenset(
|
||||
n
|
||||
for n in names
|
||||
if n not in AMBIGUOUS_COMMANDS
|
||||
and (len(n) >= 3 or any(c.isdigit() for c in n))
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, Vocabulary)
|
||||
and self.full == other.full
|
||||
and self.strict == other.strict
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.full, self.strict))
|
||||
|
||||
|
||||
def vocabulary(repo: Path) -> Vocabulary:
|
||||
"""Build the command vocabulary from the repo plus the standard list."""
|
||||
return Vocabulary(
|
||||
set(STANDARD_COMMANDS) | dependency_names(repo) | function_names(repo)
|
||||
)
|
||||
|
||||
|
||||
EMPTY_VOCABULARY = Vocabulary(set())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token grammar
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# A token may not start inside a word, a path, a code span, a history
|
||||
# expansion, or a hyphenated compound: `` `zoxide` ``-backed must not see
|
||||
# `-backed` as a flag, `and/or` must not see `/or` as a path, and `!-N` must
|
||||
# not see `-N` as one either.
|
||||
BEFORE = r"(?<![\w`$/\\~.=+!-])"
|
||||
# Ruling out a trailing `/` keeps a partially-recognised slash run
|
||||
# (`grep/cp/mv/wget`, where only `grep` is in the vocabulary) from being
|
||||
# wrapped one limb at a time.
|
||||
AFTER = r"(?![\w`/])"
|
||||
|
||||
_SEG = r"[\w.@+-]+"
|
||||
# The last segment of a path may not end in `.`, so a sentence-final full
|
||||
# stop stays outside the span. A segment that is nothing but dots (`..`,
|
||||
# `...`) is the exception: there the dots are the segment.
|
||||
_LAST = r"(?:[\w.@+-]*[\w@+-]|\.+)"
|
||||
_EXT = "|".join(PATH_EXTENSIONS)
|
||||
|
||||
# Key chords: `Ctrl-R`, `Ctrl+Alt+F`. Both separators appear in the manual.
|
||||
_MODIFIER = r"(?:Ctrl|Alt|Shift|Super|Meta|Cmd|Opt)"
|
||||
_KEY = (
|
||||
r"(?:F\d{1,2}|Tab|Enter|Return|Space|Esc|Escape|Backspace|Delete|Insert"
|
||||
r"|Home|End|Up|Down|Left|Right|PgUp|PgDn|[A-Za-z0-9])"
|
||||
)
|
||||
KEYBIND = rf"{_MODIFIER}(?:[+-]{_MODIFIER})*[+-]{_KEY}"
|
||||
|
||||
# `$EDITOR`, `${var}`, `$XDG_CONFIG_HOME/aichat/roles/cli.md`.
|
||||
VAR = rf"\$\{{?[A-Za-z_]\w*\}}?(?:(?:/{_SEG})*/{_LAST})?"
|
||||
|
||||
PATH = (
|
||||
rf"(?:~|\.{{1,2}})/(?:{_SEG}/)*(?:{_LAST})?" # ~/… ./… ../…
|
||||
rf"|/(?:{_SEG}/)+(?:{_LAST})?" # /etc/sudoers.d/nofail-toggle
|
||||
rf"|(?:{_SEG}/)+[\w@+-][\w.@+-]*\.(?:{_EXT})" # conf.d/abbr.fish
|
||||
rf"|[\w@+-][\w.@+-]*\.(?:{_EXT})" # config.fish
|
||||
rf"|(?:{_SEG}\.)+{_SEG}/" # conf.d/
|
||||
)
|
||||
|
||||
# `-a`, `--dry-run`, `--color=auto`. A bare `--` (this manual's ASCII em
|
||||
# dash) never matches: a letter has to follow. A single-hyphen flag is
|
||||
# capped at five characters and may not contain a hyphen, so a hyphenated
|
||||
# compound continued across a conjunction ("filesystem-inspection and
|
||||
# -modification") is not mistaken for one.
|
||||
FLAG = r"--[A-Za-z][\w-]*(?:=[\w.,:/@+-]+)?|-[A-Za-z][A-Za-z0-9]{0,4}(?:=[\w.,:/@+-]+)?"
|
||||
|
||||
# `XDG_CONFIG_HOME`, `NO_TMUX=1`. An underscore is required, so ordinary
|
||||
# acronyms (`URL`, `AGPL`, `TCP`) are never touched.
|
||||
ENVVAR = r"[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+(?:=[\w.,:/@+-]+)?"
|
||||
|
||||
# snake_case: `__fish_config_op_aliases`, `_fdc_bins`, `fish_greeting`,
|
||||
# `prompt_pwd`, `expand_bang_*`. An internal underscore is required, which
|
||||
# is also what keeps `_emphasised_` markdown out of the match.
|
||||
IDENT = r"_{0,2}[a-z][a-z0-9]*(?:_(?:[a-z0-9]+|\*))+"
|
||||
|
||||
# Regions that are already code, or are markup rather than prose. `url`
|
||||
# also covers `git@host:owner/repo.git` and `ssh://…`, whose scheme would
|
||||
# otherwise be read as a bare command name.
|
||||
PROTECTED = (
|
||||
r"(?P<code>``+.+?``+|`[^`\n]*`)"
|
||||
r"|(?P<link>\[[^\]\n]*\]\([^)\n]*\))"
|
||||
r"|(?P<url>[A-Za-z][\w+.-]*://\S+|[\w.-]+@[\w.-]+(?::\S+)?)"
|
||||
r"|(?P<tag></?[A-Za-z][^>\n]*?/?>)"
|
||||
)
|
||||
|
||||
ARROW = r"(?:->|→|=>)"
|
||||
# Shortest comma run that reads as a list of tools rather than as prose.
|
||||
MIN_RUN_NAMES = 3
|
||||
# `, and` must be tried before a bare `,` so the conjunction is a separator
|
||||
# and not an item.
|
||||
RUN_SPLIT = r",?\s+(?:and|or)\s+|,\s*"
|
||||
RUN_SPLIT_RE = re.compile(RUN_SPLIT)
|
||||
# A command name inside a chain is followed by `->`, so the usual "no
|
||||
# trailing hyphen" guard has to make room for exactly that.
|
||||
_CMD_END = r"(?!\w)(?!-(?!>))"
|
||||
|
||||
|
||||
def _alternation(names) -> str:
|
||||
"""Regex alternation over names, longest first so `rg` can't beat `rga`."""
|
||||
if not names:
|
||||
return r"(?!)"
|
||||
return "|".join(re.escape(n) for n in sorted(names, key=lambda s: (-len(s), s)))
|
||||
|
||||
|
||||
def _atom(vocab: Vocabulary) -> str:
|
||||
cmd = rf"(?:{_alternation(vocab.strict)})(?![\w-])"
|
||||
return rf"(?:{KEYBIND}|{VAR}|{PATH}|{FLAG}|{ENVVAR}|{IDENT}|{cmd})"
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=4)
|
||||
def _scanner(vocab: Vocabulary) -> re.Pattern:
|
||||
"""The single pass over a line: protected regions plus wrappable tokens."""
|
||||
full = rf"(?:{_alternation(vocab.full)})"
|
||||
chain_link = rf"(?:{full}{_CMD_END}|{VAR})"
|
||||
name = rf"{full}(?![\w-])"
|
||||
return re.compile(
|
||||
PROTECTED
|
||||
# `ls->eza, cat->bat`: a shadow chain. Position makes even an
|
||||
# ambiguous name unmistakably a command here.
|
||||
+ rf"|(?P<chain>{BEFORE}{chain_link}(?:\s*{ARROW}\s*{chain_link})+{AFTER})"
|
||||
# `cargo, starship, uv, zoxide`: a run of nothing but tool names.
|
||||
+ rf"|(?P<run>{BEFORE}{name}(?:,\s*{name})+"
|
||||
+ rf"(?:,?\s+(?:and|or)\s+{name})?{AFTER})"
|
||||
# `-a/--all`: slash-joined atoms, each wrapped on its own.
|
||||
+ rf"|(?P<group>{BEFORE}{_atom(vocab)}(?:/{_atom(vocab)})*{AFTER})"
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=4)
|
||||
def _atom_re(vocab: Vocabulary) -> re.Pattern:
|
||||
return re.compile(_atom(vocab))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Table cells that are whole command lines
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The abbreviation tables' second column is an expansion, not a sentence:
|
||||
# `sudo -s`, `cd ../..`, `journalctl -p 3 -xb`. Wrapping only the flag would
|
||||
# leave a bare `sudo` in front of a code span; the cell wants to be one span.
|
||||
#
|
||||
# Whether a column holds command lines is decided for the column as a whole
|
||||
# -- one cell is far too little evidence, as `zoxide frecency-based
|
||||
# navigation` (prose, in a column of prose) and `docker context ls` (a
|
||||
# command, in a column of commands) open identically.
|
||||
CELL_TOKEN_RE = re.compile(r"^[\w$~./=:;@+*?%'\"-]+$")
|
||||
CELL_NAME_RE = re.compile(r"^[a-z][\w.+-]*$")
|
||||
CELL_OPERATORS = frozenset((r"\|", "|", "&&", "||", ">", ">>", "<", ";"))
|
||||
MAX_CELL_TOKENS = 8
|
||||
COMMAND_COLUMN_RATIO = 0.7
|
||||
MIN_COMMAND_COLUMN_ROWS = 3
|
||||
|
||||
|
||||
def _cell_tokens(cell: str) -> list[str] | None:
|
||||
"""Tokenise a cell that could be a command line, or None if it can't be."""
|
||||
text = cell.strip()
|
||||
if not text or any(c in text for c in "`<([)]"):
|
||||
return None
|
||||
tokens = text.split()
|
||||
if not (1 <= len(tokens) <= MAX_CELL_TOKENS):
|
||||
return None
|
||||
for token in tokens:
|
||||
if token in CELL_OPERATORS:
|
||||
continue
|
||||
if not CELL_TOKEN_RE.match(token):
|
||||
return None
|
||||
if token[:1].isupper() or token.lower() in STOPWORDS:
|
||||
return None
|
||||
return tokens
|
||||
|
||||
|
||||
def _is_command_cell(cell: str, vocab: Vocabulary) -> bool:
|
||||
"""True when a cell in a command column really is one command line."""
|
||||
tokens = _cell_tokens(cell)
|
||||
if tokens is None:
|
||||
return False
|
||||
return tokens[0] in vocab.full or bool(CELL_NAME_RE.match(tokens[0]))
|
||||
|
||||
|
||||
def _opens_with_command(cell: str, vocab: Vocabulary) -> bool:
|
||||
"""The per-cell evidence the column vote is counted from."""
|
||||
tokens = _cell_tokens(cell)
|
||||
return tokens is not None and tokens[0] in vocab.full
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Line classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HEADING_RE = re.compile(r"^\s{0,3}#{1,6}\s")
|
||||
TABLE_ROW_RE = re.compile(r"^\s*\|.*\|\s*$")
|
||||
TABLE_RULE_RE = re.compile(r"^\s*\|[\s:|-]+\|\s*$")
|
||||
IMPORT_RE = re.compile(r"^\s*import\s")
|
||||
FILE_TREE_OPEN = "<FileTree"
|
||||
FILE_TREE_CLOSE = "</FileTree>"
|
||||
CELL_SPLIT_RE = re.compile(r"(?<!\\)\|")
|
||||
|
||||
# A four-space indent is this manual's code block. The site never sees one
|
||||
# -- prettify() has already turned it into a fence by the time this module
|
||||
# runs -- but build_concat() keeps the indented form, because that is what
|
||||
# pandoc and `config-help` want, and its contents are code that must not be
|
||||
# rewritten: the table of contents alone would otherwise have `ov`, `bat`,
|
||||
# `less` and `cat` wrapped inside a code block.
|
||||
INDENTED_CODE = " "
|
||||
|
||||
|
||||
def _skip_line(line: str) -> bool:
|
||||
"""True for a line that must be passed through untouched.
|
||||
|
||||
Headings are excluded because Starlight derives anchors -- and this
|
||||
pipeline derives `LinkCard` hrefs -- from their text. A line opening
|
||||
with `<` is component markup, whose attributes are JSX, not markdown.
|
||||
"""
|
||||
stripped = line.strip()
|
||||
return bool(
|
||||
not stripped
|
||||
or HEADING_RE.match(line)
|
||||
or IMPORT_RE.match(line)
|
||||
or stripped.startswith("<")
|
||||
or TABLE_RULE_RE.match(line)
|
||||
)
|
||||
|
||||
|
||||
def _row_cells(line: str) -> list[str]:
|
||||
return CELL_SPLIT_RE.split(line)
|
||||
|
||||
|
||||
def _command_columns(rows: list[str], vocab: Vocabulary) -> set[int]:
|
||||
"""Which column indices of one table hold command lines rather than prose."""
|
||||
votes: dict[int, list[int]] = {}
|
||||
for line in rows:
|
||||
if TABLE_RULE_RE.match(line):
|
||||
continue
|
||||
for index, cell in enumerate(_row_cells(line)):
|
||||
if not cell.strip() or "`" in cell:
|
||||
continue
|
||||
votes.setdefault(index, []).append(_opens_with_command(cell, vocab))
|
||||
return {
|
||||
index
|
||||
for index, seen in votes.items()
|
||||
if len(seen) >= MIN_COMMAND_COLUMN_ROWS
|
||||
and sum(seen) / len(seen) >= COMMAND_COLUMN_RATIO
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The pass
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Spans this pass creates are marked, not back-ticked, until the very end:
|
||||
# adjacent ones are merged (`eza` `-l` `-a` -> `eza -l -a`), and only spans
|
||||
# this pass created may take part in that.
|
||||
MARK = "\x01"
|
||||
MERGE_RE = re.compile(rf"{MARK} {MARK}")
|
||||
|
||||
|
||||
def _mark(text: str) -> str:
|
||||
return f"{MARK}{text}{MARK}"
|
||||
|
||||
|
||||
def _wrap_atoms(text: str, atom_re: re.Pattern) -> str:
|
||||
"""Mark each atom of a slash-joined group, keeping the separators.
|
||||
|
||||
Rescanning the group rather than capturing during the first match keeps
|
||||
the grammar readable; the round-trip check makes that shortcut safe --
|
||||
if the rescan disagrees with the original match, nothing is changed.
|
||||
"""
|
||||
wrapped = atom_re.sub(lambda m: _mark(m.group(0)), text)
|
||||
if wrapped.replace(MARK, "") != text:
|
||||
return text
|
||||
return wrapped
|
||||
|
||||
|
||||
def _wrap_split(text: str, separator: str) -> str:
|
||||
"""Mark each item of a separated run, keeping the separators."""
|
||||
parts = re.split(rf"({separator})", text)
|
||||
return "".join(p if i % 2 else _mark(p) for i, p in enumerate(parts))
|
||||
|
||||
|
||||
def _transform(text: str, scanner: re.Pattern, atom_re: re.Pattern, vocab: Vocabulary) -> str:
|
||||
def repl(m: re.Match) -> str:
|
||||
group = m.lastgroup
|
||||
if group == "chain":
|
||||
return _wrap_split(m.group(0), rf"\s*{ARROW}\s*")
|
||||
if group == "run":
|
||||
# A long run anchored by at least one unambiguous tool name is
|
||||
# a list of commands; two names, one of them a word like
|
||||
# `function`, is a sentence.
|
||||
names = RUN_SPLIT_RE.split(m.group(0))
|
||||
if len(names) < MIN_RUN_NAMES or not any(
|
||||
n in vocab.strict for n in names
|
||||
):
|
||||
# Not a list after all -- hand the text back to the
|
||||
# ordinary token rules rather than swallowing it.
|
||||
return _wrap_atoms(m.group(0), atom_re)
|
||||
return _wrap_split(m.group(0), RUN_SPLIT)
|
||||
if group == "group":
|
||||
return _wrap_atoms(m.group(0), atom_re)
|
||||
return m.group(0)
|
||||
|
||||
return scanner.sub(repl, text)
|
||||
|
||||
|
||||
def _transform_line(
|
||||
line: str,
|
||||
scanner: re.Pattern,
|
||||
atom_re: re.Pattern,
|
||||
vocab: Vocabulary,
|
||||
command_columns: set[int],
|
||||
) -> str:
|
||||
if not command_columns:
|
||||
return _transform(line, scanner, atom_re, vocab)
|
||||
|
||||
out = []
|
||||
for index, cell in enumerate(_row_cells(line)):
|
||||
if index in command_columns and _is_command_cell(cell, vocab):
|
||||
body = cell.strip()
|
||||
lead = cell[: len(cell) - len(cell.lstrip())]
|
||||
trail = cell[len(cell.rstrip()) :]
|
||||
out.append(f"{lead}{_mark(body)}{trail}")
|
||||
else:
|
||||
out.append(_transform(cell, scanner, atom_re, vocab))
|
||||
return "|".join(out)
|
||||
|
||||
|
||||
def _finish(line: str) -> str:
|
||||
"""Merge abutting new spans, then turn the marks into backticks."""
|
||||
return MERGE_RE.sub(" ", line).replace(MARK, "`")
|
||||
|
||||
|
||||
def add_code_spans(text: str, vocab: Vocabulary = EMPTY_VOCABULARY) -> str:
|
||||
"""Wrap code-shaped tokens in `text` in inline code spans.
|
||||
|
||||
`text` is a rendered page body (no frontmatter). Fenced blocks,
|
||||
indented code blocks, `<FileTree>` bodies, headings, component markup,
|
||||
existing code spans, link targets and URLs are left exactly as they
|
||||
are.
|
||||
"""
|
||||
scanner = _scanner(vocab)
|
||||
atom_re = _atom_re(vocab)
|
||||
|
||||
lines = text.split("\n")
|
||||
eligible = [False] * len(lines)
|
||||
in_fence = False
|
||||
in_tree = False
|
||||
for i, line in enumerate(lines):
|
||||
if FENCE_RE.match(line):
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
if in_fence:
|
||||
continue
|
||||
if FILE_TREE_OPEN in line:
|
||||
in_tree = True
|
||||
if in_tree:
|
||||
if FILE_TREE_CLOSE in line:
|
||||
in_tree = False
|
||||
continue
|
||||
if line.startswith(INDENTED_CODE):
|
||||
continue
|
||||
eligible[i] = not _skip_line(line)
|
||||
|
||||
# Command columns are a property of a whole table, so the contiguous
|
||||
# runs of table rows are resolved before any line is rewritten.
|
||||
columns: list[set[int]] = [set() for _ in lines]
|
||||
start = None
|
||||
for i, line in enumerate(lines + [""]):
|
||||
is_row = i < len(lines) and eligible[i] and TABLE_ROW_RE.match(line)
|
||||
if is_row and start is None:
|
||||
start = i
|
||||
elif not is_row and start is not None:
|
||||
found = _command_columns(lines[start:i], vocab)
|
||||
for j in range(start, i):
|
||||
columns[j] = found
|
||||
start = None
|
||||
|
||||
return "\n".join(
|
||||
_finish(_transform_line(line, scanner, atom_re, vocab, columns[i]))
|
||||
if eligible[i]
|
||||
else line
|
||||
for i, line in enumerate(lines)
|
||||
)
|
||||
+1017
-327
File diff suppressed because it is too large
Load Diff
+109
-10
@@ -27,6 +27,7 @@ scrollback=## Scrollback History
|
||||
__fish_scrollback_history_dir=## Scrollback History
|
||||
__fish_scrollback_history_max_files=## Scrollback History
|
||||
scrollback-dir=## Scrollback History
|
||||
misc-vars=## Other
|
||||
|
||||
# ── Section 2: PATH ───────────────────────────────────────────
|
||||
path=# 2. PATH SETUP
|
||||
@@ -34,7 +35,6 @@ path=# 2. PATH SETUP
|
||||
# ── Section 3: Key Bindings ───────────────────────────────────
|
||||
keybindings=# 3. KEY BINDINGS
|
||||
bindings=# 3. KEY BINDINGS
|
||||
key-bindings=# 3. KEY BINDINGS
|
||||
keys=# 3. KEY BINDINGS
|
||||
fzf-bindings=## FZF Bindings (bundled from PatrickF1/fzf.fish)
|
||||
fzf=## FZF Bindings (bundled from PatrickF1/fzf.fish)
|
||||
@@ -67,6 +67,8 @@ cat=### cat
|
||||
copy=### copy
|
||||
du=### du
|
||||
dusize=### dusize
|
||||
dirs=### lD
|
||||
lsdirs=### lD
|
||||
ls=### ls
|
||||
lsr=### lsr
|
||||
lss=### lss
|
||||
@@ -76,6 +78,7 @@ ltr=### ltr
|
||||
lx=### lx
|
||||
mkdir=### mkdir
|
||||
mkcd=### mkcd
|
||||
mv=### mv
|
||||
poke=### poke
|
||||
rm=### rm
|
||||
rg=### rg
|
||||
@@ -110,6 +113,7 @@ parur=### parur
|
||||
deps=## 5.6 Dependency Management
|
||||
fish-deps=### fish-deps
|
||||
check-deps=### check_fish_deps
|
||||
fzf-update=### fzf-update
|
||||
system=## 5.7 System and Monitoring
|
||||
top=### top
|
||||
swapstat=### swapstat
|
||||
@@ -125,19 +129,33 @@ split=### split
|
||||
spwin=### spwin
|
||||
detach=### detach
|
||||
bkg=### bkg
|
||||
fish-mode-prompt=### fish_mode_prompt
|
||||
fish-prompt=### fish_prompt
|
||||
fish-right-prompt=### fish_right_prompt
|
||||
right-prompt=### fish_right_prompt
|
||||
jobrunner=### jobrunner
|
||||
jr=### jr
|
||||
ssh=### ssh
|
||||
clipboard=## 5.9 Clipboard
|
||||
copy-fn=### y
|
||||
p=### p
|
||||
paste=### paste
|
||||
network=## 5.10 Network
|
||||
fast=### fast
|
||||
fast-cli=### fast-cli
|
||||
speedtest=### fast-cli
|
||||
gip=### gip
|
||||
gip4=### gip4
|
||||
ipv4=### gip4
|
||||
gip6=### gip6
|
||||
ipv6=### gip6
|
||||
ping=### ping
|
||||
qr=### qr
|
||||
logging=## 5.11 Pager and Logging
|
||||
logs=### logs
|
||||
smart-exit=### smart_exit
|
||||
sponge-filter-secrets=### sponge_filter_secrets
|
||||
secret-filter=### sponge_filter_secrets
|
||||
ai=## 5.12 AI and Developer Tools
|
||||
antigravity-ide=### antigravity-ide
|
||||
agy=### agy
|
||||
@@ -152,28 +170,52 @@ devlogs=### agents-init
|
||||
claude-cli=### claude
|
||||
claude-docs=### claude-docs
|
||||
claude-pr=### claude-pr
|
||||
dops=### dops
|
||||
docker-ps=### dops
|
||||
qc=### qc
|
||||
quick-chat=### qc
|
||||
aichat=### qc
|
||||
superpowers=### superpowers
|
||||
media=## 5.13 Media and Utilities
|
||||
dng2avif=### dng2avif
|
||||
play-media=### play-media
|
||||
video=### play-media
|
||||
audio=### play-media
|
||||
mpv=### play-media
|
||||
vlc=### play-media
|
||||
spark=### spark
|
||||
steam-dl=### steam-dl
|
||||
steam=### steam-dl
|
||||
yt-dlp=### yt-dlp
|
||||
miscfns=## 5.14 Miscellaneous
|
||||
bash=### bash
|
||||
bd-pull=### bd-pull
|
||||
beads=### bd-pull
|
||||
cffetch=### cffetch
|
||||
fastfetch=### ffetch
|
||||
neofetch=### cffetch
|
||||
sysinfo=### cffetch
|
||||
cheat=### cheat
|
||||
config-help=### config-help
|
||||
config-update=### config-update
|
||||
config-settings=### config-settings
|
||||
config-toggle=### config-toggle
|
||||
toggle=### config-settings
|
||||
dockup=### dockup
|
||||
fetch=### cffetch
|
||||
ffetch=### ffetch
|
||||
fzf_configure_bindings=### fzf_configure_bindings
|
||||
joplin=### joplin
|
||||
ld=### ld
|
||||
lazydocker=### ld
|
||||
open-url=### open-url
|
||||
url-open=### open-url
|
||||
repo-open=### repo-open
|
||||
open-repo=### repo-open
|
||||
config-update=### config-update
|
||||
config-settings=### config-settings
|
||||
config-toggle=### config-settings
|
||||
toggle=### config-settings
|
||||
bash=### bash
|
||||
cheat=### cheat
|
||||
dockup=### dockup
|
||||
joplin=### joplin
|
||||
rand_string=### rand_string
|
||||
random=### rand_string
|
||||
password=### rand_string
|
||||
randomword=### rand_string
|
||||
replay=### replay
|
||||
tmux=### tmux-clean
|
||||
wake-lock=### wake-lock
|
||||
@@ -208,6 +250,7 @@ pager-hierarchy=## Pager Hierarchy
|
||||
shell-aliases=## 4.11 Shell Aliases
|
||||
kitty-logging=### kitty-logging
|
||||
watcher=### kitty-logging
|
||||
kitty-scrollback=### Scrollback History
|
||||
|
||||
# ── Section 6: Dependency Catalog ────────────────────────────
|
||||
catalog=# 6. DEPENDENCY CATALOG
|
||||
@@ -215,6 +258,8 @@ deps-catalog=# 6. DEPENDENCY CATALOG
|
||||
required=## Required
|
||||
integrations=## Integrations
|
||||
recommended=## Recommended
|
||||
optional-deps=## Optional
|
||||
terminal-emulators=## Terminal Emulators
|
||||
install-methods=## Install Methods
|
||||
|
||||
# ── Section 7: Customization ──────────────────────────────────
|
||||
@@ -231,19 +276,47 @@ minimal=## Opinionated Components (Minimal Mode)
|
||||
minimal-mode=## Opinionated Components (Minimal Mode)
|
||||
opt-out=## Opinionated Components (Minimal Mode)
|
||||
toggles=## Opinionated Components (Minimal Mode)
|
||||
agent-vault=## Agent Memory Vault
|
||||
__fish_agent_vault_dir=## Agent Memory Vault
|
||||
__fish_agent_vault_autopush=## Agent Memory Vault
|
||||
component-reference=# 8. COMPONENTS REFERENCE
|
||||
components=# 8. COMPONENTS REFERENCE
|
||||
c0=## Per-function overrides: `C0`/`always`
|
||||
always-tag=## Per-function overrides: `C0`/`always`
|
||||
c1=## C1 — Command Shadows
|
||||
command-shadows=## C1 — Command Shadows
|
||||
aliases-detail=## C1 — Command Shadows
|
||||
filesystem=### filesystem
|
||||
network-shadow=### network
|
||||
monitor=### monitor
|
||||
shell-tools=### shell-tools
|
||||
dev-tools=### dev-tools
|
||||
c2=## C2 — Startup Side-Effects
|
||||
autoexec=## C2 — Startup Side-Effects
|
||||
startup=## C2 — Startup Side-Effects
|
||||
plugin-management=### plugin-management
|
||||
pkg-wrappers=### pkg-wrappers
|
||||
venv-toggle=### venv
|
||||
telemetry=### telemetry
|
||||
sync=### sync
|
||||
c3=## C3 — Key and Environment Overrides
|
||||
overrides-detail=## C3 — Key and Environment Overrides
|
||||
bang-bang=## C3 — Key and Environment Overrides
|
||||
bindings-toggle=### key-bindings
|
||||
environment=### environment
|
||||
prompt-toggle=### prompt
|
||||
privacy=### privacy
|
||||
privacy-toggle=### privacy
|
||||
do-not-track=### privacy
|
||||
disable-telemetry=### privacy
|
||||
c4=## C4 — Terminal and Tool Integration
|
||||
integrations-detail=## C4 — Terminal and Tool Integration
|
||||
terminal-abbrs=### terminal-abbrs
|
||||
window-mgmt=### window-mgmt
|
||||
window-management=### window-mgmt
|
||||
notifications-toggle=### notifications
|
||||
history-logs=### history-logs
|
||||
pkg-upgrade=### pkg-upgrade
|
||||
c5=## C5 — Logging and Capture
|
||||
logging-detail=## C5 — Logging and Capture
|
||||
logging-sentinel=## C5 — Logging and Capture
|
||||
@@ -252,11 +325,17 @@ zellij-logging=## C5 — Logging and Capture
|
||||
tmux-logging=## C5 — Logging and Capture
|
||||
pipe-pane=## C5 — Logging and Capture
|
||||
dump-screen=## C5 — Logging and Capture
|
||||
terminal-capture=### terminal-capture
|
||||
multiplexer-capture=### multiplexer-capture
|
||||
pkg-logs=### pkg-logs
|
||||
c6=## C6 — Greeting and First-Run UI
|
||||
greeting=## C6 — Greeting and First-Run UI
|
||||
first-run=### first-run
|
||||
greeting-message=### greeting-message
|
||||
|
||||
# ── Prompt and Theme ──────────────────────────────────────────
|
||||
prompt-theme=## Prompt and Theme
|
||||
prompt=## Prompt and Theme
|
||||
starship=### Starship
|
||||
fallback-prompt=### Catppuccin Fallback Prompt
|
||||
catppuccin-prompt=### Catppuccin Fallback Prompt
|
||||
@@ -316,11 +395,31 @@ minimal-trouble=## What's with the C1-C6 stuff?
|
||||
viewing=# 13. VIEWING THIS MANUAL
|
||||
manual=# 13. VIEWING THIS MANUAL
|
||||
ov=## In the terminal
|
||||
man-page=## As a man page
|
||||
manpage=## As a man page
|
||||
jump=## In the terminal
|
||||
html=## The documentation website
|
||||
browser=## The documentation website
|
||||
site=## The documentation website
|
||||
source=## Reading the source directly
|
||||
raw-source=## Reading the source directly
|
||||
|
||||
# ── Section 14: Testing ───────────────────────────────────────
|
||||
testing=# 14. TESTING
|
||||
tests=# 14. TESTING
|
||||
|
||||
# ── Section 15: Contributing ──────────────────────────────────
|
||||
contributing=# 15. CONTRIBUTING
|
||||
contribute=# 15. CONTRIBUTING
|
||||
forge=# 15. CONTRIBUTING
|
||||
|
||||
# ── Section 16: Attribution ───────────────────────────────────
|
||||
attribution=# 16. ATTRIBUTION
|
||||
credits=# 16. ATTRIBUTION
|
||||
|
||||
# ── Section 17: License ───────────────────────────────────────
|
||||
license=# 17. LICENSE
|
||||
licensing=# 17. LICENSE
|
||||
agpl=# 17. LICENSE
|
||||
copyright=# 17. LICENSE
|
||||
|
||||
|
||||
|
||||
+807
-236
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Generate the committed opinionated-component registry.
|
||||
|
||||
Walks every `# COMPONENT` header in functions/*.fish, conf.d/*.fish, and
|
||||
config.fish and writes conf.d/__fish_config_op_registry.fish, the fish
|
||||
data file __fish_config_op_registry_lookup reads at shell startup.
|
||||
|
||||
Run manually (via __fish_config_op_registry_rebuild) after editing a
|
||||
# COMPONENT header, and automatically as a pre-step in build-manual.py
|
||||
before the manual is built.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import manualtools as mt
|
||||
|
||||
DOCS = Path(__file__).parent
|
||||
REPO = DOCS.parent
|
||||
OUTPUT = REPO / "conf.d" / "__fish_config_op_registry.fish"
|
||||
|
||||
|
||||
def collect_components() -> dict[str, list[str]]:
|
||||
"""Gather every `# COMPONENT` header across the whole repo.
|
||||
|
||||
Concatenates raw component lines when the same identity appears in
|
||||
more than one source (e.g. functions/auto-pull.fish and
|
||||
conf.d/auto-pull.fish both self-identify as "auto-pull" at runtime,
|
||||
since the guard can only ever look up the bare status
|
||||
current-function/basename string) rather than letting one silently
|
||||
overwrite the other.
|
||||
"""
|
||||
out: dict[str, list[str]] = {}
|
||||
for source in (
|
||||
mt.parse_components(REPO / "functions"),
|
||||
mt.parse_components(REPO / "conf.d"),
|
||||
mt.parse_component_file(REPO / "config.fish"),
|
||||
):
|
||||
for identity, lines in source.items():
|
||||
out.setdefault(identity, []).extend(lines)
|
||||
return out
|
||||
|
||||
|
||||
def build_registry(components: dict[str, list[str]]) -> tuple[dict[str, list[str]], list[str]]:
|
||||
"""Turn {identity: [raw COMPONENT lines]} into ({"identity:site": [tags]}, warnings).
|
||||
|
||||
A site with both always/on and always/off tagged is a contradiction:
|
||||
both are stripped and a warning is emitted, but generation continues
|
||||
-- any other real tag on that same site survives. A site whose
|
||||
effective tag set is empty after stripping produces no registry entry
|
||||
at all, which __fish_config_op_enabled already treats as always/on
|
||||
(fail-open) at guard time -- see spec §4.5.
|
||||
"""
|
||||
registry: dict[str, list[str]] = {}
|
||||
warnings: list[str] = []
|
||||
for identity, raw_lines in components.items():
|
||||
by_site: dict[str, list[str]] = {}
|
||||
for site, tag in mt.parse_component_lines(raw_lines):
|
||||
by_site.setdefault(site, []).append(tag)
|
||||
for site, tags in by_site.items():
|
||||
if "always/on" in tags and "always/off" in tags:
|
||||
label = identity if not site else f"{identity}:{site}"
|
||||
warnings.append(
|
||||
f"{label}: both always/on and always/off tagged; ignoring both"
|
||||
)
|
||||
tags = [t for t in tags if t not in ("always/on", "always/off")]
|
||||
if tags:
|
||||
registry[f"{identity}:{site}"] = list(dict.fromkeys(tags))
|
||||
return registry, warnings
|
||||
|
||||
|
||||
def render(registry: dict[str, list[str]]) -> str:
|
||||
keys = sorted(registry)
|
||||
lines = [
|
||||
"# Copyright (C) 2026 Rootiest",
|
||||
"# SPDX-License-Identifier: AGPL-3.0-or-later",
|
||||
"#",
|
||||
"# GENERATED FILE --- do not edit by hand.",
|
||||
"# Regenerate with __fish_config_op_registry_rebuild after editing a",
|
||||
"# # COMPONENT header, or automatically via docs/build-manual.py.",
|
||||
"# Source: docs/generate_component_registry.py",
|
||||
"#",
|
||||
"# This file must be sourced before any other conf.d/*.fish file that",
|
||||
"# calls the opinionated guard. That currently holds only because fish's",
|
||||
"# glob-based conf.d loading happens to sort this filename first",
|
||||
"# alphabetically among the guard-calling files -- do not rename it",
|
||||
"# without preserving that ordering.",
|
||||
"",
|
||||
]
|
||||
if not keys:
|
||||
lines.append("set -g __fish_config_op_registry_keys")
|
||||
lines.append("set -g __fish_config_op_registry_values")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
quoted_keys = [f'"{k}"' for k in keys]
|
||||
lines.append("set -g __fish_config_op_registry_keys \\")
|
||||
lines += [f" {k} \\" for k in quoted_keys[:-1]] + [f" {quoted_keys[-1]}"]
|
||||
lines.append("")
|
||||
|
||||
values = ['"' + " ".join(registry[k]) + '"' for k in keys]
|
||||
lines.append("set -g __fish_config_op_registry_values \\")
|
||||
lines += [f" {v} \\" for v in values[:-1]] + [f" {values[-1]}"]
|
||||
lines.append("")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
components = collect_components()
|
||||
registry, warnings = build_registry(components)
|
||||
for w in warnings:
|
||||
print(f" WARN {w}", file=sys.stderr)
|
||||
OUTPUT.write_text(render(registry))
|
||||
print(f"wrote {OUTPUT} ({len(registry)} entries)", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
raise SystemExit(main())
|
||||
@@ -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 -->
|
||||
|
||||
---
|
||||
|
||||
@@ -42,9 +42,11 @@ are active in Insert, Normal, and Visual modes unless noted.
|
||||
pressing Enter a second time for certain fast-path
|
||||
commands (speedtest-fast, etc.).
|
||||
|
||||
@@ FZF inline picker. Type @@ anywhere on the command
|
||||
line to open an fzf picker and insert a selection
|
||||
at the cursor position.
|
||||
@@ FZF inline picker. Type @ twice anywhere on the
|
||||
command line to open an fzf picker and replace the
|
||||
@@ with the selection. The @@ must be typed as its
|
||||
own token: "cat @@" triggers it, but "cat@@" does
|
||||
not.
|
||||
|
||||
Ctrl+Right Accept autosuggestion one word/directory segment
|
||||
at a time. (Restores Fish 3.x behavior by binding
|
||||
|
||||
+104
-30
@@ -145,6 +145,40 @@ NOTE:
|
||||
- Disabled integration commands (spwin, tab, split, hist, logs, upgrade) print an error naming the variable that disabled them.
|
||||
- On CachyOS, the distro fish config's own aliases, history override, and bang-bang bindings are stripped per category as well.
|
||||
|
||||
### Sub-categories
|
||||
|
||||
Each of the six categories further sub-divides into two to six
|
||||
sub-categories, each with its own `__fish_config_op_<category>_<subcategory>`
|
||||
variable (e.g. `__fish_config_op_aliases_filesystem`). These follow the
|
||||
exact same truthy/falsy/unset cascade one level deeper: an explicit
|
||||
sub-category value overrides the master switch and the parent category's
|
||||
setting, and an unset sub-category inherits from its parent category (which
|
||||
in turn inherits from `__fish_config_opinionated`). Run config-settings and
|
||||
press Enter on a category row to browse and toggle its sub-categories
|
||||
interactively. See [Components Reference](/08-components-reference/) for the
|
||||
full sub-category breakdown of every category.
|
||||
|
||||
|
||||
## Agent Memory Vault
|
||||
|
||||
__fish_agent_vault_dir
|
||||
|
||||
Overrides the agent memory vault location. Defaults to
|
||||
$XDG_DATA_HOME/agent-vault (or ~/.local/share/agent-vault).
|
||||
|
||||
__fish_agent_vault_autopush
|
||||
|
||||
When set to 1, agents-vault also pushes on wrapper launch. Defaults to
|
||||
off: the vault commits locally on every launch and pushes from the
|
||||
Claude Code SessionEnd hook or an explicit agents-vault --push. That
|
||||
push is synchronous, so with autopush on the pull and the push are
|
||||
each capped at 20 seconds; an explicit --push is left uncapped.
|
||||
|
||||
NOTE:
|
||||
With autopush off and no SessionEnd hook installed, backups accumulate
|
||||
locally and never reach the remote. Run agents-vault --status to check
|
||||
how far ahead the vault is.
|
||||
|
||||
|
||||
## Prompt and Theme
|
||||
|
||||
@@ -154,11 +188,14 @@ The primary prompt is Starship, initialized by conf.d/starship.fish.
|
||||
Configure it via ~/.config/starship.toml.
|
||||
|
||||
conf.d/starship.fish defines a fish_prompt wrapper that only activates when
|
||||
starship is in PATH. It emits OSC 133;A (prompt start) immediately before
|
||||
Starship renders and OSC 133;B (input start) immediately after, placing both
|
||||
markers on the prompt line itself. This allows ov to use them as sticky
|
||||
section headers when browsing scrollback logs. Without Starship, fish's
|
||||
built-in prompt handles these markers automatically.
|
||||
starship is in PATH and C3 overrides are enabled (see Opinionated
|
||||
Components above). It emits OSC 133;A (prompt start) immediately before
|
||||
Starship renders and OSC 133;B (input start) immediately after, placing
|
||||
both markers on the prompt line itself. This allows ov to use them as
|
||||
sticky section headers when browsing scrollback logs. It also prints a
|
||||
blank line before the prompt, skipped in private mode or on a freshly
|
||||
cleared screen. Without Starship, fish's built-in prompt handles these
|
||||
markers automatically.
|
||||
|
||||
### Catppuccin Fallback Prompt
|
||||
|
||||
@@ -166,47 +203,84 @@ When Starship is absent or C3 overrides are disabled, a built-in nim-style
|
||||
two-line prompt activates from functions/fish_prompt.fish. No external
|
||||
dependencies — fish builtins only.
|
||||
|
||||
Layout:
|
||||
Layout (a dim job line appears between the two rows for each running
|
||||
background job):
|
||||
|
||||
┬─[user@host:~/path] (main)
|
||||
│ nvim notes.md
|
||||
╰─>$
|
||||
|
||||
Elements:
|
||||
|
||||
user Yellow (Catppuccin Yellow); red if root
|
||||
@host Blue (local) or Teal (SSH)
|
||||
~/path prompt_pwd abbreviation (Catppuccin Text)
|
||||
(main) Current git branch in Catppuccin Pink; omitted outside repos
|
||||
─[V:name] Active Python venv basename; omitted when none
|
||||
─[N/I/R/V] Vi-mode indicator when vi bindings are active
|
||||
┬─ / ╰─> Connector lines: Catppuccin Green on success, Red on failure
|
||||
Segment Meaning
|
||||
────────────────────────────────────────────────────────────────
|
||||
user Yellow (Catppuccin Yellow); red if root
|
||||
@host Blue (local) or Teal (SSH)
|
||||
~/path prompt_pwd abbreviation (Catppuccin Text)
|
||||
─[N/I/R/V/O] Vi-mode indicator (Normal/Insert/Replace/Visual/Operator);
|
||||
shown only when vi or hybrid key bindings are active
|
||||
─[V:name] Active Python venv basename; omitted when none
|
||||
(main) Current git branch in Catppuccin Pink, with ↑/↓
|
||||
upstream-tracking arrows when applicable;
|
||||
omitted outside repos
|
||||
┬─ / ╰─> Connector lines: Catppuccin Green on success,
|
||||
Red on failure
|
||||
|
||||
The right prompt (fish_right_prompt.fish) always renders, regardless of C3
|
||||
state. On failure it shows a red ✘ and the exit code; on success it shows
|
||||
only the dim timestamp. When starship is installed and C3 is enabled, the
|
||||
active Docker context is also shown (if non-default):
|
||||
The right prompt (fish_right_prompt.fish) always renders, independently of
|
||||
which left prompt is active:
|
||||
|
||||
✘ 1 myctx Fri Jun 12 00:51:21 2026 ← failed, starship+C3 active
|
||||
✘ 1 Fri Jun 12 00:51:21 2026 ← failed, fallback prompt
|
||||
Fri Jun 12 00:51:21 2026 ← success (no ✘)
|
||||
Segment Shown when
|
||||
────────────────────────────────────────────────────────────────
|
||||
✘ <code> The previous command exited non-zero (red)
|
||||
<context> docker and starship are both installed, C3
|
||||
overrides are enabled, and the active Docker
|
||||
context is set and non-default
|
||||
<timestamp> Always (dim, Catppuccin Overlay0)
|
||||
|
||||
The exit-status and Docker segments are independent — for example, right
|
||||
after a failing command with a non-default Docker context active:
|
||||
|
||||
✘ 1 myctx Fri Jun 12 00:51:21 2026
|
||||
|
||||
A successful command with the same Docker context shows the segment too:
|
||||
|
||||
myctx Fri Jun 12 00:51:21 2026
|
||||
|
||||
And without Starship (or with C3 disabled, or Docker not installed), only
|
||||
the exit-status prefix and timestamp ever appear:
|
||||
|
||||
✘ 1 Fri Jun 12 00:51:21 2026
|
||||
|
||||
### FZF
|
||||
|
||||
FZF is themed to Catppuccin Mocha via FZF_DEFAULT_OPTS set in
|
||||
integrations/fzf.fish. The colors applied:
|
||||
FZF is themed to Catppuccin Mocha via FZF_DEFAULT_OPTS, set in
|
||||
conf.d/theme.fish (opinionated; disabled by `__fish_config_op_overrides`,
|
||||
see Opinionated Components above). The colors applied:
|
||||
|
||||
Background: #1E1E2E (base) #313244 (surface0)
|
||||
Foreground: #CDD6F4 (text)
|
||||
Highlights: #F38BA8 (red) #CBA6F7 (mauve) #B4BEFE (lavender)
|
||||
Hex Role Catppuccin name
|
||||
────────────────────────────────────────────────────────
|
||||
#1E1E2E Background Base
|
||||
#313244 Highlighted background Surface0
|
||||
#45475A Selected background Surface1
|
||||
#CDD6F4 Foreground Text
|
||||
#F38BA8 Highlight / header Red
|
||||
#CBA6F7 Info / prompt Mauve
|
||||
#B4BEFE Marker Lavender
|
||||
#F5E0DC Spinner / pointer Rosewater
|
||||
#6C7086 Border Overlay0
|
||||
|
||||
To customize, override FZF_DEFAULT_OPTS in local.fish.
|
||||
To customize, override FZF_DEFAULT_OPTS in local.fish — it is sourced after
|
||||
conf.d/theme.fish on every session, so a `set -Ux FZF_DEFAULT_OPTS ...`
|
||||
there always wins.
|
||||
|
||||
### Catppuccin Mocha Syntax Highlighting
|
||||
|
||||
The Catppuccin Mocha theme ships with this config in themes/ and is applied
|
||||
on first run via `conf.d/first_run.fish`. Colors are stored in fish_variables
|
||||
(universal). To switch variants, install a different theme from themes/:
|
||||
automatically on first run via `conf.d/first_run.fish` (gated by
|
||||
`__fish_config_op_autoexec`; see Opinionated Components above). Colors are
|
||||
stored in fish_variables (universal). Three other bundled variants are
|
||||
available in themes/ — Latte, Frappé, and Macchiato. To switch:
|
||||
|
||||
fish_config theme save "Catppuccin Latte"
|
||||
fish_config theme choose "Catppuccin Latte"
|
||||
|
||||
`---`
|
||||
---
|
||||
|
||||
@@ -31,3 +31,35 @@ all of these commands.
|
||||
When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files
|
||||
are permanently deleted, not trashed. There is no intermediate safety net.
|
||||
|
||||
## Sub-categories
|
||||
|
||||
`__fish_config_op_aliases` sub-divides into six sub-categories, each with
|
||||
its own `__fish_config_op_aliases_<slug>` toggle:
|
||||
|
||||
## filesystem
|
||||
|
||||
`ls`, `cat`, `cd`, `du`, `mkdir`, `rm`, `mv`, and `cd`/zoxide navigation --
|
||||
the everyday filesystem-inspection and -modification shadows.
|
||||
|
||||
## search
|
||||
|
||||
`rg`, with its Kitty hyperlink formatting.
|
||||
|
||||
## network
|
||||
|
||||
`ping`, `ssh`, and `yt-dlp` -- shadows that talk to the network.
|
||||
|
||||
## monitor
|
||||
|
||||
`top` -> `btop`.
|
||||
|
||||
## shell-tools
|
||||
|
||||
`bash` (XDG bashrc + `$SHELL` reset), `less` (`$PAGER` fallback chain),
|
||||
and the `help config` interception.
|
||||
|
||||
## dev-tools
|
||||
|
||||
`claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor
|
||||
launcher), plus `agy`.
|
||||
|
||||
|
||||
@@ -43,3 +43,28 @@ branches, or repos without a remote. The handler fires once per repo entry
|
||||
(not on every sub-directory `cd`). The registry is machine-local at
|
||||
`$__fish_user_dots_path/auto-pull.list` (defaults to `~/.config/.user-dots/fish/auto-pull.list`) and is never committed.
|
||||
|
||||
## Sub-categories
|
||||
|
||||
`__fish_config_op_autoexec` sub-divides into five sub-categories, each
|
||||
with its own `__fish_config_op_autoexec_<slug>` toggle:
|
||||
|
||||
## plugin-management
|
||||
|
||||
Fisher bootstrap on first run.
|
||||
|
||||
## pkg-wrappers
|
||||
|
||||
`paru`/`yay` wrapper generation.
|
||||
|
||||
## venv
|
||||
|
||||
Automatic Python virtualenv activation.
|
||||
|
||||
## telemetry
|
||||
|
||||
The WakaTime hook's startup bootstrap.
|
||||
|
||||
## sync
|
||||
|
||||
Auto-pull background fast-forward, and the user-dots convenience symlink.
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ all of them.
|
||||
Catppuccin colors 30+ fish_color_* variables set to Mocha palette
|
||||
FZF_DEFAULT_OPTS FZF themed to Catppuccin Mocha colors
|
||||
Right prompt fish_right_prompt: exit code (on failure) + dim timestamp; always rendered; Docker context added when starship+C3 active
|
||||
DO_NOT_TRACK=1 Universal telemetry opt-out for tools and AI agents
|
||||
DISABLE_TELEMETRY=1 Telemetry opt-out for telemetry-aware CLIs
|
||||
|
||||
The bang-bang system spans `key_bindings.fish`, `abbr.fish`, `puffer.fish`, and
|
||||
six `expand_bang_*.fish` functions. All are gated together — disabling C3
|
||||
@@ -33,3 +35,28 @@ When C3 is disabled, `exit` falls back to `builtin exit` with no scrollback
|
||||
capture, no Kitty IPC, and no file I/O on exit. The scrollback capture block
|
||||
is independently controlled by C5 (see below).
|
||||
|
||||
## Sub-categories
|
||||
|
||||
`__fish_config_op_overrides` sub-divides into four sub-categories, each
|
||||
with its own `__fish_config_op_overrides_<slug>` toggle:
|
||||
|
||||
## key-bindings
|
||||
|
||||
Vi mode, autopair, puffer key intercepts, bang-bang history expansion,
|
||||
and `smart_exit`'s plain-exit path.
|
||||
|
||||
## environment
|
||||
|
||||
`$PATH`, `$PAGER`/`$EDITOR`/`$GPG_TTY`, and `$CDPATH`.
|
||||
|
||||
## prompt
|
||||
|
||||
Starship, the right prompt, Catppuccin syntax/prompt colors, and FZF
|
||||
theming (`$FZF_DEFAULT_OPTS`) -- all driven by the same guard as a single
|
||||
unit, not independently toggleable from each other.
|
||||
|
||||
## privacy
|
||||
|
||||
`$DO_NOT_TRACK` and `$DISABLE_TELEMETRY` environment variables for
|
||||
telemetry opt-out across CLI tools, runtimes, and AI agents.
|
||||
|
||||
|
||||
@@ -22,3 +22,28 @@ Disabled integration commands (`spwin`, `tab`, `split`, `hist`, `logs`, `upgrade
|
||||
a colored error to stderr naming the variable that disabled them rather than
|
||||
silently failing.
|
||||
|
||||
## Sub-categories
|
||||
|
||||
`__fish_config_op_integrations` sub-divides into five sub-categories,
|
||||
each with its own `__fish_config_op_integrations_<slug>` toggle:
|
||||
|
||||
## terminal-abbrs
|
||||
|
||||
The Kitty/WezTerm abbreviation set.
|
||||
|
||||
## window-mgmt
|
||||
|
||||
`spwin`, `tab`, `split`.
|
||||
|
||||
## notifications
|
||||
|
||||
`done`'s completion notifications, and the WakaTime activity hook.
|
||||
|
||||
## history-logs
|
||||
|
||||
`hist`, `logs`.
|
||||
|
||||
## pkg-upgrade
|
||||
|
||||
`upgrade`.
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ CAUTION: This configuration is capable of silently recording terminal output and
|
||||
Component What it captures
|
||||
───────────────────────────────────────────────────────────────────────────
|
||||
Scrollback capture Terminal session output saved to:
|
||||
`~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log`
|
||||
~/.terminal_history/scrollback_YYYY-MM-DD_HH-MM-SS.log
|
||||
tmux pane capture Continuous pane stream via pipe-pane, saved to:
|
||||
`~/.terminal_history/tmux_<session>-w<win>-p<pane>_YYYY-MM-DD_HH-MM-SS.log`
|
||||
~/.terminal_history/tmux_<session>-w<win>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
|
||||
zellij pane capture Pane scrollback snapshot on shell exit, saved to:
|
||||
`~/.terminal_history/zellij_<session>-p<pane>_YYYY-MM-DD_HH-MM-SS.log`
|
||||
~/.terminal_history/zellij_<session>-p<pane>_YYYY-MM-DD_HH-MM-SS.log
|
||||
paru wrapper All paru/AUR output captured to:
|
||||
`~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log`
|
||||
~/.terminal_history/paru_YYYY-MM-DD_HH-MM-SS.log
|
||||
yay wrapper All yay/AUR output captured to:
|
||||
`~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log`
|
||||
Kitty watcher `watcher.py` captures scrollback when Kitty closes
|
||||
~/.terminal_history/yay_YYYY-MM-DD_HH-MM-SS.log
|
||||
Kitty watcher watcher.py captures scrollback when Kitty closes
|
||||
|
||||
NOTE: **Turning off logging does not delete any existing logs.**
|
||||
They remain in `$SCROLLBACK_HISTORY_DIR` (defaults to: `~/.terminal_history/`)
|
||||
@@ -58,7 +58,7 @@ Ctrl-D, or a logout), because that is when the fish_exit handler runs. It does
|
||||
NOT capture when you close a pane or quit zellij through zellij itself:
|
||||
|
||||
- Closing a pane signals the shell and tears the pane down concurrently, so
|
||||
even if the handler runs, `dump-screen` may find the pane buffer already
|
||||
even if the handler runs, dump-screen may find the pane buffer already
|
||||
gone.
|
||||
- Quitting zellij kills the zellij server, and `dump-screen` needs a live
|
||||
server to read from — there is nothing left to snapshot.
|
||||
@@ -93,7 +93,7 @@ start, so it appears without any action on your part.
|
||||
Disabling `__fish_config_op_logging` (or leaving it unset):
|
||||
1. Creates the sentinel immediately in every open shell.
|
||||
2. Removes `~/.local/bin/paru` and `~/.local/bin/yay` logging wrappers;
|
||||
bare `/usr/bin/paru` and `/usr/bin/yay` are used instead.
|
||||
bare /usr/bin/paru and /usr/bin/yay are used instead.
|
||||
3. Kitty's `watcher.py` reads the sentinel on each save attempt and
|
||||
skips capture — no Kitty restart required.
|
||||
4. smart_exit stops saving scrollback logs.
|
||||
@@ -112,3 +112,22 @@ Note: C3 and C5 compose independently. C3 controls whether the smart_exit
|
||||
wrapper is active at all; C5 controls only the scrollback-capture block
|
||||
inside it. With C3 disabled, exit is plain builtin exit regardless of C5.
|
||||
|
||||
## Sub-categories
|
||||
|
||||
`__fish_config_op_logging` sub-divides into three sub-categories, each
|
||||
with its own `__fish_config_op_logging_<slug>` toggle (all still opt-in
|
||||
by default, inherited from C5's own opt-in behavior -- see §3 of the
|
||||
design spec):
|
||||
|
||||
## terminal-capture
|
||||
|
||||
Kitty watcher scrollback capture, and `smart_exit`'s logging-guard path.
|
||||
|
||||
## multiplexer-capture
|
||||
|
||||
tmux `pipe-pane` and zellij `dump-screen` capture.
|
||||
|
||||
## pkg-logs
|
||||
|
||||
`paru`/`yay` AUR log wrappers.
|
||||
|
||||
|
||||
@@ -13,3 +13,16 @@ When C6 is disabled, no greeting is printed by this config. Any greeting
|
||||
set by the distro or other configs runs normally — this config simply does
|
||||
not override it.
|
||||
|
||||
## Sub-categories
|
||||
|
||||
`__fish_config_op_greeting` sub-divides into two sub-categories, each
|
||||
with its own `__fish_config_op_greeting_<slug>` toggle:
|
||||
|
||||
## first-run
|
||||
|
||||
The first-run welcome banner.
|
||||
|
||||
## greeting-message
|
||||
|
||||
The per-session `fish_greeting` override.
|
||||
|
||||
|
||||
@@ -12,9 +12,24 @@ category variable.
|
||||
|
||||
Category Description
|
||||
──────────────────────────────────────────────────────────────────────────
|
||||
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (`rm`, `cp`) to be safe by default
|
||||
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (rm, cp) to be safe by default
|
||||
C2 [Startup Side-Effects](/08-components-reference/02-c2-startup-side-effects/) — Bootstraps Fisher, generates wrappers, auto-activates venvs
|
||||
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides `cd`, sets Vi mode, binds `<CR>` to `smart_enter`
|
||||
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides cd, sets Vi mode, binds <CR> to smart_enter
|
||||
C4 [Integrations](/08-components-reference/04-c4-terminal-and-tool-integration/) — Kitty/Wezterm integrations, starship hooks, fzf theme
|
||||
C5 [Logging and Capture](/08-components-reference/05-c5-logging-and-capture/) — Session logs, command duration
|
||||
C6 [Greeting & First-Run UI](/08-components-reference/06-c6-greeting-and-first-run-ui/) — Custom startup banner
|
||||
|
||||
Each category further sub-divides into two to six sub-categories (25 in
|
||||
total) with their own `__fish_config_op_<category>_<subcategory>` toggles
|
||||
-- see that category's page for its sub-category list.
|
||||
|
||||
## Per-function overrides: `C0`/`always`
|
||||
|
||||
Every guarded function or file can also carry a reserved `always/on` or
|
||||
`always/off` tag in its `# COMPONENT` header, independent of every C1-C6
|
||||
category and sub-category toggle and invisible to `config-settings`. An
|
||||
`always/off` tag disables that function unconditionally; an `always/on`
|
||||
tag enables it unconditionally, ignoring the state of every other tagged
|
||||
sub-category. This is a per-function escape hatch for cases too granular
|
||||
or too idiosyncratic to justify a taxonomy entry -- edit the header
|
||||
directly and run `__fish_config_op_registry_rebuild` to apply the change.
|
||||
|
||||
@@ -27,13 +27,18 @@ The following plugins are fully managed by Fisher. Their files are installed
|
||||
into the repo directory by Fisher and are listed in `.gitignore` — do not
|
||||
commit them. Fisher installs and updates them automatically.
|
||||
|
||||
- `jorgebucaran/fisher` — Plugin manager itself
|
||||
- `meaningful-ooo/sponge` — Remove failed commands from history
|
||||
- [`jorgebucaran/fisher`](https://github.com/jorgebucaran/fisher) — Plugin manager itself
|
||||
- [`meaningful-ooo/sponge`](https://github.com/meaningful-ooo/sponge) — Remove failed commands from history
|
||||
|
||||
## Sponge History Filtering
|
||||
|
||||
Sponge removes failed commands from history and, via conf.d/sponge_privacy.fish,
|
||||
also filters privacy-sensitive commands through three layers:
|
||||
also filters privacy-sensitive commands through three layers. Detection is
|
||||
heuristic — pattern- and variable-name-based — so this reduces the risk of a
|
||||
credential landing in persistent history; it is not a guarantee that no
|
||||
secret can ever reach it, and it is not a substitute for rotating a
|
||||
credential that gets typed in plaintext. Treat it as a safety net, not a
|
||||
vault.
|
||||
|
||||
Layer 1 — Static patterns (universal, persistent across sessions):
|
||||
Commands matching any of these structural signatures are never recorded:
|
||||
@@ -58,6 +63,20 @@ Layer 3 — Per-command filter (sponge_filter_secrets):
|
||||
Catches credentials in variables exported after login, such as tokens
|
||||
sourced from a project .env file mid-session.
|
||||
|
||||
A match is actively deleted from history, not stored and redacted. Sponge
|
||||
queues a matched command on `fish_postexec` and purges anything past
|
||||
`sponge_delay` entries on the very next `fish_prompt`, immediately forcing
|
||||
a `history save`. With this config's (upstream) defaults, that means a
|
||||
matched command is gone from disk within about one prompt cycle — it is
|
||||
not left sitting in persistent history for the rest of the session.
|
||||
|
||||
This timing depends on `sponge_purge_only_on_exit` staying `false`, which
|
||||
is sponge's own default and is not overridden here. Turning it on defers
|
||||
all purging to the `fish_exit` event instead of the next prompt — and
|
||||
because `fish_exit` does not fire on a killed or crashed session, a
|
||||
matched command purged only on exit can survive indefinitely if the shell
|
||||
never exits cleanly. Leave this setting off.
|
||||
|
||||
To add your own persistent patterns:
|
||||
|
||||
set -U -a sponge_regex_patterns 'your-regex-here'
|
||||
@@ -85,11 +104,11 @@ releases. Installing them through Fisher would overwrite these customizations.
|
||||
|
||||
Bundled components and their upstream origins:
|
||||
|
||||
- `catppuccin/fish` → `themes/` + `conf.d/theme.fish`
|
||||
- `PatrickF1/fzf.fish` → `functions/_fzf_*.fish` + `conf.d/fzf.fish`
|
||||
- `franciscolourenco/done` → `conf.d/done.fish`
|
||||
- `jorgebucaran/autopair.fish` → `functions/_autopair_*.fish` + `conf.d/autopair.fish`
|
||||
- `nickeb96/puffer-fish` → `functions/_puffer_fish_*.fish` + `conf.d/puffer.fish`
|
||||
- [`catppuccin/fish`](https://github.com/catppuccin/fish) → `themes/` + `conf.d/theme.fish`
|
||||
- [`PatrickF1/fzf.fish`](https://github.com/PatrickF1/fzf.fish) → `functions/_fzf_*.fish` + `conf.d/fzf.fish`
|
||||
- [`franciscolourenco/done`](https://github.com/franciscolourenco/done) → `conf.d/done.fish`
|
||||
- [`jorgebucaran/autopair.fish`](https://github.com/jorgebucaran/autopair.fish) → `functions/_autopair_*.fish` + `conf.d/autopair.fish`
|
||||
- [`nickeb96/puffer-fish`](https://github.com/nickeb96/puffer-fish) → `functions/_puffer_fish_*.fish` + `conf.d/puffer.fish`
|
||||
|
||||
Do not run `fisher install` for these — it will overwrite the customized
|
||||
versions. To update their behavior, edit the relevant bundled files directly.
|
||||
@@ -98,10 +117,10 @@ versions. To update their behavior, edit the relevant bundled files directly.
|
||||
|
||||
The `fish_plugins` file at the config root:
|
||||
|
||||
- `jorgebucaran/fisher` — Plugin manager itself
|
||||
- `meaningful-ooo/sponge` — Remove failed commands from history
|
||||
- [`jorgebucaran/fisher`](https://github.com/jorgebucaran/fisher) — Plugin manager itself
|
||||
- [`meaningful-ooo/sponge`](https://github.com/meaningful-ooo/sponge) — Remove failed commands from history
|
||||
|
||||
To update all Fisher-managed plugins, run `fisher update` or `fish-deps
|
||||
update` which calls it as its first step.
|
||||
To update all Fisher-managed plugins, run `fisher update` or
|
||||
`fish-deps update` which calls it as its first step.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,9 +208,9 @@ This configuration groups its opinionated behaviors into six categories (C1–C6
|
||||
|
||||
Category Description
|
||||
──────────────────────────────────────────────────────────────────────────
|
||||
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (`rm`, `cp`) to be safe by default
|
||||
C1 [Command Shadows](/08-components-reference/01-c1-command-shadows/) — Wraps destructive commands (rm, cp) to be safe by default
|
||||
C2 [Startup Side-Effects](/08-components-reference/02-c2-startup-side-effects/) — Bootstraps Fisher, generates wrappers, auto-activates venvs
|
||||
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides `cd`, sets Vi mode, binds `<CR>` to `smart_enter`
|
||||
C3 [Overrides](/08-components-reference/03-c3-key-and-environment-overrides/) — Overrides cd, sets Vi mode, binds <CR> to smart_enter
|
||||
C4 [Integrations](/08-components-reference/04-c4-terminal-and-tool-integration/) — Kitty/Wezterm integrations, starship hooks, fzf theme
|
||||
C5 [Logging and Capture](/08-components-reference/05-c5-logging-and-capture/) — Session logs, command duration
|
||||
C6 [Greeting & First-Run UI](/08-components-reference/06-c6-greeting-and-first-run-ui/) — Custom startup banner
|
||||
@@ -237,6 +237,13 @@ Re-enable everything:
|
||||
|
||||
set -Ue __fish_config_opinionated
|
||||
|
||||
Each category also has two to six sub-categories (e.g.
|
||||
`__fish_config_op_aliases_filesystem`) that can be checked, disabled, or
|
||||
reset the same way — `set -U __fish_config_op_<category>_<subcategory> off`
|
||||
and `set -Ue __fish_config_op_<category>_<subcategory>` work identically to
|
||||
the category-level recipes above, just one level more granular. See
|
||||
[Components Reference](/08-components-reference/) for the full list.
|
||||
|
||||
For an interactive alternative to setting these variables by hand, run `config-settings`.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Testing
|
||||
manTitle: 14. TESTING
|
||||
sidebar:
|
||||
order: 18
|
||||
helpKeywords:
|
||||
- testing
|
||||
- tests
|
||||
- test-suite
|
||||
- run-tests
|
||||
---
|
||||
|
||||
<!-- README: Testing -->
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Contributing
|
||||
manTitle: 15. CONTRIBUTING
|
||||
sidebar:
|
||||
order: 19
|
||||
helpKeywords:
|
||||
- contributing
|
||||
- contribute
|
||||
- pull-request
|
||||
- fork
|
||||
- issues
|
||||
- forge
|
||||
---
|
||||
|
||||
<!-- README: Contributing -->
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Attribution
|
||||
manTitle: 16. ATTRIBUTION
|
||||
sidebar:
|
||||
order: 20
|
||||
helpKeywords:
|
||||
- attribution
|
||||
- credits
|
||||
- zoxide
|
||||
---
|
||||
|
||||
<!-- README: Attribution -->
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: License
|
||||
manTitle: 17. LICENSE
|
||||
sidebar:
|
||||
order: 21
|
||||
helpKeywords:
|
||||
- license
|
||||
- licensing
|
||||
- agpl
|
||||
- copyright
|
||||
---
|
||||
|
||||
<!-- README: License -->
|
||||
+72
-5
@@ -62,6 +62,7 @@ HEADER_LABEL = re.compile(r"^#\s+([A-Z][A-Z ]*[A-Z])\s*$")
|
||||
FUNC_DEF = re.compile(r"^\s*function\s+(\S+)")
|
||||
SECTIONS = (
|
||||
"CATEGORY",
|
||||
"COMPONENT",
|
||||
"DEPENDENCIES",
|
||||
"SYNOPSIS",
|
||||
"DESCRIPTION",
|
||||
@@ -109,6 +110,19 @@ def _trailing_blanks(lines: list[str]) -> int:
|
||||
return n
|
||||
|
||||
|
||||
def _block_identity(path: Path, lines: list[str], end: int, blocks_count: int) -> str:
|
||||
"""Resolve a header block's associated name.
|
||||
|
||||
A file carrying exactly one header is associated with its own stem, so
|
||||
a `function` nested inside a `type -q` guard still resolves. A file
|
||||
with several headers walks forward to the next `function` definition.
|
||||
"""
|
||||
if blocks_count == 1:
|
||||
return path.stem
|
||||
after = (m.group(1) for ln in lines[end:] if (m := FUNC_DEF.match(ln)))
|
||||
return next(after, path.stem)
|
||||
|
||||
|
||||
def parse_functions(root: Path) -> dict[str, dict[str, list[str]]]:
|
||||
"""Parse the comment header above every documented public function.
|
||||
|
||||
@@ -128,11 +142,7 @@ def parse_functions(root: Path) -> dict[str, dict[str, list[str]]]:
|
||||
lines = path.read_text(encoding="utf-8").split("\n")
|
||||
blocks = _header_blocks(lines)
|
||||
for end, sections in blocks:
|
||||
if len(blocks) == 1:
|
||||
name = path.stem
|
||||
else:
|
||||
after = (m.group(1) for ln in lines[end:] if (m := FUNC_DEF.match(ln)))
|
||||
name = next(after, path.stem)
|
||||
name = _block_identity(path, lines, end, len(blocks))
|
||||
if name.startswith("_") or "CATEGORY" not in sections:
|
||||
continue
|
||||
out[name] = {
|
||||
@@ -185,6 +195,63 @@ def parse_abbreviations(root: Path) -> dict[str, list[dict]]:
|
||||
return out
|
||||
|
||||
|
||||
SITE_LINE_RE = re.compile(r"^site\s+(\S+):\s*(\S+)$")
|
||||
|
||||
|
||||
def parse_component_lines(lines: list[str]) -> list[tuple[str, str]]:
|
||||
"""Parse raw `# COMPONENT` body lines into (site, tag) pairs.
|
||||
|
||||
A line of the form `site <slug>: <tag>` scopes <tag> to that site; a
|
||||
bare `<tag>` line belongs to the default (unnamed) site, keyed "".
|
||||
Blank lines are skipped.
|
||||
"""
|
||||
out: list[tuple[str, str]] = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
m = SITE_LINE_RE.match(line)
|
||||
if m:
|
||||
out.append((m.group(1), m.group(2)))
|
||||
else:
|
||||
out.append(("", line))
|
||||
return out
|
||||
|
||||
|
||||
def _parse_component_blocks(path: Path) -> dict[str, list[str]]:
|
||||
"""Parse every `# COMPONENT` header block in one file.
|
||||
|
||||
Unlike parse_functions, there is no `# CATEGORY` gate and no
|
||||
underscore exclusion: component classification applies to every
|
||||
function/script, public or private, documented in the manual or not
|
||||
-- the registry needs to see every guarded identity, not just the
|
||||
ones that appear in the public function reference.
|
||||
"""
|
||||
lines = path.read_text(encoding="utf-8").split("\n")
|
||||
blocks = _header_blocks(lines)
|
||||
out: dict[str, list[str]] = {}
|
||||
for end, sections in blocks:
|
||||
if "COMPONENT" not in sections:
|
||||
continue
|
||||
name = _block_identity(path, lines, end, len(blocks))
|
||||
body = sections["COMPONENT"]
|
||||
out[name] = body[: len(body) - _trailing_blanks(body)]
|
||||
return out
|
||||
|
||||
|
||||
def parse_component_file(path: Path) -> dict[str, list[str]]:
|
||||
"""Parse `# COMPONENT` header block(s) in one specific file (e.g. config.fish)."""
|
||||
return _parse_component_blocks(path)
|
||||
|
||||
|
||||
def parse_components(root: Path) -> dict[str, list[str]]:
|
||||
"""Parse `# COMPONENT` header blocks across every `*.fish` file under root."""
|
||||
out: dict[str, list[str]] = {}
|
||||
for path in sorted(root.glob("*.fish")):
|
||||
out.update(_parse_component_blocks(path))
|
||||
return out
|
||||
|
||||
|
||||
def _sort_key(entry: Path) -> tuple:
|
||||
"""Order by sidebar.order when present, else by filename. Stable."""
|
||||
target = entry / "index.md" if entry.is_dir() else entry
|
||||
|
||||
@@ -3,6 +3,9 @@ dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# starlight-plugin-icons safelist cache
|
||||
.starlight-icons/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
|
||||
@@ -20,12 +20,59 @@ python3 docs/build-manual.py --site
|
||||
`docs/verify-manual.py` validates both sources before you build; run it
|
||||
first if you've touched a header or a manual page.
|
||||
|
||||
## Inline code spans
|
||||
|
||||
Function headers are read as plain text (by `config-help`, by `funcsave`,
|
||||
by anyone opening the `.fish` file), so they're authored without backticks
|
||||
— `-a/--all`, not `` `-a`/`--all` ``. `docs/codespans.py` puts the
|
||||
backticks on at render time, as the last step of `prettify()`.
|
||||
|
||||
`build_concat()` runs the same pass, so the man page and `config-help`
|
||||
mark code the way the site does rather than only where the SSOT happened
|
||||
to backtick something by hand. `config-help` then renders those spans
|
||||
bold and drops the delimiters, since a terminal pager would otherwise
|
||||
show them as literal punctuation.
|
||||
|
||||
It recognises flags, `$vars`, `SCREAMING_SNAKE` env vars, snake_case
|
||||
identifiers (`__fish_config_op_aliases`, `fish_greeting`), paths and
|
||||
filenames, key chords (`Ctrl-R`), shadow chains (`ls->eza`), runs of tool
|
||||
names (`btop, dust, duf, …`), whole command lines in a table column of
|
||||
command lines, and command names it knows — the `_fdc_*` catalog in
|
||||
`functions/_fish_deps_catalog.fish`, the `functions/` directory listing,
|
||||
and a standard-command list in the module.
|
||||
|
||||
Names that also read as English (`find`, `top`, `screen`) are listed in
|
||||
`AMBIGUOUS_COMMANDS` and are never wrapped on sight; they still count
|
||||
where position already proves they're a command. Add to that list rather
|
||||
than removing a rule if a wrap ever reads wrong.
|
||||
|
||||
Fenced blocks, indented blocks, existing code spans, headings, link
|
||||
targets, URLs, component markup, and `<FileTree>` bodies are never
|
||||
touched. Leaving a token alone is always the safe outcome, so every rule
|
||||
bails out when it isn't sure.
|
||||
|
||||
Indented blocks matter only to the concat — `prettify()` has already
|
||||
fenced them by the time the site is rendered — but there they are the
|
||||
table of contents and every section 5 entry, which must stay verbatim.
|
||||
|
||||
## llms.txt
|
||||
|
||||
The [`starlight-llms-txt`](https://www.npmjs.com/package/starlight-llms-txt)
|
||||
plugin emits `llms.txt`, `llms-full.txt`, and `llms-small.txt` alongside the
|
||||
built pages — no configuration needed, it just walks the generated content.
|
||||
|
||||
## Icons
|
||||
|
||||
[`starlight-plugin-icons`](https://docs.rettend.me/starlight-plugin-icons)
|
||||
+ [UnoCSS](https://unocss.dev) (`uno.config.ts`) render icons from any
|
||||
[Iconify](https://icones.js.org) set as `i-<collection>:<name>` classes.
|
||||
The Gitea link in the header uses it (see
|
||||
`src/components/starlight/SocialIcons.astro`) to show the real Gitea logo
|
||||
instead of Starlight's generic `code-branch` icon. Sidebar (`sidebar:
|
||||
true`) and codeblock (`codeblock: true`) icon support are wired up in
|
||||
`astro.config.mjs` but unused so far — see the plugin docs for the
|
||||
`icon:` sidebar syntax if you want to add them.
|
||||
|
||||
## Development
|
||||
|
||||
```fish title="local dev server"
|
||||
|
||||
+54
-39
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import UnoCSS from 'unocss/astro';
|
||||
import Icons from 'starlight-plugin-icons';
|
||||
import starlightLinksValidator from 'starlight-links-validator';
|
||||
import starlightCatppuccin from '@catppuccin/starlight';
|
||||
import starlightLlmsTxt from 'starlight-llms-txt';
|
||||
@@ -9,45 +10,59 @@ export default defineConfig({
|
||||
prerenderConflictBehavior: 'ignore',
|
||||
site: 'https://fish.rootiest.fyi',
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'Rootiest Fish Config',
|
||||
description: 'Reference manual for the rootiest fish configuration.',
|
||||
favicon: '/favicon.svg',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
UnoCSS(),
|
||||
...Icons({
|
||||
sidebar: true,
|
||||
codeblock: true,
|
||||
extractSafelist: true,
|
||||
starlight: {
|
||||
title: 'Rootiest Fish Config',
|
||||
description: 'Reference manual for the rootiest fish configuration.',
|
||||
favicon: '/favicon.svg',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
},
|
||||
social: [
|
||||
{
|
||||
icon: 'code-branch',
|
||||
label: 'Gitea',
|
||||
href: 'https://git.rootiest.dev/rootiest/fish-config',
|
||||
},
|
||||
{
|
||||
icon: 'github',
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/rootiest/fish-config',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
SocialIcons: './src/components/starlight/SocialIcons.astro',
|
||||
},
|
||||
head: [
|
||||
{
|
||||
tag: 'script',
|
||||
content: 'document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("starlight-file-tree").forEach(tree => { tree.querySelectorAll("details").forEach((d, i) => { if (i !== 0) d.removeAttribute("open"); }); }); });',
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
starlightLinksValidator(),
|
||||
starlightCatppuccin({
|
||||
dark: { flavor: "mocha", accent: "green" },
|
||||
light: { flavor: "latte", accent: "sky" },
|
||||
}),
|
||||
starlightLlmsTxt(),
|
||||
],
|
||||
expressiveCode: {
|
||||
// Shiki ships both Catppuccin flavours; Starlight picks by the
|
||||
// reader's colour scheme, matching the palette in catppuccin.css.
|
||||
themes: ['catppuccin-mocha', 'catppuccin-latte'],
|
||||
styleOverrides: {
|
||||
borderRadius: '0.4rem',
|
||||
borderColor: 'var(--sl-color-gray-5)',
|
||||
codeFontSize: '0.875rem',
|
||||
},
|
||||
},
|
||||
sidebar,
|
||||
},
|
||||
social: [
|
||||
{
|
||||
icon: 'code-branch',
|
||||
label: 'Gitea',
|
||||
href: 'https://git.rootiest.dev/rootiest/fish-config',
|
||||
},
|
||||
],
|
||||
head: [
|
||||
{
|
||||
tag: 'script',
|
||||
content: 'document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("starlight-file-tree").forEach(tree => { tree.querySelectorAll("details").forEach((d, i) => { if (i !== 0) d.removeAttribute("open"); }); }); });',
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
starlightLinksValidator(),
|
||||
starlightCatppuccin({
|
||||
dark: { flavor: "mocha", accent: "green" },
|
||||
light: { flavor: "latte", accent: "sky" },
|
||||
}),
|
||||
starlightLlmsTxt(),
|
||||
],
|
||||
expressiveCode: {
|
||||
// Shiki ships both Catppuccin flavours; Starlight picks by the
|
||||
// reader's colour scheme, matching the palette in catppuccin.css.
|
||||
themes: ['catppuccin-mocha', 'catppuccin-latte'],
|
||||
styleOverrides: {
|
||||
borderRadius: '0.4rem',
|
||||
borderColor: 'var(--sl-color-gray-5)',
|
||||
codeFontSize: '0.875rem',
|
||||
},
|
||||
},
|
||||
sidebar,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Generated
+1389
-1
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,13 @@
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.41.4",
|
||||
"@catppuccin/starlight": "^2.1.0",
|
||||
"@iconify-json/pajamas": "^1.2.15",
|
||||
"@unocss/astro": "^66.7.5",
|
||||
"astro": "^7.0.2",
|
||||
"sharp": "^0.35.3",
|
||||
"starlight-links-validator": "^0.25.2",
|
||||
"starlight-llms-txt": "^0.11.0"
|
||||
"starlight-llms-txt": "^0.11.0",
|
||||
"starlight-plugin-icons": "^1.1.6",
|
||||
"unocss": "^66.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,50 @@
|
||||
---
|
||||
import config from 'virtual:starlight/user-config';
|
||||
import { Icon } from '@astrojs/starlight/components';
|
||||
|
||||
// Maps a social link's `label` to a UnoCSS iconify icon class, letting a
|
||||
// link use an icon outside Starlight's built-in set (e.g. a real Gitea
|
||||
// logo instead of the generic `code-branch` icon).
|
||||
const customIcons: Record<string, string> = {
|
||||
Gitea: 'i-pajamas:gitea',
|
||||
};
|
||||
|
||||
const links = config.social || [];
|
||||
---
|
||||
|
||||
{
|
||||
links.length > 0 && (
|
||||
<>
|
||||
{links.map(({ label, href, icon }) => {
|
||||
const customIcon = customIcons[label];
|
||||
return (
|
||||
<a href={href} rel="me" class="sl-flex">
|
||||
<span class="sr-only">{label}</span>
|
||||
{customIcon ? <span class={`social-icon ${customIcon}`} aria-hidden="true" /> : <Icon name={icon} size="1.5em" />}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
@layer starlight.core {
|
||||
a {
|
||||
color: var(--sl-color-text-accent);
|
||||
padding: 0.5em;
|
||||
margin: -0.5em;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--sl-color-white);
|
||||
}
|
||||
.social-icon {
|
||||
/* !important: the i-pajamas:* class comes from UnoCSS, which emits
|
||||
unlayered CSS. Unlayered rules always win over anything in a
|
||||
@layer regardless of specificity or source order, so a plain
|
||||
override here is silently ignored no matter how it's written. */
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'unocss';
|
||||
import { presetStarlightIcons } from 'starlight-plugin-icons/uno';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [presetStarlightIcons()],
|
||||
});
|
||||
+658
-6
@@ -9,6 +9,7 @@ import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import codespans
|
||||
import manualtools as mt
|
||||
|
||||
# docs/build-manual.py follows this repo's hyphenated CLI-script naming
|
||||
@@ -130,6 +131,38 @@ def _parsed_functions() -> dict[str, dict[str, list[str]]]:
|
||||
return mt.parse_functions(Path(__file__).parent.parent / "functions")
|
||||
|
||||
|
||||
def _parsed_components() -> dict[str, list[str]]:
|
||||
import generate_component_registry as gcr
|
||||
|
||||
return gcr.collect_components()
|
||||
|
||||
|
||||
_C0_TAGS = {"always/on", "always/off"}
|
||||
_TAXONOMY_FILES = {
|
||||
"aliases": "01-c1-command-shadows.md",
|
||||
"autoexec": "02-c2-startup-side-effects.md",
|
||||
"overrides": "03-c3-key-and-environment-overrides.md",
|
||||
"integrations": "04-c4-terminal-and-tool-integration.md",
|
||||
"logging": "05-c5-logging-and-capture.md",
|
||||
"greeting": "06-c6-greeting-and-first-run-ui.md",
|
||||
}
|
||||
|
||||
|
||||
def _load_taxonomy() -> dict[str, set[str]]:
|
||||
"""{category: {sub-category slugs}}, parsed from `## <slug>` headings
|
||||
in each category's docs/manual/08-components-reference/ file."""
|
||||
ref_root = Path(__file__).parent / "manual" / "08-components-reference"
|
||||
taxonomy: dict[str, set[str]] = {}
|
||||
for category, filename in _TAXONOMY_FILES.items():
|
||||
_, body = mt.parse(ref_root / filename)
|
||||
taxonomy[category] = {
|
||||
m.group(1)
|
||||
for ln in body.split("\n")
|
||||
if (m := re.match(r"^## ([a-z][a-z0-9-]*)$", ln))
|
||||
}
|
||||
return taxonomy
|
||||
|
||||
|
||||
def test_every_categorised_function_produces_one_entry():
|
||||
import build_manual
|
||||
|
||||
@@ -184,6 +217,36 @@ def test_dependencies_resolve():
|
||||
assert not dangling, "unresolvable # DEPENDENCIES:\n " + "\n ".join(dangling)
|
||||
|
||||
|
||||
def test_every_component_resolves_to_a_taxonomy_entry():
|
||||
"""Every non-C0 # COMPONENT tag must resolve to a documented sub-category."""
|
||||
taxonomy = _load_taxonomy()
|
||||
unknown = []
|
||||
for identity, raw_lines in _parsed_components().items():
|
||||
for site, tag in mt.parse_component_lines(raw_lines):
|
||||
if tag in _C0_TAGS:
|
||||
continue
|
||||
if "/" not in tag:
|
||||
unknown.append(f"{identity}: malformed tag {tag!r}")
|
||||
continue
|
||||
category, subcat = tag.split("/", 1)
|
||||
if category not in taxonomy or subcat not in taxonomy[category]:
|
||||
unknown.append(f"{identity}: {tag}")
|
||||
assert not unknown, "# COMPONENT tags with no taxonomy entry:\n " + "\n ".join(unknown)
|
||||
|
||||
|
||||
def warn_c0_tags_never_combine_with_contradiction():
|
||||
"""Warn -- never fail -- on always/on + always/off contradictions this
|
||||
repo's own generator would warn about -- this is a direct repo-content
|
||||
check, independent of running the generator, so CI surfaces them even
|
||||
if someone forgets to regenerate. Warnings are non-fatal (spec §4.5);
|
||||
this exists to print them prominently in CI output."""
|
||||
from generate_component_registry import build_registry
|
||||
|
||||
_, warnings = build_registry(_parsed_components())
|
||||
for w in warnings:
|
||||
print(f" WARN {w}")
|
||||
|
||||
|
||||
def warn_public_functions_without_category():
|
||||
"""Warn — never fail — on a public function carrying no `# CATEGORY`.
|
||||
|
||||
@@ -205,6 +268,63 @@ def warn_public_functions_without_category():
|
||||
print(" " + ", ".join(orphans))
|
||||
|
||||
|
||||
# The guard's own supporting infrastructure: these files' bodies (function
|
||||
# signature, SYNOPSIS/EXAMPLE prose) legitimately contain the literal text
|
||||
# "__fish_config_op_enabled" without being a *caller* of the guard, so they
|
||||
# are permanently exempt from warn_functions_without_component's substring
|
||||
# check below.
|
||||
_GUARD_INFRA_FILES = {
|
||||
"__fish_config_op_enabled.fish",
|
||||
"__fish_config_op_cascade.fish",
|
||||
"__fish_config_op_registry_lookup.fish",
|
||||
}
|
||||
|
||||
|
||||
def warn_functions_without_component():
|
||||
"""Warn -- never fail -- on a documented function calling the
|
||||
opinionated guard but carrying no `# COMPONENT` section.
|
||||
|
||||
Mirrors warn_public_functions_without_category: a function that never
|
||||
opted into the header convention at all (no # SYNOPSIS) is silently
|
||||
out of scope, matching spec §4.5's fail-open tiering.
|
||||
"""
|
||||
repo = Path(__file__).parent.parent
|
||||
components = _parsed_components()
|
||||
orphans = []
|
||||
for p in list((repo / "functions").glob("*.fish")) + list((repo / "conf.d").glob("*.fish")):
|
||||
if p.name in _GUARD_INFRA_FILES:
|
||||
continue
|
||||
text = p.read_text(encoding="utf-8")
|
||||
if "__fish_config_op_enabled" not in text or "# SYNOPSIS" not in text:
|
||||
continue
|
||||
if p.stem not in components:
|
||||
orphans.append(str(p.relative_to(repo)))
|
||||
if orphans:
|
||||
print(f" WARN {len(orphans)} function(s) call the opinionated guard but lack # COMPONENT:")
|
||||
print(" " + ", ".join(sorted(orphans)))
|
||||
|
||||
|
||||
def warn_unused_taxonomy_entries():
|
||||
"""Warn -- never fail -- on a documented sub-category with zero tagged functions."""
|
||||
taxonomy = _load_taxonomy()
|
||||
used: dict[str, set[str]] = {c: set() for c in taxonomy}
|
||||
for raw_lines in _parsed_components().values():
|
||||
for _site, tag in mt.parse_component_lines(raw_lines):
|
||||
if tag in _C0_TAGS or "/" not in tag:
|
||||
continue
|
||||
category, subcat = tag.split("/", 1)
|
||||
if category in used:
|
||||
used[category].add(subcat)
|
||||
unused = [
|
||||
f"{category}/{subcat}"
|
||||
for category, subcats in taxonomy.items()
|
||||
for subcat in sorted(subcats - used[category])
|
||||
]
|
||||
if unused:
|
||||
print(f" WARN {len(unused)} taxonomy entr{'y has' if len(unused) == 1 else 'ies have'} zero tagged functions:")
|
||||
print(" " + ", ".join(unused))
|
||||
|
||||
|
||||
def _without_section_5(text: str) -> str:
|
||||
"""Drop `# 5. FUNCTIONS REFERENCE` through the start of section 6.
|
||||
|
||||
@@ -351,8 +471,8 @@ def test_prettify_splits_an_entry_block():
|
||||
"option table was not converted to a markdown table"
|
||||
)
|
||||
assert (
|
||||
"\nFalls back to /usr/bin/rm when trash is unavailable." in out
|
||||
), "trailing prose stayed indented"
|
||||
"\nFalls back to `/usr/bin/rm` when trash is unavailable." in out
|
||||
), "trailing prose stayed indented (or lost its path code span)"
|
||||
|
||||
|
||||
def test_as_table_converts_option_blocks():
|
||||
@@ -507,19 +627,42 @@ def test_as_ruled_table_rejects_ambiguous_columns():
|
||||
|
||||
|
||||
def test_prettify_leaves_reference_tables_alone():
|
||||
"""Column-aligned blocks are data, not shell, and must not be fenced."""
|
||||
"""Column-aligned blocks too small for a real table are data, not shell,
|
||||
and must not get shell syntax highlighting — but they still need SOME
|
||||
fence, since indentation alone doesn't survive MDX (see
|
||||
test_prettify_fallback_fences_instead_of_indenting)."""
|
||||
import build_manual
|
||||
|
||||
table = " XDG_CONFIG_HOME ~/.config\n XDG_CACHE_HOME ~/.cache"
|
||||
assert "```" not in build_manual.prettify(table), "a reference table got fenced"
|
||||
out = build_manual.prettify(table)
|
||||
assert "```fish" not in out, f"a reference table got shell-highlighted:\n{out}"
|
||||
assert "```text" in out, f"a reference table lost its fence:\n{out}"
|
||||
|
||||
binds = " n / nv / neovim nvim\n e edit"
|
||||
assert "```" not in build_manual.prettify(binds), "an abbreviation table got fenced"
|
||||
out = build_manual.prettify(binds)
|
||||
assert "```fish" not in out, f"an abbreviation table got shell-highlighted:\n{out}"
|
||||
assert "```text" in out, f"an abbreviation table lost its fence:\n{out}"
|
||||
|
||||
shell = " set -U __fish_user_dots_path /path/to/dots"
|
||||
assert "```fish" in build_manual.prettify(shell), "a shell block was not fenced"
|
||||
|
||||
|
||||
def test_prettify_fallback_fences_instead_of_indenting():
|
||||
"""The catch-all fallback must emit a fenced block, not bare indentation.
|
||||
|
||||
MDX (used for any page that also carries an <Aside> or <FileTree>) has
|
||||
no indented-code-block syntax: a plain 4-space-indented block silently
|
||||
renders as flowed paragraph text there, collapsing every line break. A
|
||||
fenced block is the only fallback shape that's safe in both MDX and
|
||||
plain Markdown.
|
||||
"""
|
||||
import build_manual
|
||||
|
||||
para = ["✘ 1 Fri Jun 12 00:51:21 2026 ← failed"]
|
||||
out = build_manual._render_para(para, None, False)
|
||||
assert out == "```text\n" + para[0] + "\n```", f"unexpected fallback output:\n{out}"
|
||||
|
||||
|
||||
def test_prettify_titles_paths_and_commented_examples():
|
||||
"""A bare file path or a leading '# in x.fish' comment become a title."""
|
||||
import build_manual
|
||||
@@ -686,6 +829,61 @@ def test_as_file_tree_accepts_deeper_trees():
|
||||
), f"unexpected file tree output:\n{out}"
|
||||
|
||||
|
||||
def test_as_file_tree_expands_functions_and_completions():
|
||||
"""The functions/ and completions/ branches list real directory contents."""
|
||||
import build_manual
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
functions_dir = root / "functions"
|
||||
functions_dir.mkdir()
|
||||
(functions_dir / "zeta.fish").write_text("")
|
||||
(functions_dir / "Alpha.fish").write_text("")
|
||||
|
||||
completions_dir = root / "completions"
|
||||
completions_dir.mkdir()
|
||||
(completions_dir / "bd.fish").write_text("")
|
||||
|
||||
original = dict(build_manual.EXPANDABLE_TREE_DIRS)
|
||||
build_manual.EXPANDABLE_TREE_DIRS["functions/"] = functions_dir
|
||||
build_manual.EXPANDABLE_TREE_DIRS["completions/"] = completions_dir
|
||||
try:
|
||||
para = [
|
||||
"~/.config/fish/",
|
||||
"├── functions/ Custom functions, one per file",
|
||||
"└── completions/ Tab completion scripts",
|
||||
]
|
||||
out = build_manual._as_file_tree(para)
|
||||
finally:
|
||||
build_manual.EXPANDABLE_TREE_DIRS.clear()
|
||||
build_manual.EXPANDABLE_TREE_DIRS.update(original)
|
||||
|
||||
assert out == (
|
||||
"<FileTree>\n"
|
||||
"- ~/.config/fish/\n"
|
||||
" - functions/ Custom functions, one per file\n"
|
||||
" - Alpha.fish\n"
|
||||
" - zeta.fish\n"
|
||||
" - completions/ Tab completion scripts\n"
|
||||
" - bd.fish\n"
|
||||
"</FileTree>"
|
||||
), f"unexpected file tree output:\n{out}"
|
||||
|
||||
|
||||
def test_as_file_tree_leaves_unrelated_branches_unexpanded():
|
||||
"""Only the mapped directory names get expanded; everything else is untouched."""
|
||||
import build_manual
|
||||
|
||||
para = [
|
||||
"$__fish_user_dots_path/",
|
||||
"├── secrets.fish API keys, tokens, passwords, personal identifiers",
|
||||
"└── local.fish Machine-specific paths, env vars, and sourcing secrets",
|
||||
]
|
||||
out = build_manual._as_file_tree(para)
|
||||
assert "secrets.fish" in out and "local.fish" in out
|
||||
assert len(out.splitlines()) == 5, f"unexpected expansion of unrelated branches:\n{out}"
|
||||
|
||||
|
||||
def test_customization_notes_render_as_aside():
|
||||
"""The real 07-customization NOTE paragraph converts to one intact <Aside>."""
|
||||
import build_manual
|
||||
@@ -698,7 +896,10 @@ def test_customization_notes_render_as_aside():
|
||||
assert aside.count(" - ") == 4, f"expected exactly 4 bullets inside the aside:\n{aside}"
|
||||
assert "- Command shadows (rm, cat, ls, ...) react immediately" in aside
|
||||
assert "- With aliases disabled, rm falls back to bare `command rm`" in aside
|
||||
assert "- Disabled integration commands (spwin, tab, split, hist, logs, upgrade)" in aside
|
||||
assert (
|
||||
"- Disabled integration commands "
|
||||
"(`spwin`, `tab`, `split`, `hist`, `logs`, `upgrade`)" in aside
|
||||
)
|
||||
assert "- On CachyOS, the distro fish config's own aliases" in aside
|
||||
|
||||
|
||||
@@ -827,6 +1028,454 @@ def test_site_avoids_reserved_dir():
|
||||
)
|
||||
|
||||
|
||||
def test_parse_component_lines_default_and_named_sites():
|
||||
lines = [
|
||||
"aliases/filesystem",
|
||||
"site exit-plain: overrides/key-bindings",
|
||||
"site logging-guard: logging/terminal-capture",
|
||||
"",
|
||||
" ",
|
||||
]
|
||||
got = mt.parse_component_lines(lines)
|
||||
assert got == [
|
||||
("", "aliases/filesystem"),
|
||||
("exit-plain", "overrides/key-bindings"),
|
||||
("logging-guard", "logging/terminal-capture"),
|
||||
], f"unexpected parse: {got}"
|
||||
|
||||
|
||||
def test_parse_components_includes_underscore_prefixed_and_uncategorised():
|
||||
"""Unlike parse_functions, parse_components has no # CATEGORY gate and
|
||||
no underscore exclusion -- every guarded identity must be visible."""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "__private_helper.fish").write_text(
|
||||
"# COMPONENT\n"
|
||||
"# logging/terminal-capture\n"
|
||||
"function __private_helper\n"
|
||||
"end\n"
|
||||
)
|
||||
(root / "no_category.fish").write_text(
|
||||
"# COMPONENT\n"
|
||||
"# aliases/filesystem\n"
|
||||
"#\n"
|
||||
"# SYNOPSIS\n"
|
||||
"# no_category\n"
|
||||
"function no_category\n"
|
||||
"end\n"
|
||||
)
|
||||
got = mt.parse_components(root)
|
||||
assert got["__private_helper"] == ["logging/terminal-capture"]
|
||||
assert got["no_category"] == ["aliases/filesystem"]
|
||||
|
||||
|
||||
def test_parse_components_resolves_multi_header_file_to_function_name():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "multi.fish").write_text(
|
||||
"# COMPONENT\n"
|
||||
"# aliases/filesystem\n"
|
||||
"function first_fn\n"
|
||||
"end\n"
|
||||
"\n"
|
||||
"# COMPONENT\n"
|
||||
"# aliases/network\n"
|
||||
"function second_fn\n"
|
||||
"end\n"
|
||||
)
|
||||
got = mt.parse_components(root)
|
||||
assert got == {
|
||||
"first_fn": ["aliases/filesystem"],
|
||||
"second_fn": ["aliases/network"],
|
||||
}, f"unexpected resolution: {got}"
|
||||
|
||||
|
||||
def test_parse_component_file_single_file():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = Path(d) / "config.fish"
|
||||
path.write_text(
|
||||
"# COMPONENT\n"
|
||||
"# site greeting-block: greeting/greeting-message\n"
|
||||
)
|
||||
got = mt.parse_component_file(path)
|
||||
assert got == {"config": ["site greeting-block: greeting/greeting-message"]}
|
||||
|
||||
|
||||
def test_build_registry_strips_on_off_contradiction_with_warning():
|
||||
import generate_component_registry as gcr
|
||||
|
||||
components = {"contradictory_fn": ["always/on", "always/off", "aliases/filesystem"]}
|
||||
registry, warnings = gcr.build_registry(components)
|
||||
assert registry["contradictory_fn:"] == ["aliases/filesystem"], (
|
||||
f"the non-contradictory tag should survive: {registry}"
|
||||
)
|
||||
assert len(warnings) == 1 and "contradictory_fn" in warnings[0]
|
||||
|
||||
|
||||
def test_build_registry_drops_empty_effective_tag_sets():
|
||||
import generate_component_registry as gcr
|
||||
|
||||
components = {"only_contradictory": ["always/on", "always/off"]}
|
||||
registry, warnings = gcr.build_registry(components)
|
||||
assert "only_contradictory:" not in registry, (
|
||||
"a site stripped down to nothing must produce no registry entry "
|
||||
"(fail-open: absence of an entry already means always/on at guard time)"
|
||||
)
|
||||
assert len(warnings) == 1
|
||||
|
||||
|
||||
def test_build_registry_keeps_sites_independent():
|
||||
import generate_component_registry as gcr
|
||||
|
||||
components = {
|
||||
"smart_exit": [
|
||||
"site exit-plain: overrides/key-bindings",
|
||||
"site logging-guard: logging/terminal-capture",
|
||||
]
|
||||
}
|
||||
registry, warnings = gcr.build_registry(components)
|
||||
assert registry["smart_exit:exit-plain"] == ["overrides/key-bindings"]
|
||||
assert registry["smart_exit:logging-guard"] == ["logging/terminal-capture"]
|
||||
assert not warnings
|
||||
|
||||
|
||||
def test_collect_components_merges_identity_collisions_across_sources():
|
||||
"""functions/auto-pull.fish and conf.d/auto-pull.fish both self-identify
|
||||
as "auto-pull" at runtime -- the guard only ever has the bare
|
||||
status current-function/basename string to look up with -- so
|
||||
collect_components must concatenate their raw COMPONENT lines
|
||||
rather than letting conf.d's entry silently overwrite functions'."""
|
||||
import generate_component_registry as gcr
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
(root / "functions").mkdir()
|
||||
(root / "conf.d").mkdir()
|
||||
(root / "functions" / "auto-pull.fish").write_text(
|
||||
"# COMPONENT\n"
|
||||
"# autoexec/sync\n"
|
||||
"function auto-pull\n"
|
||||
"end\n"
|
||||
)
|
||||
(root / "conf.d" / "auto-pull.fish").write_text(
|
||||
"# COMPONENT\n"
|
||||
"# autoexec/sync\n"
|
||||
)
|
||||
(root / "config.fish").write_text("")
|
||||
|
||||
orig_repo = gcr.REPO
|
||||
try:
|
||||
gcr.REPO = root
|
||||
got = gcr.collect_components()
|
||||
finally:
|
||||
gcr.REPO = orig_repo
|
||||
|
||||
assert got["auto-pull"] == ["autoexec/sync", "autoexec/sync"], (
|
||||
f"both sources' tags should survive the merge, not overwrite: {got}"
|
||||
)
|
||||
|
||||
|
||||
def test_render_registry_is_valid_fish_and_round_trips():
|
||||
"""Sourcing render()'s output must leave the two arrays in the exact
|
||||
shape __fish_config_op_registry_lookup expects -- checked via the real
|
||||
lookup helper (functions/__fish_config_op_registry_lookup.fish, Task
|
||||
2) rather than re-parsing the generated text by hand.
|
||||
|
||||
Written to a real temp file rather than piped through `source
|
||||
/dev/stdin`: fish 3.7 (Ubuntu 24.04's packaged version, used in CI)
|
||||
rejects `/dev/stdin` with "is not a file" when it's backed by a pipe,
|
||||
even though fish 4.8 accepts it -- sourcing an actual file on disk is
|
||||
the portable form across fish versions.
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import generate_component_registry as gcr
|
||||
|
||||
registry = {
|
||||
"rm:": ["aliases/filesystem"],
|
||||
"smart_exit:exit-plain": ["overrides/key-bindings"],
|
||||
}
|
||||
text = gcr.render(registry)
|
||||
|
||||
repo = Path(__file__).parent.parent
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
registry_path = Path(d) / "registry.fish"
|
||||
registry_path.write_text(text)
|
||||
proc = subprocess.run(
|
||||
[
|
||||
"fish", "-c",
|
||||
f"source {repo}/functions/__fish_config_op_registry_lookup.fish; "
|
||||
f"source {registry_path}; "
|
||||
"__fish_config_op_registry_lookup rm ''; echo status=$status",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert "aliases/filesystem" in proc.stdout, f"unexpected output: {proc.stdout!r} {proc.stderr!r}"
|
||||
assert "status=0" in proc.stdout, f"lookup did not report found: {proc.stdout!r}"
|
||||
|
||||
|
||||
def test_build_manual_regenerates_registry_before_building():
|
||||
"""docs/build-manual.py must regenerate the registry as a pre-step."""
|
||||
import build_manual
|
||||
|
||||
assert hasattr(build_manual, "generate_component_registry"), (
|
||||
"build-manual.py must import generate_component_registry so its "
|
||||
"main() can be called as a pre-step before --site/--concat run"
|
||||
)
|
||||
|
||||
|
||||
def test_committed_registry_matches_headers():
|
||||
"""The committed conf.d/__fish_config_op_registry.fish must match what
|
||||
generate_component_registry.py would produce right now from the current
|
||||
`# COMPONENT` headers -- otherwise CI has nothing catching drift."""
|
||||
import generate_component_registry as gcr
|
||||
|
||||
registry, _ = gcr.build_registry(gcr.collect_components())
|
||||
assert gcr.render(registry) == gcr.OUTPUT.read_text(), (
|
||||
"conf.d/__fish_config_op_registry.fish is stale — run "
|
||||
"__fish_config_op_registry_rebuild"
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# codespans: inline code spans added at site-render time
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_REPO = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def _spans(text: str) -> str:
|
||||
return codespans.add_code_spans(text, codespans.vocabulary(_REPO))
|
||||
|
||||
|
||||
def test_codespans_wraps_each_half_of_a_flag_pair():
|
||||
"""`-a/--all` is the manual's usual way of naming a flag and its alias."""
|
||||
got = _spans("Use -a/--all to include both, or -s/--stdout to print.")
|
||||
assert got == "Use `-a`/`--all` to include both, or `-s`/`--stdout` to print.", got
|
||||
|
||||
|
||||
def test_codespans_wraps_override_variables_and_snake_case():
|
||||
got = _spans("Disabled via __fish_config_op_aliases; see _fdc_bins and fish_greeting.")
|
||||
assert got == (
|
||||
"Disabled via `__fish_config_op_aliases`; see `_fdc_bins` and `fish_greeting`."
|
||||
), got
|
||||
|
||||
|
||||
def test_codespans_wraps_paths_vars_env_and_key_chords():
|
||||
cases = {
|
||||
"Sourced from ~/.config/fish/config.fish.": (
|
||||
"Sourced from `~/.config/fish/config.fish`."
|
||||
),
|
||||
"honoring $XDG_CONFIG_HOME/aichat/roles/cli.md.": (
|
||||
"honoring `$XDG_CONFIG_HOME/aichat/roles/cli.md`."
|
||||
),
|
||||
"Launches with NO_TMUX=1 set.": "Launches with `NO_TMUX=1` set.",
|
||||
"end the session with Ctrl-D or Ctrl+Alt+F.": (
|
||||
"end the session with `Ctrl-D` or `Ctrl+Alt+F`."
|
||||
),
|
||||
}
|
||||
for source, want in cases.items():
|
||||
assert _spans(source) == want, f"{source!r} -> {_spans(source)!r}"
|
||||
|
||||
|
||||
def test_codespans_leaves_existing_spans_and_fences_alone():
|
||||
body = "\n".join(
|
||||
[
|
||||
"Already `--wrapped` here.",
|
||||
"",
|
||||
"```fish",
|
||||
"rm -e --empty ~/.config/fish",
|
||||
"```",
|
||||
"",
|
||||
"## --not-a-flag-heading",
|
||||
"",
|
||||
"<Aside type=\"note\" title=\"Note\">",
|
||||
"See --verbose.",
|
||||
"</Aside>",
|
||||
]
|
||||
)
|
||||
got = _spans(body).split("\n")
|
||||
assert got[0] == "Already `--wrapped` here.", got[0]
|
||||
assert got[3] == "rm -e --empty ~/.config/fish", "a fenced line was rewritten"
|
||||
assert got[6] == "## --not-a-flag-heading", "a heading was rewritten"
|
||||
assert got[8].startswith("<Aside"), "component markup was rewritten"
|
||||
assert got[9] == "See `--verbose`.", "aside body was not processed"
|
||||
|
||||
|
||||
def test_codespans_leaves_file_tree_bodies_alone():
|
||||
"""<FileTree> list items are filenames the component renders itself."""
|
||||
body = "<FileTree>\n- ~/.config/fish/\n - config.fish\n</FileTree>"
|
||||
assert _spans(body) == body
|
||||
|
||||
|
||||
def test_codespans_leaves_links_and_urls_alone():
|
||||
cases = [
|
||||
"See [`fish-deps`](/reference/dependency-management/fish-deps/) for more.",
|
||||
"Clone from ssh://git@host/owner/repo.git today.",
|
||||
"Docs live at https://fish.rootiest.fyi/07-customization/ online.",
|
||||
]
|
||||
for source in cases:
|
||||
assert _spans(source) == source, f"{source!r} -> {_spans(source)!r}"
|
||||
|
||||
|
||||
def test_codespans_ignores_prose_that_only_looks_like_code():
|
||||
"""Every one of these has bitten a looser version of the pattern set."""
|
||||
cases = [
|
||||
"the registry -- not the cache -- is rebuilt",
|
||||
"the everyday filesystem-inspection and -modification shadows",
|
||||
"expand !^, !*, !-N and !?str? abbreviations",
|
||||
"grep/cp/mv/wget flag injection",
|
||||
"resolved and/or rejected",
|
||||
"a _really_ important caveat",
|
||||
"the TCP and AGPL acronyms",
|
||||
]
|
||||
for source in cases:
|
||||
assert _spans(source) == source, f"{source!r} -> {_spans(source)!r}"
|
||||
|
||||
|
||||
def test_codespans_wraps_a_command_shadow_chain():
|
||||
got = _spans("Falls back through ov -> bat -> man -> less -> cat.")
|
||||
assert got == "Falls back through `ov` -> `bat` -> `man` -> `less` -> `cat`.", got
|
||||
|
||||
|
||||
def test_codespans_wraps_long_runs_of_tool_names_only():
|
||||
"""Two names, one of them an English word, is a sentence -- not a list."""
|
||||
got = _spans("Supports paru, yay, pacman, apt, dnf, zypper, yum, brew, and pkg.")
|
||||
assert got == (
|
||||
"Supports `paru`, `yay`, `pacman`, `apt`, `dnf`, `zypper`, `yum`, "
|
||||
"`brew`, and `pkg`."
|
||||
), got
|
||||
got = _spans("the custom rm function, trashy, or trash-cli")
|
||||
assert got == "the custom rm function, `trashy`, or trash-cli", got
|
||||
|
||||
|
||||
def test_codespans_wraps_whole_cells_only_in_command_columns():
|
||||
"""A column of expansions becomes code; a column of prose stays prose."""
|
||||
commands = "\n".join(
|
||||
[
|
||||
"| Abbreviation | Description |",
|
||||
"|---|---|",
|
||||
"| `..` | cd .. |",
|
||||
"| `jctl` | journalctl -p 3 -xb |",
|
||||
"| `sudu` | sudo -s |",
|
||||
"| `kt` | kitty (Kitty only) |",
|
||||
]
|
||||
).split("\n")
|
||||
got = _spans("\n".join(commands)).split("\n")
|
||||
assert got[2] == "| `..` | `cd ..` |", got[2]
|
||||
assert got[3] == "| `jctl` | `journalctl -p 3 -xb` |", got[3]
|
||||
assert got[4] == "| `sudu` | `sudo -s` |", got[4]
|
||||
assert got[5] == "| `kt` | `kitty` (Kitty only) |", got[5]
|
||||
|
||||
prose = "\n".join(
|
||||
[
|
||||
"| Command | Active behavior |",
|
||||
"|---|---|",
|
||||
"| `cd` | zoxide frecency-based navigation |",
|
||||
"| `top` | btop resource monitor |",
|
||||
"| `mkdir` | verbose path-tree display on creation |",
|
||||
"| `history` | timestamps prepended to every entry |",
|
||||
]
|
||||
)
|
||||
got = _spans(prose).split("\n")
|
||||
assert got[2] == "| `cd` | `zoxide` frecency-based navigation |", got[2]
|
||||
assert got[3] == "| `top` | `btop` resource monitor |", got[3]
|
||||
|
||||
|
||||
def test_codespans_merges_abutting_new_spans():
|
||||
"""One command line reads as one span, not as a row of adjacent ones."""
|
||||
got = _spans("| `ls` | eza -l -a --icons --hyperlink | system ls |")
|
||||
assert got == "| `ls` | `eza -l -a --icons --hyperlink` | system ls |", got
|
||||
|
||||
|
||||
def test_codespans_vocabulary_comes_from_the_deps_catalog():
|
||||
"""A tool added to _fish_deps_catalog.fish needs no second list."""
|
||||
names = codespans.dependency_names(_REPO)
|
||||
assert {"fzf", "zoxide", "prettyping"} <= names, sorted(names)[:20]
|
||||
vocab = codespans.vocabulary(_REPO)
|
||||
assert "zoxide" in vocab.strict, "an unambiguous tool name should be wrappable"
|
||||
assert "find" not in vocab.strict, "an English word must not be wrappable on sight"
|
||||
assert "find" in vocab.full, "…but it still counts as a command-line opener"
|
||||
|
||||
|
||||
def test_codespans_leaves_indented_code_blocks_alone():
|
||||
"""A four-space block is code, whatever it happens to contain.
|
||||
|
||||
The concat keeps the indented form pandoc wants, so unlike the site
|
||||
this pass meets real indented blocks -- the table of contents among
|
||||
them, which is nothing but a list of command names.
|
||||
"""
|
||||
body = "\n".join(
|
||||
[
|
||||
"Pick a viewer:",
|
||||
"",
|
||||
" 1. ov + bat section navigation",
|
||||
" 2. less plain text with --jump",
|
||||
"",
|
||||
"Then run config-help.",
|
||||
]
|
||||
)
|
||||
got = _spans(body).split("\n")
|
||||
assert got[2] == " 1. ov + bat section navigation", got[2]
|
||||
assert got[3] == " 2. less plain text with --jump", got[3]
|
||||
assert got[5] == "Then run `config-help`.", "prose after the block was skipped"
|
||||
|
||||
|
||||
def test_codespans_reach_the_man_page_pipeline():
|
||||
"""Prose is marked identically wherever it is rendered.
|
||||
|
||||
`build_concat` runs the same pass `build_site` does, so a token the
|
||||
site typesets as code is typeset as code in the man page and
|
||||
`config-help` too, instead of only where the SSOT hand-wrote a
|
||||
backtick.
|
||||
"""
|
||||
import build_manual
|
||||
|
||||
manual = Path(__file__).parent / "manual"
|
||||
text = build_manual.build_concat(manual)
|
||||
assert "`local.fish`" in text, "prose code spans never reached the concat"
|
||||
assert "`tmux`" in text, "a vocabulary command was not wrapped in the concat"
|
||||
|
||||
|
||||
def test_concat_code_spans_never_straddle_a_line():
|
||||
"""`config-help` pairs backticks one line at a time.
|
||||
|
||||
Its `string replace` filters run per line, so a span split across a
|
||||
line break -- ``run `fish-deps\\nupdate` `` -- leaves an unpaired
|
||||
backtick the pager then shows literally. Markdown is happy to wrap
|
||||
one, so nothing else catches this.
|
||||
"""
|
||||
import build_manual
|
||||
|
||||
text = build_manual.build_concat(Path(__file__).parent / "manual")
|
||||
odd = [
|
||||
(n, line)
|
||||
for n, line in enumerate(text.split("\n"), 1)
|
||||
if line.count("`") % 2
|
||||
]
|
||||
assert not odd, f"unpaired backtick, span wraps a line: {odd[:3]}"
|
||||
|
||||
|
||||
def test_concat_section_five_stays_verbatim():
|
||||
"""Section 5's entries are indented blocks, not prose.
|
||||
|
||||
They are generated from the `functions/*.fish` headers and pandoc sets
|
||||
them verbatim, so a backtick there would be a literal character on the
|
||||
page rather than markup.
|
||||
"""
|
||||
import build_manual
|
||||
|
||||
text = build_manual.build_concat(Path(__file__).parent / "manual")
|
||||
body = text.split("\n# 5. ", 1)[1].split("\n# 6. ", 1)[0]
|
||||
offenders = [
|
||||
line for line in body.split("\n") if line.startswith(" ") and "`" in line
|
||||
]
|
||||
assert not offenders, f"backticks inside verbatim entries: {offenders[:3]}"
|
||||
|
||||
|
||||
TESTS = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
|
||||
|
||||
@@ -840,6 +1489,9 @@ def main() -> int:
|
||||
print(f" FAIL {t.__name__}: {e}", file=sys.stderr)
|
||||
failed += 1
|
||||
warn_public_functions_without_category()
|
||||
warn_functions_without_component()
|
||||
warn_unused_taxonomy_entries()
|
||||
warn_c0_tags_never_combine_with_contradiction()
|
||||
print(f"\n{len(TESTS) - failed}/{len(TESTS)} passed")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# autoexec/venv
|
||||
#
|
||||
# SYNOPSIS
|
||||
# __auto_source_fallback_venv
|
||||
#
|
||||
@@ -16,7 +19,7 @@ function __auto_source_fallback_venv --on-variable PWD
|
||||
status --is-command-substitution; and return
|
||||
|
||||
# Opinionated guard (C2): no automatic venv activation when disabled.
|
||||
__fish_config_op_enabled __fish_config_op_autoexec; or return
|
||||
__fish_config_op_enabled (status current-function); or return
|
||||
|
||||
# 1. Skip if direnv is already managing this directory
|
||||
if set -q DIRENV_DIR; or test -e ".envrc"
|
||||
|
||||
@@ -182,7 +182,7 @@ function __config_settings_draw
|
||||
|
||||
# ── Keybind hint ──────────────────────────────────────────────────────
|
||||
# string pad is width-aware (arrows count as 1 column)
|
||||
set -l hint " ↑↓/kj move ←→/hl set Tab page q quit"
|
||||
set -l hint " ↑↓/kj move ←→/hl set Enter sub-cats Tab pg q quit"
|
||||
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint) $c_reset
|
||||
|
||||
# ── Bottom border ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_draw_subcat <cur_row> <cur_scope> <category_var>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Renders the sub-category drill-down page for one C1-C6 category:
|
||||
# the category's own toggle at the top (still meaningful as the cascade
|
||||
# default for its sub-categories), then one row per sub-category from
|
||||
# __config_settings_subcats, sized dynamically instead of the fixed
|
||||
# 6-row layout __config_settings_draw uses for the category list.
|
||||
# Follows the same width-tier and center-padding conventions as
|
||||
# __config_settings_draw so the panel doesn't visibly jump between the
|
||||
# two pages.
|
||||
#
|
||||
# Label and description fields are defensively truncated to their field
|
||||
# width before padding (string pad only ever grows a string, never
|
||||
# shrinks it) -- sub-category labels/descriptions are static data from
|
||||
# __config_settings_subcats, not authored per width-tier the way
|
||||
# __config_settings_draw's own category descriptions are, so a couple of
|
||||
# them are longer than the narrower tiers' fields (e.g. "Notifications"
|
||||
# is 13 chars against a 12-char label field; several descriptions run
|
||||
# well past the 17-char field at the narrowest tier). Truncating keeps
|
||||
# the box perfectly rectangular in every case instead of only in the
|
||||
# cases the static text happens to fit.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# cur_row 0-based highlighted row (0 = the category's own toggle;
|
||||
# 1..N = sub-category rows)
|
||||
# cur_scope "universal" or "session"
|
||||
# category_var One of the six __fish_config_op_<category> names
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always
|
||||
#
|
||||
# EXAMPLE
|
||||
# __config_settings_draw_subcat 1 universal __fish_config_op_aliases
|
||||
function __config_settings_draw_subcat
|
||||
set -l cur_row $argv[1]
|
||||
set -l cur_scope $argv[2]
|
||||
set -l category_var $argv[3]
|
||||
|
||||
set -l c_ok (set_color green)
|
||||
set -l c_err (set_color red)
|
||||
set -l c_dim (set_color brblack)
|
||||
set -l c_sel (set_color --bold magenta)
|
||||
set -l c_head (set_color --bold cyan)
|
||||
set -l c_reset (set_color normal)
|
||||
|
||||
set -l rows (__config_settings_subcats $category_var)
|
||||
set -l n (count $rows)
|
||||
|
||||
# ── Width tier: matches __config_settings_draw's 6-col-per-side steps ──
|
||||
set -l iw 50
|
||||
if test "$COLUMNS" -ge 90
|
||||
set iw 76
|
||||
else if test "$COLUMNS" -ge 86
|
||||
set iw 72
|
||||
else if test "$COLUMNS" -ge 82
|
||||
set iw 68
|
||||
end
|
||||
set -l HBR (string repeat -n $iw '─')
|
||||
set -l p (string repeat -n (math --scale=0 "max(0, ($COLUMNS - ($iw + 2)) / 2)") ' ')
|
||||
|
||||
# Label field is 13 wide (one wider than __config_settings_draw's 12) --
|
||||
# the longest real sub-category label ("Notifications") is 13 chars.
|
||||
# Description field absorbs the difference so every row still totals
|
||||
# iw+2, matching the surrounding box lines exactly.
|
||||
set -l label_w 13
|
||||
set -l desc_w (math $iw - 34)
|
||||
|
||||
set -l cat_label (string replace -r '^__fish_config_op_' '' -- $category_var)
|
||||
# Scope indicator: toggling a row on this page writes -U (Universal,
|
||||
# persistent) or -g (Session, this-shell-only) -- the title must say
|
||||
# which, since it isn't otherwise visible anywhere on the page.
|
||||
set -l scope_label Universal
|
||||
test "$cur_scope" = session; and set scope_label Session
|
||||
# Title layout is "┌─ Sub-categories: <label> (<scope>) ───┐"; the
|
||||
# dash count must absorb every visible char added around cat_label so
|
||||
# the line still totals iw+2, matching the surrounding box exactly --
|
||||
# see the DESCRIPTION doc comment above for why this is hand-verified,
|
||||
# not eyeballed.
|
||||
set -l title_dashes (math $iw - (string length -- $cat_label) - (string length -- $scope_label) - 22)
|
||||
printf '%s┌─%s Sub-categories: %s (%s)%s %s┐\n' \
|
||||
$p $c_head "$cat_label" $scope_label "$c_reset" (string repeat -n (math "max(0, $title_dashes)") '─')
|
||||
|
||||
printf '%s│%s│\n' $p $HBR
|
||||
|
||||
# Row 0: the category's own toggle, still meaningful as the cascade
|
||||
# default any DEFAULT-valued sub-category below falls back to.
|
||||
set -l cat_val (__config_settings_get_val $category_var $cur_scope)
|
||||
set -l cat_badge
|
||||
switch $cat_val
|
||||
case on
|
||||
set cat_badge "$c_ok"" ON$c_reset"
|
||||
case off
|
||||
set cat_badge "$c_err""OFF $c_reset"
|
||||
case '*'
|
||||
set cat_badge "$c_dim""DEFAULT$c_reset"
|
||||
end
|
||||
set -l cat_curs " "
|
||||
if test $cur_row -eq 0
|
||||
set cat_curs "$c_sel▶$c_reset "
|
||||
end
|
||||
set -l cat_desc "cascade default"
|
||||
if test $iw -ge 68
|
||||
set cat_desc "default for all sub-cats below"
|
||||
end
|
||||
if test $iw -ge 72
|
||||
set cat_desc "default for all sub-categories below"
|
||||
end
|
||||
printf '%s│ %s%s [ %s ] %s │\n' $p $cat_curs \
|
||||
(string pad -r -w $label_w -- "(category)") $cat_badge \
|
||||
(string pad -r -w $desc_w -- (string sub -l $desc_w -- $cat_desc))
|
||||
|
||||
printf '%s│ %s │\n' $p (string repeat -n (math $iw - 6) '─')
|
||||
|
||||
for i in (seq 1 $n)
|
||||
set -l fields (string split -- \t $rows[$i])
|
||||
set -l slug $fields[1]
|
||||
set -l label $fields[2]
|
||||
set -l desc $fields[3]
|
||||
set -l subcat_var "$category_var"_(string replace -a -- '-' '_' $slug)
|
||||
|
||||
set -l val (__config_settings_get_val $subcat_var $cur_scope)
|
||||
set -l badge
|
||||
switch $val
|
||||
case on
|
||||
set badge "$c_ok"" ON$c_reset"
|
||||
case off
|
||||
set badge "$c_err""OFF $c_reset"
|
||||
case '*'
|
||||
set badge "$c_dim""DEFAULT$c_reset"
|
||||
end
|
||||
|
||||
set -l curs " "
|
||||
if test $i -eq $cur_row
|
||||
set curs "$c_sel▶$c_reset "
|
||||
end
|
||||
|
||||
set -l lpad (string pad -r -w $label_w -- (string sub -l $label_w -- $label))
|
||||
set -l dpad (string pad -r -w $desc_w -- (string sub -l $desc_w -- $desc))
|
||||
printf '%s│ %s%s [ %s ] %s │\n' $p $curs $lpad $badge $dpad
|
||||
end
|
||||
|
||||
printf '%s│%s│\n' $p $HBR
|
||||
set -l hint " ↑↓/kj move ←→/hl set Esc back q quit"
|
||||
printf '%s│%s%s%s│\n' $p $c_dim (string pad -r -w $iw -- $hint) $c_reset
|
||||
printf '%s└%s┘\n' $p $HBR
|
||||
end
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
# DESCRIPTION
|
||||
# Reads a single keypress directly from the controlling terminal in raw
|
||||
# mode and echoes a normalized token naming the key. Bypasses fish's
|
||||
# `read` builtin, whose interactive line editor swallows Tab and arrow
|
||||
# keys (and prints a `read> ` prompt) — none of which is usable for a TUI.
|
||||
# read builtin, whose interactive line editor swallows Tab and arrow
|
||||
# keys (and prints a "read> " prompt) — none of which is usable for a TUI.
|
||||
#
|
||||
# The terminal is put into raw, no-echo mode with a 0.1s inter-byte timer
|
||||
# (`stty raw -echo min 1 time 1`) so a multi-byte escape sequence (e.g.
|
||||
# (stty raw -echo min 1 time 1) so a multi-byte escape sequence (e.g.
|
||||
# an arrow key, ESC [ A) is captured in one read while a lone key returns
|
||||
# promptly. Original terminal settings are always restored before return.
|
||||
#
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __config_settings_subcats <category_variable>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Prints the sub-category rows for one C1-C6 category, one per line as
|
||||
# "<slug>\t<label>\t<description>". This is a fish-native copy of the
|
||||
# taxonomy authored in docs/manual/08-components-reference/*.md (Task 9)
|
||||
# -- kept here as a static table rather than parsed from markdown at
|
||||
# draw time, since config-settings redraws on every keypress and every
|
||||
# terminal resize.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# category_variable One of the six __fish_config_op_<category> names
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always
|
||||
#
|
||||
# RETURNS
|
||||
# Tab-separated "<slug>\t<label>\t<description>" rows, one per line
|
||||
#
|
||||
# EXAMPLE
|
||||
# __config_settings_subcats __fish_config_op_aliases
|
||||
function __config_settings_subcats --description 'List the sub-categories for one opinionated-component category'
|
||||
switch $argv[1]
|
||||
case __fish_config_op_aliases
|
||||
printf '%s\t%s\t%s\n' \
|
||||
filesystem Filesystem "ls, cat, cd, du, mkdir, rm, mv, zoxide" \
|
||||
search Search "rg" \
|
||||
network Network "ping, ssh, yt-dlp" \
|
||||
monitor Monitor "top" \
|
||||
shell-tools Shell-tools "bash, less, help" \
|
||||
dev-tools Dev-tools "claude, edit, agy"
|
||||
case __fish_config_op_autoexec
|
||||
printf '%s\t%s\t%s\n' \
|
||||
plugin-management Plugins "Fisher bootstrap" \
|
||||
pkg-wrappers Pkg-wrappers "paru/yay wrapper generation" \
|
||||
venv Venv "Python auto-activation" \
|
||||
telemetry Telemetry "WakaTime hook bootstrap" \
|
||||
sync Sync "auto-pull, user-dots symlink"
|
||||
case __fish_config_op_overrides
|
||||
printf '%s\t%s\t%s\n' \
|
||||
key-bindings Key-bindings "vi-mode, autopair, puffer, bang-bang" \
|
||||
environment Environment "PATH, PAGER, EDITOR, CDPATH" \
|
||||
prompt Prompt "Starship, right prompt, theme + FZF colors" \
|
||||
privacy Privacy "DO_NOT_TRACK, DISABLE_TELEMETRY"
|
||||
case __fish_config_op_integrations
|
||||
printf '%s\t%s\t%s\n' \
|
||||
terminal-abbrs Term-abbrs "Kitty/WezTerm abbreviations" \
|
||||
window-mgmt Window-mgmt "spwin, tab, split" \
|
||||
notifications Notifications "done, WakaTime hook" \
|
||||
history-logs History-logs "hist, logs" \
|
||||
pkg-upgrade Pkg-upgrade "upgrade"
|
||||
case __fish_config_op_logging
|
||||
printf '%s\t%s\t%s\n' \
|
||||
terminal-capture Term-capture "Kitty watcher, smart_exit scrollback" \
|
||||
multiplexer-capture Multiplexer "tmux, zellij" \
|
||||
pkg-logs Pkg-logs "paru/yay AUR log wrappers"
|
||||
case __fish_config_op_greeting
|
||||
printf '%s\t%s\t%s\n' \
|
||||
first-run First-run "welcome banner" \
|
||||
greeting-message Greeting "fish_greeting override"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __fish_config_op_cascade <category_variable> [<subcategory_variable>]
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Evaluates the opt-out cascade for one C1-C6 classification: an
|
||||
# explicit truthy/falsy sub-category variable wins outright; otherwise
|
||||
# falls back to the category variable; otherwise falls back to the
|
||||
# master switch __fish_config_opinionated. A category in the opt-in
|
||||
# list (currently just __fish_config_op_logging, C5) defaults to
|
||||
# disabled when nothing in the chain is explicit, and the master switch
|
||||
# cannot enable it -- this is data-driven here instead of a hardcoded
|
||||
# string comparison, so any sub-category nested under an opt-in
|
||||
# category inherits "off unless explicit" for free through the cascade,
|
||||
# with no per-sub-category special-casing.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# category_variable Name (without $) of a C1-C6 category variable
|
||||
# subcategory_variable Optional name (without $) of a sub-category
|
||||
# variable nested under that category
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Enabled
|
||||
# 1 Disabled
|
||||
#
|
||||
# EXAMPLE
|
||||
# __fish_config_op_cascade __fish_config_op_aliases
|
||||
# __fish_config_op_cascade __fish_config_op_aliases __fish_config_op_aliases_filesystem
|
||||
function __fish_config_op_cascade --description 'Evaluate the sub-category -> category -> master opt-out cascade'
|
||||
set -l opt_in_categories __fish_config_op_logging
|
||||
|
||||
set -l chain $argv[1]
|
||||
if test (count $argv) -ge 2 -a -n "$argv[2]"
|
||||
set chain $argv[2] $argv[1]
|
||||
end
|
||||
|
||||
for var_name in $chain
|
||||
__fish_variable_check $var_name
|
||||
set -l s $status
|
||||
if test $s -eq 0
|
||||
return 0
|
||||
end
|
||||
if test $s -eq 1
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
# Every variable in the chain was unset/unrecognized. chain[-1] is
|
||||
# always the category variable (present whether or not a
|
||||
# sub-category was given) -- opt-in categories default to disabled
|
||||
# and the master switch cannot override that.
|
||||
if contains -- $chain[-1] $opt_in_categories
|
||||
return 1
|
||||
end
|
||||
|
||||
__fish_variable_check __fish_config_opinionated
|
||||
if test $status -eq 1
|
||||
return 1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
@@ -2,63 +2,68 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __fish_config_op_enabled <category_variable>
|
||||
# __fish_config_op_enabled <identity> [<site>]
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Guard predicate for opinionated components (AGENTS.md Task #3).
|
||||
# The category variable is evaluated first via __fish_variable_check:
|
||||
# an explicit truthy value (1/true/yes/on/y) enables the component
|
||||
# regardless of the master switch; an explicit falsy value
|
||||
# (0/false/no/off/n) disables it regardless of the master switch.
|
||||
# Only when the category variable is unset or unrecognized (status 2
|
||||
# or 3) does the master switch __fish_config_opinionated apply: a
|
||||
# falsy master disables every unset-category component at once.
|
||||
# Unset master with unset category → enabled (active by default).
|
||||
# Guard predicate for an opinionated component. <identity> is computed
|
||||
# by the caller, never hand-typed as a category name: (status
|
||||
# current-function) inside a function body, (status basename) at
|
||||
# top-level conf.d/*.fish or config.fish code (fish has no API for a
|
||||
# callee to introspect its own caller, so the caller must compute and
|
||||
# pass its own identity -- see the spec's "A note on self-identifying").
|
||||
# A trailing .fish is stripped so a status-basename identity and a
|
||||
# status-current-function identity land in the same key space.
|
||||
#
|
||||
# One exception: __fish_config_op_logging (C5) is opt-in, because it
|
||||
# writes terminal output to disk. Unset or unrecognized means disabled,
|
||||
# and the master switch cannot enable it — only an explicit truthy
|
||||
# value turns logging on.
|
||||
# Looks up "<identity>:<site>" (site defaults to the empty/unnamed site)
|
||||
# in the generated component registry. No registry entry (unclassified,
|
||||
# or a doc header with no # COMPONENT section) resolves to enabled --
|
||||
# the same fail-open default as an explicit always/on tag, so
|
||||
# user-authored and third-party functions that never call this guard in
|
||||
# the first place are unaffected, and one that somehow does is never
|
||||
# silently broken by a missing header. A found always/off tag
|
||||
# disables unconditionally; a found always/on tag enables
|
||||
# unconditionally, short-circuiting before any other tagged
|
||||
# sub-category is evaluated. Otherwise every tagged sub-category must
|
||||
# pass the cascade (AND semantics).
|
||||
#
|
||||
# ARGUMENTS
|
||||
# category_variable Name (without $) of the category opt-out variable:
|
||||
# __fish_config_op_aliases, __fish_config_op_autoexec,
|
||||
# __fish_config_op_overrides,
|
||||
# __fish_config_op_integrations,
|
||||
# __fish_config_op_logging, or
|
||||
# __fish_config_op_greeting
|
||||
# identity (status current-function) or (status basename)
|
||||
# site Optional site slug (see # COMPONENT header grammar);
|
||||
# omitted for the default/unnamed site
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Component enabled (category explicitly truthy; or category unset and master not falsy — except C5 logging, which requires an explicit truthy value)
|
||||
# 1 Component disabled (category explicitly falsy; or category unset and master falsy; or C5 logging unset; or no argument with falsy master)
|
||||
# 0 Component enabled
|
||||
# 1 Component disabled
|
||||
#
|
||||
# EXAMPLE
|
||||
# if __fish_config_op_enabled __fish_config_op_aliases
|
||||
# if not __fish_config_op_enabled (status current-function)
|
||||
# alias grep='grep --color=auto'
|
||||
# end
|
||||
function __fish_config_op_enabled --description 'Check whether an opinionated component category is enabled'
|
||||
__fish_variable_check $argv[1]
|
||||
set -l cat_status $status
|
||||
# if not __fish_config_op_enabled (status current-function) exit-plain
|
||||
# builtin exit
|
||||
# end
|
||||
function __fish_config_op_enabled --description 'Guard for an opinionated component, identified by its own caller'
|
||||
set -l identity (string replace -r '\.fish$' '' -- $argv[1])
|
||||
set -l site $argv[2]
|
||||
|
||||
if test $cat_status -eq 0
|
||||
set -l tags (__fish_config_op_registry_lookup $identity $site)
|
||||
if test $status -ne 0
|
||||
return 0
|
||||
end
|
||||
|
||||
if test $cat_status -eq 1
|
||||
if contains -- always/off $tags
|
||||
return 1
|
||||
end
|
||||
|
||||
# C5 logging is opt-in: it writes terminal output to disk, so an unset or
|
||||
# unrecognized value means off — the master switch cannot enable it.
|
||||
if test "$argv[1]" = __fish_config_op_logging
|
||||
return 1
|
||||
if contains -- always/on $tags
|
||||
return 0
|
||||
end
|
||||
|
||||
# Status 3 (garbage) defers to master — an unrecognized value is not an opt-out.
|
||||
__fish_variable_check __fish_config_opinionated
|
||||
if test $status -eq 1
|
||||
return 1
|
||||
for tag in $tags
|
||||
set -l parts (string split -m 1 -- / $tag)
|
||||
set -l category_var "__fish_config_op_$parts[1]"
|
||||
set -l subcat_var "__fish_config_op_$parts[1]_"(string replace -a -- '-' '_' $parts[2])
|
||||
__fish_config_op_cascade $category_var $subcat_var
|
||||
or return 1
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __fish_config_op_registry_lookup <identity> <site>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Looks up the "<identity>:<site>" key in the generated component
|
||||
# registry ($__fish_config_op_registry_keys /
|
||||
# $__fish_config_op_registry_values, sourced from
|
||||
# conf.d/__fish_config_op_registry.fish at shell startup) and prints its
|
||||
# tags, one per line. <site> is empty string for the default/unnamed
|
||||
# site.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# identity Function name (status current-function) or file basename
|
||||
# with any trailing .fish stripped (status basename)
|
||||
# site Site slug, or empty string for the default site
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Found: tags printed to stdout
|
||||
# 1 Not found: nothing printed
|
||||
#
|
||||
# RETURNS
|
||||
# Matching tags, one per line, printed to stdout
|
||||
#
|
||||
# EXAMPLE
|
||||
# set -l tags (__fish_config_op_registry_lookup rm "")
|
||||
# or return 0 # unclassified: caller treats this as always-on
|
||||
function __fish_config_op_registry_lookup --description 'Look up the COMPONENT tags for an identity:site pair'
|
||||
set -l key "$argv[1]:$argv[2]"
|
||||
set -l idx (contains -i -- $key $__fish_config_op_registry_keys)
|
||||
if test -z "$idx"
|
||||
return 1
|
||||
end
|
||||
string split ' ' -- $__fish_config_op_registry_values[$idx]
|
||||
return 0
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# __fish_config_op_registry_rebuild
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Regenerates conf.d/__fish_config_op_registry.fish from every
|
||||
# # COMPONENT header in functions/*.fish, conf.d/*.fish, and
|
||||
# config.fish, then re-sources it into the current session so the
|
||||
# change takes effect immediately. Run manually after editing a
|
||||
# # COMPONENT header; never run automatically at shell startup --
|
||||
# parsing every header on every new shell would be wasted work on
|
||||
# every session that isn't actively editing a header.
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Registry regenerated
|
||||
# 1 python3 is not available, or generation failed
|
||||
#
|
||||
# EXAMPLE
|
||||
# __fish_config_op_registry_rebuild
|
||||
function __fish_config_op_registry_rebuild --description 'Regenerate the opinionated-component registry from # COMPONENT headers'
|
||||
if not type -q python3
|
||||
echo "__fish_config_op_registry_rebuild: python3 not found" >&2
|
||||
return 1
|
||||
end
|
||||
python3 "$__fish_config_dir/docs/generate_component_registry.py"
|
||||
or return 1
|
||||
source "$__fish_config_dir/conf.d/__fish_config_op_registry.fish"
|
||||
end
|
||||
@@ -1,6 +1,9 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# logging/terminal-capture
|
||||
#
|
||||
# SYNOPSIS
|
||||
# __fish_config_sync_logging
|
||||
#
|
||||
@@ -29,7 +32,7 @@ function __fish_config_sync_logging --description 'Sync C5 logging state: sentin
|
||||
set -l yay_wrapper "$HOME/.local/bin/yay"
|
||||
set -l wrapper_version 1
|
||||
|
||||
if __fish_config_op_enabled __fish_config_op_logging
|
||||
if __fish_config_op_enabled (status current-function)
|
||||
# Logging enabled: remove sentinel and regenerate wrappers if binaries exist
|
||||
rm -f $sentinel
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# autoexec/sync
|
||||
#
|
||||
# SYNOPSIS
|
||||
# __fish_user_dots_link
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Manages the git-ignored `user-dots` convenience symlink in the fish config
|
||||
# Manages the git-ignored user-dots convenience symlink in the fish config
|
||||
# directory ($__fish_config_dir/user-dots), pointing it at the resolved
|
||||
# $__fish_user_dots_path so the private overlay can be browsed from
|
||||
# ~/.config/fish/.
|
||||
@@ -41,7 +44,7 @@ function __fish_user_dots_link --description 'Manage the user-dots convenience s
|
||||
end
|
||||
|
||||
# Enabled: creation is a C2 startup side-effect.
|
||||
__fish_config_op_enabled __fish_config_op_autoexec; or return 0
|
||||
__fish_config_op_enabled (status current-function); or return 0
|
||||
|
||||
test -d "$__fish_user_dots_path"; or return 0
|
||||
if test -L "$link"
|
||||
|
||||
@@ -5,23 +5,47 @@
|
||||
# __fzf_inline_picker
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Opens an interactive fzf session and injects the selected item directly
|
||||
# into the command line at the cursor position. Bound to @@ by default.
|
||||
# Repaints the prompt after selection or cancellation.
|
||||
# Bound to the @ key. Self-inserts @ normally; on a second consecutive @
|
||||
# (detected by looking behind at the current token rather than making fish
|
||||
# buffer ahead for a chord), opens an interactive fzf session listing both
|
||||
# files and directories under the current directory, with a preview pane
|
||||
# (bat-highlighted for text, image-rendered for pictures via
|
||||
# _fzf_preview_file), and replaces the bare @ with the selected item.
|
||||
# Cancelling leaves a literal @@ behind. Repaints the prompt after
|
||||
# selection or cancellation.
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always; no-op if fzf is cancelled
|
||||
# 0 Always
|
||||
#
|
||||
# EXAMPLE
|
||||
# # Press @@ at the command prompt to open fzf and insert the selected item.
|
||||
# # Press @ twice at the command prompt to open fzf and insert the
|
||||
# # selected item in place of the second @, then keep typing to extend it
|
||||
# # (e.g. a trailing /subdir).
|
||||
function __fzf_inline_picker
|
||||
# Open fzf and capture selection
|
||||
set -l selection (fzf)
|
||||
if test (commandline -t) = @
|
||||
# Directly use fd binary to avoid output buffering delay caused by a fd
|
||||
# alias, if any. Debian-based distros install fd as fdfind.
|
||||
set -f fd_cmd (command -v fdfind || command -v fd || echo "fd")
|
||||
set -f --append fd_cmd --color=always $fzf_fd_opts
|
||||
|
||||
if test -n "$selection"
|
||||
# Injects text instantly at the cursor
|
||||
commandline -i (string escape -- $selection)
|
||||
set -l selection ($fd_cmd 2>/dev/null | _fzf_wrapper --ansi \
|
||||
--height=90% \
|
||||
--layout=reverse \
|
||||
--border=rounded \
|
||||
--border-label=' Insert Path ' \
|
||||
--prompt='@@ -> ' \
|
||||
--header='Enter: Insert Ctrl-/: Toggle Preview Esc: Cancel' \
|
||||
--bind='ctrl-/:toggle-preview' \
|
||||
--preview='_fzf_preview_file {}' \
|
||||
--preview-window='right:50%:wrap:border-left')
|
||||
|
||||
if test -n "$selection"
|
||||
commandline -t -- (string escape -- $selection)
|
||||
else
|
||||
commandline -t -- @@
|
||||
end
|
||||
commandline -f repaint
|
||||
else
|
||||
commandline -i @
|
||||
end
|
||||
# Refresh the line display
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Appends any patterns not already covered by the project's .gitignore.
|
||||
# Uses `git check-ignore` for accurate rule matching (catches wildcards
|
||||
# Uses git check-ignore for accurate rule matching (catches wildcards
|
||||
# and parent-dir globs). Falls back to a plain string search when the
|
||||
# root is not a git repository. Leading `/` is stripped from each pattern
|
||||
# root is not a git repository. Leading / is stripped from each pattern
|
||||
# before the path-based check so root-anchored patterns (e.g. /AGENTS.md)
|
||||
# are matched correctly.
|
||||
#
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# _agents_repo_ensure_symlink <link> <target>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Idempotently makes <link> a symlink pointing at the directory <target>.
|
||||
#
|
||||
# Only directories are ever linked. The agent file-editing tools resolve a
|
||||
# symlinked directory transparently but refuse to write through a
|
||||
# symlinked file, so linking a file would silently break every later edit;
|
||||
# a non-directory target is refused outright.
|
||||
#
|
||||
# A missing target is refused rather than linked, because a dangling
|
||||
# memory/ symlink makes agent memory writes fail -- strictly worse than
|
||||
# having no backup at all.
|
||||
#
|
||||
# When <link> is an existing real directory, its contents are copied into
|
||||
# <target> without clobbering (cp -n) before the directory is replaced by
|
||||
# the link, so adopting a populated live directory never overwrites the
|
||||
# copy already in the vault.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# link Path that should become the symlink
|
||||
# target Existing directory the link should point at
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Link is correct (created, repinned, or already right)
|
||||
# 1 Refused (non-directory target, missing target, non-directory link) or
|
||||
# a copy, remove, or link operation failed
|
||||
#
|
||||
# RETURNS
|
||||
# A single "→ ..." progress line on stdout when something changed;
|
||||
# nothing at all when the link was already correct.
|
||||
#
|
||||
# EXAMPLE
|
||||
# _agents_repo_ensure_symlink ~/.claude/projects/-home-u-proj/memory \
|
||||
# ~/.local/share/agent-vault/projects/host-user-proj/claude/memory
|
||||
function _agents_repo_ensure_symlink --argument-names link target
|
||||
test -n "$link" -a -n "$target"; or return 1
|
||||
|
||||
if test -e "$target"; and not test -d "$target"
|
||||
echo "_agents_repo_ensure_symlink: refusing non-directory target: $target" >&2
|
||||
return 1
|
||||
end
|
||||
if not test -d "$target"
|
||||
echo "_agents_repo_ensure_symlink: target does not exist: $target" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
if test -L "$link"
|
||||
set -l cur (path resolve "$link")
|
||||
set -l want (path resolve "$target")
|
||||
test "$cur" = "$want"; and return 0
|
||||
rm -f "$link"; or return 1
|
||||
else if test -d "$link"
|
||||
set -l contents (command ls -A "$link" 2>/dev/null)
|
||||
if test (count $contents) -gt 0
|
||||
command cp -rn "$link/." "$target/"; or return 1
|
||||
end
|
||||
rm -rf "$link"; or return 1
|
||||
else if test -e "$link"
|
||||
echo "_agents_repo_ensure_symlink: refusing to replace non-directory: $link" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
mkdir -p (path dirname "$link"); or return 1
|
||||
ln -s "$target" "$link"; or return 1
|
||||
echo "→ Linked "(path basename "$link")" → $target"
|
||||
end
|
||||
+15
-11
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -18,13 +18,13 @@
|
||||
# conveniences (e.g. backs one wrapper
|
||||
# function) that only matter if you already
|
||||
# use that specific tool. Skipped by
|
||||
# `fish-deps install`/`sync` unless
|
||||
# `--optional` (or `--all`) is passed.
|
||||
# fish-deps install/sync unless
|
||||
# --optional (or --all) is passed.
|
||||
# term Terminal Emulators — GPU-accelerated terminal emulators
|
||||
# (kitty, wezterm) that only matter if one
|
||||
# of them is your actual terminal. Skipped
|
||||
# by `fish-deps install`/`sync` unless
|
||||
# `--terminals` (or `--all`) is passed.
|
||||
# by fish-deps install/sync unless
|
||||
# --terminals (or --all) is passed.
|
||||
# int Integrations — opt-in third-party services requiring
|
||||
# their own account/setup (wakatime,
|
||||
# tailscale).
|
||||
@@ -36,27 +36,27 @@ function _fish_deps_catalog
|
||||
set -g _fdc_bins \
|
||||
uv cargo fish starship fzf zoxide direnv paru yay \
|
||||
wakatime tailscale \
|
||||
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen
|
||||
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen mpv vlc
|
||||
|
||||
set -g _fdc_tiers \
|
||||
rec rec req rec req rec rec rec rec \
|
||||
int int \
|
||||
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt
|
||||
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt opt opt
|
||||
|
||||
set -g _fdc_cargo \
|
||||
"" "" "" starship "" zoxide "" "" "" \
|
||||
"" "" \
|
||||
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" ""
|
||||
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" "" "" ""
|
||||
|
||||
set -g _fdc_pm \
|
||||
uv cargo fish starship fzf zoxide direnv "" yay \
|
||||
wakatime tailscale \
|
||||
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen
|
||||
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen mpv vlc
|
||||
|
||||
set -g _fdc_special \
|
||||
curl-uv rustup-installer git-cargo-fish curl-installer fzf-update "" "" paru-build yay-build \
|
||||
wakatime-binary "" \
|
||||
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" ""
|
||||
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" "" "" ""
|
||||
end
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
@@ -17,6 +17,8 @@ function _fzf_preview_file --description "Print a preview for the given file bas
|
||||
if set --query fzf_preview_file_cmd
|
||||
# need to escape quotes to make sure eval receives file_path as a single arg
|
||||
eval "$fzf_preview_file_cmd '$file_path'"
|
||||
else if command -q file; and string match -q 'image/*' -- (command file --brief --mime-type -- "$file_path" 2>/dev/null)
|
||||
_fzf_preview_image "$file_path"
|
||||
else
|
||||
bat --style=numbers --color=always "$file_path"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# helper function for _fzf_preview_file
|
||||
function _fzf_preview_image --description "Render an image preview using the best available tool for the current terminal."
|
||||
set -f file_path $argv
|
||||
|
||||
set -l cols (set --query FZF_PREVIEW_COLUMNS; and echo $FZF_PREVIEW_COLUMNS; or echo 80)
|
||||
set -l lines (set --query FZF_PREVIEW_LINES; and echo $FZF_PREVIEW_LINES; or echo 24)
|
||||
set -l left (set --query FZF_PREVIEW_LEFT; and echo $FZF_PREVIEW_LEFT; or echo 0)
|
||||
set -l top (set --query FZF_PREVIEW_TOP; and echo $FZF_PREVIEW_TOP; or echo 0)
|
||||
|
||||
# Terminals that implement the kitty graphics protocol (kitty itself,
|
||||
# WezTerm, and Ghostty) can render via `kitten icat` even when it's not
|
||||
# the active terminal, since `kitten` is just an escape-sequence emitter.
|
||||
set -l kitty_capable 0
|
||||
if set --query KITTY_WINDOW_ID
|
||||
or test "$TERM" = xterm-kitty
|
||||
or contains -- "$TERM_PROGRAM" WezTerm ghostty
|
||||
or set --query GHOSTTY_RESOURCES_DIR
|
||||
set kitty_capable 1
|
||||
end
|
||||
|
||||
if test $kitty_capable -eq 1; and command -q kitten
|
||||
# --place is measured from the top-left of the whole terminal, not the
|
||||
# preview pane, so FZF_PREVIEW_LEFT/TOP (added to fzf specifically for
|
||||
# this integration) are required to land the image in the right spot.
|
||||
command kitten icat --clear --transfer-mode=memory --unicode-placeholder \
|
||||
--stdin=no --place="$cols"x"$lines"@"$left"x"$top" -- "$file_path" 2>/dev/null
|
||||
else if command -q chafa
|
||||
command chafa --size="$cols"x"$lines" -- "$file_path"
|
||||
else if command -q viu
|
||||
command viu --width $cols -- "$file_path"
|
||||
else if command -q timg
|
||||
command timg -g "$cols"x"$lines" -- "$file_path"
|
||||
else
|
||||
set_color yellow
|
||||
echo "No image preview tool available (install kitty, chafa, viu, or timg)."
|
||||
set_color normal
|
||||
command file --brief -- "$file_path"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# SYNOPSIS
|
||||
# _fzf_preview_media <file_path>
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Preview helper for play-media. Looks for a thumbnail already generated
|
||||
# by a desktop file manager (Dolphin, Nautilus, GNOME Videos, ...) in the
|
||||
# freedesktop thumbnail cache and renders it via _fzf_preview_image if
|
||||
# found. Otherwise falls back to ffprobe-formatted metadata (duration,
|
||||
# codec, resolution, tags) when ffprobe is installed, or plain file
|
||||
# output as a last resort. Neither the thumbnail cache lookup nor ffprobe
|
||||
# are tracked in fish-deps: both are best-effort, matching how the
|
||||
# image-preview tool chain (kitten/chafa/viu/timg) is already handled.
|
||||
#
|
||||
# ARGUMENTS
|
||||
# file_path Path to the audio/video file to preview
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Always
|
||||
#
|
||||
# EXAMPLE
|
||||
# _fzf_preview_media ./Videos/clip.mp4
|
||||
function _fzf_preview_media
|
||||
set -f file_path $argv
|
||||
if not test -e "$file_path"
|
||||
echo "$file_path doesn't exist." >&2
|
||||
return 0
|
||||
end
|
||||
|
||||
set -l abs_path (realpath -- "$file_path" 2>/dev/null)
|
||||
|
||||
# freedesktop.org thumbnail spec: md5 of the file:// URI names the
|
||||
# cached thumbnail. Dolphin/Nautilus/GNOME Videos already populate this
|
||||
# cache, so reuse it instead of generating a new thumbnail ourselves.
|
||||
set -l uri "file://"(string escape --style=url -- "$abs_path")
|
||||
set -l hash (echo -n "$uri" | md5sum | string split ' ')[1]
|
||||
set -l cache_home (set --query XDG_CACHE_HOME; and echo $XDG_CACHE_HOME; or echo "$HOME/.cache")
|
||||
|
||||
for size in normal large x-large xx-large
|
||||
set -l thumb "$cache_home/thumbnails/$size/$hash.png"
|
||||
if test -f "$thumb"
|
||||
_fzf_preview_image "$thumb"
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
if command -q ffprobe
|
||||
set -l info (command ffprobe -v error -show_entries \
|
||||
format=duration,bit_rate:format_tags=title,artist,album:stream=codec_name,codec_type,width,height \
|
||||
-of default=noprint_wrappers=1 -- "$abs_path" 2>/dev/null)
|
||||
if test -n "$info"
|
||||
printf '%s\n' $info
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
command file --brief -- "$abs_path"
|
||||
end
|
||||
@@ -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,6 +1,9 @@
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# COMPONENT
|
||||
# logging/multiplexer-capture
|
||||
#
|
||||
# SYNOPSIS
|
||||
# _zellij_dump_log
|
||||
#
|
||||
@@ -8,7 +11,7 @@
|
||||
# Captures the current Zellij pane's scrollback to a timestamped log in
|
||||
# SCROLLBACK_HISTORY_DIR (default ~/.terminal_history). Zellij has no live
|
||||
# output-streaming facility like tmux's pipe-pane, so this performs a one-shot
|
||||
# `zellij action dump-screen --full` — intended to run on shell exit. Old
|
||||
# zellij action dump-screen --full — intended to run on shell exit. Old
|
||||
# zellij_*.log files are pruned via _prune_terminal_logs to stay within
|
||||
# SCROLLBACK_HISTORY_MAX_FILES.
|
||||
#
|
||||
@@ -23,7 +26,7 @@
|
||||
# EXAMPLE
|
||||
# _zellij_dump_log
|
||||
function _zellij_dump_log --description 'Dump the current Zellij pane scrollback to a log file, with pruning'
|
||||
__fish_config_op_enabled __fish_config_op_logging; or return 0
|
||||
__fish_config_op_enabled (status current-function); or return 0
|
||||
set -q ZELLIJ; or return 0
|
||||
type -q zellij; or return 0
|
||||
|
||||
|
||||
+63
-56
@@ -4,6 +4,9 @@
|
||||
# CATEGORY
|
||||
# 12-ai-and-developer-tools
|
||||
#
|
||||
# DEPENDENCIES
|
||||
# _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
|
||||
#
|
||||
# SYNOPSIS
|
||||
# agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
|
||||
# [-q | --quiet] [-s | --silent] [-h | --help]
|
||||
@@ -50,11 +53,16 @@
|
||||
#
|
||||
# With no flags, runs both --agents and --plugins setup; --agents re-runs
|
||||
# only the AGENTS.md / symlink step and --plugins only the plans/specs/
|
||||
# devlogs wiring step. Managed paths are added to .gitignore. The sub-repo
|
||||
# is pulled first when it has an upstream, and at the end of every
|
||||
# invocation any uncommitted changes inside it are auto-committed so
|
||||
# agent-made edits are captured automatically. Fully idempotent: a second
|
||||
# run produces no output and no new commits.
|
||||
# devlogs wiring step. Managed paths are added to .gitignore. At the end
|
||||
# of every invocation any uncommitted changes inside the sub-repo are
|
||||
# auto-committed so agent-made edits are captured automatically. Fully
|
||||
# idempotent: a second run produces no output and no new commits.
|
||||
#
|
||||
# The commit is local only. Nothing here fetches or pushes: the wrappers
|
||||
# call this synchronously before starting an agent, and a network round
|
||||
# trip there blocks the launch until an unreachable remote times out and
|
||||
# can prompt for credentials with nobody watching. A sub-repo that has an
|
||||
# upstream is pulled by hand, on the user's own schedule.
|
||||
#
|
||||
# Called automatically by the claude and agy wrappers on every invocation.
|
||||
#
|
||||
@@ -68,7 +76,8 @@
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 Setup completed successfully
|
||||
# 1 Fatal error (git init failed, move failed, etc.)
|
||||
# 1 Fatal error (git init failed, move failed, the AGENTS/ commit was
|
||||
# rejected, or an unresolved rebase blocked it)
|
||||
#
|
||||
# EXAMPLE
|
||||
# agents-init
|
||||
@@ -77,7 +86,7 @@
|
||||
# agents-init --quiet
|
||||
function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec files and plugin dirs'
|
||||
set -l c_head (set_color --bold cyan)
|
||||
set -l c_cmd (set_color --bold white)
|
||||
set -l c_cmd (set_color --bold)
|
||||
set -l c_flag (set_color yellow)
|
||||
set -l c_ok (set_color green)
|
||||
set -l c_warn (set_color yellow)
|
||||
@@ -165,7 +174,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
test $verbose -eq 1; and echo "$c_ok→ Created AGENTS/.version (1.0.0)$c_reset"
|
||||
end
|
||||
|
||||
set -l _tools (_agents_init_install_tools "$agents_dir")
|
||||
set -l _tools (_agents_repo_install_tools "$agents_dir")
|
||||
if test -n "$_tools"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok$_tools$c_reset"
|
||||
@@ -259,38 +268,26 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked AGENTS/CLAUDE.md → AGENTS/AGENTS.md$c_reset"
|
||||
end
|
||||
|
||||
# ── Root symlink: AGENTS.md → AGENTS/AGENTS.md ───────────────────────
|
||||
set -l _need_link 0
|
||||
if not test -L "$root/AGENTS.md"
|
||||
set _need_link 1
|
||||
else if test (readlink "$root/AGENTS.md") != AGENTS/AGENTS.md
|
||||
rm -f "$root/AGENTS.md"
|
||||
set _need_link 1
|
||||
end
|
||||
if test $_need_link -eq 1
|
||||
if not ln -s AGENTS/AGENTS.md "$root/AGENTS.md"
|
||||
echo "$c_err""Error: could not create AGENTS.md symlink$c_reset" >&2
|
||||
return 1
|
||||
# Root symlinks point at files, not directories, so they cannot use
|
||||
# _agents_repo_ensure_symlink (which is directory-only by design).
|
||||
for pair in "AGENTS.md:AGENTS/AGENTS.md" "CLAUDE.md:AGENTS/CLAUDE.md"
|
||||
set -l name (string split -f1 ':' -- $pair)
|
||||
set -l want (string split -f2 ':' -- $pair)
|
||||
set -l need 0
|
||||
if not test -L "$root/$name"
|
||||
set need 1
|
||||
else if test (readlink "$root/$name") != "$want"
|
||||
rm -f "$root/$name"
|
||||
set need 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked AGENTS.md → AGENTS/AGENTS.md$c_reset"
|
||||
end
|
||||
|
||||
# ── Root symlink: CLAUDE.md → AGENTS/CLAUDE.md ───────────────────────
|
||||
set -l _need_link 0
|
||||
if not test -L "$root/CLAUDE.md"
|
||||
set _need_link 1
|
||||
else if test (readlink "$root/CLAUDE.md") != AGENTS/CLAUDE.md
|
||||
rm -f "$root/CLAUDE.md"
|
||||
set _need_link 1
|
||||
end
|
||||
if test $_need_link -eq 1
|
||||
if not ln -s AGENTS/CLAUDE.md "$root/CLAUDE.md"
|
||||
echo "$c_err""Error: could not create CLAUDE.md symlink$c_reset" >&2
|
||||
return 1
|
||||
if test $need -eq 1
|
||||
if not ln -s "$want" "$root/$name"
|
||||
echo "$c_err""Error: could not create $name symlink$c_reset" >&2
|
||||
return 1
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked $name → $want$c_reset"
|
||||
end
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok→ Linked CLAUDE.md → AGENTS/CLAUDE.md$c_reset"
|
||||
end
|
||||
|
||||
# ── .gitignore ────────────────────────────────────────────────────────
|
||||
@@ -461,24 +458,30 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
end
|
||||
|
||||
# ──────────────────────── Auto-commit AGENTS/ ────────────────────────────
|
||||
# Pull first when an upstream is configured so the local .version reflects
|
||||
# any remote bumps before we add to it (no-op for local-only repos).
|
||||
if git -C "$agents_dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
|
||||
git -C "$agents_dir" pull --rebase --autostash -q 2>/dev/null
|
||||
end
|
||||
git -C "$agents_dir" add -A 2>/dev/null
|
||||
set -l status_out (git -C "$agents_dir" status --porcelain 2>/dev/null)
|
||||
if test -n "$status_out"
|
||||
set -l msg "chore: sync AGENTS repository"
|
||||
test $did_init -eq 1; and set msg "chore: initialize AGENTS repository"
|
||||
if git -C "$agents_dir" -c commit.gpgsign=false commit -q -m "$msg" 2>/dev/null
|
||||
set changed 1
|
||||
if test $verbose -eq 1
|
||||
set -l sha (git -C "$agents_dir" rev-parse --short HEAD 2>/dev/null)
|
||||
set -l realmsg (git -C "$agents_dir" log -1 --pretty=%s 2>/dev/null)
|
||||
echo "$c_ok→ Committed AGENTS/ ($sha) $c_dim$realmsg$c_reset"
|
||||
end
|
||||
end
|
||||
# Purely local: no fetch, no push. This function runs synchronously on
|
||||
# every agent launch, and a network round trip there blocks the launch
|
||||
# for as long as an unreachable remote takes to time out. Committing
|
||||
# never needed one -- see _agents_repo_sync.
|
||||
#
|
||||
# Every way the commit can fail is an arm of its own. A sync that did
|
||||
# not commit means agent-made edits were not captured, so it is a
|
||||
# failure rather than a line to walk past -- and the missing `-ne 0`
|
||||
# arm was not a cosmetic gap: fish resolves a branchless `if` to 0, so
|
||||
# a hook-rejected commit fell straight through to a reported success.
|
||||
set -l msg "chore: sync AGENTS repository"
|
||||
test $did_init -eq 1; and set msg "chore: initialize AGENTS repository"
|
||||
set -l sync_out (_agents_repo_sync "$agents_dir" "$msg")
|
||||
set -l sync_rc $status
|
||||
set -l failed 0
|
||||
if test $sync_rc -eq 2
|
||||
echo "$c_warn→ AGENTS/ has an unresolved rebase; nothing committed$c_reset" >&2
|
||||
set failed 1
|
||||
else if test $sync_rc -ne 0
|
||||
echo "$c_err""Error: the AGENTS/ commit failed; nothing recorded$c_reset" >&2
|
||||
set failed 1
|
||||
else if test -n "$sync_out"
|
||||
set changed 1
|
||||
test $verbose -eq 1; and echo "$c_ok$sync_out$c_reset"
|
||||
end
|
||||
|
||||
# Quiet summary: one line at the end, only if something actually changed
|
||||
@@ -489,4 +492,8 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
|
||||
echo "$c_ok→ Synced AGENTS scaffolding$c_reset"
|
||||
end
|
||||
end
|
||||
|
||||
# Explicit, because the branchless `if` above resolves to 0 and would
|
||||
# otherwise be this function's exit status.
|
||||
test $failed -eq 0
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+14
-5
@@ -4,8 +4,11 @@
|
||||
# CATEGORY
|
||||
# 12-ai-and-developer-tools
|
||||
#
|
||||
# COMPONENT
|
||||
# aliases/dev-tools
|
||||
#
|
||||
# DEPENDENCIES
|
||||
# agents-init
|
||||
# agents-init, agents-vault
|
||||
#
|
||||
# SYNOPSIS
|
||||
# agy [ARGS...]
|
||||
@@ -15,9 +18,14 @@
|
||||
# sub-repository is initialized and any agent-made changes are committed
|
||||
# before launch. Delegates all scaffold and commit logic to agents-init
|
||||
# --quiet (full setup), which ensures AGENTS/ is scaffolded and CLAUDE.md
|
||||
# is symlinked to AGENTS/AGENTS.md in the current project. Arguments are
|
||||
# forwarded verbatim to the real agy binary, except for -r/--resume which
|
||||
# are translated to -c/--continue.
|
||||
# is symlinked to AGENTS/AGENTS.md in the current project.
|
||||
#
|
||||
# Also syncs the host-scoped agent memory vault (agents-vault). agy has
|
||||
# no session-end hook, so its memory is captured on the next launch
|
||||
# rather than at session end.
|
||||
#
|
||||
# Arguments are forwarded verbatim to the real agy binary, except for
|
||||
# -r/--resume which are translated to -c/--continue.
|
||||
#
|
||||
# Opinionated component (C1): when disabled via __fish_config_op_aliases
|
||||
# (or the __fish_config_opinionated master), the command is passed through
|
||||
@@ -35,12 +43,13 @@
|
||||
# agy -i "initial prompt"
|
||||
# agy models
|
||||
function agy --wraps=agy --description 'agy wrapper: auto-initializes AGENTS/ sub-repo before launch'
|
||||
if not __fish_config_op_enabled __fish_config_op_aliases
|
||||
if not __fish_config_op_enabled (status current-function)
|
||||
command agy $argv
|
||||
return $status
|
||||
end
|
||||
|
||||
agents-init --quiet
|
||||
agents-vault --quiet
|
||||
|
||||
for i in (seq (count $argv))
|
||||
if test "$argv[$i]" = "-r"
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
# CATEGORY
|
||||
# 04-git-and-version-control
|
||||
#
|
||||
# COMPONENT
|
||||
# autoexec/sync
|
||||
#
|
||||
# SYNOPSIS
|
||||
# auto-pull [list]
|
||||
# auto-pull add [PATH]
|
||||
@@ -40,7 +43,7 @@
|
||||
# auto-pull remove qmk_firmware
|
||||
function auto-pull --description 'Manage the auto-pull repository registry'
|
||||
set -l c_head (set_color --bold cyan)
|
||||
set -l c_cmd (set_color --bold white)
|
||||
set -l c_cmd (set_color --bold)
|
||||
set -l c_flag (set_color yellow)
|
||||
set -l c_ok (set_color green)
|
||||
set -l c_warn (set_color yellow)
|
||||
@@ -136,7 +139,7 @@ function auto-pull --description 'Manage the auto-pull repository registry'
|
||||
return 0
|
||||
|
||||
case status
|
||||
if __fish_config_op_enabled __fish_config_op_autoexec
|
||||
if __fish_config_op_enabled (status current-function)
|
||||
echo "$c_ok""auto-pull: ENABLED$c_reset $c_dim(C2 auto-execution on)$c_reset"
|
||||
else
|
||||
echo "$c_warn""auto-pull: DISABLED$c_reset $c_dim(via __fish_config_op_autoexec)$c_reset"
|
||||
|
||||
+4
-1
@@ -4,6 +4,9 @@
|
||||
# CATEGORY
|
||||
# 14-miscellaneous
|
||||
#
|
||||
# COMPONENT
|
||||
# aliases/shell-tools
|
||||
#
|
||||
# SYNOPSIS
|
||||
# bash [args...]
|
||||
#
|
||||
@@ -18,7 +21,7 @@
|
||||
# bash
|
||||
function bash --wraps='bash' --description 'bash switches to bash shell'
|
||||
# Opinionated guard (C1): fall back to bare command bash when disabled.
|
||||
if not __fish_config_op_enabled __fish_config_op_aliases
|
||||
if not __fish_config_op_enabled (status current-function)
|
||||
command bash $argv
|
||||
return $status
|
||||
end
|
||||
|
||||
+5
-1
@@ -25,7 +25,11 @@
|
||||
function bkg --description 'Execute bkg'
|
||||
# Check if a command was provided as an argument.
|
||||
if test -z "$argv[1]"
|
||||
echo "Usage: bkg <command> [arguments...]"
|
||||
set -l c_head (set_color --bold cyan)
|
||||
set -l c_cmd (set_color --bold)
|
||||
set -l c_arg (set_color cyan)
|
||||
set -l c_reset (set_color normal)
|
||||
echo "$c_head""Usage:$c_reset $c_cmd""bkg$c_reset $c_arg""<command> [arguments...]$c_reset"
|
||||
return 1
|
||||
end
|
||||
|
||||
|
||||
+4
-1
@@ -4,6 +4,9 @@
|
||||
# CATEGORY
|
||||
# 01-file-and-directory
|
||||
#
|
||||
# COMPONENT
|
||||
# aliases/filesystem
|
||||
#
|
||||
# SYNOPSIS
|
||||
# cat [args...]
|
||||
#
|
||||
@@ -21,7 +24,7 @@
|
||||
# cat ~/projects/myapp
|
||||
function cat --wraps='bat' --description 'Use bat for files, ls for directories, and raw cat for ANSI logs'
|
||||
# Opinionated guard (C1): fall back to bare command cat when disabled.
|
||||
if not __fish_config_op_enabled __fish_config_op_aliases
|
||||
if not __fish_config_op_enabled (status current-function)
|
||||
command cat $argv
|
||||
return $status
|
||||
end
|
||||
|
||||
+12
-2
@@ -4,8 +4,11 @@
|
||||
# CATEGORY
|
||||
# 12-ai-and-developer-tools
|
||||
#
|
||||
# COMPONENT
|
||||
# aliases/dev-tools
|
||||
#
|
||||
# DEPENDENCIES
|
||||
# agents-init
|
||||
# agents-init, agents-vault
|
||||
#
|
||||
# SYNOPSIS
|
||||
# claude [ARGS...]
|
||||
@@ -16,6 +19,12 @@
|
||||
# Delegates all scaffold and commit logic to agents-init --quiet (full
|
||||
# setup), which ensures AGENTS/ is scaffolded and CLAUDE.md is symlinked
|
||||
# to AGENTS/AGENTS.md in the current project.
|
||||
#
|
||||
# Also syncs the host-scoped agent memory vault (agents-vault), which
|
||||
# tracks curated memory living outside the project tree. The vault
|
||||
# commits on launch but does not push; pushing happens from the Claude
|
||||
# Code SessionEnd hook or an explicit agents-vault --push.
|
||||
#
|
||||
# All arguments are forwarded verbatim to the real claude binary.
|
||||
#
|
||||
# Opinionated component (C1): when disabled via __fish_config_op_aliases
|
||||
@@ -33,12 +42,13 @@
|
||||
# claude --resume
|
||||
# claude "Explain the recent changes"
|
||||
function claude --wraps=claude --description 'claude wrapper: auto-links AGENTS.md as CLAUDE.md'
|
||||
if not __fish_config_op_enabled __fish_config_op_aliases
|
||||
if not __fish_config_op_enabled (status current-function)
|
||||
command claude $argv
|
||||
return $status
|
||||
end
|
||||
|
||||
agents-init --quiet
|
||||
agents-vault --quiet
|
||||
|
||||
command claude $argv
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user