English · 简体中文
Browse problems, read statements, write solutions in your own editor, and run them against LeetCode's real judge — without leaving the shell. Problems worth a second look go on a spaced-repetition deck that remembers what you have re-solved today and follows you between machines.
lc # the full-screen browser: pick, edit, test, submit
lc pick 322 --lang python3 # writes ~/leetcode/0322-coin-change/solution.py and opens it
lc test # run the samples on LeetCode's judge
lc submit # submit for realContents · Install · Log in · The browser · Review deck · Syncing between machines · Vim · Settings · All commands · Where files live · WSL
brew install elliott-byte/tap/lc-cli
# or, with uv:
uv tool install git+https://github.com/Elliott-byte/lc-cliFrom a checkout of this repo, uv tool install . — re-run it after changing
the source. To work on lc instead, use a local venv:
uv venv && uv pip install -e '.[dev]' && .venv/bin/python -m pytestlc talks to leetcode.com as you, using your browser's session cookies.
lc loginreads them straight from a local browser (Chrome, Firefox, Safari, Edge, …)
and verifies them. Not signed in yet? It opens the LeetCode login page — sign
in there, press Enter, and lc picks up the fresh cookies. On macOS the OS
may ask once for keychain access; reading Safari's cookies needs Full Disk
Access for your terminal.
If no browser store is readable (remote box, exotic browser):
lc login --paste # copy LEETCODE_SESSION + csrftoken from
# DevTools → Application → Cookies
lc login --session … --csrf … # scriptedCookies are written to ~/.lc/cookies.json with 0600 permissions, or read
from $LEETCODE_SESSION / $LEETCODE_CSRF if you'd rather not store them on
disk. The session expires every couple of weeks — just run lc login again.
Bare lc (or lc tui) opens two panes: problems on the left, the statement
on the right. On a fresh machine it downloads the problem index by itself.
The loop is: move to a problem, enter to open it in your editor, write, quit
the editor back to the list, r to run the samples, s to submit — repeat.
enter on a problem you already started reopens your existing file.
Today's daily challenge is pinned to the top with a yellow ★ and selected
when the app opens, so it is always one keypress away; D jumps back to it.
The status bar names the day it belongs to and when the next one lands —
★ daily 08-15, next in 43m — because LeetCode rotates at UTC midnight, so
east of Greenwich your local date runs ahead of the daily on screen for part
of the morning.
The ✔/✗ marks stay fresh by themselves: coming back from the editor re-reads
the local index, so a \s submit made inside Vim shows up immediately.
ctrl+r refreshes on demand — instant and local, unlike R, which
re-downloads the whole index.
| Key | Action |
|---|---|
↑ ↓ |
Move through the list |
/ |
Filter (esc or enter returns to the list) |
enter / p |
Set up the solution file and open your editor |
r |
Run the samples |
s |
Submit |
tab |
Switch between the Problems and Review tabs |
m |
Save this problem to the review deck |
c |
Settings |
d / t |
Cycle the difficulty / status filter |
o |
Open the problem on leetcode.com |
D |
Jump to today's daily challenge |
? |
Every key, including the ones kept off the footer |
q |
Quit |
The footer shows the solving loop only. c (settings), d/t (filters),
o (open on leetcode.com), D (jump to the daily), ctrl+r (refresh from
the local index) and R (re-download it) all still work — press ? for the
full list.
Some problems deserve a second — and a fifth — meeting. Press m and the
problem joins the Review tab, where it climbs Ebbinghaus's forgetting
curve: level 1 comes back after 1 day, then 2, 4, 7, 15, out to a year at
level 10.
Due problems sort to the top and the tab title counts them: Review (3).
You grade, lc remembers. Re-solve a due problem and submit: lc marks the
row — green with a ✔ when it passed, red with a ✗ when it did not — and
leaves the level alone. Press + to move it up (or - to drop it) and the
mark clears. Levels stay yours: a problem solved by luck, or after peeking,
should not quietly buy itself another month.
Submits count from anywhere — the TUI, lc submit in a shell, \s in Vim —
so a problem you solved in Vim is already green when you get back to the
list. The mark describes today: it clears when you grade the problem, and
otherwise fades overnight.
Nothing is ever added behind your back. Only m, Vim's \m, and
lc review add put a problem on the deck.
On the Review tab:
| Key | Action |
|---|---|
+ / - |
Set the level by hand (rescheduled from today) |
z |
Postpone this problem to tomorrow |
Z |
Postpone everything due — the "not today" button |
x |
Take it off the deck |
g |
Sync with your git repo |
enter |
Open it in your editor, as usual |
The same deck from the shell:
lc review # the deck, soonest due first
lc review add 322 -l 3 # save by hand, starting at level 3
lc review add # ...or the problem directory you are standing in
lc review level 322 5 # set a level
lc review postpone # everything due moves to tomorrow
lc review rm 322The curve is yours to shape — one gap per level, in days, and the number of entries is the number of levels:
lc config curve 1,2,4,7,15,30 # six levels
lc config curve reset # back to the Ebbinghaus defaultPoint lc at a git repo you own and the deck follows you around — a laptop and a WSL box stay in step.
lc config repo https://github.com/you/lc-review.git
lc review sync # pull, merge, push (or press g in the Review tab)
lc review pull # bring the repo's deck down only
lc review push # publish this machine's deck onlyUse the https:// URL unless you have an SSH key GitHub accepts — with gh
logged in, https just works. A failed sync says what went wrong and what to
try next.
The Review tab carries a status strip once a repo is configured:
| Meaning | |
|---|---|
✔ synced 2h ago |
the deck matches the repo as of the last sync |
↑ 3 changes to push · synced 2h ago |
you have graded things since |
○ not synced yet — press g |
configured, never synced |
✗ last sync failed: … |
the reason is shown, with a hint |
The strip is worked out from local files only — redrawing the deck never reaches for the network — so "synced" means "agreed with the repo when we last talked to it", not "checked GitHub just now". With no repo configured it is hidden entirely.
How merging works. lc merges in Python, not in git, so you are never asked to resolve a conflict. Both sides are unioned, and where both machines know a problem the copy edited most recently wins: every change stamps a UTC timestamp, so same-day edits on two machines still order correctly. Removals travel too — taking a problem off the deck leaves a tombstone, which is just another edit, so the other machine drops it instead of handing the problem back. Re-adding a removed problem revives it at level 1. If both machines push at the same moment, lc redoes the sync and you never see it.
lc keeps a private clone in ~/.lc/review-repo and writes two files there:
review.json, the deck it reads back, and REVIEW.md, the same deck as a
linked table so the repo page is readable on GitHub. It never writes a
README.md, so pointing lc at a repo that has one is safe.
lc setup vimDrops a small plugin into ~/.vim/plugin/lc.vim — no .vimrc edits, delete
the file to uninstall. If no editor is configured yet, it also sets
lc config editor vim, so lc pick drops you straight into Vim.
In a solution buffer (any file next to a .lc.json), normal mode:
| Key | Action |
|---|---|
\t |
save, then lc test |
\s |
save, then lc submit |
\p |
show/hide the problem statement in a left split |
\o |
open the problem page in your browser (figures render there) |
\m |
save this problem to the review deck |
\q |
save everything, then quit — back to the TUI/shell |
Opening a solution file opens the statement beside it automatically — hide it
with \p (or q inside the pane), bring it back with \p, or set
let g:lc_auto_statement = 0 to open it manually. :q means "leave" from
either window — the pane is an accessory, not a second document to close
separately — and so do :qa, :x, ZZ and \q. Anything unsaved still
vetoes it, exactly as in plain Vim.
The pane shows the statement fully rendered — lc show in a small terminal
split, same colors and example boxes as the CLI — whenever your Vim has
+terminal (or on Neovim). Without it, the directory's raw README.md is
used; let g:lc_statement_render = 0 forces the plain file everywhere.
Python buffers are kept space-indented: Tab inserts spaces, and pasted tabs
are converted on save. LeetCode's starters use spaces and the judge answers a
single stray tab with a TabError — let g:lc_python_indent = 0 disables it.
The keys are listed on each window's status line, shortened to fit a narrow
split (let g:lc_statusline = 0 leaves your own status line alone). The
cursor starts in the solution, and the pane is read-only: the statement is
there to read, not to type in. ctrl+w h goes over to it and ctrl+w l
comes back. The keys work from the pane as well, so it does not matter which
window has the cursor — \t saves the solution and runs from the problem's
directory either way, whatever directory you launched Vim in. <leader> is backslash unless you remapped it. After upgrading
lc, re-run lc setup vim --force; for Neovim, copy the file to
~/.config/nvim/plugin/.
Press c in the TUI for the settings screen: workspace, default language,
editor, review repo and the memory curve — with a live preview of what the
curve you are typing means. ctrl+s saves, esc cancels.
The same settings from the shell:
lc config show
lc config lang go # default language for `lc pick`
lc config workspace ~/code/leetcode
lc config editor "code -w" # otherwise $EDITOR / $VISUAL is used
lc config curve 1,2,4,7,15,30 # days per review level
lc config repo https://github.com/you/lc-review.git| Command | What it does |
|---|---|
lc list [keyword] |
Browse the problem set. -d easy, -t "Two Pointers", -s todo, --free |
lc show 322 |
Print the statement, formatted for a terminal |
lc pick 322 -l go |
Create a solution file from the starter code and open $EDITOR |
lc edit 322 |
Reopen an existing solution |
lc test |
Run the sample cases on LeetCode's judge |
lc submit |
Submit; prints verdict, runtime and percentiles |
lc daily |
Today's daily challenge (--pick to start it immediately) |
lc random -d medium |
A random unsolved problem |
lc review |
The review deck (add / rm / level / postpone) |
lc review sync |
Sync the deck with your git repo (pull / push too) |
lc stat |
Your solve counts by difficulty |
lc history 322 |
Your recent submissions for a problem |
lc code |
Print the current solution, highlighted |
lc tags |
Topic tags, ranked by problem count |
lc sync |
Refresh the local problem index |
lc setup vim |
Install the Vim keybindings |
lc tui |
Full-screen browser (also just bare lc) |
lc test, lc submit, lc code, lc edit, lc history and lc review add
take no argument inside a problem directory — they read it from .lc.json.
lc test and lc submit exit non-zero on a failed verdict, so they compose:
lc test && lc submit -ySolutions live in a plain, user-visible workspace, easy to keep under version control:
~/leetcode/
0322-coin-change/
README.md the statement, as Markdown
solution.py starter code + your work
.lc.json which problem and language this is
The whole solution file is submitted; the header lc writes is a comment in the target language, so the judge ignores it.
Everything lc owns is under ~/.lc (set $LC_HOME to move it):
| File | What it is |
|---|---|
config.json |
your settings |
review.json |
the review deck — user data, never rebuilt |
cookies.json |
session cookies, 0600 |
cache.db |
problem index + cached statements — delete any time, lc rebuilds it |
review-repo/ |
the private clone used for syncing |
Everything above works inside the distro unchanged — install with the same
uv tool install line, and the TUI, judge runs and vim plugin behave as on
any Linux. The Windows boundary is handled where it shows: pages open through
the Windows browser (explorer.exe, or wslu's wslview), and lc login
reads Windows Firefox profiles through /mnt/c, so Firefox users log in
automatically. Windows Chrome and Edge seal their cookie stores with keys only
the browser itself can open — sign in with lc login --paste there; the login
flow reminds you.
- Accepted verdicts end in fireworks — a burst for
lc test, a four-shot volley forlc submit— and a failed one in a little figure sinking to its knees (orz); after a failed submit, a rain cloud rolls in over it. Both skip pipes and scripts automatically, andLC_NO_FX=1turns them off everywhere. - Premium problems need a premium account; lc says so rather than failing oddly.
lc testandlc submitrun on LeetCode's judge, not locally, so the verdict and the runtime percentiles are the real ones.- Requests go to leetcode.com at a human pace. Scripting a loop over many problems will get you rate-limited; lc surfaces that as a plain error.