keystrike 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- keystrike-1.1.0/.gitignore +16 -0
- keystrike-1.1.0/LICENSE +21 -0
- keystrike-1.1.0/PKG-INFO +175 -0
- keystrike-1.1.0/README.md +150 -0
- keystrike-1.1.0/pyproject.toml +109 -0
- keystrike-1.1.0/src/keystrike/__init__.py +1 -0
- keystrike-1.1.0/src/keystrike/__main__.py +9 -0
- keystrike-1.1.0/src/keystrike/app.py +111 -0
- keystrike-1.1.0/src/keystrike/application/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/application/build_lesson.py +202 -0
- keystrike-1.1.0/src/keystrike/application/learn_budget_use_cases.py +33 -0
- keystrike-1.1.0/src/keystrike/application/prepare_practice.py +53 -0
- keystrike-1.1.0/src/keystrike/application/session_use_cases.py +372 -0
- keystrike-1.1.0/src/keystrike/application/settings_use_cases.py +74 -0
- keystrike-1.1.0/src/keystrike/application/stats_use_cases.py +107 -0
- keystrike-1.1.0/src/keystrike/application/sync_use_cases.py +41 -0
- keystrike-1.1.0/src/keystrike/cli.py +95 -0
- keystrike-1.1.0/src/keystrike/domain/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/domain/aggregate.py +179 -0
- keystrike-1.1.0/src/keystrike/domain/confidence.py +221 -0
- keystrike-1.1.0/src/keystrike/domain/daily_learn.py +79 -0
- keystrike-1.1.0/src/keystrike/domain/enums.py +29 -0
- keystrike-1.1.0/src/keystrike/domain/generator.py +120 -0
- keystrike-1.1.0/src/keystrike/domain/learn_order.py +37 -0
- keystrike-1.1.0/src/keystrike/domain/markov.py +84 -0
- keystrike-1.1.0/src/keystrike/domain/models.py +119 -0
- keystrike-1.1.0/src/keystrike/domain/null_adapters.py +55 -0
- keystrike-1.1.0/src/keystrike/domain/protocols.py +83 -0
- keystrike-1.1.0/src/keystrike/domain/regression.py +97 -0
- keystrike-1.1.0/src/keystrike/domain/session.py +38 -0
- keystrike-1.1.0/src/keystrike/domain/sync_merge.py +156 -0
- keystrike-1.1.0/src/keystrike/infrastructure/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/infrastructure/aggregates_cache.py +62 -0
- keystrike-1.1.0/src/keystrike/infrastructure/atomic_write.py +13 -0
- keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/_grid.py +61 -0
- keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/colemak.py +3 -0
- keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/colemak_dh.py +11 -0
- keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/dvorak.py +3 -0
- keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/qwerty.py +3 -0
- keystrike-1.1.0/src/keystrike/infrastructure/clock.py +9 -0
- keystrike-1.1.0/src/keystrike/infrastructure/code_generators/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/infrastructure/id_gen.py +22 -0
- keystrike-1.1.0/src/keystrike/infrastructure/languages/__init__.py +32 -0
- keystrike-1.1.0/src/keystrike/infrastructure/languages/data/en_markov.json.gz +0 -0
- keystrike-1.1.0/src/keystrike/infrastructure/layout_repo.py +38 -0
- keystrike-1.1.0/src/keystrike/infrastructure/layout_toml.py +98 -0
- keystrike-1.1.0/src/keystrike/infrastructure/paths.py +56 -0
- keystrike-1.1.0/src/keystrike/infrastructure/session_repo_jsonl.py +161 -0
- keystrike-1.1.0/src/keystrike/infrastructure/settings_repo_toml.py +73 -0
- keystrike-1.1.0/src/keystrike/infrastructure/sync_git.py +197 -0
- keystrike-1.1.0/src/keystrike/presentation/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/presentation/bindings.py +10 -0
- keystrike-1.1.0/src/keystrike/presentation/screens/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/presentation/screens/home.py +102 -0
- keystrike-1.1.0/src/keystrike/presentation/screens/practice.py +170 -0
- keystrike-1.1.0/src/keystrike/presentation/screens/settings.py +145 -0
- keystrike-1.1.0/src/keystrike/presentation/screens/stats.py +179 -0
- keystrike-1.1.0/src/keystrike/presentation/textual_app.py +119 -0
- keystrike-1.1.0/src/keystrike/presentation/theme.py +7 -0
- keystrike-1.1.0/src/keystrike/presentation/widgets/__init__.py +0 -0
- keystrike-1.1.0/src/keystrike/presentation/widgets/hud.py +92 -0
- keystrike-1.1.0/src/keystrike/presentation/widgets/kb_heatmap.py +125 -0
- keystrike-1.1.0/src/keystrike/presentation/widgets/typing_area.py +71 -0
keystrike-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksandr Shelemetev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
keystrike-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keystrike
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Adaptive drills for your weakest keys
|
|
5
|
+
Project-URL: Homepage, https://github.com/egno/keystrike
|
|
6
|
+
Project-URL: Repository, https://github.com/egno/keystrike
|
|
7
|
+
Project-URL: Issues, https://github.com/egno/keystrike/issues
|
|
8
|
+
Author: Aleksandr Shelemetev
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: keybr,textual,tui,tutor,typing
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Education
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: platformdirs>=4.2
|
|
22
|
+
Requires-Dist: textual>=0.85
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Keystrike
|
|
27
|
+
|
|
28
|
+
Adaptive drills for your weakest keys — an offline terminal typing tutor inspired by
|
|
29
|
+
[keybr.com](https://www.keybr.com/). Stats stay on your machine; nothing phones home.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
Keystrike tracks how fast and accurately you type each key, unlocks new letters as
|
|
36
|
+
you hit your targets, and generates practice text that overweight your current weak
|
|
37
|
+
spot. Switch keyboard layouts (QWERTY, Dvorak, Colemak, Colemak Mod-DH, or your own
|
|
38
|
+
TOML) and each layout keeps its own history.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Adaptive engine** — row-weighted key unlock order, speed+accuracy confidence
|
|
43
|
+
gates, Markov word drills with a guaranteed focus letter.
|
|
44
|
+
- **Per-layout stats** — heatmap with per-key confidence and urgency; WPM/accuracy
|
|
45
|
+
trends; press a key on the heatmap for that key's history.
|
|
46
|
+
- **Daily learn budget** — optional cap on adaptive minutes per calendar day.
|
|
47
|
+
- **Custom layouts** — drop `*.toml` files into your config layouts directory; no
|
|
48
|
+
restart needed.
|
|
49
|
+
- **Git backup sync** — optional CLI to push/pull settings and sessions to a private
|
|
50
|
+
remote (union-merge sessions, last-write-wins settings).
|
|
51
|
+
- **Offline by default** — JSONL session logs and a local stats cache under
|
|
52
|
+
platformdirs paths; sync is opt-in.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
Requires **Python 3.12+** and a terminal with **raw-mode keyboard input**
|
|
57
|
+
([Terminal setup](#terminal-setup)).
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pipx install keystrike # or: uv tool install keystrike
|
|
61
|
+
keystrike # launch TUI (default)
|
|
62
|
+
keystrike --version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or from source:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/egno/keystrike
|
|
69
|
+
cd keystrike
|
|
70
|
+
uv sync --no-dev
|
|
71
|
+
uv run keystrike
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
### Home
|
|
77
|
+
|
|
78
|
+
| Key | Action |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `Enter` | Start adaptive practice |
|
|
81
|
+
| `s` | Stats |
|
|
82
|
+
| `o` | Settings |
|
|
83
|
+
| `l` | Cycle layout |
|
|
84
|
+
| `Ctrl+Q` | Quit |
|
|
85
|
+
|
|
86
|
+
On other screens, `Esc` / `q` goes back. Settings saves with `Ctrl+S`.
|
|
87
|
+
|
|
88
|
+
### Practice
|
|
89
|
+
|
|
90
|
+
Adaptive mode disables backspace — mistakes stay in the record so confidence scores
|
|
91
|
+
stay honest. The HUD shows live accuracy, remaining daily learn time, and the current
|
|
92
|
+
focus key. When a session ends, the next lesson starts automatically unless you go
|
|
93
|
+
back or hit your daily limit.
|
|
94
|
+
|
|
95
|
+
### Stats
|
|
96
|
+
|
|
97
|
+
Recent sessions are listed below the heatmap. **Press any key on the heatmap** to
|
|
98
|
+
drill into that key's confidence trend; `Esc` / `q` returns to the overview.
|
|
99
|
+
|
|
100
|
+
### Settings
|
|
101
|
+
|
|
102
|
+
| Setting | Default | Notes |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| Layout | `qwerty` | Any bundled or custom layout |
|
|
105
|
+
| Target speed | 46 WPM | Or CPM — your unlock threshold |
|
|
106
|
+
| Letters unlocked up front | 16 | Force-unlocked before confidence gating |
|
|
107
|
+
| Daily learn limit | 10 min | `0` = unlimited |
|
|
108
|
+
|
|
109
|
+
## Backup sync
|
|
110
|
+
|
|
111
|
+
Opt-in git sync for backing up or moving data between machines. Requires `git` on
|
|
112
|
+
`PATH` and a **private** remote (sessions contain your typing history).
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
keystrike sync init git@github.com:you/keystrike-backup.git # one-time
|
|
116
|
+
keystrike sync push # local → remote
|
|
117
|
+
keystrike sync pull # remote → local (rebuilds stats cache)
|
|
118
|
+
keystrike sync status # diff summary
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Sync merges sessions by `session_id` (union), resolves settings by `updated_at`
|
|
122
|
+
(last-write-wins), and copies layout files both ways. The stats cache is excluded —
|
|
123
|
+
pull triggers a rebuild.
|
|
124
|
+
|
|
125
|
+
## Data locations
|
|
126
|
+
|
|
127
|
+
| Path | Contents |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `~/.config/keystrike/settings.toml` | User settings |
|
|
130
|
+
| `~/.config/keystrike/layouts/` | Custom layout TOML files |
|
|
131
|
+
| `~/.config/keystrike/sync.toml` | Sync remote config (after `sync init`) |
|
|
132
|
+
| `~/.local/share/keystrike/sessions/` | Session JSONL logs (Linux) |
|
|
133
|
+
| `~/Library/Application Support/keystrike/` | Same on macOS |
|
|
134
|
+
|
|
135
|
+
Windows uses `%APPDATA%` / `%LOCALAPPDATA%` equivalents via [platformdirs](https://github.com/tox-dev/platformdirs).
|
|
136
|
+
|
|
137
|
+
## Terminal setup
|
|
138
|
+
|
|
139
|
+
Keystrike captures every keystroke in raw mode via [Textual](https://textual.textualize.io/).
|
|
140
|
+
|
|
141
|
+
Works out of the box in **Terminal.app**, **iTerm2**, and most Linux terminals.
|
|
142
|
+
|
|
143
|
+
### Windows
|
|
144
|
+
|
|
145
|
+
Use **[Windows Terminal](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701)** —
|
|
146
|
+
the legacy conhost host has unreliable raw-mode support. Run `keystrike` inside WT,
|
|
147
|
+
not cmd.exe. Raw mode is not verified on every Windows build; please
|
|
148
|
+
[open an issue](https://github.com/egno/keystrike/issues) if something breaks.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv sync # runtime + dev (pytest, pyright)
|
|
154
|
+
uv run pytest -q
|
|
155
|
+
uv run pyright
|
|
156
|
+
uv sync --all-groups # + ruff + snapshot tests (desktop only)
|
|
157
|
+
uv run ruff check
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Dependency groups: `dev` (default, pure Python — safe on Termux), `lint` (Ruff),
|
|
161
|
+
`snapshot` (`pytest-textual-snapshot`). Keep native-wheel tools out of `dev`; see
|
|
162
|
+
`pyproject.toml` for why.
|
|
163
|
+
|
|
164
|
+
Regenerate the demo GIF after UI snapshot changes:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
uv sync --group snapshot
|
|
168
|
+
uv run pytest tests/presentation/test_snapshots.py --snapshot-update
|
|
169
|
+
uv pip install pillow cairosvg # one-off; system cairo on macOS/Linux
|
|
170
|
+
uv run python scripts/generate_demo_gif.py
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Keystrike
|
|
2
|
+
|
|
3
|
+
Adaptive drills for your weakest keys — an offline terminal typing tutor inspired by
|
|
4
|
+
[keybr.com](https://www.keybr.com/). Stats stay on your machine; nothing phones home.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
Keystrike tracks how fast and accurately you type each key, unlocks new letters as
|
|
11
|
+
you hit your targets, and generates practice text that overweight your current weak
|
|
12
|
+
spot. Switch keyboard layouts (QWERTY, Dvorak, Colemak, Colemak Mod-DH, or your own
|
|
13
|
+
TOML) and each layout keeps its own history.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Adaptive engine** — row-weighted key unlock order, speed+accuracy confidence
|
|
18
|
+
gates, Markov word drills with a guaranteed focus letter.
|
|
19
|
+
- **Per-layout stats** — heatmap with per-key confidence and urgency; WPM/accuracy
|
|
20
|
+
trends; press a key on the heatmap for that key's history.
|
|
21
|
+
- **Daily learn budget** — optional cap on adaptive minutes per calendar day.
|
|
22
|
+
- **Custom layouts** — drop `*.toml` files into your config layouts directory; no
|
|
23
|
+
restart needed.
|
|
24
|
+
- **Git backup sync** — optional CLI to push/pull settings and sessions to a private
|
|
25
|
+
remote (union-merge sessions, last-write-wins settings).
|
|
26
|
+
- **Offline by default** — JSONL session logs and a local stats cache under
|
|
27
|
+
platformdirs paths; sync is opt-in.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
Requires **Python 3.12+** and a terminal with **raw-mode keyboard input**
|
|
32
|
+
([Terminal setup](#terminal-setup)).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pipx install keystrike # or: uv tool install keystrike
|
|
36
|
+
keystrike # launch TUI (default)
|
|
37
|
+
keystrike --version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or from source:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/egno/keystrike
|
|
44
|
+
cd keystrike
|
|
45
|
+
uv sync --no-dev
|
|
46
|
+
uv run keystrike
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Home
|
|
52
|
+
|
|
53
|
+
| Key | Action |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `Enter` | Start adaptive practice |
|
|
56
|
+
| `s` | Stats |
|
|
57
|
+
| `o` | Settings |
|
|
58
|
+
| `l` | Cycle layout |
|
|
59
|
+
| `Ctrl+Q` | Quit |
|
|
60
|
+
|
|
61
|
+
On other screens, `Esc` / `q` goes back. Settings saves with `Ctrl+S`.
|
|
62
|
+
|
|
63
|
+
### Practice
|
|
64
|
+
|
|
65
|
+
Adaptive mode disables backspace — mistakes stay in the record so confidence scores
|
|
66
|
+
stay honest. The HUD shows live accuracy, remaining daily learn time, and the current
|
|
67
|
+
focus key. When a session ends, the next lesson starts automatically unless you go
|
|
68
|
+
back or hit your daily limit.
|
|
69
|
+
|
|
70
|
+
### Stats
|
|
71
|
+
|
|
72
|
+
Recent sessions are listed below the heatmap. **Press any key on the heatmap** to
|
|
73
|
+
drill into that key's confidence trend; `Esc` / `q` returns to the overview.
|
|
74
|
+
|
|
75
|
+
### Settings
|
|
76
|
+
|
|
77
|
+
| Setting | Default | Notes |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| Layout | `qwerty` | Any bundled or custom layout |
|
|
80
|
+
| Target speed | 46 WPM | Or CPM — your unlock threshold |
|
|
81
|
+
| Letters unlocked up front | 16 | Force-unlocked before confidence gating |
|
|
82
|
+
| Daily learn limit | 10 min | `0` = unlimited |
|
|
83
|
+
|
|
84
|
+
## Backup sync
|
|
85
|
+
|
|
86
|
+
Opt-in git sync for backing up or moving data between machines. Requires `git` on
|
|
87
|
+
`PATH` and a **private** remote (sessions contain your typing history).
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
keystrike sync init git@github.com:you/keystrike-backup.git # one-time
|
|
91
|
+
keystrike sync push # local → remote
|
|
92
|
+
keystrike sync pull # remote → local (rebuilds stats cache)
|
|
93
|
+
keystrike sync status # diff summary
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Sync merges sessions by `session_id` (union), resolves settings by `updated_at`
|
|
97
|
+
(last-write-wins), and copies layout files both ways. The stats cache is excluded —
|
|
98
|
+
pull triggers a rebuild.
|
|
99
|
+
|
|
100
|
+
## Data locations
|
|
101
|
+
|
|
102
|
+
| Path | Contents |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `~/.config/keystrike/settings.toml` | User settings |
|
|
105
|
+
| `~/.config/keystrike/layouts/` | Custom layout TOML files |
|
|
106
|
+
| `~/.config/keystrike/sync.toml` | Sync remote config (after `sync init`) |
|
|
107
|
+
| `~/.local/share/keystrike/sessions/` | Session JSONL logs (Linux) |
|
|
108
|
+
| `~/Library/Application Support/keystrike/` | Same on macOS |
|
|
109
|
+
|
|
110
|
+
Windows uses `%APPDATA%` / `%LOCALAPPDATA%` equivalents via [platformdirs](https://github.com/tox-dev/platformdirs).
|
|
111
|
+
|
|
112
|
+
## Terminal setup
|
|
113
|
+
|
|
114
|
+
Keystrike captures every keystroke in raw mode via [Textual](https://textual.textualize.io/).
|
|
115
|
+
|
|
116
|
+
Works out of the box in **Terminal.app**, **iTerm2**, and most Linux terminals.
|
|
117
|
+
|
|
118
|
+
### Windows
|
|
119
|
+
|
|
120
|
+
Use **[Windows Terminal](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701)** —
|
|
121
|
+
the legacy conhost host has unreliable raw-mode support. Run `keystrike` inside WT,
|
|
122
|
+
not cmd.exe. Raw mode is not verified on every Windows build; please
|
|
123
|
+
[open an issue](https://github.com/egno/keystrike/issues) if something breaks.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv sync # runtime + dev (pytest, pyright)
|
|
129
|
+
uv run pytest -q
|
|
130
|
+
uv run pyright
|
|
131
|
+
uv sync --all-groups # + ruff + snapshot tests (desktop only)
|
|
132
|
+
uv run ruff check
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Dependency groups: `dev` (default, pure Python — safe on Termux), `lint` (Ruff),
|
|
136
|
+
`snapshot` (`pytest-textual-snapshot`). Keep native-wheel tools out of `dev`; see
|
|
137
|
+
`pyproject.toml` for why.
|
|
138
|
+
|
|
139
|
+
Regenerate the demo GIF after UI snapshot changes:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv sync --group snapshot
|
|
143
|
+
uv run pytest tests/presentation/test_snapshots.py --snapshot-update
|
|
144
|
+
uv pip install pillow cairosvg # one-off; system cairo on macOS/Linux
|
|
145
|
+
uv run python scripts/generate_demo_gif.py
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "keystrike"
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "Adaptive drills for your weakest keys"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Aleksandr Shelemetev" }]
|
|
9
|
+
keywords = ["typing", "tutor", "keybr", "textual", "tui"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: End Users/Desktop",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Education",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"textual>=0.85",
|
|
23
|
+
"platformdirs>=4.2",
|
|
24
|
+
"typer>=0.12",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
keystrike = "keystrike.cli:app"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/egno/keystrike"
|
|
32
|
+
Repository = "https://github.com/egno/keystrike"
|
|
33
|
+
Issues = "https://github.com/egno/keystrike/issues"
|
|
34
|
+
|
|
35
|
+
# Runtime deps are in [project].dependencies above.
|
|
36
|
+
# uv sync --no-dev → runtime only
|
|
37
|
+
# uv sync → runtime + dev (default group; pure Python)
|
|
38
|
+
# uv sync --all-groups → + lint (Ruff; desktop only)
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8",
|
|
42
|
+
"pytest-asyncio>=0.24",
|
|
43
|
+
"pyright>=1.1.380",
|
|
44
|
+
]
|
|
45
|
+
lint = [
|
|
46
|
+
"ruff>=0.6", # manylinux wheels only; sdist → maturin (no Android wheel)
|
|
47
|
+
]
|
|
48
|
+
snapshot = [
|
|
49
|
+
"pytest-textual-snapshot>=1.1", # jinja2 → markupsafe; no Android wheels
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["hatchling"]
|
|
54
|
+
build-backend = "hatchling.build"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/keystrike"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
exclude = [
|
|
61
|
+
"/.claude",
|
|
62
|
+
"/.github",
|
|
63
|
+
"/tests",
|
|
64
|
+
"/docs",
|
|
65
|
+
"/scripts",
|
|
66
|
+
"/uv.lock",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
line-length = 100
|
|
71
|
+
target-version = "py312"
|
|
72
|
+
src = ["src", "tests"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = [
|
|
76
|
+
"E", "F", "I", "B", "UP", "SIM", "RUF", "PL", "PT", "ANN",
|
|
77
|
+
]
|
|
78
|
+
ignore = [
|
|
79
|
+
"ANN401", # allow Any where genuinely needed
|
|
80
|
+
"PLR0913", # constructor with many injected deps is fine
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.per-file-ignores]
|
|
84
|
+
"tests/**" = ["ANN", "PLR2004", "S101"]
|
|
85
|
+
|
|
86
|
+
[tool.pyright]
|
|
87
|
+
include = ["src", "tests"]
|
|
88
|
+
extraPaths = ["."]
|
|
89
|
+
pythonVersion = "3.12"
|
|
90
|
+
typeCheckingMode = "strict"
|
|
91
|
+
reportMissingTypeStubs = false
|
|
92
|
+
reportUnknownMemberType = false
|
|
93
|
+
|
|
94
|
+
[[tool.pyright.executionEnvironments]]
|
|
95
|
+
root = "tests"
|
|
96
|
+
reportMissingParameterType = false
|
|
97
|
+
reportUnknownParameterType = false
|
|
98
|
+
reportUnknownArgumentType = false
|
|
99
|
+
reportUnknownVariableType = false
|
|
100
|
+
reportUnknownMemberType = false
|
|
101
|
+
reportPrivateUsage = false
|
|
102
|
+
|
|
103
|
+
[tool.pytest.ini_options]
|
|
104
|
+
asyncio_mode = "auto"
|
|
105
|
+
testpaths = ["tests"]
|
|
106
|
+
addopts = "-ra --strict-markers -m 'not snapshot'"
|
|
107
|
+
markers = [
|
|
108
|
+
"snapshot: visual regression (requires pytest-textual-snapshot)",
|
|
109
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Composition root: wire all dependencies and return a ready-to-run KeystrikeApp."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from random import Random
|
|
5
|
+
|
|
6
|
+
from keystrike.application.build_lesson import BuildLesson
|
|
7
|
+
from keystrike.application.learn_budget_use_cases import GetDailyLearnBudget
|
|
8
|
+
from keystrike.application.prepare_practice import PreparePracticeSession
|
|
9
|
+
from keystrike.application.session_use_cases import (
|
|
10
|
+
FinishSession,
|
|
11
|
+
RecordKeystroke,
|
|
12
|
+
StartSession,
|
|
13
|
+
)
|
|
14
|
+
from keystrike.application.settings_use_cases import CycleLayout, UpdateSettings
|
|
15
|
+
from keystrike.application.stats_use_cases import GetHeatmap, GetHistory, RebuildAggregates
|
|
16
|
+
from keystrike.application.sync_use_cases import GetSyncStatus, InitSync, PullSync, PushSync
|
|
17
|
+
from keystrike.infrastructure.aggregates_cache import FileAggregatesCache
|
|
18
|
+
from keystrike.infrastructure.clock import MonotonicClock
|
|
19
|
+
from keystrike.infrastructure.id_gen import UlidGenerator
|
|
20
|
+
from keystrike.infrastructure.languages import BundledLanguageProvider
|
|
21
|
+
from keystrike.infrastructure.layout_repo import CompositeLayoutRepository
|
|
22
|
+
from keystrike.infrastructure.paths import default_paths, ensure_dirs
|
|
23
|
+
from keystrike.infrastructure.session_repo_jsonl import JsonlSessionRepository
|
|
24
|
+
from keystrike.infrastructure.settings_repo_toml import TomlSettingsRepository
|
|
25
|
+
from keystrike.infrastructure.sync_git import GitSyncGateway
|
|
26
|
+
from keystrike.presentation.textual_app import KeystrikeApp
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class SyncServices:
|
|
31
|
+
init: InitSync
|
|
32
|
+
pull: PullSync
|
|
33
|
+
push: PushSync
|
|
34
|
+
status: GetSyncStatus
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_sync() -> SyncServices:
|
|
38
|
+
paths = default_paths()
|
|
39
|
+
ensure_dirs(paths)
|
|
40
|
+
session_repo = JsonlSessionRepository(paths)
|
|
41
|
+
aggregates_cache = FileAggregatesCache(paths)
|
|
42
|
+
store = GitSyncGateway(paths)
|
|
43
|
+
rebuild = RebuildAggregates(repo=session_repo, cache=aggregates_cache)
|
|
44
|
+
return SyncServices(
|
|
45
|
+
init=InitSync(gateway=store),
|
|
46
|
+
pull=PullSync(gateway=store, rebuild=rebuild),
|
|
47
|
+
push=PushSync(gateway=store),
|
|
48
|
+
status=GetSyncStatus(gateway=store),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build() -> KeystrikeApp:
|
|
53
|
+
paths = default_paths()
|
|
54
|
+
ensure_dirs(paths)
|
|
55
|
+
|
|
56
|
+
clock = MonotonicClock()
|
|
57
|
+
id_gen = UlidGenerator()
|
|
58
|
+
session_repo = JsonlSessionRepository(paths)
|
|
59
|
+
settings_repo = TomlSettingsRepository(paths)
|
|
60
|
+
layout_repo = CompositeLayoutRepository(paths)
|
|
61
|
+
aggregates_cache = FileAggregatesCache(paths)
|
|
62
|
+
language_provider = BundledLanguageProvider()
|
|
63
|
+
|
|
64
|
+
start = StartSession(clock=clock, id_gen=id_gen)
|
|
65
|
+
record = RecordKeystroke(clock=clock, repo=session_repo)
|
|
66
|
+
finish = FinishSession(
|
|
67
|
+
clock=clock,
|
|
68
|
+
repo=session_repo,
|
|
69
|
+
aggregates_cache=aggregates_cache,
|
|
70
|
+
settings_repo=settings_repo,
|
|
71
|
+
layout_repo=layout_repo,
|
|
72
|
+
)
|
|
73
|
+
rebuild_aggregates = RebuildAggregates(repo=session_repo, cache=aggregates_cache)
|
|
74
|
+
get_heatmap = GetHeatmap(cache=aggregates_cache, settings_repo=settings_repo)
|
|
75
|
+
get_history = GetHistory(repo=session_repo)
|
|
76
|
+
get_daily_learn_budget = GetDailyLearnBudget(
|
|
77
|
+
clock=clock,
|
|
78
|
+
repo=session_repo,
|
|
79
|
+
settings_repo=settings_repo,
|
|
80
|
+
)
|
|
81
|
+
cycle_layout = CycleLayout(settings_repo=settings_repo, layout_repo=layout_repo)
|
|
82
|
+
update_settings = UpdateSettings(repo=settings_repo)
|
|
83
|
+
build_lesson = BuildLesson(
|
|
84
|
+
layout_repo=layout_repo,
|
|
85
|
+
aggregates_cache=aggregates_cache,
|
|
86
|
+
settings_repo=settings_repo,
|
|
87
|
+
language_provider=language_provider,
|
|
88
|
+
rng=Random(),
|
|
89
|
+
)
|
|
90
|
+
prepare_practice = PreparePracticeSession(
|
|
91
|
+
settings_repo=settings_repo,
|
|
92
|
+
layout_repo=layout_repo,
|
|
93
|
+
build_lesson=build_lesson,
|
|
94
|
+
get_daily_learn_budget=get_daily_learn_budget,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return KeystrikeApp(
|
|
98
|
+
clock=clock,
|
|
99
|
+
start=start,
|
|
100
|
+
record=record,
|
|
101
|
+
finish=finish,
|
|
102
|
+
settings_repo=settings_repo,
|
|
103
|
+
layout_repo=layout_repo,
|
|
104
|
+
prepare_practice=prepare_practice,
|
|
105
|
+
rebuild_aggregates=rebuild_aggregates,
|
|
106
|
+
get_heatmap=get_heatmap,
|
|
107
|
+
get_history=get_history,
|
|
108
|
+
cycle_layout=cycle_layout,
|
|
109
|
+
update_settings=update_settings,
|
|
110
|
+
get_daily_learn_budget=get_daily_learn_budget,
|
|
111
|
+
)
|
|
File without changes
|