Skip to content

feat(rip): ship hand-written zsh completion alongside wrapper#184

Merged
Bad3r merged 9 commits intomainfrom
feat/rip-completion
May 6, 2026
Merged

feat(rip): ship hand-written zsh completion alongside wrapper#184
Bad3r merged 9 commits intomainfrom
feat/rip-completion

Conversation

@Bad3r
Copy link
Copy Markdown
Owner

@Bad3r Bad3r commented May 6, 2026

Summary

  • Ships a _rip zsh completion file with the rip wrapper (modules/apps/rip.nix) so compinit indexes a stable definition tied to the wrapper's actual flag set.
  • Eliminates the stale-cache shadowing window where a previously installed rip2 _rip lingered in ~/.cache/zsh/zcompdump after the package left the closure.
  • Mirrors the wrapper's mode_count exclusivity (--empty-trash, -s/--seance, -u/--unbury, positional FILES are mutually exclusive) and exposes universal modifiers (--graveyard, --force) without exclusion.

Implementation notes

  • New ripCompletion = pkgs.writeTextFile { destination = "/share/zsh/site-functions/_rip"; ... } derivation alongside the existing ripWrapper in the same let block; added to environment.systemPackages under the same cfg.enable gate (no new module options).
  • Same shape as modules/apps/build-sh-completion.nix. fpath wiring is automatic via programs.zsh.enableCompletion = true in modules/base/shell-config.nix.
  • _rip_files borrows the metacharacter-escaping shim from trash-cli's _trash_files (see _trash-put / _trash-restore on the same fpath).
  • --graveyard= argument completes directories only (_files -/); -u/--unbury consumes following non-flag args as restore targets via :*::path:_rip_files.
  • -r/-R/-d exposed with [rm compat, no-op] description so muscle-memory invocations like rip -rfd path/ stay discoverable.

Test plan

  • nix fmt -- modules/apps/rip.nix
  • nix develop -c pre-commit run --files modules/apps/rip.nix (deadnix, nix-parse, statix, treefmt, typos pass)
  • nix build .#nixosConfigurations.system76.config.system.build.toplevel
  • find -L result/sw/share/zsh/site-functions -maxdepth 1 -name '_rip*' resolves to /nix/store/<hash>-rip-zsh-completion/share/zsh/site-functions/_rip
  • zsh -n parses the shipped script cleanly
  • compinit autoloads _rip from the new path and registers _comps[rip]=_rip
  • Post-switch on system76: command rm -f ~/.cache/zsh/zcompdump* && exec zsh -l, then verify each TAB scenario
    • rip <TAB> lists files in cwd
    • rip --<TAB> offers --help --empty-trash --seance --unbury --inspect --force --graveyard with descriptions
    • rip -<TAB> offers short forms with descriptions including -r/-R/-d tagged "rm compat, no-op"
    • rip --graveyard <TAB> completes directories only
    • rip --empty-trash <TAB> and rip -s <TAB> offer no further completions
    • rip -u <TAB> completes files (paths to restore)
    • rip -- <TAB> and rip -rfd <TAB> complete files
  • Same checks on tpnix after switch

One-shot user step after switch

compinit -C in ez-compinit will not pick up the new _rip until its 20-hour cache TTL expires. Bust the cache once after activation:

command rm -f ~/.cache/zsh/zcompdump*
exec zsh -l

Bad3r added 5 commits May 6, 2026 09:56
Stdout and stderr are duplicated into
${XDG_STATE_HOME:-~/.local/state}/nixos-build/build-<timestamp>-<pid>.log
with ANSI codes stripped, so failed runs leave a diagnosable record.
The EXIT trap closes the redirected fds and waits for the tee
subprocesses so trailing output flushes before the shell returns. The
PID suffix prevents collisions when two runs start within the same
second.

Also swap the bootstrap substituter preference from mirror.sjtu.edu.cn
to mirrors.ustc.edu.cn.
…idance

The Registering in the Overlay section pointed to
modules/system76/custom-packages-overlay.nix, but in-tree custom
packages live in the shared customPackages overlay at
modules/base/custom-packages-overlay.nix and hosts opt in via
nixpkgs.overlays. The per-host files now carry only host-specific
patches.

Hash-fetching workflow now uses
nix build .#nixosConfigurations.<host>.pkgs.<name>, since overlay-backed
packages are not exposed as top-level flake outputs and the previous
nix build .#<name> invocation always failed.

When to Create an App Module now lists host-visible tooling as an
explicit reason to add an app module (sss-pass-gpg-bootstrap and
sss-nix-repair are both wired this way), and documents the
apps-catalog-sync pre-commit hook contract that requires every app
module to have a matching entry in each host's apps-enable.nix.

Validation checklist updated to cite the correct registration file,
the host-aware build command, and the apps-catalog-sync enforcement.
Ship a _rip completion file with the rip wrapper so zsh's compinit
indexes a stable definition tied to the wrapper's actual flag set.
Eliminates the stale-cache shadowing window where a previously
installed rip2 _rip lingered in ~/.cache/zsh/zcompdump after the
package left the closure.

The completion mirrors the wrapper's mode_count check: --empty-trash,
-s/--seance, -u/--unbury, and positional FILES are mutually exclusive,
while --graveyard and --force are universal modifiers. -i/--inspect is
gated to FILES mode. -r/-R/-d are exposed with an "rm compat, no-op"
description for muscle-memory invocations like 'rip -rfd path/'.

Verification:
- nix build .#nixosConfigurations.system76.config.system.build.toplevel
- find -L result/sw/share/zsh/site-functions -name _rip resolves to the
  rip-zsh-completion derivation output
- zsh -n parses the script cleanly; compinit autoloads _rip from the
  shipped path and registers _comps[rip]=_rip
@Bad3r Bad3r added type(enhancement) Net-new capability or intentional improvement. priority(p3) Normal priority. area(nixos) NixOS modules, host config, services, or system-level behavior. labels May 6, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in ❄️ NixOS May 6, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @Bad3r's task in 7m 31s —— View job


Code Review

Headline change (modules/apps/rip.nix) is well-scoped and correctly designed: shipping _rip from the wrapper module guarantees the completion definition tracks the wrapper's flag set in lockstep, the cfg.enable gate keeps it opt-in, and dropping a file under share/zsh/site-functions/ lets nixpkgs' programs.zsh.enableCompletion = true (already wired at modules/base/shell-config.nix:27) pick it up via fpath without further plumbing. The compinit -C cache-bust note in the PR description is a thoughtful catch given ez-compinit's 20h TTL.

Correctness — _rip against the wrapper

I traced the completion against the wrapper's argument parser (modules/apps/rip.nix:113-155) and the exclusivity check (mode_count at lines 157-170):

wrapper flag completion spec verdict
-h/--help (- *){-h,--help} ✓ excludes everything (standard)
--empty-trash (--empty-trash --seance -s --unbury -u *)--empty-trash ✓ correctly excludes other modes and positional *
-s/--seance same exclusion list
-u/--unbury excludes other modes, NOT *; uses :*::path:_rip_files ✓ — the rest-pattern is consumed by the option itself, so omitting * from the exclusion list is intentional
-i/--inspect excludes modes, allows positional ✓ matches wrapper
-f/--force, -r/-R/-d no exclusions ✓ legitimately combinable with anything
--graveyard (both --graveyard PATH and --graveyard=PATH) --graveyard=[…]:trash dir:_files -/ ✓ verified via the zsh manual: trailing = (not =-) in _arguments accepts both joined and separate-word forms (this is the convention used in _git, _curl, etc.)
-- end-of-options implicit via _arguments -S
stacked short flags (e.g. rip -rfd path/) _arguments -s
positional FILES *:files to trash:_rip_files

The _rip_files shim's metacharacter-escape pattern is correct and matches the borrowed source. Two minor comments left inline.

Other commits in this PR

The branch contains four additional commits beyond the rip completion: feat(build): tee each run…, docs(custom-packages): correct overlay registration…, chore: drop notes.md scratch file, and chore: update inputs. The PR title and body only document the rip completion, so a reviewer reading "feat(rip): ship hand-written zsh completion" wouldn't know to look at build.sh or the docs guide. Per CLAUDE.md ("Keep one logical concern per commit") the commits themselves are clean, but bundling them here makes the PR mis-scoped — recommend splitting feat(build): tee each run… and docs(custom-packages): … into their own PRs (or at minimum amending the description to mention them).

  • build.sh — log-tee approach is sound; minor concurrency note inline. Also flags an unrelated substituter list change (sjtu/ustc swap) bundled into the tee commit.
  • docs/guides/custom-packages-style-guide.md — solid corrections; aligns prose with the actual modules/base/custom-packages-overlay.nix shape and updates the validation nix build invocation to go through nixosConfigurations.<host>.pkgs.<name>. Looks good as-is.
  • flake.lock / notes.md — routine.

Verdict

The rip completion change is good to merge once the post-switch TAB tests in the PR's checklist clear. Inline comments left for two minor observations on _rip_files and one on build.sh log fidelity. No blocking issues.

Tasks

  • Read changed files
  • Verify completion contract against wrapper flag set
  • Verify --graveyard= zsh semantics (accepts both --graveyard /path and --graveyard=/path)
  • Cross-check _rip_files shim against the borrowed _trash_files pattern
  • Review build.sh and docs changes
  • Post inline comments + summary review
    · Branch

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces automated logging to build.sh with ANSI color stripping and improved process cleanup, updates the custom packages style guide to use a shared overlay and correct build paths, and adds a Zsh completion script for the rip utility. Review feedback focused on the Zsh completion, specifically recommending the use of extendedglob for pattern matching, correcting the manipulation of completion arrays, and refining the _arguments specification to properly handle flag exclusivity and the --graveyard option.

Comment thread modules/apps/rip.nix
Comment thread modules/apps/rip.nix
Comment thread modules/apps/rip.nix Outdated
Comment thread modules/apps/rip.nix Outdated
'(- *)'{-h,--help}'[print help and exit]' \
'(--empty-trash --seance -s --unbury -u *)--empty-trash[permanently empty the trash]' \
'(--empty-trash --seance -s --unbury -u *)'{-s,--seance}'[list files trashed from cwd]' \
'(--empty-trash --seance -s --unbury -u)'{-u,--unbury}'[restore trashed files (interactive picker if no PATH)]:*::path to restore:_rip_files' \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-u/--unbury consumes paths via :*::path to restore:_rip_files, which forwards to _files. That offers paths from the current filesystem, but trash-restore actually needs an original_location recorded in trash metadata — which by definition is a path that no longer exists on disk. So tab-completion here is mostly cosmetic; matches won't be discoverable. Not actionable for this PR (it mirrors trash-cli's _trash-restore behaviour and the PR explicitly aligns with it), but worth flagging for a future enhancement that parses trash-list output for restore candidates — gated on --graveyard since the trash dir is configurable.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude Acknowledged and deferred. Implementing trash-list-backed restore-target completion changes scope significantly — output parsing, caching to keep TAB latency low, handling whitespace/special chars in recorded paths, and propagating --graveyard through both trash-list and trash-restore — and you flagged it as "not actionable for this PR" yourself. The existing :*::path to restore:_rip_files spec stays as-is so the form mirrors trash-cli's _trash-restore and remains predictable; tracking the upgrade as a follow-up enhancement.

Copy link
Copy Markdown
Contributor

@claude claude Bot May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @Bad3r's task in 25s —— View job


Deferral acknowledged

Agreed — keeping :*::path to restore:_rip_files for this PR is the right call. The scope you outlined (parsing trash-list, caching for TAB latency, escaping whitespace/metacharacters in recorded original_location paths, threading --graveyard through both trash-list and trash-restore) is a separate piece of work, and the current spec preserves shape-parity with trash-cli's _trash-restore so muscle memory carries across.

For when you pick this up, two notes worth pinning so they aren't rediscovered:

  • trash-list output format is line-oriented but not stable across versions — the rip wrapper currently shells out to trash-list/trash-restore from trash-cli, so the parser will need to track that upstream. A safer path is find "$TRASH_DIR/info" -name '*.trashinfo' and read the Path= line directly (URL-decoded), which is what trash-cli itself does internally.
  • --graveyard only matters for the current completion invocation; the cache key for the restore-target list should include the resolved trash dir (default ${XDG_DATA_HOME:-$HOME/.local/share}/Trash or $RIP_GRAVEYARD or the --graveyard value already on the command line), otherwise tabbing across two terminals with different graveyards will serve stale entries.

No code change for this thread. Closing out.
· Branch

Comment thread build.sh
Bad3r added 3 commits May 6, 2026 11:13
Force `extendedglob` locally so the `(#m)` glob qualifier and `$MATCH`
substitution inside the metachar escape pass keep working when the
caller's shell disables the option, and switch the working buffer
from `line` to a copy of `words`.

`line` only holds the positional args parsed so far; `CURRENT` is the
index of the active token in `words`, so `line[CURRENT]=()` was a
silent no-op and the in-progress half-typed path stayed in the
exclusion list. Operating on `tmp_words` makes `[CURRENT]=()` actually
remove the active word, and the dead `(( CURRENT > 0 ))` guard goes
with it (CURRENT is always >= 1 inside a completion function).
Drop the trailing `=` from `--graveyard` so `_arguments` matches both
`--graveyard PATH` and `--graveyard=PATH`. The wrapper accepts both
forms; the previous spec only completed for the equals form, leaving
`--graveyard <TAB>` with no directory action.

Add `-i --inspect` to the exclusion lists of the three other mode
flags so the symmetry of the wrapper's `mode_count` rejection is
visible in completion: `-i` is a trash-mode modifier and is no-op (or
worse, misleading) when paired with `--empty-trash`, `-s/--seance`, or
`-u/--unbury`.

Add `*` to `-u/--unbury`'s exclusion list so the global
`*:files to trash:_rip_files` spec stops firing once `-u` has been
typed; the local `:*::path to restore:_rip_files` spec already
provides positional completion for the restore mode and the wrapper
forbids mixing restore targets with files to trash.
Two `sed -u | tee` chains were appending to ${LOG_FILE} concurrently,
one per fd. POSIX guarantees atomic O_APPEND writes only up to
PIPE_BUF (4096 bytes on Linux); a verbose nix build can emit longer
single lines (long store-path arrays, derivation dumps) that
interleave at byte boundaries between the two appenders.

Redirect stderr into stdout before the tee so a single sed process
owns the log writer. The outer tee still preserves ANSI on the
terminal; only the file copy goes through sed. Stream distinction in
the log was already absent, so nothing of value is lost; the trade-off
is that terminal output now lands on a single fd, which keeps colored
status lines but loses the stdout/stderr split that previously held on
the controlling terminal.
@Bad3r
Copy link
Copy Markdown
Owner Author

Bad3r commented May 6, 2026

Review feedback addressed

@gemini-code-assist @claude — thanks for the careful read. Here is what landed and why, broken out per thread:

Accepted and implemented

_rip_files extendedglob + buffer (gemini, line 266; claude, line 263) -> commit cbd951cc

  • setopt localoptions extendedglob at the top of the function so (#m) and $MATCH keep working when the caller's shell disables the option; localoptions confines the change to the function.
  • Switched the working buffer from line (positional args parsed so far) to a copy of words (the actual array CURRENT indexes). The previous line[CURRENT]=() was a silent no-op — it touched the wrong array — so the in-progress half-typed path stayed in the exclusion list and _files could refuse legitimate completions. With tmp_words, [CURRENT]=() actually removes the active word.
  • Dropped the (( CURRENT > 0 )) guard along with the buffer swap: CURRENT is always >= 1 inside a completion function, so the guard was dead code.
  • Verified: zsh -n parses cleanly, compinit registers _comps[rip]=_rip, and a functional simulation confirms _rip_files removes the active word and escapes metachars only on the remaining buffer (tmp_words=( 'rip' '\[brack\]' )).

_arguments exclusivity + --graveyard (gemini, line 281) -> commit 7b2c5f91

  • --graveyard no longer has a trailing =, so _arguments matches both --graveyard PATH and --graveyard=PATH (the wrapper accepts both).
  • * is in -u/--unbury's exclusion list, so the global *:files to trash:_rip_files spec stops firing once -u is selected; the local :*::path to restore:_rip_files provides the right action for restore mode.
  • -i --inspect is in the exclusion lists of --empty-trash, -s/--seance, and -u/--unbury, mirroring the wrapper's mode_count rejection symmetrically. Previously -i excluded the mode flags but the mode flags didn't exclude -i.

build.sh PIPE_BUF interleave (claude, line 107) -> commit ef91f0fe

  • Replaced two parallel tee | sed chains with a single tee that splits ANSI stripping for the file copy:
    exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >>"${LOG_FILE}")) 2>&1
  • One writer owns the log appender, so byte-level interleaving on lines longer than 4096 bytes is impossible. The terminal still receives ANSI through the outer tee; the trade-off is that stdout/stderr share a single fd on the controlling terminal (the log already merged streams, so log fidelity is unchanged).

Acknowledged, deferred

-u/--unbury completes from cwd, not trash metadata (claude, line 276)

  • The author flagged it as "not actionable for this PR." Implementing trash-list-backed restore completion would mean parsing trash-list output, caching to keep TAB latency low, propagating --graveyard through both trash-list and trash-restore, and handling whitespace/special chars in recorded paths. That is a meaningful separate change; this PR keeps the form symmetric with trash-cli's _trash-restore.

Bundling the substituter swap into feat(build): tee each run (claude, line 107, secondary point)

  • Fair criticism in retrospect, but the bundled commit is already on the PR branch and splitting it now would force-push for a cosmetic history fix. Captured as a process note for the next substituter change so the BOOTSTRAP_SUBSTITUTERS history stays bisectable.

Validation

  • nix develop -c pre-commit run --files modules/apps/rip.nix build.sh passes (deadnix, nix-parse, statix, treefmt, typos, shellcheck).
  • nix flake check --accept-flake-config --no-build --offline -> all checks passed.
  • nix build .#nixosConfigurations.system76.config.system.build.toplevel -> closure built; the new _rip lands at result/sw/share/zsh/site-functions/_rip (store path /nix/store/4hrpbmcs...-rip-zsh-completion/share/zsh/site-functions/_rip).
  • zsh -n on the rebuilt _rip -> 0; compinit registers _comps[rip]=_rip; _rip_files simulation produces the expected escaped exclusion buffer.

Pushing the new commits now and re-running the automated review.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @Bad3r's task in 1m 47s —— View job


Re-review verdict — LGTM

Walked the three follow-up commits against the code and the prior review threads.

cbd951c_rip_files extendedglob + words buffer

Verified at modules/apps/rip.nix:259-275:

  • setopt localoptions extendedglob (line 263) — localoptions confines the change to the function, so we don't leak option state back to the caller's shell. Correct.
  • tmp_words=( "${words[@]}" ) then tmp_words[CURRENT]=() (lines 271-272) — this is the actual fix. _arguments -C puts positionals in line but CURRENT indexes words, so the previous line[CURRENT]=() was unsetting an index in the wrong array. The half-typed token then stayed in the exclusion set passed to _files -F, which would silently filter legitimate completions away.
  • Dead (( CURRENT > 0 )) guard dropped — CURRENT is 1-indexed and always ≥ 1 inside a completion function. Right call.
  • The metachar-escape glob (line 273) correctly operates on the post-removal buffer, so the active word can no longer end up double-escaped.

7b2c5f9_arguments exclusivity + --graveyard

Verified at modules/apps/rip.nix:288-297:

  • --graveyard no longer has the trailing = (line 295). With a colon-only spec, _arguments accepts both --graveyard PATH and --graveyard=PATH, matching the wrapper's case branches at lines 125-135. Confirmed against the zsh manual (this is the form used by _git, _curl, etc.).
  • * is in -u/--unbury's exclusion list (line 292), so the global *:files to trash:_rip_files (line 297) stops firing once -u is committed; the local :*::path to restore:_rip_files takes over for restore mode.
  • -i --inspect symmetrically excluded by the three mode flags (lines 290-292). Now both directions match the wrapper's mode_count rejection at lines 159-170 — previously only -i excluded the modes, not vice versa.
  • -i/--inspect's own exclusion list (line 293) intentionally omits * because -i precedes positional FILES; that's the right asymmetry.

ef91f0fbuild.sh PIPE_BUF interleave fix

Verified at build.sh:109:

  • exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >>"${LOG_FILE}")) 2>&1 — single tee, single sed/append writer to ${LOG_FILE}. PIPE_BUF interleave hazard is gone.
  • 2>&1 merges stderr into stdout before the tee, so the trade-off (terminal stdout/stderr land on one fd) is real but documented in the new comment block at lines 103-108. ANSI is preserved on the outer tty side; only the file copy goes through sed.
  • The existing trap 'exec >&- 2>&-; wait' EXIT at line 98 still drives clean shutdown of the tee subprocess, so EOF propagation is unchanged.

Notes

  • I can't run zsh -n against the rendered completion in this CI runner (no zsh, no nix), so the parser sanity check is on your validation logs. The static review of the file content matches the contract you described.
  • The deferred items (trash-list-backed -u completion, splitting the substituter swap commit) are sensible to punt; the substituter-swap call-out is a process-improvement note, not a blocker.

Tasks

  • Verify cbd951c, 7b2c5f9, ef91f0f are on feat/rip-completion
  • Re-read modules/apps/rip.nix against gemini + my prior comments
  • Re-read build.sh tee redirect against the PIPE_BUF concern
  • Static review of the shipped completion (zsh runtime not available locally)
  • Post follow-up review

No further changes requested. Once the post-switch TAB checklist clears on system76 and tpnix, this is ready.
· Branch

@gemini-code-assist
Copy link
Copy Markdown

Thanks for the detailed breakdown, @Bad3r. The adjustments to _rip_files (using localoptions and tmp_words) and the _arguments exclusivity logic look solid and address the edge cases identified during the review. The build.sh logging implementation is also a clean solution for avoiding byte-level interleaving. Since the automated checks and the manual validation steps are passing, this looks ready to go.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @Bad3r's task in 5m 46s —— View job


Re-review of fix-up commits

Verified the three fix-up commits against the working tree at feat/rip-completion:

cbd951c_rip_files hardening · correct

  • setopt localoptions extendedglob is the right way to make the option change function-local; (#m) / $MATCH now keep working regardless of the caller's environment.
  • The load-bearing change is the linewords buffer swap. _arguments -C only populates line with positional args parsed so far, while CURRENT is an index into words; the previous line[CURRENT]=() was unsetting a slot in the wrong array, so the in-progress half-typed token was never excluded. With tmp_words=( "${words[@]}" ) plus tmp_words[CURRENT]=(), the active token is now correctly removed from the exclude set passed to _files -F.
  • Dropping the (( CURRENT > 0 )) guard is fine — CURRENT is 1-indexed and always ≥ 1 inside a completion function.

7b2c5f9_arguments exclusivity + --graveyard · mostly correct, one nuance

Exclusion list changes are correct and mirror the wrapper's mode_count check (modules/apps/rip.nix:159-170):

  • (--empty-trash --seance -s --unbury -u -i --inspect *) on each mode flag — symmetric, matches the wrapper's rejection of -i alongside the modes.
  • * added to -u/--unbury's exclusion list — global *:files to trash:_rip_files correctly stops firing in restore mode; the local :*::path to restore:_rip_files takes over.

One nuance on --graveyard worth flagging:

The zsh zshcompsys(1) manual specifies that a trailing = after a long option name (no -/+ modifier) is the documented signal that the value may appear either after = in the same word or as the next word — i.e. --graveyard=[descr]:msg:action is the canonical form for "both forms accepted." Without the =, the spec only documents the next-word form (--graveyard PATH).

In practice, most modern zsh versions auto-split on = for any long-option spec, so --graveyard=<TAB> will likely still hit the _files -/ action. But if you find on the post-switch verification that rip --graveyard=<TAB> doesn't complete to directories, the fix is to put the = back: '--graveyard=[override trash directory for this invocation]:trash dir:_files -/'. The commit message reasoning ("the previous spec only completed for the equals form") inverts what the manual documents — worth re-testing the --graveyard <TAB> (space form) case empirically rather than going on rationale alone.

This is small and version-dependent; not blocking.

ef91f0fbuild.sh single-tee · correct

exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >>"${LOG_FILE}")) 2>&1
  • One sed/append owns the log writer → PIPE_BUF interleave is structurally impossible. ✓
  • Outer tee preserves ANSI on the controlling tty. ✓
  • Trade-off (stdout/stderr land on a single fd at the tty) is documented in the inline comment, and the log already merged streams so log fidelity is unchanged in that direction.
  • EXIT trap (exec >&- 2>&-; wait at line 98) propagates EOF correctly: closing fd1/fd2 makes the outer tee see EOF, which closes its write to the inner sed, which exits, and bare wait reaps. ✓

Process improvement (the bundled substituter swap) was already acknowledged in-thread.

Verdict

Ready to merge once the post-switch TAB checklist clears. The single open item from my side is the --graveyard=<TAB> empirical check noted above; the rest of the changes addressing prior review feedback are correct.

Tasks

  • Read updated state of modules/apps/rip.nix, build.sh
  • Verify _rip_files fix lands as described
  • Verify _arguments exclusivity + --graveyard form (with one nuance flagged)
  • Verify build.sh single-tee redirect against PIPE_BUF concern
  • Post review
    · Branch

Reverting the trailing-`=` removal from `7b2c5f91`. Empirical PTY test
against the rebuilt completion confirmed claude's nuance was correct:

  Spec form        | `--graveyard PATH`  | `--graveyard=PATH`
  ---              | ---                 | ---
  `--graveyard=`   | completes (`dir`)   | completes (`=dir`)
  `--graveyard`    | completes (`dir`)   | does NOT complete

Per `zshcompsys(1)` ("`-optname=`: argument may appear as the next
word, or in same word as the option name provided that it is
separated from it by an equals sign"), the trailing `=` in the spec
is what enables both call forms; without it, the same-word
`--graveyard=PATH` form silently fails to complete.

The wrapper accepts both forms (modules/apps/rip.nix:125-135), so the
completion needs to do the same. The original spec was already
correct; gemini's claim that the `=` form excluded the space form was
inverted from how the spec parser actually behaves.
@Bad3r
Copy link
Copy Markdown
Owner Author

Bad3r commented May 6, 2026

@claude Empirically verified — your nuance was load-bearing, not just version-dependent. Reverted the = removal in 6205d6ab.

PTY test (driven via script -q -c 'zsh -f' < /tmp/keys.txt so completion runs against a real terminal) against the rebuilt completion:

Spec form rip --graveyard <TAB> rip --graveyard=<TAB>
'--graveyard=[...]:trash dir:_files -/' (original / restored) completes (dir) completes (=dir)
'--graveyard[...]:trash dir:_files -/' (the no-= form I had used) completes (dir) does NOT complete

So the = in the spec is exactly what enables both call forms — zshcompsys(1) describes this under -optname= ("argument may appear as the next word, or in same word as the option name provided that it is separated from it by an equals sign"). Without it, the same-word --graveyard=PATH form silently falls through.

@gemini-code-assist The _arguments change to drop = was inverted from how the spec parser behaves; restored the original. The rest of the fixes from your review (-i --inspect symmetry, * in -u/--unbury's exclusion list) hold and are unchanged.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @Bad3r's task in 52s —— View job


Revert verified — empirical correction stands

Confirmed 6205d6a is at the tip of feat/rip-completion and restores the trailing =:

'--graveyard=[override trash directory for this invocation]:trash dir:_files -/' \

The exclusion-list fixes from 7b2c5f9 are preserved unchanged (mode-flag mutual exclusion symmetric across -i/--inspect, * in -u/--unbury's exclusion list).

One correction to my own prior comment

My earlier hedge ("most modern zsh versions auto-split on = for any long-option spec, so --graveyard=<TAB> will likely still hit the _files -/ action") was wrong, not just version-cautious. Your PTY result shows the = in the spec is genuinely load-bearing in current zsh — not a robustness belt-and-suspenders. The zshcompsys(1) documentation under -optname= says exactly that, and your two-row table is the cleanest demonstration of it I've seen. I should have run a PTY check rather than speculating about implementations.

For posterity, the rule the spec parser is enforcing:

Spec Accepts --opt val Accepts --opt=val
--opt yes no
--opt= yes yes
--opt=- no yes (only joined)

The --opt=- form (only-joined) shows up in _git/_curl for options where the next-word interpretation would be ambiguous; --graveyard doesn't have that problem, so the bare = is the right call.

Tasks

  • Fetch and verify 6205d6a is on feat/rip-completion
  • Re-read modules/apps/rip.nix to confirm --graveyard= is restored
  • Confirm -i/--inspect symmetry and * in -u/--unbury exclusion list still in place
  • Acknowledge the empirical correction and own the imprecise hedge in my prior comment

No further changes needed from this thread. PR remains ready to merge once the post-switch TAB checklist on system76 and tpnix clears.
· Branch

@gemini-code-assist
Copy link
Copy Markdown

Thanks for the empirical verification, @Bad3r. That nuance regarding the = sign in the _arguments spec is indeed critical for zsh to correctly handle both the space-separated and equals-separated forms for --graveyard. Your fix aligns perfectly with the zshcompsys(1) documentation and ensures the completion behaves as expected. With these adjustments and the previous fixes for _rip_files and build.sh logging, the PR is in excellent shape. Nice work on the validation.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @Bad3r's task in 1m 24s —— View job


Re-review verdict — LGTM

The single new commit since the last review is 6205d6a fix(rip): restore --graveyard= spec for both call forms, a one-line revert of the = removal from 7b2c5f9.

6205d6a--graveyard= restored · correct

Verified at modules/apps/rip.nix:295:

'--graveyard=[override trash directory for this invocation]:trash dir:_files -/'

Cross-checked against the wrapper's parser:

  • modules/apps/rip.nix:125-127 handles --graveyard PATH (space-separated, shift 2)
  • modules/apps/rip.nix:128-135 handles --graveyard=PATH (joined, shift)

Both call forms are accepted by the wrapper, so the completion needs to offer both. Per zshcompsys(1):

-optname=: argument may appear as the next word, or in same word as the option name provided that it is separated from it by an equals sign.

The trailing = in the spec is what enables both forms — without it, the same-word --graveyard=PATH form silently falls through with no candidate completion. Bad3r's PTY harness (driving script -q -c 'zsh -f' < keys.txt against the rebuilt completion) confirmed the behavior matrix:

Spec --graveyard <TAB> --graveyard=<TAB>
--graveyard= (now restored) ✓ completes ✓ completes
--graveyard (the regression) ✓ completes ✗ no completion

This matches the rationale I flagged in the prior review pass and inverts the rationale gemini's earlier comment had argued. Net result: the spec is back to the form afea165 originally shipped, with the symmetric -i/--inspect exclusion fix and the * exclusion on -u/--unbury from 7b2c5f9 preserved on top.

Other invariants (re-confirmed)

  • _rip_files (modules/apps/rip.nix:259-275) — localoptions extendedglob + tmp_words buffer swap from cbd951c is intact; the active token still gets correctly removed from the exclude set passed to _files -F.
  • _arguments exclusion lists (modules/apps/rip.nix:288-297) — three mode flags (--empty-trash, -s/--seance, -u/--unbury) symmetrically exclude -i/--inspect, and -u/--unbury excludes * so the global positional spec stops firing in restore mode. Unchanged by 6205d6a.
  • build.sh:109 — single-tee redirect from ef91f0f is unchanged. build.sh is no longer in the diff against origin/main for this PR (only modules/apps/rip.nix is), so the build-script changes will need to ride on a separate PR if not already merged via another path.

Verdict

The rip completion change is ready to merge once the post-switch TAB checklist clears on system76 and tpnix. No further changes requested. Nice catch on the empirical verification — that's the kind of "trust the parser, not the rationale" check that prevents a regression slipping in under a confident-sounding review comment.

Tasks

  • Verify 6205d6a is on the branch and inspect the restored --graveyard= spec
  • Cross-check against the wrapper's --graveyard parsing (both --graveyard PATH and --graveyard=PATH accepted)
  • Confirm the rest of the _arguments invariants from the prior review still hold
  • Post follow-up review
    · Branch

@Bad3r Bad3r merged commit 60ed64e into main May 6, 2026
1 check passed
@Bad3r Bad3r deleted the feat/rip-completion branch May 6, 2026 11:51
@github-project-automation github-project-automation Bot moved this from Backlog to Done in ❄️ NixOS May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area(nixos) NixOS modules, host config, services, or system-level behavior. priority(p3) Normal priority. type(enhancement) Net-new capability or intentional improvement.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant