Fold And Cluster Entries — a Unix-style CLI that groups, ranks, and pages structured command output.
face is a standalone result organizer. Query tools — rg, cargo, gh,
your CI logs, your CSV exports, your LLM agent's JSON — answer what are the
results?. face answers the next question: how should this result set be
grouped, ranked, and paged?
One binary. One job. No daemon, no UI, no config file required.
$ rg TODO --json | face
face: 412 items
[1] src/cli.rs 38 ████████
[2] src/core.rs 27 █████▋
[3] src/parser.rs 19 ████
... 44 more clusters (face --cluster N to drill)
Most tools stop at here is a flat list of N matches. The moment N grows past a screen, the next question is always the same:
- "Which file has the most hits?"
- "Show me only the high-confidence ones."
- "Group by repo, then by severity, then page through the top bucket."
That step is face. Pipe any structured stream in, get a grouped, paged
summary out. Drill into any cluster by number or by ID. Save the envelope and
re-process it without re-running the upstream command. No new query language
to learn — face reads the data and decides.
- Zero-config, zero-flag by default.
cmd | facealways produces something useful. Every flag exists to override an automatic decision — none are required. - Reads JSON, JSONL, CSV, and TSV with sniffing. Force the parser with
--format-inif you must. - Auto-detects the items array, the score field, the numeric distribution preset, and a per-axis grouping strategy.
- Six numeric and categorical strategies plus five algorithmic string-similarity flavors (token, n-gram, edit, LSH, simhash). No neural black-box clustering.
- Nested grouping to any depth via
--by FIELD,FIELD,...or repeatable--within FIELD. - Self-consumable JSON envelope.
face --format=json | teeand re-pipe for instant redrills — no auto-cache, no daemon, no implicit state. - Seven output formats — human tree, JSON (nested or flat), JSONL items,
TSV, CSV, Markdown — TTY-aware coloring,
NO_COLORhonored. - Inline
--interactivepicker for the moments you want to click around without standing up a TUI.
The full design rationale lives in docs/design.md.
With Homebrew, after the tap has been updated by a GitHub release:
brew install oops-rs/tap/faceFrom crates.io, after the crates are published:
cargo install faceOr build from source today:
# Latest from the main branch
cargo install --git https://github.com/oops-rs/face --bin face
# Or from a local clone
git clone https://github.com/oops-rs/face
cd face
cargo install --path crates/face-cliface targets edition 2024 / resolver 3, so a recent stable Rust toolchain
(1.85+) is required to build from source.
Note —
faceis at0.1.0. The CLI surface is implemented end-to-end and stable enough for daily use, but the JSON envelope's exact shape may still evolve before1.0. See Status for details.
rg TODO --json | face
cargo test --message-format=json | face
gh pr list --json number,title,labels --limit 200 | face
psql -c "SELECT ..." --csv | face
cat results.tsv | faceNo flags. face sniffs the format, finds the items, picks a grouping field,
and prints a tree. With --verbose you get a one-line provenance summary on
stderr explaining what it decided:
$ rg TODO --json | face --verbose
→ 412 items by data.path.text (auto, from rg JSONL)
face: 412 items
...
# 1-based ordinal — matches the [N] labels in human output
face --cluster 1 < results.json
# Canonical comma form
face --cluster file:src/cli.rs < results.json
# Or as repeatable axis=value pairs
face --cluster file=src/cli.rs --cluster score=excellent < results.jsonWhen the destination is a pipe, drilled output defaults to JSONL items
(one record per line, no envelope) so jq, fzf, awk, and friends just
work:
face --cluster 1 < results.json | jq '.path'
face --cluster file:src/cli.rs < results.json | fzfPass --format=human to force the tree view through a pipe, or
--format=json to get the full envelope back.
# Group by file, band the score within each file
face --by file --within score --bands 4 < results.json
# Chain --by axes (comma is "next nesting level")
face --by repo,file,kind < results.jsonAuto-detection runs per axis, so each --within without an explicit
strategy picks its own.
face --format=json produces a self-consumable envelope. Pipe it back in and
face re-processes the cached clusters instead of re-running upstream:
cmd | face --format=json > /tmp/face.json
face --cluster file:src/cli.rs < /tmp/face.json
face --cluster file:src/cli.rs --page 2 < /tmp/face.json
face --format=markdown < /tmp/face.json
face --within tag < /tmp/face.json # re-clusterNo auto-cache, no daemon — just a shell redirect you control.
cmd | face --schema # describe input shape, no grouping
cmd | face --explain # full reasoning for what face would decide
cmd | face --verbose # provenance line on stderr alongside normal outputExample:
$ rg TODO --json | face --explain
input:
format jsonl (sniffed: 412 newline-separated JSON objects)
items . (input is a flat record stream)
score (none detected)
strategy:
axis 1 data.path.text (auto: most-discriminating string field, cardinality=47)
output:
format human (default)
color auto (stdout is a TTY)
cmd | face --interactiveArrow keys to navigate, Enter to drill, Space to multi-select at the leaf
level, Esc / q to back out. When stdout isn't a TTY, face falls back
silently to the configured non-interactive format — scripts that
conditionally pass --interactive work uniformly in CI.
Detected per axis from the data; override with --strategy.
| Strategy | Auto-picked when | Override |
|---|---|---|
exact |
Enum-ish field (low cardinality, high coverage) | --strategy=exact |
prefix |
Path-like strings (/, ::, or . separators) |
--strategy=prefix=2 |
top |
Free-form strings (top-N by frequency + (other)) |
--strategy=top=10 |
bands |
Continuous numeric (default 5 equal-width bands) | --strategy=bands --bands=N |
quantiles |
Heavily skewed numeric distributions | --strategy=quantiles |
natural |
One-dimensional Jenks breaks | --strategy=natural |
similar |
Algorithmic string clustering | --strategy=similar=token (or ngram, edit, lsh, simhash) |
Numeric clusters render highest-to-lowest by default so score-like data leads with the strongest bucket. Cluster labels come from the data itself — value ranges for numeric strategies, the field value for enum strategies, the shared prefix for prefix strategies.
--format= |
Use it for |
|---|---|
human |
Reading. Tree-rendered, colored if TTY, with bars. |
json |
Programmatic consumers. Nested envelope, stable schema. |
json-flat |
jq pipelines that prefer flat iteration. |
jsonl-items |
Downstream record consumers. One item per line. |
tsv |
Spreadsheets, fzf, awk / cut. |
csv |
Spreadsheets. RFC-4180 quoting. |
markdown |
Pasting into PRs, issues, docs. |
-j is shorthand for --format=json. Color follows
--color={auto,always,never} (default auto) and respects NO_COLOR and
CLICOLOR_FORCE.
When --cluster is set, no --format was passed, and stdout is a pipe,
face automatically emits jsonl-items for the drilled page so jq
pipelines stay clean.
Optional. If absent, every default is built into the binary. Lookup order:
$FACE_CONFIG(full path)$XDG_CONFIG_HOME/face/config.toml~/.config/face/config.toml
[output]
default_format = "human"
per_page = 20
color = "auto"
[detect]
score_candidates = ["score", "rank", "relevance", "confidence", "bm25"]
items_candidates = ["items", "results", "data", "hits", "matches"]
[strategy]
default_bands = 5
[interactive]
save_threshold = 200
[presets.my-search]
score = "relevance"
invert = false
scale = 1.0Precedence:
CLI flag > $FACE_* env > config.toml > built-in default
--verbose provenance and --explain say which layer won when something
looks unexpected. The full schema lives in docs/design.md
§13.
ff() {
local saved
saved=$(mktemp)
"$@" | face --format=json > "$saved"
local id
id=$(face --format=tsv < "$saved" | fzf --with-nth=2,3 | cut -f1)
[ -n "$id" ] && face --cluster "$id" < "$saved"
}
ff rg TODO --json
ff cargo test --message-format=jsoncmd | face --format=csv > clusters.csv
open clusters.csv # macOS
xdg-open clusters.csv # Linuxcmd | face --format=markdown | pbcopy # macOS
cmd | face --format=markdown | xclip # Linuxcrates/
face-cli/ binary `face` + thin CLI library
face-core/ detection, grouping, clustering, paging, envelope
docs/
design.md canonical CLI / data-model design reference
0.1.0. The full design surface in docs/design.md is
implemented end-to-end:
- JSON / JSONL / CSV / TSV input with sniffing and
--format-inoverride - Auto-detection: items array, score field, preset, per-axis strategy
- Strategies:
exact,prefix,top,bands,quantiles,natural, andsimilar(token / n-gram / edit / LSH / simhash) - Nested grouping via
--by FIELD,FIELD,...and repeatable--within FIELD - Cluster addressing: 1-based ordinal, canonical comma ID, or
axis=value - Pagination via
--page/--per-page - Self-consumable envelope:
face --format=jsonround-trips through stdin - Output formats:
human,json,json-flat,jsonl-items,tsv,csv,markdown - Inline
--interactivepicker (the only carve-out from "no UI") - Optional TOML config under
$XDG_CONFIG_HOME/face/
The CLI surface is considered stable enough for daily use. The 0.x line
means the JSON envelope's exact shape may still shift if a downstream
consumer surfaces a need before 1.0.
Issues and PRs welcome. Before opening a PR:
cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --allThe design doc is the source of truth for the CLI surface; when in doubt, match it.
MIT — see LICENSE.