vimniscient 0.2.2__tar.gz → 0.3.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.
- {vimniscient-0.2.2 → vimniscient-0.3.0}/PKG-INFO +22 -7
- {vimniscient-0.2.2 → vimniscient-0.3.0}/README.md +21 -6
- {vimniscient-0.2.2 → vimniscient-0.3.0}/pyproject.toml +1 -1
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/app.py +68 -9
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cli.py +24 -0
- vimniscient-0.3.0/src/vimniscient/doctor.py +70 -0
- vimniscient-0.3.0/src/vimniscient/eval.yaml +136 -0
- vimniscient-0.3.0/src/vimniscient/evaluation.py +58 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/generate.py +90 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d1-dot.yaml +2 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d6-visual.yaml +3 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d1-macros.yaml +2 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d5-visual-block.yaml +2 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d6-substitute.yaml +1 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d1-ranges.yaml +4 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d2-sort-filter.yaml +2 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d3-regex.yaml +2 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d4-norm.yaml +1 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d5-motions.yaml +1 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons.py +5 -0
- vimniscient-0.3.0/src/vimniscient/missions.py +44 -0
- vimniscient-0.3.0/src/vimniscient/missions.yaml +121 -0
- vimniscient-0.3.0/src/vimniscient/nvim/mission.lua +84 -0
- vimniscient-0.3.0/src/vimniscient/placement.py +63 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/progress.py +18 -10
- vimniscient-0.3.0/src/vimniscient/quick.py +49 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/session.py +27 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/today.py +1 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/transfer.py +1 -1
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/builtin_lessons.py +15 -8
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/main.py +4 -1
- vimniscient-0.3.0/src/vimniscient/ui/quick.py +56 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_app.py +7 -0
- vimniscient-0.3.0/tests/check_missions.py +49 -0
- vimniscient-0.3.0/tests/check_placement.py +78 -0
- vimniscient-0.3.0/tests/check_quick.py +47 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_transfer.py +1 -1
- vimniscient-0.3.0/tests/live_mission.py +45 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/.gitignore +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/LICENSE +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/extras/nvim/vimniscient.lua +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/__init__.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/__main__.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/answer.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cards/foot.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cards/lazyvim.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cards/shell.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cards/vim.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cards/zathura.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/cards.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/diagnose.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/drills/omarchy.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/drills.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/endless.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/habits.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/keys.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/sources.md +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d1-words.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d2-pairs.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d3-sentences.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d4-find.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d5-lines.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d6-counts.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w1d7-review.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d2-search.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d3-cgn.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d4-case-numbers.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d5-insert.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w2d7-review.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d2-macros-more.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d3-registers.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d4-marks.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w3d7-review.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d6-indent.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w4d7-review.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w5d1-arguments.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w5d2-any-quote-bracket.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w5d3-functions.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w5d4-small-objects.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w5d5-comments.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w5d6-review.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w6d1-code.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w6d2-notes.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/lessons/w6d3-data.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/mnemonics.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/mnemonics.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/nvim/keymaps.lua +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/nvim/runner.lua +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/reminders.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/settings.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/srs.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/transfer.yaml +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/__init__.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/cards.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/common.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/drills.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/lesson.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/ui/stats.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/src/vimniscient/vimgolf.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_answers.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_diagnose.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_drills.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_generate.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_lessons.lua +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_lessons.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_timing.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/check_today.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/live_drills.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/live_endless.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/live_lessons.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/live_retry.py +0 -0
- {vimniscient-0.2.2 → vimniscient-0.3.0}/tests/live_review.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vimniscient
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Get fluent in Vim in fifteen minutes a day, in your own Neovim.
|
|
5
5
|
Project-URL: Homepage, https://vimniscient.com
|
|
6
6
|
Project-URL: Source, https://github.com/dialectmaterial/Vimniscient
|
|
@@ -85,10 +85,8 @@ circles split by a line; `q` records a macro, a quill writing down what you
|
|
|
85
85
|
do. Some come with a bit of history: `:t` copies lines because the old `ed`
|
|
86
86
|
editor called a copy a *transfer*, and `grep` is named after `ed`'s `g/re/p`.
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Vimniscient itself is built on. Every one has a source in
|
|
91
|
-
[`lessons/sources.md`](https://github.com/dialectmaterial/Vimniscient/blob/main/src/vimniscient/lessons/sources.md).
|
|
88
|
+
Week 6 and a few code tasks are the exception: they're realistic on purpose,
|
|
89
|
+
so that you practise on the kind of text you'll really edit.
|
|
92
90
|
|
|
93
91
|
After you answer a card, and at the end of a lesson for the tasks you missed,
|
|
94
92
|
Vimniscient shows what each part of the keys stands for:
|
|
@@ -187,9 +185,22 @@ lesson, and your progress with a keystroke-efficiency graph.
|
|
|
187
185
|
| `Tab`, `j`/`k` | Move between panes and through lists |
|
|
188
186
|
| `e` | Endless mode |
|
|
189
187
|
| `c` / `g` / `p` | Cards / VimGolf / placement test |
|
|
188
|
+
| `z` | Quick practice: 1, 2 or 5 minutes on one skill, navigation drills, or a multi-file mission |
|
|
189
|
+
| `x` | Choose: edits with no hints, where you pick the strategy |
|
|
190
190
|
| `s` | Stats |
|
|
191
191
|
| `q` | Quit |
|
|
192
192
|
|
|
193
|
+
## Is it working?
|
|
194
|
+
|
|
195
|
+
`vimniscient eval pre` is a short test on texts from no lesson. Take it before
|
|
196
|
+
you've trained much, then `vimniscient eval post` after some weeks and
|
|
197
|
+
`vimniscient eval delayed` a few weeks after that. `vimniscient eval` compares
|
|
198
|
+
them: solved, median time, and keys against the reference. Results stay on your
|
|
199
|
+
machine.
|
|
200
|
+
|
|
201
|
+
`vimniscient doctor` checks that Neovim and your config work with the trainer.
|
|
202
|
+
`vimniscient placement reset` undoes a placement test.
|
|
203
|
+
|
|
193
204
|
## It adapts to your setup
|
|
194
205
|
|
|
195
206
|
| You have | You get |
|
|
@@ -288,8 +299,8 @@ key stands for, so that the edit leaves it true: `ciw is change inner word:
|
|
|
288
299
|
it swaps the wh▮ile word` (fix it with `ciw`). What each key officially means,
|
|
289
300
|
and the hook to use where it means nothing, is in
|
|
290
301
|
[`mnemonics.yaml`](https://github.com/dialectmaterial/Vimniscient/blob/main/src/vimniscient/mnemonics.yaml). The app reads the same
|
|
291
|
-
file to say what a key stands for after you answer.
|
|
292
|
-
|
|
302
|
+
file to say what a key stands for after you answer. Every task needs at least
|
|
303
|
+
two texts. Memory science belongs in lesson intros, with a source in
|
|
293
304
|
[`lessons/sources.md`](https://github.com/dialectmaterial/Vimniscient/blob/main/src/vimniscient/lessons/sources.md). Code tasks stay
|
|
294
305
|
code, with the meaning in names and strings.
|
|
295
306
|
|
|
@@ -303,6 +314,10 @@ uv run python tests/check_timing.py # task times: old progress datab
|
|
|
303
314
|
uv run python tests/check_generate.py # generated texts: reproducible, and they solve in Neovim
|
|
304
315
|
uv run python tests/check_diagnose.py # strategy feedback: no reference solution sets it off
|
|
305
316
|
uv run python tests/check_transfer.py # Choose: every way solves it in Neovim, nothing graded
|
|
317
|
+
uv run python tests/check_quick.py # quick practice: the right tasks for the time, nothing graded
|
|
318
|
+
uv run python tests/check_missions.py # missions: every way does the whole job on real files
|
|
319
|
+
uv run python tests/check_placement.py # placement, the evaluation and doctor
|
|
320
|
+
uv run python tests/live_mission.py # a mission in your real Neovim, in a hidden tmux session
|
|
306
321
|
uv run python tests/check_today.py # the daily budget, SRS scheduling, card introductions, hardtime
|
|
307
322
|
uv run python tests/check_app.py # every screen, driven headless
|
|
308
323
|
uv run python tests/live_lessons.py --all # types every solution into the real runner (needs tmux)
|
|
@@ -62,10 +62,8 @@ circles split by a line; `q` records a macro, a quill writing down what you
|
|
|
62
62
|
do. Some come with a bit of history: `:t` copies lines because the old `ed`
|
|
63
63
|
editor called a copy a *transfer*, and `grep` is named after `ed`'s `g/re/p`.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Vimniscient itself is built on. Every one has a source in
|
|
68
|
-
[`lessons/sources.md`](https://github.com/dialectmaterial/Vimniscient/blob/main/src/vimniscient/lessons/sources.md).
|
|
65
|
+
Week 6 and a few code tasks are the exception: they're realistic on purpose,
|
|
66
|
+
so that you practise on the kind of text you'll really edit.
|
|
69
67
|
|
|
70
68
|
After you answer a card, and at the end of a lesson for the tasks you missed,
|
|
71
69
|
Vimniscient shows what each part of the keys stands for:
|
|
@@ -164,9 +162,22 @@ lesson, and your progress with a keystroke-efficiency graph.
|
|
|
164
162
|
| `Tab`, `j`/`k` | Move between panes and through lists |
|
|
165
163
|
| `e` | Endless mode |
|
|
166
164
|
| `c` / `g` / `p` | Cards / VimGolf / placement test |
|
|
165
|
+
| `z` | Quick practice: 1, 2 or 5 minutes on one skill, navigation drills, or a multi-file mission |
|
|
166
|
+
| `x` | Choose: edits with no hints, where you pick the strategy |
|
|
167
167
|
| `s` | Stats |
|
|
168
168
|
| `q` | Quit |
|
|
169
169
|
|
|
170
|
+
## Is it working?
|
|
171
|
+
|
|
172
|
+
`vimniscient eval pre` is a short test on texts from no lesson. Take it before
|
|
173
|
+
you've trained much, then `vimniscient eval post` after some weeks and
|
|
174
|
+
`vimniscient eval delayed` a few weeks after that. `vimniscient eval` compares
|
|
175
|
+
them: solved, median time, and keys against the reference. Results stay on your
|
|
176
|
+
machine.
|
|
177
|
+
|
|
178
|
+
`vimniscient doctor` checks that Neovim and your config work with the trainer.
|
|
179
|
+
`vimniscient placement reset` undoes a placement test.
|
|
180
|
+
|
|
170
181
|
## It adapts to your setup
|
|
171
182
|
|
|
172
183
|
| You have | You get |
|
|
@@ -265,8 +276,8 @@ key stands for, so that the edit leaves it true: `ciw is change inner word:
|
|
|
265
276
|
it swaps the wh▮ile word` (fix it with `ciw`). What each key officially means,
|
|
266
277
|
and the hook to use where it means nothing, is in
|
|
267
278
|
[`mnemonics.yaml`](https://github.com/dialectmaterial/Vimniscient/blob/main/src/vimniscient/mnemonics.yaml). The app reads the same
|
|
268
|
-
file to say what a key stands for after you answer.
|
|
269
|
-
|
|
279
|
+
file to say what a key stands for after you answer. Every task needs at least
|
|
280
|
+
two texts. Memory science belongs in lesson intros, with a source in
|
|
270
281
|
[`lessons/sources.md`](https://github.com/dialectmaterial/Vimniscient/blob/main/src/vimniscient/lessons/sources.md). Code tasks stay
|
|
271
282
|
code, with the meaning in names and strings.
|
|
272
283
|
|
|
@@ -280,6 +291,10 @@ uv run python tests/check_timing.py # task times: old progress datab
|
|
|
280
291
|
uv run python tests/check_generate.py # generated texts: reproducible, and they solve in Neovim
|
|
281
292
|
uv run python tests/check_diagnose.py # strategy feedback: no reference solution sets it off
|
|
282
293
|
uv run python tests/check_transfer.py # Choose: every way solves it in Neovim, nothing graded
|
|
294
|
+
uv run python tests/check_quick.py # quick practice: the right tasks for the time, nothing graded
|
|
295
|
+
uv run python tests/check_missions.py # missions: every way does the whole job on real files
|
|
296
|
+
uv run python tests/check_placement.py # placement, the evaluation and doctor
|
|
297
|
+
uv run python tests/live_mission.py # a mission in your real Neovim, in a hidden tmux session
|
|
283
298
|
uv run python tests/check_today.py # the daily budget, SRS scheduling, card introductions, hardtime
|
|
284
299
|
uv run python tests/check_app.py # every screen, driven headless
|
|
285
300
|
uv run python tests/live_lessons.py --all # types every solution into the real runner (needs tmux)
|
|
@@ -8,22 +8,24 @@ Omarchy theme in use (Catppuccin Mocha's own palette by default).
|
|
|
8
8
|
|
|
9
9
|
from collections.abc import Callable
|
|
10
10
|
from dataclasses import replace
|
|
11
|
+
from datetime import datetime
|
|
11
12
|
|
|
12
13
|
from textual.app import App
|
|
13
14
|
from textual.color import Color
|
|
14
15
|
from textual.content import Content
|
|
15
16
|
|
|
16
|
-
from vimniscient import drills, endless, reminders, today, transfer, vimgolf
|
|
17
|
+
from vimniscient import drills, endless, missions, placement, quick, reminders, today, transfer, vimgolf
|
|
17
18
|
from vimniscient.cards import has_lazyvim, load_decks
|
|
18
19
|
from vimniscient.lessons import Lesson, load_lessons, two_rounds
|
|
19
20
|
from vimniscient.progress import Progress
|
|
20
|
-
from vimniscient.session import COLORS, TaskResult, mouse_wheel, run_endless, run_lesson
|
|
21
|
-
from vimniscient.ui.builtin_lessons import endless_lesson, placement_lesson, review_lesson, transfer_lesson, vimgolf_lesson
|
|
21
|
+
from vimniscient.session import COLORS, TaskResult, mouse_wheel, run_endless, run_lesson, run_mission
|
|
22
|
+
from vimniscient.ui.builtin_lessons import endless_lesson, mission_lesson, placement_lesson, quick_lesson, review_lesson, transfer_lesson, vimgolf_lesson
|
|
22
23
|
from vimniscient.ui.cards import CardScreen
|
|
23
24
|
from vimniscient.ui.common import CATPPUCCIN, DEFAULT_THEME, omarchy_theme, plural
|
|
24
25
|
from vimniscient.ui.drills import DrillScreen
|
|
25
26
|
from vimniscient.ui.lesson import LessonScreen, ResultsScreen
|
|
26
27
|
from vimniscient.ui.main import MainScreen
|
|
28
|
+
from vimniscient.ui.quick import QuickScreen
|
|
27
29
|
from vimniscient.ui.stats import StatsScreen
|
|
28
30
|
|
|
29
31
|
|
|
@@ -45,7 +47,7 @@ class VimniscientApp(App):
|
|
|
45
47
|
|
|
46
48
|
#main { height: 1fr; }
|
|
47
49
|
#left { width: 46; max-width: 45%; }
|
|
48
|
-
#plan { height:
|
|
50
|
+
#plan { height: 10; }
|
|
49
51
|
#lesson-list { height: 1fr; }
|
|
50
52
|
#right { width: 1fr; }
|
|
51
53
|
#detail-pane { height: 1fr; }
|
|
@@ -189,6 +191,8 @@ class VimniscientApp(App):
|
|
|
189
191
|
elif step_id == "vimgolf":
|
|
190
192
|
if not progress.vimgolf_done_today():
|
|
191
193
|
return self.start_vimgolf
|
|
194
|
+
elif step_id == "quick":
|
|
195
|
+
return self.start_quick
|
|
192
196
|
elif step_id == "transfer":
|
|
193
197
|
picks = transfer.pick(self.lessons, progress)
|
|
194
198
|
if picks:
|
|
@@ -245,24 +249,79 @@ class VimniscientApp(App):
|
|
|
245
249
|
self.notify("No cards due. Come back tomorrow.")
|
|
246
250
|
|
|
247
251
|
def action_placement(self) -> None:
|
|
252
|
+
search = placement.Search(self.lessons)
|
|
253
|
+
self.push_screen(LessonScreen(placement_lesson(search.round()), start=lambda done: self.placement_round(search, done)))
|
|
254
|
+
|
|
255
|
+
def placement_round(self, search: placement.Search, done: Callable[[], None]) -> None:
|
|
256
|
+
tasks = search.round()
|
|
257
|
+
|
|
248
258
|
def on_results(results: list[TaskResult]) -> str:
|
|
249
259
|
self.progress.record(results)
|
|
250
|
-
|
|
251
|
-
|
|
260
|
+
search.update(results)
|
|
261
|
+
search.seen |= {r.task.item_id for r in results}
|
|
262
|
+
if not search.finished:
|
|
263
|
+
return "That round is done. The next one follows from how it went."
|
|
264
|
+
known = search.known()
|
|
265
|
+
self.progress.mark_known(known, search.seen)
|
|
266
|
+
if not known:
|
|
252
267
|
return "No lessons skipped. Start from Week 1 · Day 1."
|
|
253
|
-
return "Known already: " + ", ".join(f"{p.label} ({p.title})" for p in
|
|
268
|
+
return "Known already: " + ", ".join(f"{p.label} ({p.title})" for p in known)
|
|
254
269
|
|
|
255
|
-
|
|
270
|
+
def after() -> None:
|
|
271
|
+
self.placement_round(search, done) if not search.finished else done()
|
|
272
|
+
|
|
273
|
+
self.start_lesson(placement_lesson(tasks), on_results, after)
|
|
256
274
|
|
|
257
275
|
def action_endless(self) -> None:
|
|
258
276
|
self.run_step("endless", lambda: None)
|
|
259
277
|
|
|
278
|
+
def action_quick(self) -> None:
|
|
279
|
+
self.run_step("quick", lambda: None)
|
|
280
|
+
|
|
281
|
+
def start_quick(self, done: Callable[[], None]) -> None:
|
|
282
|
+
finished = {lesson.id for lesson in self.lessons if self.progress.is_done(lesson)}
|
|
283
|
+
pools = quick.pools(self.lessons, finished)
|
|
284
|
+
mission = missions.pick(self.lessons, self.progress)
|
|
285
|
+
|
|
286
|
+
def chosen(choice: tuple[str, int] | None) -> None:
|
|
287
|
+
if choice is None:
|
|
288
|
+
done()
|
|
289
|
+
return
|
|
290
|
+
skill, minutes = choice
|
|
291
|
+
if skill == "mission":
|
|
292
|
+
self.start_mission(mission, done)
|
|
293
|
+
return
|
|
294
|
+
count, seed = quick.how_many(minutes, self.progress.typical_task_seconds()), datetime.now().isoformat()
|
|
295
|
+
tasks = (quick.navigation(count, self.lessons[0].tasks[0], seed) if skill == quick.NAVIGATION
|
|
296
|
+
else quick.tasks(pools[skill], count, seed))
|
|
297
|
+
|
|
298
|
+
def on_results(results: list[TaskResult]) -> str:
|
|
299
|
+
self.progress.record_ungraded(results, "quick")
|
|
300
|
+
return ""
|
|
301
|
+
|
|
302
|
+
self.start_lesson(quick_lesson(quick.title(skill), tasks), on_results, done)
|
|
303
|
+
|
|
304
|
+
self.push_screen(QuickScreen({skill: len(tasks) for skill, tasks in pools.items()}, mission.title if mission else ""), chosen)
|
|
305
|
+
|
|
306
|
+
def start_mission(self, mission: missions.Mission, done: Callable[[], None]) -> None:
|
|
307
|
+
with self.suspend():
|
|
308
|
+
result = run_mission(mission)
|
|
309
|
+
mouse_wheel(False)
|
|
310
|
+
self.progress.record_ungraded([result], "mission")
|
|
311
|
+
ways = "\n".join(mission.ways)
|
|
312
|
+
note = Content(f"{'Another way' if result.status == 'solved' else 'Ways to do it'}\n{ways}")
|
|
313
|
+
|
|
314
|
+
def after(again: bool) -> None:
|
|
315
|
+
self.start_mission(mission, done) if again else done()
|
|
316
|
+
|
|
317
|
+
self.push_screen(ResultsScreen(mission_lesson(mission), [result], note), after)
|
|
318
|
+
|
|
260
319
|
def action_transfer(self) -> None:
|
|
261
320
|
self.run_step("transfer", lambda: None)
|
|
262
321
|
|
|
263
322
|
def start_transfer(self, picks: list[transfer.Challenge], done: Callable[[], None]) -> None:
|
|
264
323
|
def on_results(results: list[TaskResult]) -> Content:
|
|
265
|
-
self.progress.
|
|
324
|
+
self.progress.record_ungraded(results, "transfer")
|
|
266
325
|
by_goal = {c.task.goal: c for c in picks}
|
|
267
326
|
lines = [f"{r.task.goal} {' or '.join(ways)}" for r in results
|
|
268
327
|
if (ways := transfer.another_way(by_goal[r.task.goal], r.keys, r.status == "solved"))]
|
|
@@ -18,6 +18,11 @@ def main() -> None:
|
|
|
18
18
|
remind.add_argument("--check", action="store_true", help="a follow-up reminder firing")
|
|
19
19
|
budget = sub.add_parser("minutes", help="how long today's routine is meant to take (15 by default)")
|
|
20
20
|
budget.add_argument("minutes", type=int, nargs="?", help="set it; leave it out to see it")
|
|
21
|
+
place = sub.add_parser("placement", help="undo the placement test")
|
|
22
|
+
place.add_argument("action", choices=["reset"])
|
|
23
|
+
sub.add_parser("doctor", help="check that Neovim and your setup work with the trainer")
|
|
24
|
+
evaluation = sub.add_parser("eval", help="measure your progress: a test now, one later, compared")
|
|
25
|
+
evaluation.add_argument("stage", choices=["pre", "post", "delayed", "report"], nargs="?", default="report")
|
|
21
26
|
setup = sub.add_parser("reminders", help="daily practice reminders through Omarchy")
|
|
22
27
|
setup.add_argument("action", choices=["install", "uninstall", "status"], nargs="?", default="status")
|
|
23
28
|
args = parser.parse_args()
|
|
@@ -46,6 +51,14 @@ def main() -> None:
|
|
|
46
51
|
settings.set_minutes(args.minutes)
|
|
47
52
|
print(f"Today's routine takes about {settings.minutes()} minutes. ({settings.CONFIG})")
|
|
48
53
|
return
|
|
54
|
+
if args.command == "doctor":
|
|
55
|
+
from vimniscient import doctor
|
|
56
|
+
|
|
57
|
+
ok = True
|
|
58
|
+
for check in doctor.checks():
|
|
59
|
+
print(f"{check.mark} {check.name}: {check.detail}")
|
|
60
|
+
ok = ok and (check.passed or not check.required)
|
|
61
|
+
raise SystemExit(0 if ok else 1)
|
|
49
62
|
if args.command == "reminders":
|
|
50
63
|
from vimniscient import reminders
|
|
51
64
|
|
|
@@ -69,6 +82,17 @@ def main() -> None:
|
|
|
69
82
|
from vimniscient.progress import Progress
|
|
70
83
|
from vimniscient.session import run_endless, run_lesson
|
|
71
84
|
|
|
85
|
+
if args.command == "placement":
|
|
86
|
+
print(f"Placement undone: {Progress().reset_placement()} lessons are back on the list.")
|
|
87
|
+
return
|
|
88
|
+
if args.command == "eval":
|
|
89
|
+
from vimniscient import evaluation as evaluate
|
|
90
|
+
|
|
91
|
+
progress = Progress()
|
|
92
|
+
if args.stage != "report":
|
|
93
|
+
progress.record_ungraded(run_lesson(evaluate.lesson(args.stage)), f"eval-{args.stage}")
|
|
94
|
+
print(evaluate.report(progress))
|
|
95
|
+
return
|
|
72
96
|
if args.command == "endless":
|
|
73
97
|
from vimniscient import endless
|
|
74
98
|
from vimniscient.cards import has_lazyvim
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""`vimniscient doctor`: does this machine work with the trainer? Required
|
|
2
|
+
checks stop it working; the rest say what you'd be missing."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import sqlite3
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from vimniscient.cards import has_lazyvim, has_plugin
|
|
12
|
+
|
|
13
|
+
MIN_NVIM = (0, 10)
|
|
14
|
+
MIN_SIZE = (80, 24)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Check:
|
|
19
|
+
name: str
|
|
20
|
+
passed: bool
|
|
21
|
+
detail: str
|
|
22
|
+
required: bool = True
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def mark(self) -> str:
|
|
26
|
+
return "✓" if self.passed else "✗" if self.required else "·"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _nvim() -> list[Check]:
|
|
30
|
+
if not shutil.which("nvim"):
|
|
31
|
+
return [Check("Neovim", False, f"not found; install {MIN_NVIM[0]}.{MIN_NVIM[1]} or newer")]
|
|
32
|
+
out = subprocess.run(["nvim", "--version"], capture_output=True, text=True).stdout
|
|
33
|
+
found = re.search(r"v(\d+)\.(\d+)", out)
|
|
34
|
+
version = (int(found[1]), int(found[2])) if found else (0, 0)
|
|
35
|
+
good = version >= MIN_NVIM
|
|
36
|
+
checks = [Check("Neovim", good, f"{version[0]}.{version[1]}" + ("" if good else f"; {MIN_NVIM[0]}.{MIN_NVIM[1]} or newer needed"))]
|
|
37
|
+
try:
|
|
38
|
+
run = subprocess.run(["nvim", "--headless", "-i", "NONE", "+qa"], capture_output=True, text=True, timeout=30)
|
|
39
|
+
problem = next((line for line in run.stderr.splitlines() if re.search(r"(?i)error", line)), "")
|
|
40
|
+
checks.append(Check("Your config", run.returncode == 0 and not problem, problem[:90] or "starts cleanly"))
|
|
41
|
+
except subprocess.TimeoutExpired:
|
|
42
|
+
checks.append(Check("Your config", False, "Neovim didn't start within 30 seconds"))
|
|
43
|
+
return checks
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _terminal() -> Check:
|
|
47
|
+
size = shutil.get_terminal_size()
|
|
48
|
+
good = size.columns >= MIN_SIZE[0] and size.lines >= MIN_SIZE[1]
|
|
49
|
+
return Check("Terminal size", good, f"{size.columns}×{size.lines}" + ("" if good else f"; {MIN_SIZE[0]}×{MIN_SIZE[1]} or larger is needed"), required=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _data() -> Check:
|
|
53
|
+
from vimniscient.progress import DATA_DIR
|
|
54
|
+
|
|
55
|
+
path = DATA_DIR / "progress.db"
|
|
56
|
+
try:
|
|
57
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
sqlite3.connect(path).execute("SELECT 1")
|
|
59
|
+
return Check("Progress data", True, str(path))
|
|
60
|
+
except (OSError, sqlite3.Error) as error:
|
|
61
|
+
return Check("Progress data", False, f"{path}: {error}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def checks() -> list[Check]:
|
|
65
|
+
optional = [
|
|
66
|
+
Check("LazyVim", has_lazyvim(), "week 5 lessons and the LazyVim cards" + ("" if has_lazyvim() else " stay hidden"), required=False),
|
|
67
|
+
Check("hardtime.nvim", has_plugin("hardtime.nvim"), "habit cards and drills", required=False),
|
|
68
|
+
Check("Hyprland", bool(os.environ.get("HYPRLAND_INSTANCE_SIGNATURE")), "Omarchy drills", required=False),
|
|
69
|
+
]
|
|
70
|
+
return [*_nvim(), _data(), _terminal(), *optional]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# The evaluation: forms a and b are the same eight skills on different texts,
|
|
2
|
+
# none of them from a lesson. `pre` uses a, `post` b, `delayed` a again, so a
|
|
3
|
+
# change can't just be remembering the test. Realistic on purpose. Never graded.
|
|
4
|
+
forms:
|
|
5
|
+
a:
|
|
6
|
+
- goal: 'Change the port to 9000.'
|
|
7
|
+
start: |-
|
|
8
|
+
▮server = connect("db.local", 5432)
|
|
9
|
+
target: |-
|
|
10
|
+
server = connect("db.local", 9000)
|
|
11
|
+
ways: ['f5ciw9000<Esc>']
|
|
12
|
+
- goal: 'Delete the line with the password.'
|
|
13
|
+
start: |-
|
|
14
|
+
▮user = "ana"
|
|
15
|
+
password = "hunter2"
|
|
16
|
+
host = "web1"
|
|
17
|
+
target: |-
|
|
18
|
+
user = "ana"
|
|
19
|
+
host = "web1"
|
|
20
|
+
ways: ['jdd']
|
|
21
|
+
- goal: 'Change "draft" to "final" in both places.'
|
|
22
|
+
start: |-
|
|
23
|
+
▮status: draft
|
|
24
|
+
note: this is a draft
|
|
25
|
+
target: |-
|
|
26
|
+
status: final
|
|
27
|
+
note: this is a final
|
|
28
|
+
ways: [':%s/draft/final<CR>']
|
|
29
|
+
- goal: 'Put each name in quotes.'
|
|
30
|
+
start: |-
|
|
31
|
+
▮anna
|
|
32
|
+
bert
|
|
33
|
+
carl
|
|
34
|
+
target: |-
|
|
35
|
+
"anna"
|
|
36
|
+
"bert"
|
|
37
|
+
"carl"
|
|
38
|
+
ways: [':%s/.*/"&"/<CR>']
|
|
39
|
+
- goal: 'Delete the arguments of the call.'
|
|
40
|
+
start: |-
|
|
41
|
+
▮result = process(data, retries, timeout)
|
|
42
|
+
target: |-
|
|
43
|
+
result = process()
|
|
44
|
+
ways: ['f(di(']
|
|
45
|
+
- goal: 'Move the last line to the top.'
|
|
46
|
+
start: |-
|
|
47
|
+
one
|
|
48
|
+
two
|
|
49
|
+
▮zero
|
|
50
|
+
target: |-
|
|
51
|
+
zero
|
|
52
|
+
one
|
|
53
|
+
two
|
|
54
|
+
ways: [':m0<CR>']
|
|
55
|
+
- goal: 'Sort the lines.'
|
|
56
|
+
start: |-
|
|
57
|
+
▮pear
|
|
58
|
+
apple
|
|
59
|
+
mango
|
|
60
|
+
target: |-
|
|
61
|
+
apple
|
|
62
|
+
mango
|
|
63
|
+
pear
|
|
64
|
+
ways: [':sort<CR>']
|
|
65
|
+
- goal: 'Delete from here to the end of the line.'
|
|
66
|
+
start: |-
|
|
67
|
+
keep this▮, drop all of this
|
|
68
|
+
target: |-
|
|
69
|
+
keep this
|
|
70
|
+
ways: ['D']
|
|
71
|
+
b:
|
|
72
|
+
- goal: 'Change the timeout to 60.'
|
|
73
|
+
start: |-
|
|
74
|
+
▮client = open("api.host", 30)
|
|
75
|
+
target: |-
|
|
76
|
+
client = open("api.host", 60)
|
|
77
|
+
ways: ['f3ciw60<Esc>']
|
|
78
|
+
- goal: 'Delete the line with the token.'
|
|
79
|
+
start: |-
|
|
80
|
+
▮name = "bo"
|
|
81
|
+
token = "abc123"
|
|
82
|
+
path = "/srv"
|
|
83
|
+
target: |-
|
|
84
|
+
name = "bo"
|
|
85
|
+
path = "/srv"
|
|
86
|
+
ways: ['jdd']
|
|
87
|
+
- goal: 'Change "todo" to "done" in both places.'
|
|
88
|
+
start: |-
|
|
89
|
+
▮state: todo
|
|
90
|
+
again: still todo
|
|
91
|
+
target: |-
|
|
92
|
+
state: done
|
|
93
|
+
again: still done
|
|
94
|
+
ways: [':%s/todo/done<CR>']
|
|
95
|
+
- goal: 'Put each word in quotes.'
|
|
96
|
+
start: |-
|
|
97
|
+
▮red
|
|
98
|
+
green
|
|
99
|
+
blue
|
|
100
|
+
target: |-
|
|
101
|
+
"red"
|
|
102
|
+
"green"
|
|
103
|
+
"blue"
|
|
104
|
+
ways: [':%s/.*/"&"/<CR>']
|
|
105
|
+
- goal: 'Delete the arguments of the call.'
|
|
106
|
+
start: |-
|
|
107
|
+
▮value = compute(left, right, scale)
|
|
108
|
+
target: |-
|
|
109
|
+
value = compute()
|
|
110
|
+
ways: ['f(di(']
|
|
111
|
+
- goal: 'Move the last line to the top.'
|
|
112
|
+
start: |-
|
|
113
|
+
beta
|
|
114
|
+
gamma
|
|
115
|
+
▮alpha
|
|
116
|
+
target: |-
|
|
117
|
+
alpha
|
|
118
|
+
beta
|
|
119
|
+
gamma
|
|
120
|
+
ways: [':m0<CR>']
|
|
121
|
+
- goal: 'Sort the lines.'
|
|
122
|
+
start: |-
|
|
123
|
+
▮walnut
|
|
124
|
+
almond
|
|
125
|
+
hazelnut
|
|
126
|
+
target: |-
|
|
127
|
+
almond
|
|
128
|
+
hazelnut
|
|
129
|
+
walnut
|
|
130
|
+
ways: [':sort<CR>']
|
|
131
|
+
- goal: 'Delete from here to the end of the line.'
|
|
132
|
+
start: |-
|
|
133
|
+
the good part▮, and the rest goes
|
|
134
|
+
target: |-
|
|
135
|
+
the good part
|
|
136
|
+
ways: ['D']
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""`vimniscient eval`: does the training work? Three sittings of the same eight
|
|
2
|
+
skills, on texts from no lesson, with no hints: `pre` now, `post` after some
|
|
3
|
+
weeks of training, `delayed` a few weeks after that. Stored locally, never graded.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from functools import cache
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from statistics import median
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from vimniscient.keys import count_keys
|
|
13
|
+
from vimniscient.lessons import Lesson, Task, _parse_start
|
|
14
|
+
|
|
15
|
+
LESSON_ID = "eval"
|
|
16
|
+
HINT = "No hints in the test: :Skip what you don't know."
|
|
17
|
+
FORM = {"pre": "a", "post": "b", "delayed": "a"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cache
|
|
21
|
+
def forms() -> dict[str, tuple[dict, ...]]:
|
|
22
|
+
data = yaml.safe_load((resources.files("vimniscient") / "eval.yaml").read_text(encoding="utf-8"))
|
|
23
|
+
return {name: tuple(tasks) for name, tasks in data["forms"].items()}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def task(t: dict, index: int) -> Task:
|
|
27
|
+
start, cursor = _parse_start(t["start"])
|
|
28
|
+
return Task(goal=t["goal"], start=start, cursor=cursor, target=t["target"].split("\n"), solution="—", hint=HINT, ft="",
|
|
29
|
+
lesson_id=LESSON_ID, index=index, ref_override=min(count_keys(way) for way in t["ways"]))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def lesson(stage: str) -> Lesson:
|
|
33
|
+
tasks = [task(t, i) for i, t in enumerate(forms()[FORM[stage]])]
|
|
34
|
+
return Lesson(id=LESSON_ID, week=0, day=0, title=f"Evaluation: {stage}", intro="", keys=[], tasks=tasks)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def summary(rows: list[tuple[str, float | None, int, int]]) -> tuple[int, float | None, float | None]:
|
|
38
|
+
"""(solved, median seconds, keys against the reference) from (status, seconds, count, ref) rows."""
|
|
39
|
+
solved = [row for row in rows if row[0] == "solved"]
|
|
40
|
+
seconds = [row[1] for row in solved if row[1] is not None]
|
|
41
|
+
keys = sum(row[2] for row in solved) / sum(row[3] for row in solved) if solved else None
|
|
42
|
+
return len(solved), median(seconds) if seconds else None, keys
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def report(progress) -> str:
|
|
46
|
+
lines = [f"{'':8}{'date':12}{'solved':8}{'median':9}keys/ref"]
|
|
47
|
+
for stage in FORM:
|
|
48
|
+
rows = progress.db.execute(
|
|
49
|
+
"SELECT status, seconds, count, ref, ts FROM attempts WHERE phase = ? ORDER BY id DESC LIMIT ?",
|
|
50
|
+
(f"eval-{stage}", len(forms()[FORM[stage]]))).fetchall()
|
|
51
|
+
if not rows:
|
|
52
|
+
lines.append(f"{stage:8}not taken yet: vimniscient eval {stage}")
|
|
53
|
+
continue
|
|
54
|
+
solved, seconds, keys = summary([row[:4] for row in rows])
|
|
55
|
+
time = f"{seconds:.1f}s" if seconds is not None else "-"
|
|
56
|
+
ratio = f"{keys:.2f}" if keys is not None else "-"
|
|
57
|
+
lines.append(f"{stage:8}{rows[0][4][:10]:12}{f'{solved}/{len(rows)}':8}{time:9}{ratio}")
|
|
58
|
+
return "\n".join(lines)
|