A terminal typing trainer for programmers. Point it at a source file and it extracts real, semantically complete code blocks — functions, structs, match expressions, loops — formats them properly, and serves them one at a time as typing practice in a TUI.
Typing prose doesn't train the muscle memory programming needs: brackets, colons, generics, indentation. clikeyrs practices you on actual code, in the formatting style you'd see in a real codebase.
$ clikeyrs src/main.rs
Each practice unit is a complete, valid construct pulled from your file:
fn practice_match_expression() -> i32 {
match result {
Ok(value) => value,
Err(_) => 0,
}
}For Rust files, clikeyrs parses the source into an AST with syn, walks it
to extract practice blocks, and formats each block with prettyplease —
so you always type idiomatically formatted code, regardless of how the input
file looks:
File ──► AST parser (syn) ──► Formatter (prettyplease) ──► Dictionary ──► TUI (ratatui)
The AST pass extracts:
- complete functions, and their signatures as separate focused units
- methods from
implblocks, individually - structs, enums, traits, type aliases, constants, statics, modules
- control flow —
for/while/loop,if/if let,match— lifted out of function bodies and wrapped in a syntheticfn practice_*()so each block is valid, formattable Rust on its own
Python, JavaScript, and Go fall back to a simpler token-based parser without AST-level extraction or reformatting.
Build from source (requires a Rust toolchain):
git clone <this repository>
cd clikeyrs
cargo install --path .clikeyrs <file> [OPTIONS]
clikeyrs src/main.rs # practice on a Rust file (full AST pipeline)
clikeyrs script.py --time 15 # 15-minute session, fallback parser
clikeyrs lib.rs --seed 12345 # reproducible block order| Option | Description |
|---|---|
-t, --time <minutes> |
Limit session duration (unlimited by default) |
-s, --seed <number> |
Fix the shuffle seed for a reproducible session |
--no-comments |
Exclude comment blocks |
--include-imports |
Include use/import statements |
-d, --debug |
Print the first 5 parsed and formatted blocks, then exit |
-dv, --debug-verbose |
Print all blocks with formatting details, then exit |
-h, --help |
Show help |
Blocks are shuffled each session (seeded, via fastrand); pass --seed to
replay the same order. Session targets default to 95% accuracy and 30 WPM.
- Type the displayed block; the timer starts on your first keystroke.
- Backspace steps back and clears the error at that position.
- Tab counts as the matching run of spaces (up to 4), so indentation can be typed either way.
- Enter advances to the next line.
- Esc or Ctrl+C ends the block / exits.
--debug / --debug-verbose run the parse-and-format pipeline without
starting the TUI and print every extracted block alongside its formatted
output. This is the primary way to inspect what a given file will turn into:
1. Function Signature | Lines 5-5 | 40 chars
┌─ Original Source:
│ fn print_usage (program_name : & str) {}
├─ Formatted Output:
│ fn print_usage(program_name: &str) {}
└─ Formatting applied
| Language | Pipeline | Blocks |
|---|---|---|
Rust (.rs) |
AST (syn) + prettyplease |
functions, signatures, methods, types, control flow |
Python (.py), JavaScript (.js), Go (.go) |
token-based fallback | coarser blocks, no reformatting |
Adding a language to the AST path means implementing two traits — see Architecture.
The Makefile is the single dev entrypoint:
make check # full CI gate: fmt-check + clippy -D warnings + tests + architecture tests
make dev # fast inner loop: format + lint + test
make test # test suite (deterministic, <1s)
make arch # architecture tests only
make coverage # line-coverage gate (fails under 80%)
make audit # cargo-audit + cargo-deny
make run FILE=src/main.rs ARGS="--debug"Finish every change with make check — clippy warnings are errors and the
codebase is rustfmt-formatted.
Tests live in inline #[cfg(test)] modules next to the code and are fully
deterministic: shuffles take fixed seeds, widgets render into an in-memory
ratatui buffer, and there are no timing or network assertions.
The crate is layered, and the layering is enforced by tests/architecture.rs
(make arch):
core ◄── parsers, formatters ◄── dictionary ◄── ui
| Module | Responsibility |
|---|---|
core/ |
shared types: Language, AstBlock, SemanticBlock, statistics |
parsers/ |
extraction only, no formatting — AstParser (modern) and LanguageParser (fallback) |
formatters/ |
CodeFormatter trait; RustFormatter wraps prettyplease |
dictionary/ |
holds blocks, filters by size, creates TrainingSessions |
ui/ |
ratatui/crossterm TUI, themes, and widgets |
External crates are confined to their layer (ratatui/crossterm in ui,
syn/prettyplease in parsers/formatters), also test-enforced. Each
module directory has a README.md with its implementation patterns and
constraints.
To add a language on the AST path:
- Add a variant to
Languageinsrc/core/mod.rs(extension mapping + name). - Implement
AstParserfor it insrc/parsers/. - Implement
CodeFormatterinsrc/formatters/using the language's standard formatter. - Register both in
ParserFactory::create_ast_parserandformatters::create_formatter.