diff --git a/conf.d/tricks.fish b/conf.d/tricks.fish index 6c82fb1..43f8705 100644 --- a/conf.d/tricks.fish +++ b/conf.d/tricks.fish @@ -97,11 +97,13 @@ if __fish_config_op_enabled (status basename) tricks-bang end 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. +# Timestamped history view. Named pretty-history (not history) so it never +# shadows the fish builtin -- every function in this config that expects +# stock `history` semantics (search, --max, merge, ...) would otherwise +# silently break, which has happened more than once. Opinionated (C1 +# aliasing); when disabled, the function is never defined. if __fish_config_op_enabled (status basename) aliases-tricks - function history + function pretty-history --description 'History with timestamps prepended to every entry' builtin history --show-time='%F %T ' end end diff --git a/docs/build-manual.py b/docs/build-manual.py index e80ebca..cb7ac5f 100644 --- a/docs/build-manual.py +++ b/docs/build-manual.py @@ -703,6 +703,29 @@ ENTRY_HEADS = { } +def _classification_tags(raw: list[str]) -> list[str]: + """Split a CLASSIFICATION body into its comma-separated tags. + + A plain comma split (as `names()` uses for DEPENDENCIES) would break on + the commas inside `uses-shadow(rm, cp)`-style tags, so this only splits + on commas at paren depth 0. + """ + text = " ".join(raw) + tags: list[str] = [] + depth = 0 + start = 0 + for i, ch in enumerate(text): + if ch == "(": + depth += 1 + elif ch == ")": + depth = max(0, depth - 1) + elif ch == "," and depth == 0: + tags.append(text[start:i].strip()) + start = i + 1 + tags.append(text[start:].strip()) + return [t for t in tags if t] + + def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str: """Render one parsed function header as a manual entry body. @@ -739,6 +762,7 @@ def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str refs = [] for label, values in ( ("Dependencies", names(fn.get("DEPENDENCIES", []))), + ("Classification", _classification_tags(fn.get("CLASSIFICATION", []))), ("Used by", sorted(used_by)), ): if values: @@ -884,6 +908,7 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) - refs = [] for label, values in ( ("Dependencies", names(fn.get("DEPENDENCIES", []))), + ("Classification", _classification_tags(fn.get("CLASSIFICATION", []))), ("Used by", sorted(used_by)), ): if values: diff --git a/docs/manual/08-components-reference/01-c1-command-shadows.md b/docs/manual/08-components-reference/01-c1-command-shadows.md index eb3b5fb..a71900c 100644 --- a/docs/manual/08-components-reference/01-c1-command-shadows.md +++ b/docs/manual/08-components-reference/01-c1-command-shadows.md @@ -19,7 +19,6 @@ all of these commands. rg rg --hyperlink-format=kitty system rg mkdir verbose path-tree display on creation mkdir -p silently bash XDG bashrc + $SHELL reset on exit system bash - history timestamps prepended to every entry fish builtin history cp / mv forced -i confirmation prompt cp / mv unmodified wget forced --continue (resume downloads) system wget grep/fgrep/egrep forced --color=auto system grep variants @@ -31,6 +30,11 @@ 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. +`history` itself is never shadowed — every function in this config that +reads history depends on its stock builtin semantics. `pretty-history` +(same `aliases-tricks` toggle) is a separate command that prints history +with a timestamp prepended to every entry. + ## Sub-categories `__fish_config_op_aliases` sub-divides into six sub-categories, each with @@ -63,3 +67,47 @@ and the `help config` interception. `claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor launcher), plus `agy`. +## For function authors + +Calling one of these names bare from inside your own function means the +override runs whenever C1 (or its sub-category) is on — which may not be +what your function wants: a shadow can change stdout (`cat`'s syntax +highlighting, `mkdir`'s tree display), prompt interactively where none is +expected (`cp`/`mv`'s forced `-i`), or reshape output structurally (`ls`'s +icons/columns, `rg`'s hyperlink markers). If your function's logic depends +on stock behavior, bypass the shadow deterministically, regardless of the +toggle state: + + Shadow Bypass Why + ───────────────────────────────────────────────────────────────────────── + ls, cat, rm, less, du, command Real external + top, ping, ssh, rg, binaries — a + mkdir, bash, cp, mv, real system command + wget, grep/fgrep/egrep, exists to fall + dir/vdir, claude back to. + cd builtin cd The one true + fish builtin + in this table. + help config __original_help $argv `help` is neither + a builtin nor an + external binary + (embedded in the + fish binary + itself) — see + conf.d/help.fish + for why the + wrapper keeps its + own backup copy. + edit (nothing to bypass to) Purely our own + invention, no + stock command + exists. Call + $EDITOR/$VISUAL + yourself if you + want a plain + editor launch. + +A function's own doc header records which of these it depends on: see the +`CLASSIFICATION` label (`uses-shadow(...)` / `bypasses-shadow(...)`) in +`AGENTS/functions/CLAUDE.md`. + diff --git a/docs/manualtools.py b/docs/manualtools.py index 70054a2..5a66d92 100644 --- a/docs/manualtools.py +++ b/docs/manualtools.py @@ -64,6 +64,7 @@ SECTIONS = ( "CATEGORY", "COMPONENT", "DEPENDENCIES", + "CLASSIFICATION", "SYNOPSIS", "DESCRIPTION", "ARGUMENTS", diff --git a/functions/hist.fish b/functions/hist.fish index c0b7c7a..51c0f0d 100644 --- a/functions/hist.fish +++ b/functions/hist.fish @@ -34,7 +34,7 @@ function hist --description 'Search fish history and put it in the prompt' return 1 end - set -l selected (history | fzf --reverse --height 40% --with-nth 3..) + set -l selected (builtin history --show-time='%F %T ' | fzf --reverse --height 40% --with-nth 3..) if test -n "$selected" # Strip the timestamp for the final output