tuxedo

June 12, 2026 · View on GitHub

A fast, keyboard-driven terminal UI for todo.txt. Vim-style bindings, atomic writes, instant external-edit detection, and five hand-tuned themes — all in a single static binary.

brew install webstonehq/tap/tuxedo

CI Release License: MIT Rust

tuxedo demo

Highlights

  • Pure todo.txt. Reads and writes the standard format — every line is plain text you can edit with anything else.
  • TUI and CLI in one binary. Run tuxedo for the interactive UI, or tuxedo <command> for a todo.txt-cli-compatible command line (add, ls, do, pri, archive, …) — scriptable, with --json output and $TODO_DIR / $TODO_FILE / $DONE_FILE support.
  • Natural-language add. Type prose into the add prompt — Pay rent monthly on the first, show 3 days before due, project home — and tuxedo rewrites it to canonical todo.txt for you to review and save. Local, offline, no AI service.
  • Phone capture. Press s for a QR pointing at a tiny PWA on your machine's LAN — type tasks from your phone and they appear in the list. Captures land in a sibling inbox.txt first, so any tool that can append a line (shell, iOS Shortcuts, cron) is also a capture source.
  • Vim keys, no surprises. j / k to move, dd to delete, gg / G to jump, u to undo (50 levels), chord prompts (gg, dd, fp, fc) with a 600 ms window.
  • Command palette. : or Ctrl-P opens a fuzzy palette over every action — type a few letters, hit Enter. Same matcher as / search, ranked so start-of-label hits beat word-boundary hits beat mid-word hits.
  • Atomic, sync-friendly writes. Every change goes through write-temp-then-rename. If another process — Dropbox, an editor, a script — modifies the file, tuxedo reloads on the next keypress (or within ~250 ms while idle) and flashes a notice.
  • Sibling-file archive. A moves completed tasks to done.txt next to your file, atomically.
  • Filter, sort, multi-select. Cycle by +project or @context, sort by priority / due / file order, and bulk-complete or bulk-delete in visual mode.
  • Saved searches. Name the active /-search with fs, then recall it any time by cycling saved filters with ff. Stored as plain filter.<name> lines in the config — hand-editable like everything else.
  • Five themes, three densities. Cycle with T and D. Choices persist across runs.
  • No daemon, no database, no cloud. One file in, one file out.

Screens

Empty state • cell-bowtie mark and quick-start when the file has no tasksempty
List • list of todos, optionally groupedempty
Archive • completed tasks grouped by completion datearchive
Filter sidebar activefp cycles projects with j/k, fc cycles contexts; saved searches list under a SAVED heading with live match countsfilter
Command palette: or Ctrl-P opens a fuzzy palette over every actioncommand palette
Help? opens the full keybindings overlayhelp
How to generate the screenshots and demo

The screenshots in the table above are checked-in SVGs. Regenerate them with:

mise run screenshots

The hero GIF at the top is recorded with vhs from docs/demo.tape. Regenerate it with:

mise run demo

Themes

T opens a picker over five built-in themes, including Terminal, which respects your terminal palette.

Muted Slate (default)Dawn
muted slatedawn
NordMatrix
nordmatrix

Custom themes

Beyond the built-ins, tuxedo loads any *.toml file you drop in ${XDG_CONFIG_HOME:-$HOME/.config}/tuxedo/themes/. Each one joins the T picker in sorted filename order. Ready-made themes live in docs/themes/ — copy one in and press T:

mkdir -p ~/.config/tuxedo/themes
curl -o ~/.config/tuxedo/themes/gruvbox-dark-soft.toml \
  https://raw.githubusercontent.com/webstonehq/tuxedo/main/docs/themes/gruvbox-dark-soft.toml
Theme file format and field reference

A theme file is one key = value per line. name is the label shown in the picker; every other field is a color value. All fields are required: a file missing one, carrying an unparseable color, or whose name collides with another theme is skipped with a warning at startup.

Color values accept two forms:

  • #rrggbb — a solid hex color (case-insensitive).
  • reset or transparent — inherits the terminal emulator's own background color. Useful for bg, panel, and statusbar when you want your terminal's opacity, blur, or wallpaper to show through while keeping a custom text palette. Both keywords are case-insensitive and behave identically (same effect as the built-in Terminal theme).
FieldColors
namelabel shown in the T picker (the only non-color field)
bgwindow background
panelfilter and detail panel background
borderpanel and modal borders
fgprimary text
dimsecondary / muted text
accentlogo, headings, hints, and selection markers
cursorcurrent row, and the highlighted row in the T picker
selectionset to the same value as selected
statusbarstatus bar background
status_fgstatus bar text
mode_fg / mode_bgmode chip text / background
pri_a pri_b pri_c pri_dpriorities A through D
pri_otherpriorities E through Z
project+project tags
context@context tags
duedue: date
overduepast-due date
todaydate due today
donecompleted tasks
selectedselected-row background (visual mode) and the active filter
matchedsearch-match highlight

Install

Homebrew (macOS, Linux)

brew install webstonehq/tap/tuxedo

Prebuilt binaries

Download the archive for your platform from the latest release and put tuxedo on your PATH.

Targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc. Each archive ships with a .sha256 checksum.

From source

cargo install --git https://github.com/webstonehq/tuxedo

Or clone and build:

git clone https://github.com/webstonehq/tuxedo
cd tuxedo
cargo build --release
./target/release/tuxedo [FILE]

Requires the Rust 2024 edition (recent stable toolchain).

Usage

tuxedo is two things in one binary: an interactive TUI, and a one-shot command line. With no subcommand it launches the TUI; with a recognized subcommand it runs the command line and exits.

tuxedo [FILE]      # launch the TUI on FILE (created if missing)
tuxedo             # TUI on the default file (see resolution below)
tuxedo --sample    # open the bundled sample file in the temp dir
tuxedo <command>   # run a one-shot CLI command — see "Command-line interface"
tuxedo update      # print upgrade instructions for your install
tuxedo --help
tuxedo --version

When a newer release is available, the status bar shows ↑ <version> (tuxedo update) next to the version. The check runs in the background, is cached at $XDG_CACHE_HOME/tuxedo/latest_version.json for 24 h, and fails silently when offline. Set TUXEDO_NO_UPDATE_CHECK=1 to disable.

Which file tuxedo opens

Both the TUI and the CLI resolve the todo file the same way, in order:

  1. An explicit FILE argument (TUI only).
  2. $TODO_FILE, if set.
  3. $TODO_DIR/todo.txt, if $TODO_DIR is set.
  4. ./todo.txt in the current directory, if it exists.
  5. Otherwise a sample todo.txt in the system temp directory, so you can poke around without committing to a path.

The archive file is $DONE_FILE if set, otherwise a sibling done.txt next to the todo file. The file (and any missing parent directories) is created on first use. These are the same TODO_DIR / TODO_FILE / DONE_FILE variables todo.txt-cli uses, so an existing todo.cfg works as-is:

export TODO_DIR="$HOME/Documents/todo"
export TODO_FILE="$TODO_DIR/todo.txt"
export DONE_FILE="$TODO_DIR/done.txt"

Edits are persisted on every change via atomic write (write .tmp, rename).

If the file changes on disk (another editor, a sync client, a script), tuxedo notices on the next keypress, or within ~250 ms while idle, and reloads. The keystroke that triggered the reload is consumed — press it again to act on the fresh state — and the status bar flashes a notice.

Pressing A appends every completed task to a sibling done.txt and removes them from the working file (atomically: done.txt is written before the originals are dropped). a toggles the archive view so you can browse, un-archive, or permanently delete past tasks.

Command-line interface

When the first argument is a recognized subcommand, tuxedo runs a one-shot command instead of launching the TUI. The surface mirrors todo.txt-cli — same commands, aliases, task numbering, and output — so it's a drop-in for scripts and aliases.

tuxedo add "Pay rent +home @bank due:2026-07-01"   # or: tuxedo a "..."
tuxedo ls @bank                                     # filter by context
tuxedo do 3                                          # mark task 3 complete
tuxedo pri 3 A                                        # set priority
tuxedo archive                                        # move done tasks to done.txt
tuxedo ls --json | jq .                              # machine-readable output
CommandAliasesArgumentsDescription
addaTEXT...Add a task (natural-language dates supported, same as the n prompt).
appendappN TEXT...Append text to task N.
prependprepN TEXT...Prepend text to task N.
replaceN TEXT...Replace task N entirely.
pripN PRIORITYSet priority AZ on task N.
depridpN...Remove priority from the given tasks.
dodone, completeN...Mark tasks complete (recurring tasks spawn their next instance).
delrmN [TERM]Delete task N, or remove just TERM from it. Prompts unless -f.
archiveMove completed tasks to the done file.
listls[TERM...]List tasks. TERM is +project, @context, or free text.
listalllsa[TERM...]List the todo file and the done file.
listprilsp[PRIORITY]List prioritized tasks (optionally a single priority).
listprojlsprjList all +projects.
listconlscList all @contexts.

Task numbers are 1-based line numbers in the file, exactly as printed by list — stable regardless of how the list is filtered or sorted. list sorts by the full line (case-insensitive) and prints a TODO: X of Y tasks shown footer, matching todo.txt-cli.

Options:

  • -f, --force — skip confirmation prompts (e.g. for del).
  • --json — emit machine-readable JSON instead of text. list-style commands print an array of task objects; mutating commands print a result object. No prompts or footers are written in this mode.

Global flags may appear before the subcommand (tuxedo -f del 3).

Differences from todo.txt-cli: do marks a task complete but does not auto-archive it — completed tasks stay in the file until you run archive (or press A in the TUI), matching tuxedo's interactive model. There is no -d config-file flag; configure paths with the environment variables above.

Keybindings

KeyAction
j / next task
k / previous task
ggfirst task
Glast task
Ctrl-d / Ctrl-uhalf-page down / up

Editing

KeyAction
nadd task
e / iedit current task
xtoggle complete
dddelete task
pcycle priority A → B → C → ·
cadd or remove a context
+add a project
yycopy current line to clipboard
ybcopy current body only (no priority, dates, projects, contexts, key:value)
uundo (50 levels)

Filtering, sort, view

KeyAction
/search
fpfilter by project (j / k cycles, Esc clears)
fcfilter by context (j / k cycles, Esc clears)
ffpick a saved search (j / k cycles, Enter keeps, Esc reverts)
fssave the active /-search as a named filter
Scycle sort: priority → due → file order
venter visual / multi-select; space toggles a row
x / dd (in visual)bulk-complete / bulk-delete the selection
llist (default) view
atoggle archive view
Aarchive completed tasks → done.txt
Htoggle showing done tasks in the main list

Layout & theme

KeyAction
[toggle filter sidebar
]toggle detail sidebar
Topen theme picker
Dcycle density: compact → comfortable → cozy
Ltoggle line numbers

System

KeyAction
: / Ctrl-Pcommand palette
sshare capture QR (phone PWA)
?help overlay
,settings overlay
qquit

Two-key chord prompts (gg, dd, yy, yb, fp, fc, ff, fs) show a g… / d… / y… / f… indicator in the status-bar mode chip while the leader is armed; the window is 600 ms.

Copy uses the OSC 52 terminal escape, so it works locally and over SSH on any terminal that supports it (kitty, alacritty, wezterm, iTerm2, foot, modern xterm; tmux when set -g set-clipboard on). Older terminals will silently ignore the keystroke.

todo.txt format

Standard todo.txt lines:

(A) 2026-04-28 Call dentist @phone +health due:2026-05-08
  • (A) — priority, A through Z (omit for none)
  • 2026-04-28 — creation date in ISO 8601
  • +project — project tag
  • @context — context tag
  • key:value — extension; due:YYYY-MM-DD is recognized for sort and due-bucket grouping in the list view. Keys you'd rather not see can be hidden from the rows via hide_keys
  • rec:[+]N{d,b,w,m,y} — recurrence; on completion (x), tuxedo inserts a fresh copy of the task with due: advanced by N days, business days (Mon–Fri), weeks, months, or years. The + prefix means strict recurrence anchored to the previous due date (e.g. rec:+1m for monthly rent on the 15th); without it, the new due is computed from the completion date (e.g. rec:1w for "water plants one week after I last did").

Completed tasks are prefixed with x and a completion date:

x 2026-05-05 2026-05-01 Submit expense report +work

Recurring example:

2026-05-09 Pay rent due:2026-05-15 rec:+1m

Pressing x on the line above marks the original complete and inserts 2026-05-09 Pay rent due:2026-06-15 rec:+1m. u undoes both at once.

Natural-language add

Press n to open the add prompt. Type the task in plain English. When the buffer contains recognized phrases (dates, weekdays, recurrence, project / context names, priority), pressing Enter rewrites the draft into canonical todo.txt — review or tweak it, then Enter again to save.

What you typeWhat lands in the draft
Pay rent monthly on the first of the month, show the todo 3 days before the due date. It's part of project home and context bankPay rent +home @bank due:2026-06-01 rec:+1m t:-3d
Buy milk tomorrowBuy milk due:2026-05-12
Call mom every week starting Friday for project familyCall mom +family due:2026-05-15 rec:+1w
Submit timesheet every other friday show 1 day beforeSubmit timesheet due:2026-05-15 rec:+2w t:-1d
Daily standup high priority(A) standup rec:+1d
Annual review April 15 +work @officeAnnual review +work @office due:2027-04-15

Recognized vocabulary:

  • Datestoday, tonight, tomorrow, yesterday, weekdays (monday / mon …), months (april 15, 15th of april), in 3 days, the first of the month, ISO 2026-05-15.
  • Recurrencedaily, weekly, biweekly, monthly, yearly, annually, every monday, every 2 weeks, every other friday, every business day.
  • Thresholdshow 3 days before due, 2 weeks before due.
  • Projects / contexts — prose form project home and context bank, or the standard +home / @bank sigils.
  • Priorityhigh priority → A, medium priority → B, low priority → C, or priority A.

Parsing is rule-based and runs locally — no network calls, no API key. If the buffer already contains a due:, rec:, or t: token, tuxedo assumes you've typed canonical form and saves it directly on the first Enter.

Phone capture

Press s to start a tiny capture server on your machine's LAN address and display a QR code for it. Scan it from your phone — any modern browser — to get a minimal PWA you can install to your home screen. Type a task, tap Add, and within a tick it shows up in your task list.

Captures never touch todo.txt directly. They land in a sibling inbox.txt, which tuxedo drains on every external-change poll: each line is run through the same natural-language pipeline as the n add prompt, given a creation date if missing, and merged into todo.txt as a single undoable batch (u rolls back the whole drain at once).

That makes inbox.txt a general capture endpoint, not just a PWA backend. Anything that can append a line works as a producer:

echo "Refill prescription tomorrow" >> ~/notes/inbox.txt
echo "Call dentist due:2026-06-01" >> ~/notes/inbox.txt

Shell aliases, iOS Shortcuts writing to a synced folder, cron jobs, email-to-file gateways — pick your producer. As long as it appends a line to the sibling inbox.txt, tuxedo picks it up.

The server:

  • Binds on first s press and stays up for the rest of the session. Subsequent s presses just re-show the QR; any key dismisses the overlay.
  • Listens on 0.0.0.0:<port> so phones on the same WiFi can reach it. The port is OS-assigned on first use and persisted to config.toml so phone bookmarks survive across sessions.
  • Gates every protected route on a 64-character hex token baked into the URL path. The token is generated once, persisted to config.toml, and compared in constant time.
  • Speaks plain HTTP — trusted networks only. On a shared or public WiFi anyone passive-sniffing can recover the token. To rotate, delete share_token from config.toml and press s again.

Drains from tuxedo-managed producers are crash-safe: the capture server holds the same advisory lock as the TUI's rename-and-merge, and any staging file left over from an interrupted drain is replayed on the next session. Plain shell appends are useful for lightweight capture, but they do not take that lock; use the capture server or the same lock if a producer must be serialized with the TUI drain.

Configuration

Persisted to ${XDG_CONFIG_HOME:-$HOME/.config}/tuxedo/config.toml. Cycling theme, density, or sort, and toggling sidebars / line-numbers / done-visibility all update the file. Unknown keys are ignored, so older binaries don't break on newer files.

Two additional keys, share_token and share_port, are written by the phone capture server on first use. Treat share_token as a secret — anyone who has the value and LAN reach can append to your inbox. Delete the key from config.toml to rotate it on the next s press.

Saved searches (created with fs) are written one per line as filter.<name> = <query>, where <query> is the /-search needle. They round-trip as plain text, so you can add, rename, or delete them by editing config.toml directly; a repeated filter.<name> keeps the last value, and <name> may not contain =.

Hiding key:value tags

Some key:value extensions are for machines, not eyes — e.g. a uid: you sync against. Add a comma-separated hide_keys line to config.toml and those keys' tokens are dropped from the task rows (list and archive views):

hide_keys = uid, sync

Matching is case-insensitive. Hiding is purely visual — the tags stay on disk untouched, still serialize, and still show in the detail pane's RAW section (a deliberate escape hatch). Searches still match hidden text; the hidden characters just aren't drawn.

Development

mise run fmt      # cargo fmt --all
mise run clippy   # cargo clippy --all-targets --locked -- -D warnings
mise run test     # cargo test --locked

CI runs all three on every push and pull request. Tasks are also runnable as plain cargo commands if you don't use mise.

Acknowledgments

  • todo.txt by Gina Trapani — the format that makes a tool like this possible.
  • ratatui and crossterm — the rendering and terminal-input crates tuxedo is built on.

Roadmap

Planned and in-flight work lives in todo.txt — eat your own dog food.

Contributing

Issues and pull requests are welcome. For larger changes, please open an issue first to discuss the approach. Run mise run fmt clippy test (or the plain cargo equivalents) before submitting.

License

Released under the MIT License.