- Rust 96.7%
- Nix 3.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The overlay's label colors were hardcoded (black-on-green hints, white-on-blue selection), so they could clash with a user's theme or be hard to read, with no escape hatch. Expose them as flags, which fits the "the binding is the configuration" model — each binding can carry its own colors, no config file. --hint-fg/--hint-bg and --select-fg/--select-bg accept a named color, an ANSI index (0-255), or #rrggbb. crossterm's own Color::FromStr silently maps an unknown string to white and never errors, which would let a typo pass; the new color::parse rejects bad input at startup instead (the friendly-validation rule). Unset flags keep today's defaults. The picker's render inputs (rows, matches, labels, colors) are bundled into a Scene struct so event_loop/draw stay within the argument-count lint as styling is threaded through. Assisted-by: Claude Code Opus 4.8 Claude-Session: https://claude.ai/code/session_01636iasujvDjqGxKWm8Fro7 |
||
| .forgejo/workflows | ||
| docs | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| rustfmt.toml | ||
spoor
A standalone, terminal-agnostic hint picker for scrollback. Pipe text in,
spoor overlays single-keystroke labels on the matches you describe, and
on selection it copies, opens, or pipes the picked value to a command.
Think of it as tmux-thumbs' standalone mode rebuilt to be generic, or
kitty's hints kitten unbundled from kitty — the same
single-keystroke interaction, but on any terminal, fed by any producer, and
configured entirely from the key binding that launches it.
# Every URL on the tmux pane gets a label — open the one you point at
tmux capture-pane -p | spoor --preset url --action open
# A table full of container IDs — stop the one you pick, not the whole row
docker ps | spoor --regex '\b[0-9a-f]{12}\b' --action 'docker stop {}'
# PR numbers buried in the log — copy just the digits via a capture group
git log --oneline | spoor --regex '#(\d+)' --capture 1 --action copy
# One binding, two actions: a lowercase label copies, an UPPERCASE one opens
tmux capture-pane -p | spoor --preset url --action copy --upper-action open
Each match — not each line — gets its own label, so several IDs or URLs on one line are all individually pickable; you grab the one you can already see by a one- or two-key label, without typing a filter.
Quickstart
With Nix (flakes enabled), run spoor straight from the repo without
installing anything:
# Pipe text in over stdin, just like the cargo-built binary
tmux capture-pane -p | nix run git+https://git.natsukium.com/natsukium/spoor -- --preset url --action open
Build the binary into ./result/bin/spoor, or drop it into your profile:
nix build git+https://git.natsukium.com/natsukium/spoor # -> ./result/bin/spoor
nix profile install git+https://git.natsukium.com/natsukium/spoor
To pin it in your own flake, add spoor as an input and use its
packages.<system>.default:
{
inputs.spoor.url = "git+https://git.natsukium.com/natsukium/spoor";
# e.g. home-manager: home.packages = [ inputs.spoor.packages.${pkgs.system}.default ];
}
Without Nix, build from source with Rust (>= 1.88; see rust-version in
Cargo.toml). The felis-vt / felis-grid dependencies are
pulled from git automatically, so no sibling checkout is needed:
cargo install --git https://git.natsukium.com/natsukium/spoor # or: cargo build --release
Then bind a key in your terminal/multiplexer to launch spoor over the
scrollback — the binding is the configuration (see the examples above).
Input
spoor reads the scrollback from stdin (the pipe producers above) or from
a file path argument — spoor --preset url FILE, with - meaning stdin.
The file path is for a producer that runs spoor inside an interactive
terminal, where stdin is the keyboard rather than a data pipe: for example
felis' pipe action hands the captured region over as a temp file. Add
--cols N when the captured region is wider than spoor's own terminal (a
tmux popup narrower than the pane) so soft-wrap — and the labels — land where
they were at the source. Keys are always read from /dev/tty, so the picker
works in either case.
Two contracts set spoor apart from the tools it borrows from:
- Nothing matches unless you ask for it. There are no forced builtin
patterns: your
--regex(and any opt-in--preset) is the match set. tmux-thumbs can only add to ~14 hardcoded patterns —spoorstarts empty. - Labels land exactly on their matches.
spoorparses the piped bytes through a real VT engine and lays them onto a cell grid, so wide glyphs, combining marks, ANSI escapes, and soft-wrap are all accounted for — the alignment bug class tmux-thumbs and tmux-fingers ship.
There is no config file: the key binding that launches spoor is the
configuration (the kitty hints model). One binding does one
job — bind several keys for several jobs.
See docs/design.md for the full design and the rationale behind every choice, and docs/research.md for the competitive and implementation research behind it.
spoor is written in Rust. The genuinely hard part — turning raw terminal
bytes into correctly positioned cells — is solved by reusing the
felis-vt VT parser and felis-grid cell-grid crates as
libraries, instead of re-implementing the byte/width math that the alignment
bug class comes from. See docs/design.md ("Alignment", "Language").