codex

codex is a terminal-first ebook library and ereader manager, inspired by Calibre but built for people who live in the shell. The binary is called cdx.

Everything works over SSH — there is no GUI. Every read command speaks JSONL (--json) so it composes cleanly with jq, fzf, and shell scripts, and every subcommand has an equivalent screen in the built-in terminal UI (cdx tui).

What it does

  • Catalogs — keep one or more independent libraries, each a plain directory (a catalog.db SQLite file plus a books/ tree) that you can put under git.
  • Import — add EPUB, PDF, MOBI, and AZW3 files; codex extracts metadata and stores each book under a sanitized Author_-_Title.ext name.
  • Metadata — edit title/author/tags/rating/series and embed the changes back into the book file.
  • Search & groups — substring search with field filters, and a folder-style browse mode grouping by author, tag, series, publisher, language, or rating.
  • Kindle sync — detect USB-mounted Kindles and push/pull/sync books, with a git add -p-style interactive plan.
  • Duplicates — detect duplicate copies (by content hash or normalized title+author) and assist removal.
  • Reader — read EPUB, MOBI/AZW3 (non-DRM), PDF, TXT, and Markdown right in the terminal, with vim-style navigation and saved reading progress.

Where to start

Installation

codex ships as a single prebuilt binary. There is no cargo install / crates.io package — you download the binary for your platform.

curl -fsSL https://codex.daniel.ws/install.sh | sh

The script detects your OS and architecture, downloads the matching binary from the latest GitHub release, verifies its SHA-256 checksum, and installs it to ~/.local/bin.

Environment variables:

VariableDefaultMeaning
CDX_INSTALL_DIR$HOME/.local/binWhere to install the cdx binary
CDX_VERSIONlatest releaseInstall a specific tag (e.g. v1.0.0)

If ~/.local/bin is not on your PATH, the script prints a reminder. Add it:

export PATH="$HOME/.local/bin:$PATH"

Supported platforms

PlatformRelease asset
Linux x86_64cdx-x86_64-unknown-linux-musl (static)
macOS (Apple silicon)cdx-aarch64-apple-darwin
macOS (Intel)cdx-x86_64-apple-darwin

The Linux binary is statically linked against musl, so it runs on both glibc and musl distributions with no system dependencies. USB device sync is Linux-only; the rest of codex works on every supported platform.

Manual download

Grab the asset for your platform from the releases page, verify it, and move it onto your PATH:

target="x86_64-unknown-linux-musl"   # pick yours
tag="v1.0.0"
base="https://github.com/Danielwsx64/codex/releases/download/$tag"
curl -fsSLO "$base/cdx-$target"
curl -fsSLO "$base/cdx-$target.sha256"
sha256sum -c "cdx-$target.sha256"    # macOS: shasum -a 256 -c
chmod +x "cdx-$target"
mv "cdx-$target" ~/.local/bin/cdx

Keeping it up to date

Once installed, codex can update itself — see Updating cdx:

cdx update

Building from source

You need a Rust toolchain (MSRV 1.80):

git clone https://github.com/Danielwsx64/codex
cd codex
cargo build --release
# binary at target/release/cdx

Getting started

1. Create a catalog

A catalog is a directory you choose — it can live anywhere, including inside a git repo. codex tracks the set of catalogs in a small registry at $XDG_CONFIG_HOME/cdx/config.toml and remembers which one is current.

cdx catalog init home ~/books

This creates ~/books/catalog.db and ~/books/books/, registers it under the name home, and makes it the current catalog.

2. Add some books

cdx add ~/Downloads/*.epub ~/Downloads/dune.pdf

codex extracts basic metadata, stores each file under books/<id>/Author_-_Title.ext, and skips exact duplicates (override with --force). Supported formats: EPUB, PDF, MOBI, AZW3 (plus TXT/Markdown for the reader).

3. List and inspect

cdx ls
cdx inspect 1          # by id
cdx inspect "Dune"     # or exact title (case-insensitive)

Add --json to any read command for JSONL output:

cdx ls --json | jq -c '{id, title, author}'

4. Open the terminal UI

cdx tui

The TUI mirrors every command. Press ? on any screen for its key bindings, : for the command palette, and q (or Ctrl+C) to quit.

Global flags

These work on every subcommand:

FlagMeaning
--jsonEmit machine-readable JSONL on stdout
-v / -vv / -vvvIncrease log verbosity (info / debug / trace)
--catalog <name>Use a registered catalog other than the current one
--data-dir <path>Override the config dir (mostly for tests)

Next: learn about Catalogs and the Library.

Catalogs

codex is multi-catalog. Each catalog is a directory you pick; the registry at $XDG_CONFIG_HOME/cdx/config.toml lists them and points at the current one. On-disk layout of a catalog:

<catalog>/
  catalog.db          # SQLite metadata
  books/<id>/<file>   # the stored book files

Commands

CommandWhat it does
cdx catalog init <name> <path>Create a new catalog at <path> and register it
cdx catalog add <name> <path>Register an existing catalog directory
cdx catalog lsList registered catalogs (current + (missing))
cdx catalog use <name>Switch the current catalog
cdx catalog rm <name>Unregister a catalog (--purge deletes its files)

init and add accept --description <text> and --no-switch (register without making it current).

cdx catalog init fiction ~/lib/fiction --description "Novels"
cdx catalog add archive /mnt/nas/ebooks
cdx catalog ls
cdx catalog use archive

A catalog whose directory has disappeared from disk is shown as (missing) in cdx catalog ls rather than causing an error.

In the TUI

The Catalogs screen lists every catalog (current one marked), lets you switch with Enter and remove with confirmation. The New catalog wizard covers both init and add. Reach it from the welcome menu or the :catalogs command-palette entry.

Library

The library commands operate on books in the current catalog.

Importing

cdx add file1.epub file2.pdf ...

Accepts EPUB, PDF, MOBI, AZW3 (and TXT/Markdown for the reader). codex extracts metadata, stores each file under a sanitized Author_-_Title.ext, and skips content-duplicate imports. Use --force to import anyway.

Listing

cdx ls
cdx ls --columns id,title,author,rating
cdx ls --all-columns
cdx ls --json

Available columns: id, title, author, tags, series, rating, publisher, language, published, isbn, format, embed.

Inspecting

cdx inspect <id|title>

Accepts a numeric id or an exact (case-insensitive) title. An ambiguous title returns an error listing the candidate ids.

Editing metadata

cdx edit <id>                      # opens $EDITOR with the book's metadata as TOML
cdx tag <id> <tag>...              # add tags
cdx untag <id> <tag>...            # remove tags (--all clears every tag)
cdx rate <id> <0-5>                # set rating (0 clears)
cdx series <id> <name> --index 2   # set series + position (--clear removes it)

Any metadata change marks the book embed_status = pending so it can be embedded back into the file later — see Metadata embedding.

Searching

cdx search dune                    # substring across title/author/tags
cdx search --author tolkien        # field filters
cdx search hobbit --tag fantasy --rating 4..5

Whitespace-separated tokens are AND'd. Filters: --author, --tag (repeatable, AND), --series, --rating (exact 4 or range 3..5). --json emits JSONL.

Removing

cdx rm <id|title>          # removes from catalog and deletes the file
cdx rm <id|title> --keep   # moves the file to the cwd instead of deleting

In the TUI

The Library screen lists books with the same columns and actions (inspect / edit / open / push / delete / column selection / embed). / filters by text; g switches to group browsing.

Metadata embedding

codex stores metadata in the catalog database. Embedding writes that metadata back into the book file itself, so the values travel with the book when you copy it to a device or another app.

The embed cycle

  1. Any edit (cdx edit, cdx tag, cdx rate, cdx series, or the TUI edit modal) sets the book's embed_status to pending.
  2. Running the sync embeds the metadata into every pending book and marks it synced (EPUB/PDF) or unsupported (MOBI/AZW3, which are not rewritten).
cdx embed sync

This walks every book with status pending, embeds the metadata, and prints per-book progress plus a final summary.

Format support

FormatEmbedding
EPUB, PDFSupported — metadata is written into the file
MOBI, AZW3Not supported — marked unsupported

The embed column in cdx ls (and the Inspect screen in the TUI) shows the current status. In the TUI, press w on the Inspect screen to embed a single book.

Note: for EPUB, codex keeps a stable content fingerprint that ignores the rewritten OPF, so re-embedding does not make a book look like a duplicate.

Devices (Kindle sync)

codex syncs with USB-mounted Kindles on Linux, with support for several devices connected at once. Each device is identified by the stable serial read from the USB descriptor; you can give it an alias and use that day-to-day.

Device sync is Linux-only. On other platforms the commands compile but detect no devices.

Selecting a device

Every device command takes --device <serial|alias>. With exactly one device connected it is the implicit default; with two or more and no flag you get an error listing the candidates. The last device you used explicitly becomes the current device (per catalog), so it stays the default across runs.

Commands

CommandWhat it does
cdx device lsList detected and known devices (alias, serial, free space, book count)
cdx device alias <target> <alias>Set or rename a device alias
cdx device books [--device <a>]List books on a device with catalog presence
cdx device clean [--device <a>]Remove books from a device (never touches the catalog)
`cdx push <idtitle> [--device ]`
cdx pull <path> [--device <a>]Import a book from the device into the catalog
cdx sync [--device <a>]Bidirectional diff + interactive apply

push and pull open an interactive picker when you omit the target/path. clean supports --all and --yes; pull supports --force.

How identity works

  • Exact: the device_books sync-state table records every book codex sent or pulled (book id ↔ device path + SHA-256 + size/mtime). No guessing for these.
  • By metadata: books that arrived on the device by other means are matched on normalized title + author (casefold, NFKD without diacritics, collapsed punctuation/whitespace) — never the filename, since formats differ between ends. A genuinely ambiguous match becomes a conflict for you to resolve.

Sync

cdx sync                 # interactive, git-add-p style
cdx sync --dry-run       # print the plan only
cdx sync --yes           # accept everything (scripts)
cdx sync --verify        # full SHA-256 instead of the size+mtime fast path

The plan lists what is missing on each end, modified, missing, and match conflicts. You confirm item by item (y apply / n skip / a accept the rest / q abort). Sync never deletes on either end — removal is always manual (use cdx device clean).

In the TUI

The Devices screen lists devices (r renames, Enter opens the device's books). Presence indicators (both / local only / device only / modified) appear in the Library and device views. p in the Library pushes the selected book to the current device.

Duplicates

cdx dedup finds duplicate books in the current catalog and suggests which copy to remove. Signals are combined by union — if any method flags a group, it becomes a candidate.

  1. Content hash — SHA-256 fingerprints catch byte-identical copies and the same EPUB before/after embedding.
  2. Normalized title + author — casefold + NFKD, collapsed punctuation/whitespace; catches the same book in different formats or editions where the hash won't match.

For each group codex suggests deleting the "worst" copy: the one with the fewest filled-in metadata fields, breaking ties toward the older / weaker-embed copy. The decision is always yours — codex only suggests.

Usage

cdx dedup                    # list duplicate groups + the suggested removal
cdx dedup --by hash          # restrict the detection signal
cdx dedup --by meta
cdx dedup --by all           # default: union of both
cdx dedup --json             # JSONL, one object per group

Assisted removal:

cdx dedup --rm               # interactive picker (accepts the suggestion by default)
cdx dedup --rm --keep        # move removed files to the cwd instead of deleting
cdx dedup --yes              # accept every suggestion (scripts)

Removal reuses the cdx rm path and never deletes without confirmation (unless --yes).

In the TUI

The Duplicates action in the Library section lists the groups, highlights the suggested copy, and removes with confirmation.

Groups

Group browsing lets you navigate the catalog like a folder tree: pick a metadata field as the "grouper" and each distinct value becomes a folder. It is a mode of the Library screen, not a separate section — inside a folder you get the same columns and actions as the normal listing.

A folder's scope is exact equality (the "Jane Austen" folder contains only author = 'Jane Austen'), unlike search, which is substring. author is a single column (a book falls in one folder); tags is many-to-many (a book can appear in several folders).

CLI

cdx groups --by author
cdx groups --by tag
cdx groups --by rating
cdx groups --by publisher
cdx groups --by language
cdx groups --by series
cdx groups --by format
cdx groups --by author --json

Each row is a value plus its book count. In --json (JSONL, one object per group), the catch-all group — no author / no tags / no rating — has value: null. An empty catalog prints nothing in --json.

In the TUI

Press g in the Library to open the grouper selector (Author / Tags / Rating / Publisher / Language / Series / Format / Off). The folder level lists value + count; Enter drills in. A breadcrumb shows the current group. Esc steps back one layer at a time: clear the filter → back to folders → leave grouping → back to the welcome screen. / inside a folder filters that folder's books in memory without widening its exact scope.

TUI reader

codex can read books directly in the terminal. The reader is TUI-only — open a book from the Library (there is no CLI equivalent). Text is reflowed to the viewport and recomputed on resize.

Supported formats

FormatNotes
EPUBSpine extraction + HTML→text
TXT / MarkdownMarkdown via CommonMark; TXT read directly
MOBIPalmDOC / uncompressed; HUFF/CDIC is refused with a clear message
AZW3 (KF8)Dual-stream AZW3 reads the legacy stream; KF8-only is refused with a suggestion to convert to EPUB
PDFSingle-column text; multi-column is best-effort with a warning

DRM-protected books (Amazon Topaz / KFX / protected AZW) are detected and refused — codex does not remove DRM. Only sideloaded, DRM-free files work.

Vim-style cursor and paging:

KeysAction
h j k lMove the cursor
w b e 0 $Word / line motions
gg GTop / bottom
Space Ctrl+f Ctrl+bPage down / up
Ctrl+d Ctrl+uHalf-page down / up
[ ]Previous / next chapter
:NJump to absolute page N
:cNJump to chapter N
?Contextual help
EscBack to the Library

The footer shows chapter X/Y · page A/B. For PDF, each page is a chapter, so :cN jumps to the real document page.

Reading progress

Progress (last chapter, offset, and time) is saved when you change chapters, page, or leave the reader, and restored when you reopen the book.

Performance

Conversion results (PDF/EPUB/MOBI/AZW3) are cached on disk under the XDG cache dir and invalidated by source mtime + size. Opening runs in a background thread with an animated loading screen; the TUI stays responsive and Esc cancels.

Terminal UI

cdx tui

The TUI mirrors every CLI subcommand — both surfaces share the same domain modules, only the rendering differs. The welcome screen is the home and lists the top-level sections as navigable links (/ to move, Enter to enter):

  1. Library — list / view / edit / remove books, groups, duplicates
  2. Search — the Library's filtered mode
  3. Catalogs — the catalog registry + new-catalog wizard
  4. Devices — ereader sync

Command palette

Press : from any screen to open a vim-style input at the bottom. Commands: :library, :catalogs, :search, :devices, and :quit (:q). Tab completes the shortest unique prefix (:l, :c, :s, :d, :q). Enter runs it; Esc cancels.

Reserved keys

These are consistent across the whole TUI:

KeyMeaning
q, Ctrl+CQuit (the only exit keys)
EscBack / cancel within a screen — never quits
EnterConfirm / drill in
?Contextual help for the active screen

Per-screen bindings beyond these are documented in each screen's ? overlay.

CLI reference

cdx <command> [args]. Run cdx --help or cdx <command> --help for the authoritative, always-current text. Global flags (--json, -v/-vv/-vvv, --catalog <name>, --data-dir <path>) work on every subcommand.

Catalogs

CommandDescription
cdx catalog init <name> <path>Create + register a catalog (--description, --no-switch)
cdx catalog add <name> <path>Register an existing catalog (--description, --no-switch)
cdx catalog lsList catalogs
cdx catalog use <name>Switch the current catalog
cdx catalog rm <name>Unregister (--purge deletes files)

Library

CommandDescription
cdx add <file>...Import books (--force)
cdx lsList books (--columns, --all-columns)
cdx inspect <id|title>Show detailed metadata
cdx search <query>Search (--author, --tag, --series, --rating)
cdx edit <id>Edit metadata in $EDITOR
cdx tag <id> <tag>...Add tags
cdx untag <id> <tag>...Remove tags (--all)
cdx rate <id> <0-5>Set/clear rating
cdx series <id> <name>Set series (--index, --clear)
cdx rm <id|title>Remove book (--keep)
cdx groups --by <field>Group by author/tag/rating/publisher/language/series/format
cdx dedupFind duplicates (--by, --rm, --yes, --keep)

Metadata embedding

CommandDescription
cdx embed syncEmbed metadata into all pending books

Devices

CommandDescription
cdx device lsList devices
cdx device alias <target> <alias>Set/rename alias
cdx device booksList device books (--device)
cdx device cleanRemove books from a device (--device, --all, --yes)
cdx push <id|title>Copy to device (--device)
cdx pull <path>Import from device (--device, --force)
cdx syncBidirectional sync (--device, --dry-run, --yes, --verify)

Maintenance

CommandDescription
cdx tuiOpen the terminal UI
cdx updateSelf-update (--check, --yes) — see Updating cdx
cdx completions <shell>Print a completion script — see Shell completions

JSON output

Every read command accepts --json and emits JSONL — one JSON object per line, no surrounding array — so it streams through jq -c, head, and fzf --preview. A command with zero records prints nothing.

cdx ls --json | jq -c 'select(.rating >= 4) | {title, rating}'
cdx search --author tolkien --json | jq -r '.title'

Updating cdx

codex can update itself in place from the latest GitHub release.

Check for a newer version

cdx update --check

This reports whether a newer release exists and installs nothing. Add --json for machine-readable output:

cdx update --check --json
# {"current":"1.0.0","latest":"1.1.0","newer_available":true,"html_url":"..."}

Install the update

cdx update

codex downloads the release binary for your platform, verifies its SHA-256 checksum, and atomically replaces the running executable. You are asked to confirm first; pass --yes to skip the prompt (for scripts):

cdx update --yes

How it works

  • The target platform is chosen automatically (Linux x86_64 → the static musl binary; macOS → the matching Apple-silicon or Intel binary).
  • The download is checksum-verified before anything is replaced; a mismatch aborts without touching your installed binary.
  • The new binary is written next to the current one and swapped in with an atomic rename, so an interrupted update never leaves you with a half-written cdx.

If your platform has no prebuilt release, cdx update tells you to build from source (see Installation). You can also re-run the install script at any time.

Shell completions

cdx completions <shell> prints a completion script to stdout. Supported shells: bash, zsh, fish, elvish, powershell.

bash

# system-wide (needs bash-completion installed)
cdx completions bash | sudo tee /etc/bash_completion.d/cdx >/dev/null

# or per-user: source it from ~/.bashrc
cdx completions bash > ~/.local/share/bash-completion/completions/cdx

zsh

# put it on your $fpath, e.g.
cdx completions zsh > ~/.zfunc/_cdx
# then ensure ~/.zfunc is on the fpath before compinit in ~/.zshrc:
#   fpath=(~/.zfunc $fpath)
#   autoload -U compinit && compinit

fish

cdx completions fish > ~/.config/fish/completions/cdx.fish

Reload your shell (or source the relevant rc file) and completions for cdx subcommands and flags become available.

Dynamic completion of positional arguments (for example, completing book titles and ids in cdx inspect <TAB> against the catalog) is planned for a later release; today's completions cover subcommands and flags.