Flerp

August 17, 2026 · View on GitHub

flerp is a terminal UI for exploring and analyzing local files without leaving the command line. It opens text documents in a full-screen ratatui workspace and gives you live search, content viewing, file statistics, keyword extraction, repeated-line detection, and adjustable display settings.

It is built in Rust and currently supports plain text files, PDFs, and images. PDFs are read in full — every page of text plus the raster images embedded in them, rendered as real pixels in terminals that support a graphics protocol.

The Flerp dashboard showing structural metrics and a content preview

What Flerp Does

  • Opens a file in an interactive terminal workspace
  • Reads every page of a PDF and tracks where each page starts
  • Extracts the images embedded in a PDF and displays them as real pixels
  • Renders .png and .jpg files in the terminal
  • Shows quick structural metrics such as line count, word count, unique words, empty lines, and longest line
  • Finds the most frequent keywords in the loaded content
  • Searches file content with live updates
  • Searches a whole directory tree, PDFs included, and reports the page each match landed on
  • Supports case-sensitive, regex, and whole-word search modes
  • Lets you jump from search results directly into the viewer
  • Highlights repeated lines that appear multiple times
  • Persists UI preferences in an XDG config file
  • Includes multiple built-in color themes

Supported File Types

  • .txt and other UTF-8 readable text files
  • .pdf, including page structure and embedded raster images
  • .png, .jpg, .jpeg, rendered in the terminal

Image Rendering

flerp draws images with the terminal's own graphics protocol, so what you see is the image itself rather than character art. It picks the best available of:

ProtocolTerminals
Kitty graphicskitty, Ghostty, WezTerm, Konsole
Sixelfoot, xterm, WezTerm, Contour, Windows Terminal
iTerm2 inlineiTerm2

When none of these is available, flerp falls back to Unicode half-blocks and says so in the Media mode, so it is always clear whether you are looking at true pixels or an approximation. Pass --graphics kitty (or sixel, iterm2, halfblocks) to override detection, which is useful under multiplexers that swallow the capability query.

The same image drawn with the half-block fallback

Above is the fallback; the Media section below shows the same image over a real graphics protocol.

Images are lifted straight out of the PDF's own data. DCTDecode (JPEG) and Flate/LZW-compressed raster data are supported across grayscale, RGB, CMYK, indexed-palette and 1-bit sources, and /SMask transparency is applied so cut-out logos do not arrive as opaque boxes. JPXDecode (JPEG 2000), CCITT fax and JBIG2 images are listed with the reason they were skipped rather than silently dropped.

Interface Overview

The application is organized into various modes, reached with Tab.

Files

Run flerp with no arguments and it opens on the working directory; name a directory and it opens on that one. Either way a tree pane stands to the left of every mode, because which file is loaded is not a per-mode decision:

flerp                # the working directory
flerp ./contracts    # somewhere else

Folders are read only when you open them, so a directory of a hundred thousand nested files costs no more to browse than an empty one. Enter opens a folder or loads a file; h/l and the arrow keys fold; f moves the keyboard between the tree and the mode beside it. The loaded file is marked in the tree, and the pane's title says Files · f whenever the keyboard is somewhere else.

Search from the tree and committing the query with Enter sweeps every text file and PDF underneath it. Each row then carries its match count, and a closed folder carries the total of everything inside it, so you can see where the matches are before opening anything. Picking a file out of a swept tree lands the viewer on its first match.

The Files pane after a sweep, with match counts on every row and the chosen file open on its first match

The same thing as it happens — folding the tree open, sweeping it for a word, and dropping into the file that has the most of them:

Opening folders, searching the whole tree, and landing on a match

Dashboard

Quick summary cards, a content preview, and the currently selected search match. Shown at the top of this page.

Search results, search options, and regex validation errors. Matches are highlighted in place and counted per line.

Search mode with live highlighting across a PDF

Viewer

Scrollable file content. For PDFs it also reports the current page, the total page count, and how many images sit on the page you are reading.

The Viewer on page 2 of a PDF, with the page indicator and line window

Analyze

Structural metrics and repeated lines on the left; readability, weighted keywords and repeated phrases on the right.

Analyze mode showing structural metrics and top keywords

Media

Images found in the file, the renderer in use, and anything that could not be decoded.

An image extracted from a PDF, drawn with the Kitty graphics protocol

Settings

Theme and behavior settings, saved automatically as you change them.

Settings mode with the theme and behaviour options

Features

Search

  • Live search while typing
  • Case-sensitive toggle
  • Regex search toggle
  • Whole-word matching toggle
  • Match counts per result
  • Jump-to-line from the selected result
  • Recursive search across a directory of text files and PDFs, from the shell (see Searching a whole directory) or from the tree pane, which then shows the match count on every row (see Files)

Analysis

Structural counts:

  • Lines, words, characters, stanzas, empty lines
  • Unique words, longest line, average word length
  • Repeated lines

Content analysis:

  • Top keywords, weighted rather than merely counted (see below)
  • Repeated phrases, the multi-word runs the text keeps coming back to
  • Readability, as a LIX score with its band, sentence count and average sentence length
  • Detected language, English or Turkish, or undetermined when neither is clear

How Keywords Are Ranked

Ranking words by raw frequency describes the language, not the document: the, and, of win every English file and bir, ve, için win every Turkish one. flerp filters those out with English and Turkish stopword lists, then weights what is left by tf-idf.

There is no corpus to draw an idf from, so the document supplies its own: each paragraph counts as a separate document, falling back to lines for files with no blank lines. A word spread evenly through every paragraph is scaffolding and scores low however often it repeats; a word that clusters in a few is what those paragraphs are about and scores high. The panel shows the count and the weight side by side, so you can see when the two disagree.

Phrases come from the same stopword lists: a phrase is a run of content words with no stopword or punctuation breaking it, which is the RAKE approach and needs no grammar. A phrase is only reported if it occurs more than once, and a short phrase is dropped when a longer one already accounts for every occurrence of it — listing both "harbour extension" and "harbour extension works" at the same count says the same thing twice.

Both stopword lists always apply, whatever language was detected. Documents mix languages constantly, and filtering by the detected language alone lets the other one's function words to the top. Detection reports what the text is; it does not decide what gets filtered, so a wrong guess costs nothing.

LIX is used for readability rather than Flesch because it needs no syllable counting, which only works for English. Sentences are counted by their terminators, so abbreviations inflate the count slightly.

Paged Documents

  • Page count and current page shown while scrolling
  • [ and ] step a whole page at a time
  • Search results report the page a match landed on
  • Per-page image counts

UI and Workflow

  • Full-screen terminal interface
  • Directory tree pane beside every mode, with f to move between the two
  • Viewer fills the available height, and PageUp/PageDown move by one screenful
  • Multiple themes
  • Optional line numbers
  • Optional line wrapping
  • Adjustable content preview size
  • Adjustable keyword count
  • Saved preferences between runs

Installation

From AUR

yay -S flerp

or the latest development version built from the main branch:

yay -S flerp-git

From crates.io

cargo install flerp

From source

git clone https://github.com/Huseynteymurzade28/flerp
cd flerp
cargo install --path .

Running

Open a file

flerp path/to/file.txt

Open a PDF

flerp path/to/document.pdf

Open an image

flerp path/to/image.png

Force a graphics protocol

Detection is automatic, but a multiplexer or SSH session can swallow the capability query. Say what your terminal supports instead:

flerp --graphics kitty path/to/document.pdf

Accepts auto (the default), kitty, sixel, iterm2 and halfblocks.

Start somewhere other than the top

flerp --page 12 report.pdf          # open the viewer on page 12
flerp --search "invoice" report.pdf # open with the search already applied

Browse a directory

flerp                # the working directory
flerp ./contracts    # somewhere else

Opens the TUI with that directory as the tree pane and no file loaded yet. See Files.

Search a directory

flerp --search "invoice" ./contracts

A query alongside a directory prints matches from every text file and PDF under it rather than opening anything, so it pipes into other tools. See Searching a whole directory.

Show help

flerp --help

Show version

flerp --version

Headless Mode

Everything flerp works out about a file can be written to stdout or to disk instead of drawn in a terminal, so it composes with the rest of your shell. Passing any of --json, --text or --extract-images skips the TUI entirely, and so does naming a directory or more than one path, since the TUI holds a single file and has nowhere to put the rest.

Analysis as JSON

flerp --json report.pdf

One object on stdout: structural stats, the detected language, readability, weighted keywords, repeated phrases, repeated lines, the page table, every embedded image with its dimensions, and any images that could not be decoded along with the reason.

flerp --json report.pdf | jq '.pages | length'
flerp --json report.pdf | jq -r '.keywords[] | "\(.score)\t\(.word)"'
flerp --json report.pdf | jq -r '.phrases[] | "\(.count)\t\(.text)"'
flerp --json report.pdf | jq '.readability.lix'

Plain text

flerp --text report.pdf > report.txt

Writes the extracted text verbatim — every page of a PDF, in page order. Closing the pipe early (| head) is treated as the reader having had enough, not as an error.

Extracting embedded images

flerp --extract-images ./out report.pdf

Writes each embedded image as PNG into ./out, named page-003-img-02.png so the files sort into reading order. PNG keeps the alpha that /SMask transparency produces. Combine with --json and the written paths are reported inside the JSON object rather than beside it, so stdout stays parseable.

Searching without the TUI

flerp --search "revenue" --json report.pdf   # matches with page numbers
flerp --search "revenue" --text report.pdf   # no: --text ignores the search
flerp --search "revenue" --extract-images ./out report.pdf

With --json, matches carry their line number, page number and per-line match count. Without it, matches print grep-style as file:line:text. The search modifiers work headlessly too: -i ignore case, -e regex, -w whole word. Unlike the TUI, headless search never reads your saved settings, so a script gets the same result on any machine.

Searching a whole directory

Point --search at a directory instead of a file and flerp walks it:

flerp --search "invoice" ./contracts          # every text file and every PDF
flerp --search "invoice" ./contracts *.pdf    # directories and files together
flerp --search "invoice" --json ./contracts

This is the thing grep cannot do for you. A folder of PDFs is opaque to every line-oriented tool in the shell, and flerp reads each one's text layer, so a match inside page 14 of a scanned-in contract prints beside matches from the plain text files next to it.

Output is grep's, so it drops into the same pipelines and the same editors:

contracts/2025-terms.txt:42:invoice terms, revised
contracts/report.pdf:318:invoice total carried forward

Page numbers do not fit that format, so they come from --json, which groups matches by file and records what was searched:

flerp --search "invoice" --json ./contracts | jq -r \
  '.files[] | .file as $f | .matches[] | "\($f)  p\(.page // "-")  \(.line)"'
{
  "search": { "query": "invoice", "case_sensitive": true, "regex": false, "whole_word": false },
  "roots": ["./contracts"],
  "files_searched": 31,
  "files_matched": 2,
  "match_count": 7,
  "files": [
    {
      "file": "contracts/report.pdf",
      "kind": "pdf",
      "match_count": 5,
      "matches": [{ "line_number": 318, "page": 14, "match_count": 1, "line": "invoice total carried forward" }]
    }
  ],
  "skipped": []
}

Note that this is a different shape from the single-file object above: one file gets the full analysis, a sweep gets matches. Which one you get depends only on whether you named one file or several.

What gets searched

The walk follows the same rule grep -r does, with one addition:

  • PDFs are read, text layer and page structure. That is the point.
  • Hidden files and directories are skipped. A walk that descends into .git spends its time on object files nobody searched for.
  • Binaries are skipped, detected the way grep detects them: a NUL byte in the first 8 KiB. Only that prefix is read, so a multi-gigabyte archive costs one short read rather than a failed attempt to load it as text.
  • Images are skipped. The text flerp shows for an image is a description it wrote itself, and matching on that would report hits that are not in the file. Naming one directly reports it under skipped with that reason.
  • A file you name by hand is searched anyway, filter or no filter. Naming a file is you saying you want that file.
  • Symlinked directories are not followed, so a link pointing at an ancestor cannot send the walk round in a circle.
  • Results are sorted by path, so the same directory reports in the same order on every run.

Nothing is dropped silently: a directory the walk was not allowed to read and a file that failed to open both land in skipped under --json, and on stderr otherwise, which keeps stdout parseable.

Only searching works across several files. --text has no single text to write and --extract-images no single document to open, so both say so rather than picking a file for you.

Embedded images are never decoded during a sweep — searching does not look at them, so a folder of illustrated PDFs costs only its text.

Terminal Requirement

The TUI requires an interactive terminal session and will exit with an error if stdin or stdout is not connected to a real terminal. Headless mode has no such requirement — that is the point of it.

Keyboard Controls

The footer always shows the keys for whatever mode you are in, dropping the less important ones when the terminal is too narrow to hold them all.

Global

  • q or Ctrl-c quit the application
  • Esc cancel the open prompt, or quit when no prompt is open
  • Tab next mode, Shift-Tab previous mode
  • / open the search prompt
  • : open the go-to-line prompt; digits only, Enter jumps

Search and Filters

  • Enter apply the current search or jump to the selected result from the Search tab
  • Backspace delete the last search character
  • n next match, N previous match — both follow the match into the viewer and wrap around at the ends
  • c toggle case-sensitive search
  • r toggle regex mode
  • w toggle whole-word matching

Viewer and Dashboard Preview

Both read the same scroll offset, so these work in either mode.

  • j / Down scroll down, k / Up scroll up
  • Ctrl-d and Ctrl-u move by half a screen
  • Ctrl-f / PageDown and Ctrl-b / PageUp move by a whole screen
  • g / Home jump to the top, G / End jump to the bottom
  • [ previous page, ] next page (paged documents)

h and l are not bound to left and right: l toggles line numbers, and half of a symmetric pair would be worse than neither.

Media

  • j / k (or Up / Down) move through the images in the file
  • Enter jump the viewer to the page the selected image sits on

Search Results and Settings

  • j / k (or Up / Down) move through search results and settings rows
  • Left and Right adjust the selected setting in the Settings tab

Display Options

  • l toggle line numbers
  • z toggle line wrapping

Mouse

  • Wheel scrolls whatever is under the pointer: the file content, the match list, the image list or the settings rows
  • Click a mode in the tab strip to switch to it
  • Click a row in the match list, image list or settings list to select it
  • Clicks are ignored while a prompt is open, since the prompt covers the panes behind it

Scroll Position

The Viewer draws a scrollbar on the right edge of the content pane, with every search match marked on its track. On a long document that turns "where are my matches" from a scrolling exercise into a glance. Marks never paint over the thumb, so where you are looking stays visible, and a file that already fits on screen gets no scrollbar at all.

Screenshots

Every screenshot in this file is generated from flerp's own render buffer, so what you see is the interface as the program draws it.

Saved Settings

Flerp stores UI preferences in the XDG config directory using directories::ProjectDirs.

Typical Linux path:

~/.config/flerp/settings.toml

Saved settings include:

  • theme
  • keyword limit
  • preview line count
  • case sensitivity
  • regex mode
  • whole-word mode
  • line numbers
  • line wrapping

Development

Prerequisites

  • Rust toolchain
  • Cargo

Build

cargo build --release

Test

cargo test

Run from source

cargo run -- path/to/file.txt

Tech Stack

  • ratatui for the terminal UI (TUI)
  • crossterm for terminal input/output
  • clap for CLI argument parsing
  • pdf-extract for PDF text extraction
  • lopdf for reading embedded PDF images
  • ratatui-image for terminal graphics protocols
  • regex for advanced searching
  • serde and toml for persisted settings
  • directories for config directory resolution
  • image for decoding image data

Repository

License

Released under the MIT License. See LICENSE for the full text.

Copyright (c) 2025-2026 Huseyn Teymurzade