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.
Files changed (64) hide show
  1. keystrike-1.1.0/.gitignore +16 -0
  2. keystrike-1.1.0/LICENSE +21 -0
  3. keystrike-1.1.0/PKG-INFO +175 -0
  4. keystrike-1.1.0/README.md +150 -0
  5. keystrike-1.1.0/pyproject.toml +109 -0
  6. keystrike-1.1.0/src/keystrike/__init__.py +1 -0
  7. keystrike-1.1.0/src/keystrike/__main__.py +9 -0
  8. keystrike-1.1.0/src/keystrike/app.py +111 -0
  9. keystrike-1.1.0/src/keystrike/application/__init__.py +0 -0
  10. keystrike-1.1.0/src/keystrike/application/build_lesson.py +202 -0
  11. keystrike-1.1.0/src/keystrike/application/learn_budget_use_cases.py +33 -0
  12. keystrike-1.1.0/src/keystrike/application/prepare_practice.py +53 -0
  13. keystrike-1.1.0/src/keystrike/application/session_use_cases.py +372 -0
  14. keystrike-1.1.0/src/keystrike/application/settings_use_cases.py +74 -0
  15. keystrike-1.1.0/src/keystrike/application/stats_use_cases.py +107 -0
  16. keystrike-1.1.0/src/keystrike/application/sync_use_cases.py +41 -0
  17. keystrike-1.1.0/src/keystrike/cli.py +95 -0
  18. keystrike-1.1.0/src/keystrike/domain/__init__.py +0 -0
  19. keystrike-1.1.0/src/keystrike/domain/aggregate.py +179 -0
  20. keystrike-1.1.0/src/keystrike/domain/confidence.py +221 -0
  21. keystrike-1.1.0/src/keystrike/domain/daily_learn.py +79 -0
  22. keystrike-1.1.0/src/keystrike/domain/enums.py +29 -0
  23. keystrike-1.1.0/src/keystrike/domain/generator.py +120 -0
  24. keystrike-1.1.0/src/keystrike/domain/learn_order.py +37 -0
  25. keystrike-1.1.0/src/keystrike/domain/markov.py +84 -0
  26. keystrike-1.1.0/src/keystrike/domain/models.py +119 -0
  27. keystrike-1.1.0/src/keystrike/domain/null_adapters.py +55 -0
  28. keystrike-1.1.0/src/keystrike/domain/protocols.py +83 -0
  29. keystrike-1.1.0/src/keystrike/domain/regression.py +97 -0
  30. keystrike-1.1.0/src/keystrike/domain/session.py +38 -0
  31. keystrike-1.1.0/src/keystrike/domain/sync_merge.py +156 -0
  32. keystrike-1.1.0/src/keystrike/infrastructure/__init__.py +0 -0
  33. keystrike-1.1.0/src/keystrike/infrastructure/aggregates_cache.py +62 -0
  34. keystrike-1.1.0/src/keystrike/infrastructure/atomic_write.py +13 -0
  35. keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/__init__.py +0 -0
  36. keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/_grid.py +61 -0
  37. keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/colemak.py +3 -0
  38. keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/colemak_dh.py +11 -0
  39. keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/dvorak.py +3 -0
  40. keystrike-1.1.0/src/keystrike/infrastructure/bundled_layouts/qwerty.py +3 -0
  41. keystrike-1.1.0/src/keystrike/infrastructure/clock.py +9 -0
  42. keystrike-1.1.0/src/keystrike/infrastructure/code_generators/__init__.py +0 -0
  43. keystrike-1.1.0/src/keystrike/infrastructure/id_gen.py +22 -0
  44. keystrike-1.1.0/src/keystrike/infrastructure/languages/__init__.py +32 -0
  45. keystrike-1.1.0/src/keystrike/infrastructure/languages/data/en_markov.json.gz +0 -0
  46. keystrike-1.1.0/src/keystrike/infrastructure/layout_repo.py +38 -0
  47. keystrike-1.1.0/src/keystrike/infrastructure/layout_toml.py +98 -0
  48. keystrike-1.1.0/src/keystrike/infrastructure/paths.py +56 -0
  49. keystrike-1.1.0/src/keystrike/infrastructure/session_repo_jsonl.py +161 -0
  50. keystrike-1.1.0/src/keystrike/infrastructure/settings_repo_toml.py +73 -0
  51. keystrike-1.1.0/src/keystrike/infrastructure/sync_git.py +197 -0
  52. keystrike-1.1.0/src/keystrike/presentation/__init__.py +0 -0
  53. keystrike-1.1.0/src/keystrike/presentation/bindings.py +10 -0
  54. keystrike-1.1.0/src/keystrike/presentation/screens/__init__.py +0 -0
  55. keystrike-1.1.0/src/keystrike/presentation/screens/home.py +102 -0
  56. keystrike-1.1.0/src/keystrike/presentation/screens/practice.py +170 -0
  57. keystrike-1.1.0/src/keystrike/presentation/screens/settings.py +145 -0
  58. keystrike-1.1.0/src/keystrike/presentation/screens/stats.py +179 -0
  59. keystrike-1.1.0/src/keystrike/presentation/textual_app.py +119 -0
  60. keystrike-1.1.0/src/keystrike/presentation/theme.py +7 -0
  61. keystrike-1.1.0/src/keystrike/presentation/widgets/__init__.py +0 -0
  62. keystrike-1.1.0/src/keystrike/presentation/widgets/hud.py +92 -0
  63. keystrike-1.1.0/src/keystrike/presentation/widgets/kb_heatmap.py +125 -0
  64. keystrike-1.1.0/src/keystrike/presentation/widgets/typing_area.py +71 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .uv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .pyright/
11
+ htmlcov/
12
+ .coverage
13
+ .coverage.*
14
+ *.log
15
+ .DS_Store
16
+ .claude/
@@ -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.
@@ -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
+ ![Keystrike demo](https://raw.githubusercontent.com/egno/keystrike/v1.1.0/docs/assets/demo.gif)
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
+ ![Keystrike demo](https://raw.githubusercontent.com/egno/keystrike/v1.1.0/docs/assets/demo.gif)
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,9 @@
1
+ from keystrike.cli import app
2
+
3
+
4
+ def main() -> None:
5
+ app()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -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