A terminal EPUB reader and personal library built with Textual.
Project description
bookreader-tui
A terminal EPUB reader and personal library, built with Textual.
Open an EPUB in a focused two-column or two-page TUI, remember where you left off, organize what you've read into a small SQLite library, and render figures inline in kitty, iTerm2, WezTerm, or any sixel-capable terminal — automatically, with placeholders elsewhere.
pipx install bookreader-tui && bookreader path/to/book.epub
Why bookreader-tui?
- vs Calibre — no Qt, no GUI process, no separate viewer. Lives in your terminal where the rest of your workflow already is.
- vs
epy— full library: collections, ratings, wishlist, reading-time stats, bookmarks with notes, multi-book session continuity. - vs
mdcat/ piping to a pager — proper EPUB rendering (chapters, TOC, two-page mode, inline images) instead of a flat text dump. - vs reading on your phone — keyboard-driven, distraction-free, themed, scriptable. Your reading position syncs by EPUB identifier so the same book at a different path resumes in the same place.
Features
- Distraction-free reader — centered reading column with a typographically sane width cap, TOC sidebar, optional two-page spread.
- Persistent position — picks up exactly where you left off, per book, identified by EPUB
dc:identifier. - Library — SQLite-backed, with collections (Currently Reading, Finished, Want to Read), ratings, bookmarks with notes, per-book reading time.
- Wishlist books — track titles you intend to read before you have the file. Attach a real EPUB later.
- Inline images — kitty / iTerm2 / WezTerm / sixel via textual-image; auto-detected, runtime-toggleable with
I. - Three built-in themes (
dark,light,sepia) plus the standard Textual command palette. - Pure Python, no external services — your library lives in
~/.local/share/bookreader/. Nothing leaves your machine.
Install
The PyPI package is named bookreader-tui. The bare bookreader name was already taken on PyPI; the import path and the console script are still bookreader.
Recommended: pipx or uv tool
For end-user CLI tools, prefer an isolated install over pip install into your system Python — that path is blocked on most modern Linux distros by PEP 668 anyway.
# pipx (most popular)
pipx install bookreader-tui
# OR uv tool (faster, same idea)
uv tool install bookreader-tui
Either gives you a global bookreader command with its dependencies sandboxed in their own venv.
pip (inside a venv)
If you're already managing a virtualenv:
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install bookreader-tui
From source
git clone https://github.com/prajwalmahajan101/BookReader
cd BookReader
pipx install -e . # or: pip install -e . inside a venv
Requirements
- Python ≥ 3.11
- A terminal with at least 80 columns. 120+ recommended.
- For inline images: kitty, iTerm2, WezTerm, Ghostty, or a sixel-capable terminal (foot, mlterm, xterm with
--enable-sixel-graphics, Konsole, Windows Terminal).
Quick start
# Open a book directly — gets added to your library on first open
bookreader path/to/book.epub
# Land on the library home; pick a book with Enter
bookreader
# One-off read without touching the library
bookreader --no-library path/to/book.epub
That's it. Position, bookmarks, and reading time persist automatically.
Inline images — terminal setup
bookreader ships inline-image rendering via textual-image. On launch it sniffs your terminal and enables images if it recognizes a graphics-capable one.
What works out of the box (no config)
| Terminal | Protocol used | Notes |
|---|---|---|
| kitty | Kitty Graphics (TGP) | Best fidelity. Auto-detected via TERM=xterm-kitty or KITTY_WINDOW_ID. |
| Ghostty | Kitty Graphics (TGP) | Native support; behaves like kitty. |
| iTerm2 | iTerm2 inline images | macOS. Auto-detected via TERM_PROGRAM=iTerm.app. |
| WezTerm | Kitty / iTerm / Sixel | Supports all three; auto-detected via TERM_PROGRAM=WezTerm. |
| Konsole | Kitty (partial) | Recent versions only. |
| VS Code terminal | iTerm2 inline images | Works for static images. |
Sixel terminals (force-enable)
foot, mlterm, xterm (built with --enable-sixel-graphics), mintty, Contour, Windows Terminal. Auto-detection doesn't cover these — set the override:
BOOKREADER_IMAGES_ENABLED=1 bookreader path/to/book.epub
Terminals without graphics support
Anything else (e.g. plain xterm, gnome-terminal, Terminal.app, tmux without a graphics-capable host) renders figures as [image: alt] placeholders. Everything else works normally.
Toggle at runtime
Press I inside the reader to flip images on/off mid-session — useful for comparing or when an image is breaking your layout.
Screenshots
| Scroll mode | Two-page spread |
|---|---|
Key bindings
Reader
| Key | Action |
|---|---|
j / k |
Scroll line down / up |
space / b |
Page down / up |
n / p |
Next / previous chapter |
g / G |
Top / bottom of chapter |
t |
Toggle TOC sidebar |
2 |
Toggle two-page mode |
m |
Add a bookmark (with optional note) |
' |
List bookmarks — Enter jumps |
c |
Toggle completion of the current book |
C |
Open Collections overview |
W |
Open Wishlist overview |
I |
Toggle inline image rendering |
T |
Cycle theme |
? |
Show key hints |
q |
Save and back (or quit) |
Scrolling past the end of a chapter flows into the next one; going back from the start flows into the previous chapter's end.
Library
| Key | Action |
|---|---|
Enter / i |
Open the highlighted book |
a |
Add a book (prompts for path) |
A |
Add a wishlist entry (title + author) |
C |
Browse books grouped by collection |
W |
Browse the wishlist |
d / Delete |
Remove the highlighted book |
c |
Toggle completion |
1 … 5 |
Set rating; 0 clears |
| Tab | Switch focus between sidebar and table |
T / ? / q |
Theme / help / quit |
CLI subcommands
bookreader add path/to/book.epub # add without opening
bookreader add --wishlist --title "T" --author "A" # phantom entry, no file
bookreader attach <book-id> path/to/book.epub # promote a wishlist row
bookreader list # print every book
bookreader stats # minutes read per book
bookreader --version
Configuration
All settings are environment variables prefixed BOOKREADER_. Set them in your shell rc, your .env, or per-invocation:
| Variable | Default | Effect |
|---|---|---|
BOOKREADER_IMAGES_ENABLED |
auto-detect | 1 to force inline images on; 0 to force off. |
BOOKREADER_READING_WIDTH |
110 |
Single-column reading width in cells (60–200). |
BOOKREADER_THEME |
dark |
dark, light, or sepia. |
BOOKREADER_TWO_PAGE_DEFAULT |
0 |
1 to launch in two-page mode. |
BOOKREADER_LINE_SCROLL |
1 |
Lines per j/k press (1–10). |
BOOKREADER_PAGE_SCROLL_PCT |
90 |
Page jump as a percentage of viewport (10–100). |
BOOKREADER_SHOW_TOC_DEFAULT |
1 |
0 to start with the TOC sidebar collapsed. |
NO_COLOR |
unset | Honoured by textual — falls back to a safe palette. |
File locations (XDG)
| What | Default path |
|---|---|
| Library DB | ~/.local/share/bookreader/library.db |
| Reading-position cache | ~/.local/state/bookreader/positions.json |
| JSON bookmark fallback | ~/.local/state/bookreader/bookmarks.json |
| Log file (rotating) | ~/.local/state/bookreader/log/bookreader.log |
Honors XDG_DATA_HOME and XDG_STATE_HOME if set. Safe to back up the whole ~/.local/share/bookreader/ directory.
Upgrading from 0.x JSON store
On first launch the library service migrates ~/.local/state/bookreader/positions.json entries that match books already added; the JSON is renamed to positions.json.migrated once data flows. No manual step needed.
Troubleshooting
bookreader: command not found
You installed with pip install bookreader-tui directly into your system Python and PEP 668 blocked it, or you installed into a venv that isn't activated. Use pipx install bookreader-tui (recommended) or activate the venv.
Images render as [image: alt] placeholders even in kitty
Check that the env var hasn't been turned off: echo $BOOKREADER_IMAGES_ENABLED. If it's set to 0, unset it or set to 1. Inside the reader, press I to force-toggle. If it still doesn't render, your kitty session may be inside tmux/screen without graphics passthrough — start kitty directly to test.
Images render at low quality
The image is being upscaled into a column that's too narrow. Raise BOOKREADER_READING_WIDTH (try 140 or 160) and re-launch.
q quits while I'm typing in a wishlist title
It shouldn't — modals isolate keystrokes. If you hit this, please open an issue with your terminal + Textual version.
My EPUB has no images at all
Confirm: bookreader list shows it. Then python -c "from ebooklib import epub; b=epub.read_epub('path.epub'); print(sum(1 for i in b.get_items() if i.media_type.startswith('image/')))". If that prints 0, the EPUB itself has no embedded images.
Library got corrupted
Stop the app. Move the file aside: mv ~/.local/share/bookreader/library.db ~/.local/share/bookreader/library.db.bak. Re-launch — a fresh library is created. Use bookreader add <path> to repopulate.
I want a clean slate
rm -rf ~/.local/share/bookreader ~/.local/state/bookreader. Removes the library, positions, and logs.
Development
git clone https://github.com/prajwalmahajan101/BookReader
cd BookReader
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pre-commit install
# The full local gate (matches CI):
pytest -q
ruff format --check . && ruff check src/ tests/
mypy src/
Project layout
src/bookreader/
core/ # config, paths, logging, exceptions (no I/O)
epub/ # parsing + rendering (no UI, no DB)
state/ # JSON position + bookmark fallback
library/ # SQLite library: db, repo, service, migrations
ui/ # Textual app, screens, widgets, themes
tests/
unit/ # parsers, renderer, config
integration/ # service + screen pilot tests
docs/adr/ # architecture decision records
UI never imports a repository directly — it talks to a service.
Releasing
Tag-driven, fully automated:
# Bump version in src/bookreader/__init__.py and pyproject.toml,
# add a [X.Y.Z] section to CHANGELOG.md, commit, then:
git tag -a vX.Y.Z -m "Release X.Y.Z"
git push origin main vX.Y.Z
.github/workflows/release.yml builds the sdist + wheel, verifies the tag matches pyproject.toml, publishes to PyPI via trusted publishing, and creates a GitHub Release with the CHANGELOG-extracted notes and dist files attached.
Project background
Built solo as a phase-driven exercise: each phase is a feature branch with its own ADR, atomic commits, and merge-clean history.
- Phase 1 — Reader Core
- Phase 1.5 — Two-page mode
- Phase 2 — SQLite Library
- Phase 3 — Polish (bookmarks, sessions, phantom books, inline images)
- Phase 4 — Library curation (collections + wishlist overview)
- Phase 5 — UX polish + first PyPI release as
bookreader-tui
Architecture decisions live in docs/adr/. Version-by-version notes in CHANGELOG.md.
License
Apache-2.0 — Copyright 2026 Prajwal Mahajan.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bookreader_tui-1.0.0.tar.gz.
File metadata
- Download URL: bookreader_tui-1.0.0.tar.gz
- Upload date:
- Size: 68.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6cb59377747be689f9ae6fb58681db05be16c198ea421af83375196f39aae749
|
|
| MD5 |
9eb7204b5fe89b7418808507995cf587
|
|
| BLAKE2b-256 |
3cc36d1ee4305116995f84759d305407ec91689f48348bec5f15f15982c10032
|
Provenance
The following attestation bundles were made for bookreader_tui-1.0.0.tar.gz:
Publisher:
release.yml on prajwalmahajan101/BookReader
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bookreader_tui-1.0.0.tar.gz -
Subject digest:
6cb59377747be689f9ae6fb58681db05be16c198ea421af83375196f39aae749 - Sigstore transparency entry: 1779353096
- Sigstore integration time:
-
Permalink:
prajwalmahajan101/BookReader@3a427e0ba88518802d8d3adb13e26247feb79ae3 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/prajwalmahajan101
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a427e0ba88518802d8d3adb13e26247feb79ae3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file bookreader_tui-1.0.0-py3-none-any.whl.
File metadata
- Download URL: bookreader_tui-1.0.0-py3-none-any.whl
- Upload date:
- Size: 81.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66dc923d04d6800d0aba895a2a59add1b3e52517756ece4d32b513acb39f2d05
|
|
| MD5 |
d403d55b713d7173b2851bdced353459
|
|
| BLAKE2b-256 |
4f1f1229423157d0cf9f76fb18cdd08e0778ab6b03a5a8a40291cb592bfda4ab
|
Provenance
The following attestation bundles were made for bookreader_tui-1.0.0-py3-none-any.whl:
Publisher:
release.yml on prajwalmahajan101/BookReader
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bookreader_tui-1.0.0-py3-none-any.whl -
Subject digest:
66dc923d04d6800d0aba895a2a59add1b3e52517756ece4d32b513acb39f2d05 - Sigstore transparency entry: 1779353223
- Sigstore integration time:
-
Permalink:
prajwalmahajan101/BookReader@3a427e0ba88518802d8d3adb13e26247feb79ae3 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/prajwalmahajan101
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a427e0ba88518802d8d3adb13e26247feb79ae3 -
Trigger Event:
push
-
Statement type: