No description
  • Rust 97%
  • Nix 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
natsukium 340bc75419
All checks were successful
build / build and cache spoor (nix) (push) Successful in 5s
pr / nix flake check (push) Successful in 2s
build / build and cache spoor (macos) (push) Successful in 13s
pr / cargo fmt / clippy / test (push) Successful in 26s
build: bump felis pin to 105b0899 (#1)
## What changed

Bumps spoor's `felis-grid` / `felis-vt` git pin from `c5a63ccb` (2026-06-28)
to felis main's `105b0899` (2026-09-07) — 685 commits' worth of felis-grid /
felis-vt / felis-protocol history, including the switch of felis' IPC wire
codec from postcard to protobuf (visible here only indirectly, as `postcard`
drops and `prost`/`prost-types` appear in `Cargo.lock`; spoor itself has no
IPC surface and doesn't touch that codec).

## Why no source changes were needed

spoor only touches the library surface in `src/source.rs`: `Grid::new`,
`Parser::new`/`advance`, `Grid::cell`, the `Grapheme` enum, `Grid::cluster_str`,
and `Grid::row_soft_wrap_continued`. All of these kept their signatures across
the bump, so the only diff is the pin itself, the regenerated `Cargo.lock`,
and the vendored source hash in `flake.nix`'s `outputHashes` (which changes
on every rev bump since it hashes the fetched felis tree).

## Verified

- `cargo build --all-targets` (via `nix develop -c`) — clean
- `cargo fmt --all --check` — clean
- `cargo clippy --all-targets --all-features -- -D warnings` — clean
- `cargo nextest run --all-features` — 67/67 passed, including every
  `source.rs` width/alignment test (wide glyphs, soft-wrap continuation,
  SGR-escape column accounting) and the `matcher.rs` soft-wrap/grid tests
  that depend on `felis-grid` staying pixel-exact with felis' own grid
- `nix build .#default --print-build-logs` — succeeds with the corrected
  `outputHashes` entry
- `nix flake check --print-build-logs` — all checks pass

## Left open

Nothing outstanding for this bump. rust-version stayed at 1.88 (felis'
workspace floor is unchanged), so no MSRV bump was needed either.

Reviewed-on: #1
2026-09-07 19:22:26 +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: match on logical rows so a soft-wrapped URL stays one match 2026-08-16 01:03:29 +09:00
src feat: match on logical rows so a soft-wrapped URL stays one match 2026-08-16 01:03:29 +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: bump felis pin to 105b0899 (#1) 2026-09-07 19:22:26 +09:00
Cargo.toml build: bump felis pin to 105b0899 (#1) 2026-09-07 19:22:26 +09:00
flake.lock initial commit 2026-06-28 17:34:42 +09:00
flake.nix build: bump felis pin to 105b0899 (#1) 2026-09-07 19:22:26 +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").