⚠️ MASSIVE WARNING ⚠️
This is 100% AI-generated code. Every single line was written by Codex CLI, Gemini CLI, and Claude Code — the human has not written a single line of Rust. That said, it works well for daily use. No guarantee it won't eat your epub, delete your database, or crash your terminal. You're on your own. PRs welcome.
Rust reimplementation of the excellent CLI ebook reader epy.
repy keeps the keyboard-first reading experience familiar while adding fast
chapter rendering, inline terminal graphics, persistent annotations, and a
self-contained SQLite library.
EPUB artwork rendered directly in the reading flow.
Jump through the table of contents, then select, copy, annotate, or look up text without leaving the reader.
Status
Functional for daily use! Core reading features are complete: TUI navigation, search, bookmarks, library management, two-phase cursor/selection modes, image viewing, link/footnote handling, dictionary lookup, Wikipedia lookup, persistent highlights/comments, highlight export, and TTS (text-to-speech) all work. Text is intelligently wrapped and hyphenated. Reading state and preferences are persisted per-book.
Supported formats: EPUB, FictionBook (.fb2 and .fb2.zip), MOBI6
(.mobi), plain text (.txt), Markdown (.md), and comic book archives
(.cbz — set "inline_images": "shown" and use a graphics-capable terminal
such as kitty to see the pages). AZW/AZW3 files are accepted on a best-effort
basis; KF8-only content may not be readable by the MOBI6 parser.
See to-do.md for detailed feature status and roadmap.
Installation
Download Binaries
You can download pre-built binaries for Linux, Windows, and macOS from the GitHub Releases page.
- Linux: Download
repy-linux-x86_64(compatible with most modern distributions). - Windows: Download
repy-windows-x86_64.exe. - macOS: Download
repy-macos-universal(works natively on both Intel and Apple Silicon Macs).
After downloading, rename the file to repy (or repy.exe on Windows) and make it executable:
# Linux/macOS chmod +x repy-*-* mv repy-*-* /usr/local/bin/repy
Build from source
If you prefer to build it yourself, you need Rust and Cargo installed.
# Clone this repository git clone https://github.com/newptcai/repy.git cd repy # Build and install cargo install --path .
The bundled rusqlite feature is enabled, so no system-wide libsqlite3
installation is required; SQLite is compiled and linked as part of the build.
Usage
Opening a book
To open any EPUB, plain-text, or Markdown file (doesn't need to be in your library):
repy /path/to/book.epub repy /path/to/notes.md
Starting without arguments
If there is a reading history, repy reopens the last-read book at the last saved
position. Otherwise, it starts in the reader UI without a book loaded.
Opening books from the reading history
The EBOOK argument can be a file path, a reading-history number, or a
pattern matched case-insensitively against the title, author, and path of
history entries (the most recently read match wins):
repy -r # Print the reading history with numbers and progress repy 3 # Open the 3rd book in the reading history repy dorian # Open the most recent history entry matching "dorian"
Other options
repy -d BOOK # Dump the parsed text of an ebook to stdout (pipe to less/grep) repy -c FILE # Use a specific configuration file repy -v # Increase verbosity (for debugging) repy --debug # Enable debug output repy --export-highlights /path/to/book.epub
--export-highlights writes all persisted highlights/comments for that EPUB to
stdout. The default format is JSON (including the book identity); pass
--format md for Markdown grouped by chapter, with quotes, notes, and dates:
repy --export-highlights book.epub --format md > notes.mdSearch
Search functionality supports regular expressions.
- Start Search: Press
/to open the search input. - Incremental: Matches update live as you type, and the view previews the
first match at or after your current position.
Escwhile typing cancels and restores the original position. Invalid partial regexes simply show no matches. - History:
Up/Downwhile typing recall previous queries (persisted across sessions, most recent first, capped at 100).Downpast the newest entry restores the query you were typing. - Navigation:
Enter: Confirm the query (recorded in history). Thenj/korUp/Downbrowse results, and a secondEnterjumps and closes the window.n: Jump to the next search hit.p/N: Jump to the previous search hit.
- Clear Highlights: There is no dedicated key to clear highlights. A workaround is to press
/to start a new search (which clears existing highlights) and thenEscto cancel. - Current Hit: All matching text is highlighted in yellow; the line containing the current hit is highlighted in orange. A
match N/Mcounter is shown in the top bar and status messages while navigating withn,p, orN.
Keybindings
Press ? in the TUI to see the help window at any time (Help (?)).
Navigation
k/Up— Line Upj/Down— Line Downh/Left— Page Upl/Right— Page DownSpace— Page DownCtrl+u— Half Page UpCtrl+d— Half Page DownL— Next ChapterH— Previous Chapterg— Chapter StartG— Chapter EndHome— Book StartEnd— Book End
Jump History
Ctrl+o— Jump BackCtrl+i/Tab— Jump Forward
Display
+/-— Increase/Decrease Width=— Reset WidthT— Toggle Top Barc— Cycle Color Theme
Annotations
A— Highlights listEnterin highlights list — Jump to selected highlightein highlights list — Edit commentdin highlights list — Delete highlightdin cursor mode — Delete highlight under cursor
Windows & Tools
/— Search!— Text-to-Speech (Toggle)v— Cursor Modet— Table of Contentsm<char>— Set a persistent mark (a-z, A-Z, 0-9)`<char>— Jump to a persistent markB— Bookmarks (ato add,dto delete,Enterto jump)u— Links on Page (Enterpreviews internal links;Enteragain jumps)o— Images on PageEntershows the selected image in the terminal (kitty, iTerm2, or sixel graphics when the terminal supports them, halfblocks otherwise);Esc/qreturns to the listoopens it with the external viewer instead (default_viewersetting, thenfeh, thenxdg-open); SVG images always use the external viewer- With
"inline_images": "shown"(also toggleable in Settings), images render directly in the reading flow: space is reserved under each placeholder and the image appears once its block is fully on screen
i— Metadatar— Library (reading history merged with books found on disk)j/kto select an entryEnterto open the selected bookcto show or hide the selected book's details and cover (off by default)fto cycle among available formats for a Calibre bookRto refresh configured library directoriesdto delete the selected history entrysto cycle the sort order: recent / title / author / series / progress- Books found in
library_directoriesbut never opened show asnew/unread; history entries whose file has disappeared are marked[missing] - When enabled with
c, a responsive details panel shows metadata and all available formats; supported graphics terminals also show the cover (Calibre-stylecover.jpgfiles are used directly, otherwise the cover is read from the ebook)
R— Reading Statisticss— SettingsEnter: Activate (toggle boolean, input for dictionary client)r: Reset to default- Dictionary command templates use
%qas the query placeholder
q— Quit / Close Window
In the Table of Contents, Bookmarks, Highlights, and Library windows, press
/ to fuzzy-filter the list. Matches narrow as you type, best match first.
Enter acts on the selected entry directly, or confirms the filter so
j/k can navigate the narrowed list; Esc clears the filter (a second
Esc closes the window).
Cursor & Selection Modes
The text-selection flow is two-phase:
- Press
vin the reader to enter Cursor Mode (-- CURSOR MODE --appears in the header). - In cursor mode, move with
hjkl, word motionswbe, line motions^(first non-blank) and$(end of line), paragraph motions[and],f<char>/F<char>to jump to the next / previous occurrence of a literal character on the current line, ort<char>/T<char>to land just before / after it. All motions accept a numeric count prefix (e.g.5j,3w,2],3fa).- When the cursor is on a highlighted span, press
Enterto edit that highlight's comment. - Press
dto delete the highlight under the cursor; if it has a non-empty comment a confirmation popup is shown (ydeletes,n/Esccancels). - Press
Cto cycle the color of the highlight under the cursor (yellow → green → blue → pink → purple). New highlights use the last color chosen this way. - Rows covered by a highlight show a colored
▎margin indicator in a 1-column left gutter (reserved as soon as the book has any highlight).
- When the cursor is on a highlighted span, press
- Press
vagain to set an anchor and enter Selection Mode. - In selection mode, move with the same motions as cursor mode (
hjkl,wbe,^$,[],f<char>/F<char>,t<char>/T<char>, all with optional count prefix) to expand/shrink the character-level selection (selection can cross page boundaries). - Press
yto copy the selected text to clipboard. - Press
ato save a highlight for the selection (using the last-used highlight color). - Press
cto save a highlight and immediately edit its plain-text comment. - Press
dto run dictionary lookup on the selection. By default it triessdcv,dict, andwkdict. You can configure a custom command template in Settings (s). - Press
pto run Wikipedia lookup on the selection; the popup shows a link to the page plus the summary (10s timeout). - Press
sto search the selection with Ecosia in your browser. - Press
Escto leave selection mode back to cursor mode; pressEscagain to return to reader mode.
In both cursor and selection mode, press / to search within the currently
visible screen and jump the cursor to the first match; n / N cycle through
matches. The query is plain text (regex specials are escaped) with smartcase
matching, and spaces in the query match across line wraps and soft hyphens, so
/example will find exam- / ple even when the wrapper has split the word
across two lines. In selection mode the anchor stays put, so each jump extends
the selection.
Highlights are anchored to normalized chapter text with prefix/suffix context, so they survive text-width changes and small whitespace or formatting edits. Cross-chapter highlights are not supported yet.
Text-to-Speech (TTS)
Press ! to toggle reading aloud from the current paragraph.
- Engine Support: Defaults to
purr. Cycle through built-in presets by pressingEnteron the TTS Engine row in Settings (s):purr— KittenTTS local neural TTS (default); requires purredge-tts— Microsoft Edge neural TTS; requires edge-tts andmpvorffplaytrans— Google Translate TTS; requires translate-shell
- Custom engine: set
preferred_tts_engineinconfiguration.jsonto a command template:{}is replaced with the spoken text;{output}is replaced with a temp audio file path- If
{output}is present, repy expects the command to write audio to that path, then plays it via mpv/ffplay (with prefetch, same as edge-tts). Example:"preferred_tts_engine": "mytts --text \"{}\" --wav \"{output}\""
- If only
{}is used, the command is expected to speak the text directly (inline). Example:"preferred_tts_engine": "myengine --speed 1.5 \"{}\""
- A bare command name with no placeholders receives the text as its sole positional argument. Example:
"preferred_tts_engine": "myengine"
- Visual Feedback: The paragraph currently being read is underlined in the UI.
- Smart Scrolling: The reader automatically scrolls to keep the active paragraph visible as it progresses through the book.
- Granularity: Text is sent to the TTS engine in manageable chunks (sentence-by-sentence) to ensure responsiveness and proper UI syncing.
Configuration
The configuration file is automatically created on first run with sensible defaults.
Color Themes
repy supports four built-in color themes:
- Default: Uses terminal colors
- Dark: Gruvbox Dark theme
- Light: Gruvbox Light theme
- Sepia: Warm paper-like palette (classic e-reader sepia mode)
Press c in the reader to cycle through themes. With a book open, the selected
theme is saved for that book; otherwise it is saved in configuration.json
under Settings.color_theme.
Location
The config file location follows this priority order:
- XDG_CONFIG_HOME:
$XDG_CONFIG_HOME/repy/configuration.json - Legacy XDG:
~/.config/repy/configuration.json(if the directory exists) - Legacy home:
~/.repy/configuration.json(fallback) - Windows:
%USERPROFILE%\.repy\configuration.json
If you can't find the config file, run repy -vv to see debug output that will
show you exactly which path is being used.
Configuration options
The configuration is JSON with two sections: Setting and Keymap.
Example configuration.json:
{
"Setting": {
"default_viewer": "auto",
"dictionary_client": "sdcv",
"show_progress_indicator": true,
"page_scroll_animation": true,
"mouse_support": false,
"seamless_between_chapters": true,
"color_theme": "Default",
"preferred_tts_engine": "purr",
"tts_engine_args": [],
"library_directories": ["~/Calibre", "~/Books"],
"opds_catalogs": [
{
"name": "Project Gutenberg",
"url": "https://www.gutenberg.org/ebooks/search.opds/",
"username": null,
"password": null
}
],
"opds_download_directory": null,
"inline_images": "placeholder",
"kosync_server": "https://sync.koreader.rocks",
"kosync_username": "your-koreader-sync-user",
"kosync_password": "your-password"
},
"Keymap": {
"scroll_up": "k",
"scroll_down": "j",
"page_up": "h",
"page_down": "l",
"add_highlight": "a",
"add_highlight_comment": "c",
"show_highlights": "A",
"quit": "q",
"help": "?"
}
}OPDS catalogs
From the Library, press O to browse the catalogs in opds_catalogs. Enter
opens a catalog, navigation entry, or downloadable publication; / searches
when the server advertises OpenSearch; [ and ] page; f changes format;
c shows publication details; and h/Backspace returns to the previous feed.
OPDS 1.2 Atom catalogs are supported. Downloads run in the background, are
validated before being saved permanently, and then open in the reader. A null
download directory uses the platform Downloads directory under repy (with an
app-data fallback). Basic-auth credentials are sent only to the configured
catalog origin and passwords are never shown in the UI. Catalog entries remain
configuration-file based. The shared catalog model is version-neutral, so OPDS
2.0 support can be added as a JSON parser without changing the browser or
download pipeline.
You can modify any setting or keybinding by editing this file. Changes take effect on next restart.
KOReader progress sync
Pull-only.
repyfollows the reading position saved by KOReader but never writes its own back to the server. KOReader repositions EPUBs from a CREngine XPointer thatrepycannot generate, so pushing would only overwrite KOReader's bookmark and send a KOReader user to the start of the book.repytherefore reads progress and leaves the server record untouched.
repy can pull the reading position of an identical ebook file from KOReader's
progress-sync service. Register the account in KOReader. The server defaults to
the official public service at https://sync.koreader.rocks, so normally you
only need to set kosync_username and kosync_password. The password is stored
as plaintext in configuration.json; on Unix, repy restricts that file to the
current user (0600). repy derives the MD5 kosync authentication key in
memory.
To place a pulled position accurately, repy reads the CREngine XPointer
KOReader stores alongside the percentage (e.g.
/body/DocFragment[14]/body/p[1]/text().0). The DocFragment index pins the
exact chapter, and the element path places you within it — so a "start of
chapter 14" bookmark lands at the start of chapter 14 rather than drifting by a
paragraph. repy only reads this pointer; it still cannot generate one, which
is why sync stays pull-only. When the pointer is absent or cannot be resolved
(e.g. heavily transformed markup), repy falls back to a width-independent
content percentage — the fraction of the book's characters before your
current line. repy pulls on open and prompts before jumping when KOReader is
further ahead; the Settings window also offers a Pull KOReader progress now
action. The sync service receives only the KOReader document fingerprint,
percentage, device label, and timestamp — not the ebook, filename, highlights,
or notes.
KOReader identifies a document using sampled bytes from the file. Both devices must therefore use the same unmodified ebook file; reconversion or metadata rewrites can prevent matching.
Library directories
Set "library_directories" to a list of directories to scan for EPUB files
(~ expands to your home directory):
"library_directories": ["~/Calibre", "~/Books"]
Opening the Library window (r) then shows your reading history merged with
every book found in those directories, and refreshes the list with a
background scan. Metadata is cached in the database keyed by file path and
modification time, so repeat scans only read new or changed files.
A Calibre library works as-is: point
library_directories at the Calibre library root. repy reads the root
metadata.db through an immutable, read-only SQLite connection, obtaining
books, formats, authors, series, tags, languages, publishers, comments, and
covers without walking every ebook archive. If the database is unavailable or
its schema is incompatible, repy automatically falls back to the per-book
metadata.opf files and directory scan. Calibre's database and library files
are never written.
Mouse support
Set "mouse_support": true (or toggle it in the Settings window, where it
applies immediately) to enable the mouse:
- The wheel scrolls the reading view (3 lines per tick) and moves the selection in list windows and scrollable popups.
- Left-clicking a line that contains a link follows it; if the line has several links, the links window opens instead.
When mouse_support is off (the default), the terminal keeps its native
mouse behavior, so you can select and copy text the usual way.
Database and Reading State
repy stores reading history, last positions, jump history, marks, bookmarks, and highlights in a SQLite database.
The database file (states.db) is located in the same directory as your config file.
Database schema
-
reading_states— Current position for each bookfilepath,content_index,textwidth,row,rel_pctg, optional per-bookcolor_theme
-
library— Metadata and reading progressfilepath,last_read,title,author,reading_progress
-
library_files— Metadata cache for books found inlibrary_directoriesfilepath,mtime,title,author; refreshed by the background scan
-
bookmarks— Named bookmarks per bookid,filepath,name, plus position fields
-
jump_historyandmarks— Per-book jump list and Vim-style marks- Jump entries are row lists; marks store a one-character name plus position fields
-
reading_sessions— Reading statistics keyed by stable book identitybook_id, start/end time, duration, rows, and words
-
booksandbook_aliases— Stable EPUB identity and path aliases- Book identity uses metadata plus spine href and content fingerprints, not just file path
-
highlights— Persistent highlight anchors and plain-text comments- Stores exact text, prefix/suffix context, approximate normalized offset, color, comment, and resolution status
When you quit (q from the reader window), repy saves your current position,
updates the library entry, and flushes the active reading-statistics session.
When you open a book, it restores your last position and any stored bookmarks,
marks, jump history, highlights, and per-book theme.
Contributing
This project is still evolving. Bug reports, small focused patches, and feedback on
feature parity with epy are very welcome.


