A terminal client for LeetCode. Browse, search, solve, run, and submit without leaving the terminal — with full Premium parity and optional GitHub sync.
Website · Install · Keys · For agents · Report a bug
A real screen, not a mockup. site/tools/capture.sh runs the binary in tmux and converts what it drew.
Status: v0.1.0 released. All seven phases complete. Install with Homebrew, go install, or a prebuilt binary for macOS, Linux, or Windows.
Sync 4,013 problems locally and search them instantly offline. Edit in your editor, run
against the examples without leaving the terminal (Python, Go, C++, JS/TS), and submit to the
judge. Study plans, contests, company packs, editorials, and the premium filter are in. An
accepted solution commits itself; pushing is yours to press.
See docs/ROADMAP.md.
New session working on this repo? Read in this order — they exist so decisions are not re-derived or re-asked:
| File | What it holds |
|---|---|
docs/DECISIONS.md |
Every settled architecture decision, why, what it costs, what would reverse it. Treat as settled. |
docs/DESIGN.md |
The Departure Board visual system — palette, type treatments, motion, copy rules |
docs/ARCHITECTURE.md |
Package map, data flow, key seams, invariants |
docs/ROADMAP.md |
Phase status. Update as work lands. |
docs/AGENTS.md |
The scriptable CLI surface — exit codes, JSON, and what not to automate |
go run ./cmd/leetuiFirst run shows an empty board. Press S to sync — about 4,000 problems, a couple of
minutes at the default 2 requests/second. After that everything is local and instant.
Press a to sign in with your LeetCode session cookies if you want your solved
status and premium content. Browsing and search work signed out.
| Key | Does |
|---|---|
j k / arrows |
move |
tab |
cycle pane focus |
/ |
search titles, tags, and statements |
1 2 3 |
toggle easy / medium / hard (on the list) |
enter esc |
open a problem / back to the list |
1–9 |
open marker N (on a problem) |
u |
cycle all → unsolved → solved |
p |
cycle all → premium → free |
m M |
mark a problem / show just your list |
+ - |
important / unimportant — press again to clear |
i |
cycle all → important → unimportant |
0 esc |
clear filters |
f |
create the solution file and show its path |
e r s |
edit / run locally / submit |
l E |
choose language / editor |
P |
study plans — Top Interview 150 and friends |
c |
browse company lists, then a timeframe |
C |
contests — the schedule, and a countdown while one is live |
d |
read the official editorial |
S |
sync (press again to pause — it resumes where it stopped) |
a |
sign in |
o |
open the problem in your browser |
t T |
start-stop timer / reset |
? q |
help / quit |
leetui with no arguments opens the app. The subcommands drive the same core, so an
editor never needs a plugin:
leetui pull two-sum # folder, statement, scaffolded solution, test cases
leetui run # from inside the problem folder
leetui run path/to/solution.py
leetui submit two-sum
leetui path two-sum # prints the folder, for scripting
leetui todo add two-sum --note "from the JD"
leetui todo --json # stable array, for agents
leetui mark up two-sum # worth doing again
leetui mark down 3sum # not worth another pass
leetui mark --json --up # the redo list, for agents
leetui contest # the schedule, with a countdown
leetui contest pull weekly-contest-517 # every problem's folder, in one command
leetui contest submit weekly-contest-517 # the contest judge — the only one that scores
leetui run --watch # stays open, re-runs on save — put it in its own pane
leetui image two-sum 1 # draw a figure, on kitty / iTerm2 / WezTerm / Ghostty
leetui doctor # what this machine can do, and how to fix what it cannot
leetui --debug # trace requests to ~/.local/share/leetui/debug.log
LEETUI_DEBUG=1 leetui run # the same, for a subcommandThe trace records what was sent, what came back, and the whole body of anything that failed to decode. Session cookies are redacted before they reach it, so it is safe to attach to a bug report.
A problem can be a slug, a folder name, a path to either, or omitted to mean the current directory — so from inside nvim the buffer you are looking at is enough:
vim.keymap.set("n", "<leader>lr", ":!leetui run %<CR>")
vim.keymap.set("n", "<leader>ls", ":!leetui submit %<CR>")Exit codes are the contract: 0 passed, 1 ran and was wrong, 2 could not run. The
full surface, including what an agent should not automate, is in
docs/AGENTS.md.
m marks the problem under the cursor; M shows just your list, oldest first. The same
list is writable from a script, so an agent can queue problems from a job description and
they are waiting on the board next time you open it:
leetui todo add two-sum group-anagrams --note "phone screen prep"A todo list empties as you solve it. That is the wrong shape for the question you ask on a second pass through Top Interview 150: which of these taught me something?
So a problem also carries a verdict. + says worth doing again, - says not,
pressing the same key again withdraws it, and i cycles the board through
all → important → unimportant. The verdict survives solving — "solved, and still worth
redoing" is the whole point, and it is a state a todo list cannot express.
The mark sorts the board. Important floats to the top and is gilded; unimportant sinks to the bottom and goes grey — title, difficulty, and state, not just its mark. Unmarked sits in the middle, because "no opinion" genuinely is between the two.
# │ TODO │ MARK │ PROBLEM │ DIF │ ACC │ STATE
0146 │ │ + │ LRU Cache │ MED │ 43% │ ✓ ← gold, and first
0011 │ ● │ │ Container With Most Water │ MED │ 56% │
0027 │ │ - │ Remove Element │ esy │ 59% │ ✓ ← grey, and last
- demotes; it never deletes. The row stays on the board, stays searchable, and stays
in its study plan, because a judgment you cannot revise is not a judgment. Press - again
and it comes straight back up.
This holds in every sort — problem number, a company pack's frequency, a study plan's curriculum order, search relevance. On a second pass through Top Interview 150 that is the point: you are not working the curriculum any more, you are working the shortlist.
Like the todo list it is writable from outside the app, which is the point — hand an agent the list and let it do the triage:
leetui mark up lru-cache --note "eviction order is the whole problem"
leetui mark down remove-element
# the redo pass: solved, and still marked important
leetui mark --json --up | jq -r '.[] | select(.status == "ac") | .slug'Three states, never a score, and every operation is idempotent — so nothing has to read a
value before writing one. Full contract in docs/AGENTS.md.
An Accepted verdict sweeps through a colour band and settles into its normal green. A result that beat the field says so, reading the judge's own percentiles:
| Accepted | the sweep |
| beats >50% on runtime or memory | ✦ FAST |
| beats >50% on both | ✦ DOUBLE 50 |
| beats ≥90% on both | ✦ TOP 10 |
Better results sweep for longer. This is the only animation in leetui besides the flip, and it is deliberately easy to turn off:
:set ui.celebrate off # just the verdict
:set ui.celebrate subtle # badge and figures, no motion
:set ui.celebrate full # the sweep too — the defaultui.reduce_motion outranks full and degrades it to subtle, so there is one answer to
"will this screen move" and it is the accessibility setting. The badge is information —
the percentiles said in two words — so it survives without the motion; only the sweep is
decoration.
Make the workspace a repository and leetui keeps it current:
cd ~/leetcode && git initFrom then on an Accepted verdict commits itself — the solution, the statement, the test cases, and your notes — with a subject you would have written yourself:
solve(0146): lru cache — go, 58ms, beats 91.2%
No co-author trailer and no session link: it is your work, and git log on a solutions
repo is something people show other people.
Pushing is never automatic. Press v for the repository view — branch, what is
uncommitted, what the remote has not got, and the last few commits. p there asks once,
naming the remote's URL, and publishes only when you answer y. leetui will not pick a
remote for you, and it never runs git init on your behalf.
Turn any of it off in config.toml:
[git]
commit_on_accepted = true # false to commit by hand
commit_notes = true # false to keep notes.md out of the repo
branch = "main" # refuse to commit anywhere else
remote = "" # empty means the branch's own upstreamOne line, nothing installed first:
curl -fsSL https://raw.githubusercontent.com/Nano-AI/leetui/main/install.sh | shIt picks the binary for your machine, verifies it against the release's own checksums, and installs it somewhere on your PATH. Read it first — that goes for every install script on the internet, including this one.
brew tap Nano-AI/tap
brew trust nano-ai/tap # Homebrew gates third-party taps
brew install leetuiWith Go:
go install github.com/Nano-AI/leetui/cmd/leetui@latestgo install puts binaries in $(go env GOPATH)/bin, which is often not on your PATH.
If leetui is not found afterwards, add it:
echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.zshrc && source ~/.zshrcWithout either, take a prebuilt binary from Releases — macOS (Apple silicon and Intel), Linux (x86-64 and arm64), and Windows. One static file, no runtime to install.
leetui doctor # what this machine can and cannot do, and how to fix itdocs/demo.cast is an asciinema recording — search, open, reveal tags,
settings, help:
asciinema play docs/demo.castIt is generated from the real View(), driven through the real Update(), so it cannot
drift from the product the way a hand-recorded session does:
LEETUI_CAST=1 go test ./internal/tui -run TestRecordCastgo test ./... # offline suite
LEETUI_LIVE=1 go test ./internal/syncer ./internal/tui -v # against leetcode.com
go run ./cmd/leetui --no-motion # animation offConfig is written to ~/.config/leetui/config.toml; the database lives in
~/.local/share/leetui/. Session cookies go to your OS keychain. On a machine
without one, they go to a credential helper you configure or to a 0600 file, and
leetui tells you which — see leetui doctor.
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ L E E T U I ⏱ --:--:-- ┊ signed out │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ PROBLEMS ─────────────────────────────────────────────────────────────────────────────────── 4 ─╮
│ # │ TODO │ PROBLEM │ DIF │ ACC │ STATE │ ASKED BY │
├────────┼──────┼──────────────────────────────────────────┼─────┼──────┼───────┼──────────────────┤
│ ▌ 0001 │ │ Two Sum │ ESY │ 58% │ ✓ │ │
│ 0042 │ │ Trapping Rain Water │ HRD │ 61% │ │ │
│ 0146 │ │ LRU Cache │ MED │ 43% │ ◐ │ │
│ 1650 │ │ Lowest Common Ancestor of a Binary Tree… │ MED │ 80% │ ⊘ │ │
│ │ │ │ │ │ │ │
╰────────┴──────┴──────────────────────────────────────────┴─────┴──────┴───────┴──────────────────╯
enter open ┊ m mark ┊ M my list ┊ / search ┊ c companies ┊ 1·2·3 difficulty ┊ u unsolved ┊ S sync
Browsing is the list and nothing else, full width — the same reason
leetcode.com/problemset is a table and nothing else. DIF uses LeetCode's own
colours, teal / amber / red, so it reads without a legend. STATE is ✓ solved,
◐ tried, ⊘ locked; rows alternate a faint band so the eye can track across
one. Terminals that cannot be trusted with the width of ✓ get x ~ -
automatically.
enter opens the problem; esc comes back to exactly where you were.
╭─ PROBLEM ───────────────── 42.7% ACCEPTED ─╮╭─ SOLUTION ────────── GO ─╮
│ 146. LRU Cache MED ││ ~/leetcode/0146-lru-…/ │
│ design ┊ hash-table ┊ linked-list ││ watching · saves re-run │
│ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ │╰──────────────────────────╯
│ Design a data structure that follows the │╭─ RUN ────────── 1 FAILED ─╮
│ constraints of a Least Recently Used cache ││ case 1 FAIL │
│ ││ want [0,1] got [9,9] │
╰────────────────────────────────────────────╯╰──────────────────────────╯
e edit ┊ r run ┊ s submit ┊ d editorial ┊ l lang ┊ esc back
Press P for the curated lists — Top Interview 150, LeetCode 75, Top 100 Liked, SQL 50,
Graph Theory. Pick one and work it in the order its author intended. These are free: a
plan's contents read signed out, so a free account gets all 150 problems of Top Interview
150. The picker leads with how far through each one you are, because that is what you came
back to find out.
╭─ STUDY PLAN ──────────────────────────────── 3 OF 22 ─╮
│ top │
╰───────────────────────────────────────────────────────╯
╭─ PLANS ───────────────────────────────────────────────╮
│ ▌ Top Interview 150 Must-do List for Interview… │
│ 18/150 12% │
│ Top 100 Liked 100 Best Rated Problems │
│ 1/100 1% │
│ Premium Algo 100 premium │
╰───────────────────────────────────────────────────────╯
type to narrow ┊ ↑↓ move ┊ enter pick ┊ esc back
The board then follows the plan's own running order rather than problem numbers, and the last column becomes the chapter you are in:
# │ PROBLEM │ DIF │ ACC │ CHAPTER
1768 │ Merge Strings Altern… │ ESY │ 78% │ Array / String
1071 │ Greatest Common Divi… │ ESY │ 65% │ Array / String
392 │ Is Subsequence │ ESY │ 48% │ Two Pointers
11 │ Container With Most … │ MED │ 56% │ Two Pointers
Press c for the premium loop the website is built around — pick a company, pick how
recently it asked, work the list. Typing narrows immediately; there is only one thing to
do with a keystroke in a list of 984 companies.
╭─ COMPANY ───────────────────────────────────── 3 OF 984 ─╮
│ goo │
╰──────────────────────────────────────────────────────────╯
╭─ LISTS ──────────────────────────────────────── PREMIUM ─╮
│ ▌ Google 199 stored of 2335 │
│ Goldman Sachs 412 problems │
│ Google Cloud 38 problems │
╰──────────────────────────────────────────────────────────╯
type to narrow ┊ ↑↓ move ┊ enter pick a timeframe ┊ esc back
The chosen pack filters the board and sorts it by frequency, so what that company asks most sits at the top. That ordering is the whole reason a pack beats a tag filter.
Press C for contests. The schedule reads signed out, picking one filters the board to
that contest's problems in the contest's own running order, and the rail counts down while
one is live. Ninety minutes is not the time to learn a screen, so this one leans on the
CLI: leetui contest pull weekly-contest-517 lays out all four folders in a single
command, and leetui contest submit is a separate verb from leetui submit because the
ordinary endpoint accepts a submission during a contest, judges it, and scores nothing.
It also checks whether you are registered, and says so loudly if you are not. That is the same silent failure one level up: an unregistered submission is judged, comes back Accepted, and scores nothing, and nobody notices until the standings do not move.
Solution files are written so your editor can actually resolve them — LeetCode's snippets
have no imports, no package clause, and no ListNode, because the judge supplies all of
that. Only the marked region is submitted:
// 1. Two Sum · Easy
// https://leetcode.com/problems/two-sum/
//
// Everything above the marker is local scaffolding, for your editor
// and the local runner. Only the marked region is submitted.
#include "leetui_driver.h"
// @leetui code=start
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
}
};
// @leetui code=endvscode-leetcode's @lc code=start markers are read too, so a workspace built with that
extension works here unchanged.
leetui is the LeetCode side of the desk, not the editor. It owns browsing, search, study plans, company packs, statements, editorials, running, and submitting. The code happens in whatever you already have open — nvim in the next tmux pane, a VS Code terminal, an editor on another monitor. So the side column always names the file and says whether a save will re-run it:
╭─ SOLUTION ───────────────────────── PYTHON3 ─╮
│ ~/leetcode/0001-two-sum/solution.py │
│ watching · saves re-run the tests │
╰──────────────────────────────────────────────╯
Open that path wherever you like. Save. leetui re-runs the tests and shows the diff, without you switching back to it.
Reading and writing at the same time. Inside tmux (or zellij, WezTerm, Kitty), e
opens your editor in a pane beside leetui rather than taking the terminal — the
statement stays on screen and saving re-runs the tests, so you never switch back. A GUI
editor gets the same deal via its own window. With a terminal editor and nowhere to put a
pane, leetui hands over the terminal, opens README.md alongside the solution, and runs
the tests when you quit. Turn any of it off with editor_pane, open_statement, and
run_after_edit.
A submission queue is a departure board — work is submitted, sits pending, gets judged, resolves. So verdicts flip into place rather than appearing. That flip is the app's entire motion budget and its only loading state; there are no spinners in leetui.
Amber is the system speaking. Bone is content. Green and red belong to the judge alone — nothing else in the app is ever green or red, which is what makes a verdict land.
Full reasoning in docs/DECISIONS.md.
- Go + Bubbletea/Lipgloss/Glamour — single static binary
- Auth — a field each for
LEETCODE_SESSIONandcsrftoken, browser import as convenience, verified before it is stored, and kept in the best store your machine has - Run local, submit remote — tight loop stays offline, correctness of record comes from the judge
- Local execution: Python, Go, C++, JavaScript, TypeScript — vendored drivers, zero new dependencies. Everything else edits and submits normally and runs on the judge
- Full Premium parity — company packs, editorials, premium problems. No mock assessments
- Study plans are free and arrive whole — one request returns all 150 questions and 23 chapters, readable signed out. The inverse of company packs, whose registry is free and contents are not
- Contests are the third curated list, and have their own judge —
Cbrowses the schedule signed out and sorts a contest by its own running order;leetui contest submitis a separate verb because the ordinary endpoint accepts a submission during a contest, judges it, and scores nothing. It warns when you are not registered, which fails the same silent way - SQLite + FTS5 (
modernc.org/sqlite, pure Go) for instant offline search - Company packs sync one at a time — 984 companies × 5 timeframes is ~5,000 requests; one pack is ~24
- Solution files carry editor scaffolding — imports and driver types above
@leetui code=start; only the marked region is submitted - Disk layout is problem-first —
0146-lru-cache/{solution.go, README.md, notes.md} - Git — auto-commit on Accepted; push only from the repository view, behind a confirmation that names the remote's URL
- Editing — delegate to
$EDITOR, in a pane beside leetui where the terminal allows it; a file watcher re-runs on save - Two screens — the list is the list;
enteropens a problem,esccomes back - A todo list you can script —
min the app,leetui todo addfrom anywhere - Importance marks, also scriptable —
+/-in the app,leetui markfrom anywhere. Its own table, not a todo flag: a verdict outlives the solve, which is what makes a second pass possible - Difficulty in LeetCode's own teal / amber / red — reads without a legend
- Every column has a header — a glyph is only a glyph when something names it
- Keys — vim-first, arrows always work, everything remappable
cmd/leetui/ entrypoint
internal/
config/ auth/ leetcode/ store/ syncer/ render/ editor/ workspace/
solve/ the seam both frontends use: prepare, run, commit
vcs/ git, shelled out — status, guarded commit, explicit push
runner/ interfaces, language registry, and vendored drivers/
tui/
theme/ the only place hex values are allowed to appear
components/ frame (the bezel), flap (the signature), sparkline
doc.go the file map for this package — read it before adding one
docs/ DECISIONS · DESIGN · ARCHITECTURE · ROADMAP
The docs in docs/ are the on-ramp — DECISIONS.md first. Every architectural choice is
recorded there with what it costs and what would reverse it, so a change that contradicts
one is a conversation rather than a surprise in review.
go test ./... # offline, and must stay green
go vet ./...Two house rules, both from docs/ARCHITECTURE.md:
- Files stay in the 100–200 line range. One file, one job.
- Hex colours live in
internal/tui/themeand nowhere else.
If you change behaviour, the test that would have caught the old behaviour is the one worth writing. Several of the bugs in this repo's history were found by a test that failed against the previous commit — and a couple were shipped by tests that passed vacuously, which is the failure to watch for.
MIT licensed — see LICENSE, and NOTICE for what was adapted in.
Local codegen and execution build on leetgo (MIT).
TUI stack is Charm: Bubbletea, Lipgloss, Bubbles, Glamour.
leetui is an unofficial client and is not affiliated with LeetCode.
