keypal
Spaced-repetition practice for keyboard shortcuts. Build muscle memory for the shortcuts you actually use, scheduled with FSRS.
Why a desktop app?
- Browsers can't capture most shortcuts. Browsers intercept Ctrl+W, Ctrl+T, Ctrl+N, Ctrl+Q, etc. at the OS level before JavaScript sees them.
- Terminals intercept some shortcuts too. Terminal emulators eat shortcuts like Ctrl+Shift+F, and terminal protocols have byte collisions (Ctrl+H = Backspace, Ctrl+I = Tab).
- A Qt window captures keys cleanly. No byte collisions, no terminal-emulator interception, full modifier support.
Install
Linux (no Python required): download the .flatpak from GitHub Releases and install it:
flatpak install KeyPal-x.x.x-x86_64.flatpak
Any platform with uv installed:
uv tool install keypal
Or run without installing:
uvx keypal
To run from source (you may need to uv tool install rust-just first):
git clone https://github.com/treyhunner/keypal
cd keypal
uv sync
just run
Built-in packs
| Pack | What |
|---|---|
readline |
Common bash / readline shortcuts (Ctrl+A, Alt+F, Ctrl+W, etc.) |
python_repl |
Python 3.13+ REPL: F1/F2/F3 modes, Tab autocomplete, plus shared readline overlap |
tmux |
Your live tmux config, parsed from tmux list-keys at startup |
obsidian |
Your live Obsidian hotkeys, read from ~/.config/obsidian/.../hotkeys.json |
The tmux and obsidian packs are hybrid: a curated list of common shortcuts with friendly descriptions, with your actual key bindings substituted in if you have customs. Change a binding in tmux or Obsidian, restart keypal, and the pack reflects it.
A shared_id mechanism lets shortcuts in different packs share the same FSRS card. Practice Ctrl+A in readline and it counts toward python_repl too (since the Python REPL uses readline under the hood).
Using the app
Home screen
| Key | Action |
|---|---|
| Arrow keys | Navigate between packs |
| Enter | Start practicing the highlighted pack |
| P | Practice checked packs (multi-pack session) |
| X | Toggle pack checkbox for multi-pack selection |
| B | Browse the highlighted pack (read-only cheat sheet) |
| C | Settings (new cards per session, timing thresholds) |
| S | Stats (cards by FSRS state, per-pack progress) |
| D | Diagnostic screen for testing what key events are sent |
| Q | Quit |
In a quiz session
| Key | Action |
|---|---|
| (the shortcut) | Submit your answer |
| Space | "I don't know" (treated as wrong) |
| Enter | Continue after a correct answer |
| Y | After wrong: claim "I actually had it right" and save it as an alias |
| F4 | Skip this shortcut forever (won't appear in future sessions) |
| Esc | Back to home |
After a correct answer, three dots fill in left to right over three seconds, then auto-advances. Press Enter any time to skip the wait.
After a wrong answer
You'll see what you pressed (red), the expected combo (green), and a hint with your options. Press the correct combo to advance with practice credit, or use the keys above to skip / claim correct / dismiss.
Storage
Your data lives in ~/.local/share/keypal/ on Linux (platform-equivalent elsewhere via platformdirs):
cards.json: FSRS state for each shortcut you've practicedreview_log.jsonl: append-only log of every reviewaliases.json: alternative key combos you've taught keypal (via Y on a wrong answer)disabled.json: shortcuts you've dismissed with F4seen.json: tracks which shared shortcuts have been introduced in each packselected_packs.json: which packs are checked for multi-pack practicesettings.json: your settings (new cards per session, threshold overrides)
Override the location with the KEYPAL_DATA_DIR environment variable.
Limitations
- Your desktop environment may intercept a few shortcuts (e.g. Alt+F4 to close windows). Use the diagnostic screen (D) to confirm what keypal receives.
- Keys that physically exist on the keyboard but aren't in keypal (rare combos, complex chords beyond two keys) need to be added to a pack manually.
Contributing
See AGENTS.md for code conventions, project layout, and common gotchas.
just qa # format + lint + test
just test # tests only
Release files for keypal 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keypal-0.3.0.tar.gz | 28.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keypal-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.5 kB
Release files / keypal-0.3.0.tar.gz
| Download URL | keypal-0.3.0.tar.gz |
|---|---|
| Size | 28.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7e6bbbc10ec0128e2b71242859c6ddd852b4b6e9c5e87c8997d58bf9758767ed
|
|
BLAKE2b-256 checksum How to use checksums |
de5320ddc6b798106e3ac0f3a35cd5853123bf02f496c99b4323b3d957011571
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / keypal-0.3.0-py3-none-any.whl
| Download URL | keypal-0.3.0-py3-none-any.whl |
|---|---|
| Size | 36.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dbe6bd537e7b08f68330ed2320186e90881115a720c6a6781c106d803db1bb71
|
|
BLAKE2b-256 checksum How to use checksums |
debaad934a597dade778327fb06640f68951a88bf1d929a0b045dbbb2682eb5d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|