Skip to content

john-athan/sucher

Repository files navigation

sucher — fast terminal viewer and directory browser for markdown, spreadsheets, PDF, images, SVG, video, docx, pptx, Keynote, archives and more

sucher

CI License: MIT Made with Rust Built with ratatui

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:

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

sucher directory browser and viewers

Directory browser Markdown & docs Video & images
browser markdown video

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.
  • 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.
  • Real typography in pipe modes --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

Category Extensions Backend
Markdown .md .markdown .mdx pulldown-cmark
HTML .html .htm .xhtml 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 (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 bundled libsqlite (read-only); each table a sheet
Data — DuckDB .duckdb .ddb DuckDB ATTACH (read-only); each table a sheet
PDF .pdf pdfium (Chrome's engine) → graphics, poppler pdftocairo fallback
Image .png .jpg .jpeg .gif .webp .bmp .tiff .ico image → graphics
SVG .svg 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:

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:

:  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.

# 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:

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:

For Needs macOS
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.

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:

theme  = "catppuccin-mocha"  # or "auto" | built-in name (default: sucher-dark)
icons  = "nerd"              # "unicode" (default) | "nerd" | "none"
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).
  • Iconsunicode (geometric glyphs, renders on any font — the default), nerd (per-extension Nerd Font glyphs with per-language tints — requires a patched Nerd Font; not auto-detected, so opt in), or none (no icon column).
  • Layoutauto shows three columns (parent · current · preview, the ranger-style Miller layout) 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

s <file>            # interactive viewer (TTY)
s <dir>  /  s       # directory browser (bare `s` = current dir)
s --plain <file>    # one-shot styled dump to stdout
s <file> | less     # piped: text dump

Directoryj/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. 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.

Markdownj/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 / sourcej/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.

Spreadsheeth/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.

PDFj/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.

Imagex 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.

Archivej/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.

# 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 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)
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).
  • 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

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:

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).
  • 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.

About

Fast terminal viewer + directory browser for markdown, spreadsheets, PDF, images, video & docx — real pixels via kitty/iTerm2/sixel. One command: v <file|dir>.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages