- Rust 97%
- Nix 3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
## 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 |
||
| .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").