No description
  • Rust 96.7%
  • Nix 3.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
natsukium 279897c432
Some checks failed
build / build and cache spoor (macos) (push) Failing after 29s
build / build and cache spoor (nix) (push) Failing after 34s
pr / nix flake check (push) Failing after 39s
pr / cargo fmt / clippy / test (push) Failing after 24s
feat: add color flags for hint and selection labels
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
2026-06-29 14:02:35 +09:00
.forgejo/workflows build: depend on felis via git, add nix package and CI 2026-06-28 17:57:45 +09:00
docs feat: add color flags for hint and selection labels 2026-06-29 14:02:35 +09:00
src feat: add color flags for hint and selection labels 2026-06-29 14:02:35 +09:00
tests feat: add color flags for hint and selection labels 2026-06-29 14:02:35 +09:00
.gitignore initial commit 2026-06-28 17:34:42 +09:00
Cargo.lock build: depend on felis via git, add nix package and CI 2026-06-28 17:57:45 +09:00
Cargo.toml build: depend on felis via git, add nix package and CI 2026-06-28 17:57:45 +09:00
flake.lock initial commit 2026-06-28 17:34:42 +09:00
flake.nix build: depend on felis via git, add nix package and CI 2026-06-28 17:57:45 +09:00
README.md feat: add --upper-action for a second action on uppercase labels 2026-06-28 20:09:54 +09:00
rustfmt.toml style: add rustfmt.toml and format sources 2026-06-28 17:57:35 +09:00

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 argumentspoor --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 — spoor starts empty.
  • Labels land exactly on their matches. spoor parses 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").