<p align="center">
<img src="assets/social-preview.png" alt="sucher — fast terminal viewer and directory browser for markdown, spreadsheets, PDF, images, SVG, video, docx, pptx, Keynote, archives and more" width="820">
</p>
# sucher
[](https://github.com/john-athan/sucher/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.rust-lang.org)
[](https://ratatui.rs)
A fast terminal viewer for the files that are awkward to open in a browser —
**markdown, spreadsheets, data files (Parquet, JSONL, SQLite, DuckDB), PDF,
images, SVG, video, Word/PowerPoint/Keynote, EPUB e-books, archives, and raw
binary** — behind one tiny command:
```sh
s report.md
s data.xlsx
s data.parquet # data files open in the grid — with a `:` SQL prompt
s app.db # SQLite / DuckDB: each table a sheet
s paper.pdf
s photo.jpg
s diagram.svg
s clip.mp4
s deck.pptx
s archive.zip
s ~/projects # or a directory — browse and open files in place
s # no argument: browse the current directory
```
*Sucher* is German for the camera **viewfinder** — the little window you look
through to frame a shot. This one frames files: it picks a viewer by file
extension and renders it in place, using your terminal's graphics protocol for
real pixels where one is available.
## Demo

|  |  |  |
---
## Highlights
- **One launcher, many formats** — dispatch by extension, sensible TUI per type.
- **Directory browser** — point `s` at a folder (or run it bare) for a fast,
two-pane navigator: live preview pane, fuzzy filter, and `Enter` opens the
selection in its viewer, then drops you back where you were.
- **Recursive search** (`S`) — find files anywhere below the current directory,
streamed live as they're found (ripgrep's own walker, `.gitignore`-aware). The
same smart query as the filter, plus `content:` to grep *inside* files — and
every hit renders in the preview pane as the **actual framed file** (the PDF
page, the image, the spreadsheet grid), not a grey line of text.
- **File operations that never destroy.** Mark across folders with `Space`, then
copy/cut/paste (`y` `X` `p`), rename (`r`), create (`a`), delete (`D`). Every
batch **shows its plan before it runs**: what lands where, what got a ` (2)`
suffix to dodge a collision, what has vanished since you marked it. Nothing is
overwritten silently, and **nothing is ever permanently deleted**: delete means
the system trash, and so does anything an overwrite displaces or an undo takes
back. `U` undoes the last operation. Large copies run in the background with
live progress, so the browser stays usable while they go.
- **Handles huge files** — a 240 MB / 800k-row spreadsheet opens in ~160 ms and
stays scrollable, because sheets stream in on a background thread instead of
being loaded whole.
- **Queryable data files** — Parquet, JSONL, SQLite, and DuckDB open in the grid:
each database table becomes a tab, columns keep their real names and types
(ISO dates, not serial numbers), and a `:` SQL prompt — powered by an
**embedded DuckDB** — turns any of them into a live query over the file. It's
**lazy and uncapped**, so a billion-row Parquet opens instantly and scrolls to
the end, and **fully offline** — reading a data file never touches the network.
- **Real graphics** — images, rasterised SVGs, PDF pages, video frames, and
Keynote previews render as actual pixels via the kitty / iTerm2 / sixel
protocols (with a Unicode half-block fallback), through
[`ratatui-image`](https://crates.io/crates/ratatui-image).
- **Real typography in pipe mode** — `s --plain doc.md` emits the kitty
text-sizing protocol so headings render *larger* on supporting terminals;
detected at runtime, with graceful fallback.
- **Responsive** — event-driven redraw (no idle CPU churn) and background work
for the expensive bits.
## Supported formats
| Markdown | `.md` `.markdown` `.mdx` | [`pulldown-cmark`](https://crates.io/crates/pulldown-cmark) |
| HTML | `.html` `.htm` `.xhtml` | [`html5ever`](https://crates.io/crates/html5ever) DOM walk → markdown renderer |
| Text / source | code, `.txt` `.log`, config files, extension-less UTF-8 text | syntax-highlighted text viewer (no soft-wrap; pan + search) |
| Spreadsheet | `.xlsx`, `.xlsm` | streaming reader (zip + quick-xml) on a worker thread |
| Spreadsheet | `.xls`, `.ods`, `.xlsb`, `.csv`, `.tsv` | [`calamine`](https://crates.io/crates/calamine) (eager); csv/tsv parsed into the grid |
| Data — columnar | `.parquet` `.pq` | embedded **DuckDB** (`read_parquet`) |
| Data — line JSON | `.jsonl` `.ndjson` | DuckDB (`read_json_auto`) |
| Data — SQLite | `.sqlite` `.sqlite3` `.db` `.db3` | [`rusqlite`](https://crates.io/crates/rusqlite) bundled libsqlite (read-only); each table a sheet |
| Data — DuckDB | `.duckdb` `.ddb` | DuckDB `ATTACH` (read-only); each table a sheet |
| PDF | `.pdf` | [pdfium](https://crates.io/crates/pdfium-render) (Chrome's engine) → graphics, poppler `pdftocairo` fallback |
| Image | `.png` `.jpg` `.jpeg` `.gif` `.webp` `.bmp` `.tiff` `.ico` | [`image`](https://crates.io/crates/image) → graphics |
| SVG | `.svg` | [`resvg`](https://crates.io/crates/resvg) rasteriser → picture above scrolling source |
| Video | `.mp4` `.mov` `.mkv` `.webm` `.avi` `.m4v` | streaming `ffmpeg` pipe → graphics |
| Word | `.docx` | unzip + streaming XML → markdown renderer |
| Presentation | `.pptx` | unzip + streaming XML (slide text) → markdown renderer |
| E-book | `.epub` | unzip + spine order → HTML → markdown renderer |
| Notebook | `.ipynb` | JSON cells → markdown + code + outputs (images to gallery) |
| Keynote | `.key` | embedded QuickLook preview image → graphics |
| Archive | `.zip` `.tar` `.tar.gz` `.tgz` `.gz` | navigable table of contents (folders + path + size); no extraction |
| Binary | unrecognized non-text files | scrolling canonical hexdump (`offset │ hex │ ASCII`) |
| Directory | any folder | two-pane file browser (list + live preview) |
Comma/tab-separated values (`.csv` `.tsv`) open in the spreadsheet grid.
Legacy office binaries (`.doc` `.rtf` `.odt` `.ppt`) and audio have no viewer:
opening one shows a one-line size/modified summary rather than launching a viewer
or dumping bytes. Other archive types (`.7z` `.rar` `.xz` `.bz2` `.zst`) are
recognized but have no lister; extract them with a shell tool.
When stdout is not a TTY (piped), sucher prints a sensible text dump instead of
launching the TUI (`pdftotext` for PDF, TSV for sheets and data files, metadata for video,
styled text for markdown/html/docx/pptx/epub/ipynb, faithful bytes for text/source, raw XML for
SVG, a canonical hexdump for binary, a `size⇥path` table for archives, a plain
listing for directories).
## Data files & SQL
The files most technical users live in — **Parquet, newline-delimited JSON,
SQLite and DuckDB databases** — open in the same grid as spreadsheets, backed by
two native, statically-bundled engines behind one interface: an **embedded
DuckDB** reads Parquet/JSONL/DuckDB, and **rusqlite**'s bundled libsqlite reads
SQLite. Real column names sit in the header (not `A`/`B`/`C`),
DuckDB's canonical text gives correct ISO dates and timestamps (NULL renders
blank), and a database opens **read-only** with each table as its own sheet —
`Tab` (or `[` / `]`) cycles them, and the SQL prompt can join across them.
Sample files ship in `samples/` so you can try it straight away:
```sh
s samples/sales.parquet # one sheet, named for the file stem
s samples/app.db # SQLite: one sheet per table, switch with Tab
s samples/events.jsonl # newline-delimited JSON as a grid
```
Press `:` in the grid for an **interactive SQL prompt** that runs over the
current file; the result replaces the view (schema, rows, and `/` search all
follow it). For a single-file source, `FROM` the file's stem; for a database,
`FROM` any table or join across them:
```sql
: SELECT region, sum(revenue) AS total
FROM sales GROUP BY region ORDER BY total DESC
```
A live query shows truncated in the status bar. A parse/bind error stays in the
prompt with your text — and the previous view — intact; empty input reverts to
the base table; switching tabs drops the query.
It's **lazy and uncapped.** The grid windows rows on demand (`LIMIT`/`OFFSET`
plus a prefetch cache) and reads the schema from `DESCRIBE`, which doesn't
execute the query — so a file opens instantly regardless of size and scrolls to
the end with **no row cap**, unlike the streaming `.xlsx`/CSV backends. And it's
**fully offline**: both engines are compiled in statically and DuckDB has
extension autoinstall/autoload disabled, so reading a data file never reaches for
the network, in keeping with
sucher's local-viewer identity.
Data files sit behind the **default-on `data` Cargo feature**, so
`cargo install sucher` includes them out of the box. DuckDB is compiled from
vendored source and statically bundled — self-contained, with no build-time
download and no runtime network — which puts the release binary at **~65 MB**.
If you don't need it, `cargo install --no-default-features` builds the lean
~26 MB binary without DuckDB (Parquet then falls back to the hexdump and JSONL
to the text viewer).
## Install
Requires a recent **Rust** toolchain.
```sh
# Quickest — installs the `sucher` binary into ~/.cargo/bin:
cargo install --git https://github.com/john-athan/sucher
```
Or clone for the short `s` alias and `make` targets:
```sh
git clone https://github.com/john-athan/sucher
cd sucher
make install # builds --release, installs `sucher`, symlinks `s`
```
`make install` puts the binary in `~/.cargo/bin` and creates a short `s` symlink
next to it. (`make uninstall` removes both.)
The fast PDF path is **self-contained**: the build fetches the pinned,
checksum-verified **`libpdfium`** for your platform and embeds it in the binary
(materialised to a cache dir on first use), so `cargo install sucher` gets it
with no extra steps. Offline or sandboxed builds skip the fetch and fall back to
poppler; pre-place the library at `vendor/pdfium/<lib>` or point
`SUCHER_PDFIUM_LIB` at one to build the fast path offline, or set
`SUCHER_PDFIUM_NO_EMBED=1` to opt out.
### Optional runtime dependencies
These are only needed for the formats that shell out to them:
| PDF (fallback) | poppler (`pdftocairo`, `pdfinfo`, `pdftotext`) | `brew install poppler` |
| Video | `ffmpeg`, `ffprobe` | `brew install ffmpeg` |
The fast PDF path uses `libpdfium`, embedded in the binary at build time (no
install step); poppler remains the fallback and still powers `pdfinfo`/`pdftotext`.
For pixel-perfect images / PDF / video, use a terminal with a graphics
protocol — **kitty, ghostty, WezTerm, iTerm2**, or any sixel-capable terminal.
Without one, sucher falls back to Unicode half-blocks.
## Themes, icons & layout
The browser's palette, file icons, column layout, and git gutter are
configurable. Resolution order, highest first:
**CLI flag → environment variable → config file → default.**
```sh
s --theme catppuccin-mocha --icons nerd ~/projects
SUCHER_THEME=gruvbox-dark SUCHER_ICONS=none s
s --layout miller ~/projects # force three columns
s --no-git . # hide the git gutter
```
Config file at `$XDG_CONFIG_HOME/sucher/config.toml` (else
`~/.config/sucher/config.toml`); a missing or malformed file is ignored, never
fatal:
```toml
layout = "auto" # "auto" (default) | "miller" | "double"
git = true # show the git status gutter (default: true)
mouse = true # click rows/breadcrumb, wheel-scroll (default: true)
animate = true # folder fade + full-view zoom (default: true)
[colors] # optional per-key hex overrides, applied last
accent = "#7dd3fc"
selection = "#26324a"
```
- **Themes** — built-ins: `sucher-dark` (the default), `sucher-light`,
`catppuccin-mocha`, `gruvbox-dark`, `tokyo-night`. `theme = "auto"` picks a
light or dark default from the terminal background (`COLORFGBG` / OSC 11,
falling back to dark).
- **Icons** — `unicode` (geometric glyphs, renders on any font — the default),
`nerd` (per-extension [Nerd Font](https://www.nerdfonts.com/) glyphs with
per-language tints — **requires a patched Nerd Font**; not auto-detected, so
opt in), or `none` (no icon column).
- **Layout** — `auto` shows three columns (**parent · current · preview**, the
ranger-style [Miller layout](https://en.wikipedia.org/wiki/Miller_columns)) on
wide terminals (≥ 100 cols) and collapses to two (**current · preview**) when
narrow; `miller` forces three, `double` forces two. Toggle live with `M`.
- **Git gutter** — in a git working tree, each entry shows a colored status
marker (`●` modified · `+` added · `?` untracked · `✗` deleted · `»` renamed ·
`!` conflict); directories aggregate their descendants' changes. Absent
outside a repo or with `git = false`.
- **Repo HEAD readout** — inside a repo, the breadcrumb row shows the current
branch (or detached commit) right-aligned, with ahead/behind arrows vs the
upstream and a `●` dot when the tree is dirty: `⎇ main ↑2 ↓1 ●`. Follows the
same `git` toggle as the gutter.
- **Mouse** — click a file row to select it, click the highlighted row to open
it (or enter a folder); click a breadcrumb segment to jump there; in the
three-column layout click the left pane to go up; scroll the wheel to move the
selection. Capturing the mouse disables the terminal's own click-drag text
selection inside sucher (Shift/Option-drag still bypasses it in most
terminals); set `mouse = false` to keep native selection.
- **Animations** — entering/leaving a folder fades the new listing in; opening a
file in the full-screen image viewer zooms it up (and back down on close). Both
are time-based (~120–150 ms), interruptible by any keypress, and disabled with
`animate = false`. The folder fade is a cheap cell redraw that presents at the
display's refresh rate; the image zoom re-encodes the picture each frame, so it
runs at whatever the graphics pipeline sustains (constant duration either way).
`SUCHER_ANIM_STATS=1` prints each animation's achieved frame rate on exit.
## Usage & keys
```sh
s <file> # interactive viewer (TTY)
s <dir> / s # directory browser (bare `s` = current dir)
s --plain <file> # one-shot styled dump to stdout
**Directory** — `j`/`k` `↑`/`↓` move · `d`/`u` half-page · `g`/`G` top/bottom ·
`Enter`/`l`/`→` open file or enter folder · `h`/`←`/`Backspace` parent ·
`x` open in native app · `/` smart filter · `S` recursive search ·
`o` cycle sort (name/size/modified/ext) ·
`O` reverse sort · `.` toggle dotfiles · `M` two/three-column layout ·
`t` size/modified column · `?` key overlay · `q` quit.
**Select and act** (see *File operations* below)
`Space` mark/unmark then move down · `V` invert marks · `Ctrl-a` mark all ·
`y`/`X` copy/cut · `p` paste here · `r` rename · `a` create (trailing `/` makes
a folder) · `D` move to trash · `U` undo · `Y` yank absolute path(s) ·
`Esc` backs out one layer at a time (cancel the run, then the clipboard, then
the marks, then quit).
Press `?` for a
which-key overlay of every binding (it also shows the current sort). Click a
breadcrumb segment to jump there;
the wheel scrolls the list. The right pane renders a live
preview of the selection: **images (animated GIFs loop in place), SVGs, PDFs
(page 1), video posters, and Keynote previews as real pixels**,
**markdown/docx/pptx/epub/ipynb with full typography**, a
**grid preview for spreadsheets** (including csv/tsv), a **table of contents for
archives**, a **hexdump for binary**, a child listing for folders, and the head
of the file for text/code. Previews are cached as you move.
The `/` filter mixes free-text fuzzy matching with structured predicates, e.g.
`report kind:pdf size:>1mb modified:<7d ext:rs`. Plain words fuzzy-match the
name; four `key:value` predicates narrow by metadata:
- `kind:` — `pdf`, `image`, `video`, `audio`, `sheet`, `doc`, `markdown`,
`code`, `archive`, `folder`, `binary` (and aliases).
- `ext:` — a file extension, e.g. `ext:rs`.
- `size:` — `>1mb`, `<=100kb`, `500` (units `b`/`kb`/`mb`/`gb`/`tb`; bare = at least).
- `modified:` — file age, e.g. `<7d`, `>2w` (units `s`/`m`/`h`/`d`/`w`/`mo`/`y`).
Outside the filter, just **type a name** to jump to the first matching entry
(type-to-select); a brief pause or `Esc` ends the jump, and the vim motion keys
keep working whenever you're not mid-type.
**Search** (`S`) is the filter's recursive sibling: instead of narrowing the
current listing, it walks the whole tree from here downward and **streams matches
in live** as they're found, kept **sorted** (by the current sort — relative path
by default, so results group by folder) as they arrive rather than in the
walker's nondeterministic finish order. Type to refine; `↑`/`↓` (and `PgUp`/`PgDn`) move
through results, `Enter` opens the selected hit (or jumps into it if it's a
folder), `Esc` returns to browsing. It takes the **same query language** as the
filter — every `kind:` / `ext:` / `size:` / `modified:` predicate works — plus
one more that only makes sense across files:
- `content:` (aliases `contains:` / `grep:`) — a literal substring to find
*inside* files, e.g. `content:TODO ext:rs`. Matching is **smart-case**
(case-insensitive unless you type an uppercase letter) and each hit shows the
matching `line: text`. Powered by ripgrep's own line searcher, so binary files
are skipped and huge files stream rather than load.
The walk uses ripgrep's parallel directory walker: it honours `.gitignore`, skips
dotfiles unless `.` toggled them on, and caps at 5000 hits (surfaced in the
status line). Because every result flows through the same preview pane, a hit is
shown as the **real rendered file** — the differentiator over `fd`/`rg`/`fzf`.
**File operations.** sucher stays a viewfinder: it changes *where files are*,
never *what they contain*. There is no editing, no archive extraction, and no
writing to a spreadsheet or database. What it does do is the shell around files:
mark, copy, move, rename, create, delete.
`Space` marks the entry under the cursor and steps down, so holding it marks a
run. **The mark set is global**: it survives walking into other folders, which is
what makes "gather five files from three directories, then act once" the point of
the feature rather than a slower cursor. `V` inverts the current view, `Ctrl-a`
marks all of it. With nothing marked, every verb acts on the entry under the
cursor instead, so the keys work before you have learned marking at all.
`y` copies and `X` cuts to a clipboard that survives navigation; `p` pastes into
the directory you are standing in. `r` renames, opening pre-filled with the
cursor sitting between the name and the extension, so you can extend the name,
backspace over it, or move around it with the arrow keys without ever putting
`.txt` at risk. `a` creates, and a trailing `/` makes a folder instead of a file.
Every batch **shows its plan before anything happens**: how many items and bytes,
where each one lands, which names got a ` (2)` suffix to dodge a collision, and
which marked paths have vanished since you marked them. `Enter` authorises, `Esc`
cancels, and `o` toggles overwriting, which re-plans the batch and shows the
count it would replace in red. Collisions are suffixed by default and overwriting
is never the automatic answer.
**Nothing is ever permanently deleted.** `D` moves to the system trash, and there
is no permanent-delete binding at all. The rule is total rather than a property
of one key: an overwrite trashes whatever it displaces before writing, a move
across devices trashes the original after the copy lands, and undoing a copy
trashes what sucher created. Where no trash is available (some remote mounts) the
operation fails and says so rather than quietly falling back to `rm`.
`U` undoes the last operation, replaying the inverses of the steps that actually
happened, so a partly failed run undoes exactly the part that succeeded. A
trashing is the exception: the system trash is where those come back from, and
the report says so instead of pretending. Large operations run in the background
with live progress and can be cancelled with `Esc`; sucher refuses to quit while
one is still running rather than exiting with the work half done.
`Y` yanks the selected absolute path(s) to the system clipboard using OSC 52, so
it works over ssh with no helper binary. Delivery is up to the terminal (tmux
needs `set -g set-clipboard on`), and the status line says so rather than
claiming more than it knows.
**Markdown** — `j`/`k` `↑`/`↓` scroll · `d`/`u` half-page · `g`/`G` top/bottom ·
`t` table of contents · `/` search (`n`/`N` next/prev) · `l` link picker ·
`i` image gallery (for docx/pptx/epub/ipynb embedded media; `n`/`p` cycle) ·
`x` open in native app · `?` help · `q` quit.
**Text / source** — `j`/`k` `↑`/`↓` scroll · `d`/`u` half-page · `g`/`G`
top/bottom · `h`/`l` pan long lines · `/` search (`n`/`N` next/prev) ·
`x` open in native app · `q` quit.
**Spreadsheet** — `h`/`j`/`k`/`l` or arrows move cell · `PgUp`/`PgDn` ·
`g`/`G` top/bottom · `Tab` / `[` `]` switch sheet · `/` search all cells
(`n`/`N` cycle) · `x` open in native app · `q` quit. Status bar shows the cell
ref, value, and load progress.
**PDF** — `j`/`k`, `←`/`→`, or `space` page · `g`/`G` first/last ·
`x` open in native app · `q` quit. Rendered with **pdfium** (Chrome's engine)
when its library is present — a scanned page opens in ~30 ms instead of the
several seconds poppler's software rasteriser takes — and falls back to poppler
otherwise. Pages render off-thread with the neighbours prefetched, so stepping
through is near-instant; visited pages stay cached. `libpdfium` is embedded in
the binary at build time, so the fast path works out of the box; set
`SUCHER_PDFIUM_LIB` to override with a specific copy.
**Image** — `x` open in native app · `q` quit.
**SVG** — the rasterised picture fills the top pane; the XML source scrolls
below it with `j`/`k` `↑`/`↓` · `g`/`G` top/bottom · `x` open in native app ·
`q` quit.
**Video** — auto-plays on open · `space` play/pause · `←`/`→` ±5 s ·
`↑`/`↓` ±30 s · `,`/`.` frame step · `g`/`G` start/end · `x` open in native app ·
`q` quit. No audio.
**Archive** — `j`/`k` `↑`/`↓` move · `d`/`u` half-page · `g`/`G` top/bottom ·
`Enter`/`l` open folder · `h`/`Backspace` parent · `x` open in native app ·
`q` quit. A read-only, navigable table of contents (path + size) with a
breadcrumb; sucher lists and lets you browse folders, but never extracts.
**Binary (hex)** — `j`/`k` `↑`/`↓` scroll · `d`/`u` page · `g`/`G` top/end ·
`x` open in native app · `q` quit.
## Remote filesystems (S3, GCS, …)
sucher is a **local** viewfinder: it works on any path the operating system
gives it. So the clean way to browse a cloud bucket is to make it *look* like a
path — mount it, then point `s` at the mount. No sucher-specific setup, no
credentials for sucher to hold, and everything works over it unchanged: the
browser, every viewer, and the recursive `S` search.
```sh
# Amazon S3 — AWS's official FUSE mount (or `rclone`, below)
mount-s3 my-bucket ~/mnt/s3 # https://github.com/awslabs/mountpoint-s3
s ~/mnt/s3
# Google Cloud Storage
gcsfuse my-bucket ~/mnt/gcs # https://github.com/GoogleCloudPlatform/gcsfuse
s ~/mnt/gcs
# Anything rclone supports (S3, GCS, Azure, Backblaze, SFTP, …)
rclone mount remote:bucket ~/mnt/r --vfs-cache-mode full
s ~/mnt/r
```
Because the bytes come over the network, expect the obvious: previews and
graphics fetch on demand (a cache helps — e.g. rclone's `--vfs-cache-mode
full`), and a `content:` search downloads each candidate object, so scope it
with `ext:`/`size:` on large buckets. Name/`kind:`/`ext:`/`size:`/`modified:`
search only reads directory metadata and stays cheap.
Native cloud sourcing *inside* sucher (its own S3/GCS client, no mount) was
considered and deliberately left out for now — it would trade the zero-config
local-viewer identity for an SDK/auth/async surface, and mounts already cover
the use case. See [ADR 0007](docs/adr/0007-recursive-search.md) for the search
design that a native remote source would have to extend.
## How it works
```
main.rs dispatch by format; TTY → TUI, pipe → text dump
format.rs single classification registry (one source of truth)
dir.rs directory browser (list + live preview), opens files via main
search.rs recursive/streaming/content-aware search engine (ignore + grep)
marks.rs the browser's global multi-select set (pure, no filesystem)
fileop/ file operations: collect (bounded walk) -> plan (pure) -> execute
lineedit.rs single-line buffer with a cursor, for the rename/create prompt
markdown.rs parse → logical lines + TOC + links; width-aware wrap/layout
tui.rs markdown TUI (scroll / TOC / search / links)
text.rs source/plain-text TUI (highlight, no wrap, pan + search)
plain.rs one-shot markdown renderer (kitty text-sizing in pipe mode)
sheet.rs grid UI over a `Book` (eager calamine, streaming xlsx, or lazy data)
xlsx.rs background streaming .xlsx reader (zip + quick-xml), capped
data.rs data files via DuckDB (Parquet/JSONL/DuckDB) + rusqlite (SQLite) + SQL prompt
pdf.rs pdfium page raster (poppler fallback) + prefetch/cache, sized to the display
pdfium.rs runtime-loaded pdfium service thread (one parse, resident doc)
imgview.rs image viewer
svg.rs resvg rasteriser + split image/source viewer
video.rs streaming ffmpeg pipe + background decoder, paced w/ frame-drop
html.rs .html → markdown via html5ever DOM walk (reuses the renderer)
docx.rs .docx → markdown (reuses the markdown renderer)
pptx.rs .pptx slide text → markdown (reuses the markdown renderer)
epub.rs .epub spine → HTML → markdown (reuses html.rs + the renderer)
ipynb.rs .ipynb JSON cells → markdown + code + outputs (reuses the renderer)
keynote.rs .key → embedded QuickLook preview image
archive.rs zip/tar/gz table-of-contents lister
hex.rs canonical hexdump viewer for binary files
media.rs shared graphics pane (ratatui-image protocol probe + render)
```
Design notes:
- **One classifier.** `format.rs` owns a single `Format` registry that answers
both *which viewer opens a file* and *how the browser colours / labels it*, so
the two can never drift. Classification is a pure, unit-tested function
(extension first; a byte head disambiguates only unknown / extension-less
files). Only legacy office binaries (`.doc/.rtf/.odt/.ppt`) and audio lack a
viewer: they show a one-line size/modified summary instead of opening, and
their bytes are never fed to a text or markdown renderer.
- **Spreadsheets stream.** Only the current sheet is held in memory; rows are
parsed incrementally on a worker thread and the grid reads them live, so
opening is independent of total file size. Switching sheets frees the
previous one. A row cap bounds pathological files.
- **Three Book backends.** The grid's `Book` seam now spans three shapes — eager
(`MemBook`, calamine/CSV), capped-streaming (`StreamBook`, `.xlsx`), and
lazy-data (`DataBook`, `data.rs`) — each the right fit for its source. `DataBook`
itself holds two native engines behind one interface: DuckDB (Parquet/JSONL/
DuckDB) and rusqlite (SQLite), chosen so every format is read by the engine that
owns it and stays **offline** (both statically compiled; DuckDB extension
autoinstall/autoload off). Data reads are **lazy & uncapped** (window on demand,
schema without executing). The `:` SQL prompt is the grid's first capability
that varies by backend — a method on `Book`, not a new viewer (ADR 0016).
- **File operations decide before they act.** The hard part of a file manager is
the decision matrix, not the syscall: name collisions, a destination nested
inside its own source, the user standing in the directory being moved. So
`collect` establishes the facts (one bounded walk, symlinks recorded and never
followed), `plan` is a **pure** function from those facts to either a fully
resolved plan or a named refusal, and `execute` replays a decided list on a
background thread with nothing left to choose. The whole matrix is unit-tested
with no temp directory, and the confirm overlay can show the exact outcome
before a byte moves. `trash::delete` and `fs::rename` each sit behind a single
private seam, which is what makes "sucher never permanently deletes" a property
of the engine rather than a rule every caller has to remember.
- **PDF renders to display size** (`pdftocairo -scale-to-x <terminal px>`)
rather than a fixed DPI, and caches rendered pages.
- **Video** drives a single long-lived `ffmpeg` process emitting raw frames; a
decoder thread paces to real time and keeps only the latest frame, so a slow
terminal drops frames instead of falling behind. Effective frame rate is
bounded by how fast the terminal can transmit images, not by decoding.
## Development
```sh
make build # cargo build --release
make run # cargo run -- samples/sample.md
cargo test # unit tests (markdown layout, docx conversion, xlsx search)
```
A large-workbook benchmark is included but ignored by default:
```sh
SUCHER_BIG=/path/to/big.xlsx cargo test --release big_xlsx -- --ignored --nocapture
```
## Limitations
- DOCX/PPTX keep text structure (headings, bold/italic, lists, tables; slide
text as bullets). Embedded images are viewable in an image gallery (`i`) but
not shown inline; page layout and exact styling are dropped.
- Keynote shows the embedded QuickLook preview (cover / first slide), not
per-slide content — the IWA protobuf body isn't decoded.
- SVG rasterises shapes, gradients, and paths; `<text>` needs system fonts,
which the headless rasteriser doesn't load, so text elements may not appear.
- Archives are listed and folder-navigable, but never extracted: you can browse
into directories, though individual entries can't be opened or unpacked.
- Spreadsheet dates show as serial numbers (style table isn't read); the
streaming reader caps very large sheets. (Data files don't have this wart —
DuckDB renders dates as ISO text.)
- Data files are **read-only**: databases are attached `READ_ONLY`, and there's
no writing or editing. A `.db` that isn't actually SQLite errors on open rather
than falling back. **Arrow/Feather** files aren't supported — DuckDB's Arrow
*file* reader isn't in the bundled build and would need a network extension, so
it's deliberately excluded (Parquet covers the columnar need). A `find`/search
over a data file follows the query's scan order, not a stable row order.
- The release binary is **~65 MB** because DuckDB is compiled in and statically
bundled; build the lean ~26 MB binary with `cargo install --no-default-features`
(dropping the `data` feature and Parquet/JSONL/SQLite/DuckDB support).
- File operations act on **whole paths only**. There is no editing, no archive
extraction, no writing to a spreadsheet or database, no permission changes, no
bulk rename through an editor, and no symlink creation on paste. Operations are
browse-mode only: recursive search is a text surface where every printable key
belongs to the query, so press `Enter` into a hit's folder and act there.
- **There is no permanent delete**, by design. On a filesystem with no trash
(some remote mounts) `D` fails and says so rather than falling back to `rm`.
Restoring a trashed file is the system trash's job, not sucher's, so `U` says
where to look instead of pretending it can bring one back.
- One operation runs at a time; a second is refused rather than queued. A single
operation is capped at 50,000 entries and 64 levels of depth, and a batch past
either is refused whole before anything is mutated rather than copied halfway.
- A cross-device move (any FUSE mount) is a copy followed by trashing the
original, so undoing one leaves a recoverable duplicate in the system trash
alongside the restored tree.
- `Y` uses OSC 52, which is fire-and-forget: sucher can confirm it sent the
sequence, never that the clipboard took it. tmux needs `set -g set-clipboard on`.
- Video has no audio, and terminal frame rate is capped by image transmission.
- Inside the full-screen TUI, markdown headings use color/bold (not the
text-sizing protocol — that applies to `--plain` / pipe output).
- In the directory browser, previews render synchronously as you move the
selection, so a PDF or video poster adds a brief raster pause on first visit
(cached afterward). Video shows a poster frame, not playback — press `Enter`
to open the full player.
## License
MIT — see [LICENSE](LICENSE).