vimniscient 0.2.0__py3-none-any.whl
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/__init__.py +3 -0
- vimniscient/__main__.py +3 -0
- vimniscient/answer.py +113 -0
- vimniscient/app.py +310 -0
- vimniscient/cards/foot.yaml +19 -0
- vimniscient/cards/lazyvim.yaml +80 -0
- vimniscient/cards/shell.yaml +22 -0
- vimniscient/cards/vim.yaml +95 -0
- vimniscient/cards/zathura.yaml +32 -0
- vimniscient/cards.py +200 -0
- vimniscient/cli.py +98 -0
- vimniscient/drills/omarchy.yaml +88 -0
- vimniscient/drills.py +198 -0
- vimniscient/endless.py +60 -0
- vimniscient/generate.py +102 -0
- vimniscient/habits.py +116 -0
- vimniscient/keys.py +14 -0
- vimniscient/lessons/sources.md +59 -0
- vimniscient/lessons/w1d1-words.yaml +223 -0
- vimniscient/lessons/w1d2-pairs.yaml +188 -0
- vimniscient/lessons/w1d3-sentences.yaml +247 -0
- vimniscient/lessons/w1d4-find.yaml +184 -0
- vimniscient/lessons/w1d5-lines.yaml +249 -0
- vimniscient/lessons/w1d6-counts.yaml +351 -0
- vimniscient/lessons/w1d7-review.yaml +222 -0
- vimniscient/lessons/w2d1-dot.yaml +195 -0
- vimniscient/lessons/w2d2-search.yaml +194 -0
- vimniscient/lessons/w2d3-cgn.yaml +196 -0
- vimniscient/lessons/w2d4-case-numbers.yaml +201 -0
- vimniscient/lessons/w2d5-insert.yaml +229 -0
- vimniscient/lessons/w2d6-visual.yaml +206 -0
- vimniscient/lessons/w2d7-review.yaml +197 -0
- vimniscient/lessons/w3d1-macros.yaml +240 -0
- vimniscient/lessons/w3d2-macros-more.yaml +170 -0
- vimniscient/lessons/w3d3-registers.yaml +230 -0
- vimniscient/lessons/w3d4-marks.yaml +188 -0
- vimniscient/lessons/w3d5-visual-block.yaml +201 -0
- vimniscient/lessons/w3d6-substitute.yaml +290 -0
- vimniscient/lessons/w3d7-review.yaml +204 -0
- vimniscient/lessons/w4d1-ranges.yaml +214 -0
- vimniscient/lessons/w4d2-sort-filter.yaml +243 -0
- vimniscient/lessons/w4d3-regex.yaml +167 -0
- vimniscient/lessons/w4d4-norm.yaml +158 -0
- vimniscient/lessons/w4d5-motions.yaml +149 -0
- vimniscient/lessons/w4d6-indent.yaml +143 -0
- vimniscient/lessons/w4d7-review.yaml +175 -0
- vimniscient/lessons/w5d1-arguments.yaml +133 -0
- vimniscient/lessons/w5d2-any-quote-bracket.yaml +136 -0
- vimniscient/lessons/w5d3-functions.yaml +193 -0
- vimniscient/lessons/w5d4-small-objects.yaml +185 -0
- vimniscient/lessons/w5d5-comments.yaml +161 -0
- vimniscient/lessons/w5d6-review.yaml +120 -0
- vimniscient/lessons/w6d1-code.yaml +157 -0
- vimniscient/lessons/w6d2-notes.yaml +163 -0
- vimniscient/lessons/w6d3-data.yaml +145 -0
- vimniscient/lessons.py +184 -0
- vimniscient/mnemonics.py +141 -0
- vimniscient/mnemonics.yaml +124 -0
- vimniscient/nvim/keymaps.lua +25 -0
- vimniscient/nvim/runner.lua +711 -0
- vimniscient/progress.py +496 -0
- vimniscient/reminders.py +220 -0
- vimniscient/session.py +152 -0
- vimniscient/settings.py +33 -0
- vimniscient/srs.py +56 -0
- vimniscient/today.py +189 -0
- vimniscient/ui/__init__.py +1 -0
- vimniscient/ui/builtin_lessons.py +99 -0
- vimniscient/ui/cards.py +160 -0
- vimniscient/ui/common.py +129 -0
- vimniscient/ui/drills.py +163 -0
- vimniscient/ui/lesson.py +193 -0
- vimniscient/ui/main.py +217 -0
- vimniscient/ui/stats.py +145 -0
- vimniscient/vimgolf.py +97 -0
- vimniscient-0.2.0.dist-info/METADATA +322 -0
- vimniscient-0.2.0.dist-info/RECORD +80 -0
- vimniscient-0.2.0.dist-info/WHEEL +4 -0
- vimniscient-0.2.0.dist-info/entry_points.txt +2 -0
- vimniscient-0.2.0.dist-info/licenses/LICENSE +661 -0
vimniscient/__init__.py
ADDED
vimniscient/__main__.py
ADDED
vimniscient/answer.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Checking typed answers to cards.
|
|
2
|
+
|
|
3
|
+
Two styles: `keys` for key sequences (`ciw`, `<C-a>`, `<leader>ff`) and
|
|
4
|
+
`combo` for chords (`SUPER SHIFT + B`). Both accept the usual ways of
|
|
5
|
+
writing the same thing: `ctrl-a`, `^a` and `<C-a>`; `space ff` and
|
|
6
|
+
`<leader>ff`; `super+shift+b` and `SUPER SHIFT + B`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
LEADER = "\x00"
|
|
12
|
+
|
|
13
|
+
_TOKEN_NAMES = {
|
|
14
|
+
"esc": "<Esc>", "escape": "<Esc>",
|
|
15
|
+
"cr": "<CR>", "enter": "<CR>", "return": "<CR>",
|
|
16
|
+
"tab": "<Tab>", "bs": "<BS>", "backspace": "<BS>",
|
|
17
|
+
"space": "<Space>", "lt": "<", "bar": "|", "bslash": "\\",
|
|
18
|
+
"home": "<Home>", "end": "<End>", "del": "<Del>", "delete": "<Del>",
|
|
19
|
+
"up": "<Up>", "down": "<Down>", "left": "<Left>", "right": "<Right>",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _norm_token(match: re.Match) -> str:
|
|
24
|
+
inner = match.group(1)
|
|
25
|
+
low = inner.lower()
|
|
26
|
+
if low in ("leader", "space") and match.start() == 0:
|
|
27
|
+
return LEADER
|
|
28
|
+
if low in _TOKEN_NAMES:
|
|
29
|
+
return _TOKEN_NAMES[low]
|
|
30
|
+
m = re.fullmatch(r"([csma])-(.+)", inner, re.I)
|
|
31
|
+
if m:
|
|
32
|
+
mod = {"c": "C", "s": "S", "m": "M", "a": "M"}[m.group(1).lower()]
|
|
33
|
+
key = m.group(2)
|
|
34
|
+
key = key.lower() if mod == "C" and len(key) == 1 else key
|
|
35
|
+
return f"<{mod}-{key}>"
|
|
36
|
+
if re.fullmatch(r"f\d+", low):
|
|
37
|
+
return f"<F{low[1:]}>"
|
|
38
|
+
return f"<{inner}>"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def norm_keys(text: str, case_sensitive: bool = True) -> str:
|
|
42
|
+
s = text.strip()
|
|
43
|
+
s = re.sub(r"(?i)^(leader|space|spc)(\s+|$)", "<leader>", s)
|
|
44
|
+
s = re.sub(r"(?i)(?<![<\w])space(?![>\w])", "<Space>", s)
|
|
45
|
+
s = re.sub(r"(?i)\b(?:ctrl|control)[-+ ]+(\S)", lambda m: f"<C-{m.group(1)}>", s)
|
|
46
|
+
s = re.sub(r"(?i)\b(?:alt|meta)[-+ ]+(\S)", lambda m: f"<M-{m.group(1)}>", s)
|
|
47
|
+
s = re.sub(r"\^([A-Za-z_\[\]])", lambda m: f"<C-{m.group(1)}>", s)
|
|
48
|
+
s = re.sub(r"<([^<>\s]+)>", _norm_token, s)
|
|
49
|
+
s = re.sub(r"\s+", "", s)
|
|
50
|
+
s = s.removesuffix("<CR>") if s.startswith(":") else s
|
|
51
|
+
return s if case_sensitive else s.lower()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_MOD_ALIASES = {
|
|
55
|
+
"SUPER": "SUPER", "WIN": "SUPER", "CMD": "SUPER", "MOD": "SUPER", "LOGO": "SUPER",
|
|
56
|
+
"CTRL": "CTRL", "CONTROL": "CTRL", "CTL": "CTRL",
|
|
57
|
+
"ALT": "ALT", "OPT": "ALT", "OPTION": "ALT", "MOD1": "ALT", "META": "ALT",
|
|
58
|
+
"SHIFT": "SHIFT", "PREFIX": "PREFIX", "COPYMODE": "COPYMODE",
|
|
59
|
+
}
|
|
60
|
+
_KEY_ALIASES = {
|
|
61
|
+
"ENTER": "RETURN", "CR": "RETURN", "ESC": "ESCAPE", "DEL": "DELETE", "INS": "INSERT",
|
|
62
|
+
"PGUP": "PAGE_UP", "PAGEUP": "PAGE_UP", "PRIOR": "PAGE_UP",
|
|
63
|
+
"PGDN": "PAGE_DOWN", "PAGEDOWN": "PAGE_DOWN", "NEXT": "PAGE_DOWN",
|
|
64
|
+
"=": "EQUAL", "-": "MINUS", "+": "PLUS", "SPACEBAR": "SPACE",
|
|
65
|
+
"PRTSC": "PRINT", "PRINTSCREEN": "PRINT",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def norm_combo(text: str, case_sensitive: bool = False) -> tuple[frozenset[str], tuple[str, ...]]:
|
|
70
|
+
s = re.sub(r"(?i)copy\s+mode", "COPYMODE", text.strip())
|
|
71
|
+
s = re.sub(r"(?i)page\s+(up|down)", lambda m: f"PAGE_{m.group(1)}", s)
|
|
72
|
+
plus_key = bool(re.search(r"[\s+]\+\s*$", s)) # "CTRL + +" or "CTRL +"
|
|
73
|
+
if plus_key:
|
|
74
|
+
s = re.sub(r"\+\s*$", "", s)
|
|
75
|
+
parts: list[str] = []
|
|
76
|
+
for part in re.split(r"[\s+]+", s):
|
|
77
|
+
if len(part) > 1 and "-" in part:
|
|
78
|
+
parts.extend(p for p in part.split("-") if p)
|
|
79
|
+
elif part:
|
|
80
|
+
parts.append(part)
|
|
81
|
+
if plus_key:
|
|
82
|
+
parts.append("+")
|
|
83
|
+
mods: set[str] = set()
|
|
84
|
+
keys: list[str] = []
|
|
85
|
+
for part in parts:
|
|
86
|
+
mod = _MOD_ALIASES.get(part.upper())
|
|
87
|
+
if mod:
|
|
88
|
+
mods.add(mod)
|
|
89
|
+
continue
|
|
90
|
+
key = _KEY_ALIASES.get(part.upper(), part)
|
|
91
|
+
if not (case_sensitive and len(key) == 1):
|
|
92
|
+
key = key.upper()
|
|
93
|
+
keys.append(key)
|
|
94
|
+
# SHIFT + c is C when case matters (tmux)
|
|
95
|
+
if case_sensitive and "SHIFT" in mods and len(keys) == 1 and keys[0].isalpha() and len(keys[0]) == 1:
|
|
96
|
+
mods.discard("SHIFT")
|
|
97
|
+
keys = [keys[0].upper()]
|
|
98
|
+
return frozenset(mods), tuple(keys)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_correct(answer: str, expected: list[str], style: str, case_sensitive: bool) -> bool:
|
|
102
|
+
if not answer.strip():
|
|
103
|
+
return False
|
|
104
|
+
if style == "combo":
|
|
105
|
+
got = norm_combo(answer, case_sensitive)
|
|
106
|
+
return any(got == norm_combo(e, case_sensitive) for e in expected)
|
|
107
|
+
got = norm_keys(answer, case_sensitive)
|
|
108
|
+
return any(got == norm_keys(e, case_sensitive) for e in expected)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def pretty_keys(keys: str) -> str:
|
|
112
|
+
"""`<Space>ff` reads better as `<leader>ff`."""
|
|
113
|
+
return re.sub(r"^<Space>", "<leader>", keys)
|
vimniscient/app.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""The Textual interface.
|
|
2
|
+
|
|
3
|
+
Laid out like lazygit: everything on one screen in titled panes, Tab moves
|
|
4
|
+
focus, one key bar at the bottom, nothing animates. Cards and drills get a
|
|
5
|
+
centred focus screen, as in terminal typing trainers. Colours come from the
|
|
6
|
+
Omarchy theme in use (Catppuccin Mocha's own palette by default).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
|
|
12
|
+
from textual.app import App
|
|
13
|
+
from textual.color import Color
|
|
14
|
+
|
|
15
|
+
from vimniscient import drills, endless, reminders, today, vimgolf
|
|
16
|
+
from vimniscient.cards import has_lazyvim, load_decks
|
|
17
|
+
from vimniscient.lessons import Lesson, load_lessons, two_rounds
|
|
18
|
+
from vimniscient.progress import Progress
|
|
19
|
+
from vimniscient.session import COLORS, TaskResult, mouse_wheel, run_endless, run_lesson
|
|
20
|
+
from vimniscient.ui.builtin_lessons import endless_lesson, placement_lesson, review_lesson, vimgolf_lesson
|
|
21
|
+
from vimniscient.ui.cards import CardScreen
|
|
22
|
+
from vimniscient.ui.common import CATPPUCCIN, DEFAULT_THEME, omarchy_theme, plural
|
|
23
|
+
from vimniscient.ui.drills import DrillScreen
|
|
24
|
+
from vimniscient.ui.lesson import LessonScreen, ResultsScreen
|
|
25
|
+
from vimniscient.ui.main import MainScreen
|
|
26
|
+
from vimniscient.ui.stats import StatsScreen
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VimniscientApp(App):
|
|
30
|
+
TITLE = "Vimniscient"
|
|
31
|
+
ENABLE_COMMAND_PALETTE = False
|
|
32
|
+
CSS = """
|
|
33
|
+
Screen { background: $background; }
|
|
34
|
+
|
|
35
|
+
/* panes: rounded, titled in the border, mauve when focused (lazygit) */
|
|
36
|
+
.pane { border: round $panel; border-title-color: $text-muted; border-subtitle-color: $text-muted;
|
|
37
|
+
background: $background; padding: 0 1; }
|
|
38
|
+
.pane:focus, .pane:focus-within { border: round $primary; border-title-color: $primary; }
|
|
39
|
+
OptionList.pane { padding: 0 1; text-wrap: nowrap; text-overflow: ellipsis; }
|
|
40
|
+
OptionList.pane:focus { border: round $primary; }
|
|
41
|
+
OptionList > .option-list--option-highlighted { background: $surface; color: $foreground; text-style: none; }
|
|
42
|
+
OptionList:focus > .option-list--option-highlighted { background: $primary 25%; color: $foreground; text-style: bold; }
|
|
43
|
+
.keybar { height: 1; padding: 0 1; color: $text-muted; background: $background; }
|
|
44
|
+
|
|
45
|
+
#main { height: 1fr; }
|
|
46
|
+
#left { width: 46; max-width: 45%; }
|
|
47
|
+
#plan { height: 8; }
|
|
48
|
+
#lesson-list { height: 1fr; }
|
|
49
|
+
#right { width: 1fr; }
|
|
50
|
+
#detail-pane { height: 1fr; }
|
|
51
|
+
#progress-pane { height: auto; max-height: 10; }
|
|
52
|
+
#progress-text { height: auto; }
|
|
53
|
+
#spark { height: 2; margin-top: 1; }
|
|
54
|
+
Sparkline > .sparkline--max-color { color: $primary; }
|
|
55
|
+
Sparkline > .sparkline--min-color { color: $secondary; }
|
|
56
|
+
Markdown { padding: 0 1; margin: 0; background: $background; }
|
|
57
|
+
MarkdownBlock > .code_inline { background: $surface; color: $primary; }
|
|
58
|
+
|
|
59
|
+
/* focus screens: one centred column, like terminal typing trainers */
|
|
60
|
+
CardScreen, DrillScreen, LessonScreen { align: center middle; }
|
|
61
|
+
.column, .column:focus-within { width: 76; max-width: 100%; height: auto; max-height: 90%; padding: 1 2;
|
|
62
|
+
border: round $primary; border-title-color: $primary; }
|
|
63
|
+
.prompt { color: $primary; text-style: bold; height: auto; padding: 1 0; }
|
|
64
|
+
#card-bar, #drill-bar { height: 1; }
|
|
65
|
+
#card-input { border: round $panel; background: $background; padding: 0 1; }
|
|
66
|
+
#card-input:focus { border: round $primary; background: $background; }
|
|
67
|
+
#card-feedback { padding: 1 0 0 0; height: auto; min-height: 3; }
|
|
68
|
+
#drill-steps { height: auto; }
|
|
69
|
+
#drill-status { height: auto; padding-top: 1; }
|
|
70
|
+
CardScreen > .keybar, DrillScreen > .keybar, LessonScreen > .keybar, ResultsScreen > .keybar { dock: bottom; }
|
|
71
|
+
|
|
72
|
+
ResultsScreen { align: center middle; }
|
|
73
|
+
#results-panel { width: 96; }
|
|
74
|
+
#results-bar { height: 1; }
|
|
75
|
+
#note { padding: 0 0 1 0; color: $success; height: auto; }
|
|
76
|
+
#results-list { height: auto; padding-right: 1; }
|
|
77
|
+
#results-why { height: auto; padding: 1 0 0 0; }
|
|
78
|
+
|
|
79
|
+
#stats-view { height: 1fr; padding: 0 2; }
|
|
80
|
+
.section-label { color: $text-muted; text-style: bold; height: 1; margin-top: 1; }
|
|
81
|
+
DataTable { background: $background; height: auto; max-height: 14; margin-top: 1; }
|
|
82
|
+
DataTable > .datatable--header { background: $surface; color: $text-muted; text-style: bold; }
|
|
83
|
+
#overview { height: auto; margin-top: 1; }
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
super().__init__()
|
|
88
|
+
self.animation_level = "none" # tabs, scrolling and the rest switch instantly
|
|
89
|
+
# Week 5 teaches LazyVim's own text objects; without LazyVim it's left out.
|
|
90
|
+
self.lessons = [lesson for lesson in load_lessons() if not lesson.lazyvim or has_lazyvim()]
|
|
91
|
+
self.progress = Progress()
|
|
92
|
+
self.decks = load_decks()
|
|
93
|
+
self.drills = drills.load_drills()
|
|
94
|
+
|
|
95
|
+
def lesson(self, lesson_id: str) -> Lesson:
|
|
96
|
+
return next(lesson for lesson in self.lessons if lesson.id == lesson_id)
|
|
97
|
+
|
|
98
|
+
def on_mount(self) -> None:
|
|
99
|
+
for theme in CATPPUCCIN:
|
|
100
|
+
self.register_theme(theme)
|
|
101
|
+
theme = omarchy_theme()
|
|
102
|
+
self.theme = theme if theme in self.available_themes else DEFAULT_THEME
|
|
103
|
+
mouse_wheel(False)
|
|
104
|
+
COLORS.update(self.lesson_colors())
|
|
105
|
+
self.push_screen(MainScreen())
|
|
106
|
+
|
|
107
|
+
def lesson_colors(self) -> dict[str, str]:
|
|
108
|
+
"""The theme's colours as hex, for lessons in Neovim to draw with."""
|
|
109
|
+
theme = self.current_theme
|
|
110
|
+
background = Color.parse(theme.background or "#000000")
|
|
111
|
+
foreground = Color.parse(theme.foreground or "#ffffff")
|
|
112
|
+
named = {"primary": theme.primary, "accent": theme.accent or theme.primary,
|
|
113
|
+
"success": theme.success or theme.primary, "error": theme.error or theme.primary,
|
|
114
|
+
"panel": theme.panel or theme.surface or theme.primary}
|
|
115
|
+
return {"background": background.hex, "foreground": foreground.hex,
|
|
116
|
+
"muted": (background + background.get_contrast_text(0.6)).hex, # $text-muted is "auto 60%"
|
|
117
|
+
**{name: Color.parse(value).hex for name, value in named.items()}}
|
|
118
|
+
|
|
119
|
+
# running things
|
|
120
|
+
|
|
121
|
+
def start_lesson(
|
|
122
|
+
self,
|
|
123
|
+
lesson: Lesson,
|
|
124
|
+
on_results: Callable[[list[TaskResult]], str] | None = None,
|
|
125
|
+
done: Callable[[], None] = lambda: None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Run a lesson in Neovim, then show the results; `done` runs when the user moves on.
|
|
128
|
+
Real lessons (not review, placement or VimGolf) run in two rounds: Learn, then Recall.
|
|
129
|
+
In lessons and reviews, a skipped task is shown, typed, and asked again at the end."""
|
|
130
|
+
session = replace(lesson, tasks=two_rounds(lesson)) if lesson.week else lesson
|
|
131
|
+
with self.suspend():
|
|
132
|
+
results = run_lesson(session, retry=bool(lesson.week) or lesson.id == "review")
|
|
133
|
+
mouse_wheel(False) # coming back from Neovim
|
|
134
|
+
if on_results:
|
|
135
|
+
note = on_results(results)
|
|
136
|
+
else:
|
|
137
|
+
self.progress.record(results)
|
|
138
|
+
note = ""
|
|
139
|
+
|
|
140
|
+
def after(again: bool) -> None:
|
|
141
|
+
if again:
|
|
142
|
+
self.start_lesson(lesson, on_results, done)
|
|
143
|
+
else:
|
|
144
|
+
done()
|
|
145
|
+
|
|
146
|
+
self.push_screen(ResultsScreen(session, results, note), after)
|
|
147
|
+
|
|
148
|
+
def run_steps(self, steps: list[Callable[[Callable[[], None]], None]]) -> None:
|
|
149
|
+
"""Run screens one after another; each step calls `done` when finished."""
|
|
150
|
+
if steps:
|
|
151
|
+
steps[0](lambda: self.run_steps(steps[1:]))
|
|
152
|
+
|
|
153
|
+
def work(self) -> today.Work:
|
|
154
|
+
"""Today's cards, drills and review, as much as fits the daily budget."""
|
|
155
|
+
return today.todays_work(self.progress, self.lessons, self.decks, self.drills)
|
|
156
|
+
|
|
157
|
+
def plan(self) -> list[today.Step]:
|
|
158
|
+
return today.plan(self.progress, self.lessons, self.decks, self.drills, self.work())
|
|
159
|
+
|
|
160
|
+
def todays_drills(self) -> list[drills.Drill]:
|
|
161
|
+
return today.todays_drills(self.progress, self.drills)
|
|
162
|
+
|
|
163
|
+
def step(self, step_id: str, direct: bool = False) -> Callable[[Callable[[], None]], None] | None:
|
|
164
|
+
"""One step of today's routine, or None when there's nothing to do for it.
|
|
165
|
+
|
|
166
|
+
`direct` starts lessons without their intro screen (the main screen shows it already)."""
|
|
167
|
+
progress = self.progress
|
|
168
|
+
if step_id == "cards":
|
|
169
|
+
cards = self.work().cards
|
|
170
|
+
if cards:
|
|
171
|
+
return lambda done: self.push_screen(CardScreen(cards, self.decks, "Today's cards"), lambda _: done())
|
|
172
|
+
elif step_id == "drills":
|
|
173
|
+
todays = self.work().drills
|
|
174
|
+
if todays:
|
|
175
|
+
return lambda done: self.push_screen(DrillScreen(todays, "Omarchy drills"), lambda _: done())
|
|
176
|
+
elif step_id == "review":
|
|
177
|
+
review = self.work().review
|
|
178
|
+
if review and direct:
|
|
179
|
+
return lambda done: self.start_lesson(review_lesson(review), done=done)
|
|
180
|
+
if review:
|
|
181
|
+
return lambda done: self.push_screen(LessonScreen(review_lesson(review)), lambda _: done())
|
|
182
|
+
elif step_id == "lesson":
|
|
183
|
+
nxt = progress.next_lesson(self.lessons)
|
|
184
|
+
if nxt and direct:
|
|
185
|
+
return lambda done: self.start_lesson(nxt, done=done)
|
|
186
|
+
if nxt:
|
|
187
|
+
return lambda done: self.push_screen(LessonScreen(nxt), lambda _: done())
|
|
188
|
+
elif step_id == "vimgolf":
|
|
189
|
+
if not progress.vimgolf_done_today():
|
|
190
|
+
return self.start_vimgolf
|
|
191
|
+
elif step_id == "endless":
|
|
192
|
+
start = progress.endless_start(endless.RESTART_BELOW)
|
|
193
|
+
if direct:
|
|
194
|
+
return lambda done: self.start_endless(start, done)
|
|
195
|
+
return lambda done: self.push_screen(
|
|
196
|
+
LessonScreen(endless_lesson(start, progress.endless_best()), start=lambda d: self.start_endless(start, d)),
|
|
197
|
+
lambda _: done(),
|
|
198
|
+
)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def run_step(self, step_id: str, done: Callable[[], None], direct: bool = False) -> None:
|
|
202
|
+
step = self.step(step_id, direct)
|
|
203
|
+
if step:
|
|
204
|
+
step(done)
|
|
205
|
+
else:
|
|
206
|
+
self.notify("Nothing left for this one today.")
|
|
207
|
+
|
|
208
|
+
def action_today(self) -> None:
|
|
209
|
+
"""Cards, drills, review (plus habit drills), then the next lesson:
|
|
210
|
+
whichever are still left today. One lesson a day; the Lessons list has the rest."""
|
|
211
|
+
left = {step.id for step in today.left(self.plan())}
|
|
212
|
+
steps = [s for s in (self.step(i) for i in today.ROUTINE if i in left) if s]
|
|
213
|
+
if not steps:
|
|
214
|
+
self.notify("That's everything for today. Endless (e) or any lesson if you want more.")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
def finished(done: Callable[[], None]) -> None:
|
|
218
|
+
if not today.left(self.plan()):
|
|
219
|
+
self.notify("That's today's practice done. See you tomorrow.", title="Vimniscient")
|
|
220
|
+
done()
|
|
221
|
+
|
|
222
|
+
self.run_steps([*steps, finished])
|
|
223
|
+
|
|
224
|
+
def on_unmount(self) -> None:
|
|
225
|
+
"""Done for the day? Then the reminders still waiting can go."""
|
|
226
|
+
try:
|
|
227
|
+
reminders.sync(self.plan())
|
|
228
|
+
except Exception: # never let a reminder problem lose the session
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def action_drills(self) -> None:
|
|
232
|
+
todays = self.todays_drills() or self.drills
|
|
233
|
+
self.push_screen(DrillScreen(todays, "Omarchy drills"))
|
|
234
|
+
|
|
235
|
+
def action_cards(self) -> None:
|
|
236
|
+
cards = self.progress.due_cards(self.decks, self.lessons) + self.progress.new_cards(self.decks, self.lessons)
|
|
237
|
+
if cards:
|
|
238
|
+
self.push_screen(CardScreen(cards, self.decks, "Cards"))
|
|
239
|
+
else:
|
|
240
|
+
self.notify("No cards due. Come back tomorrow.")
|
|
241
|
+
|
|
242
|
+
def action_placement(self) -> None:
|
|
243
|
+
def on_results(results: list[TaskResult]) -> str:
|
|
244
|
+
self.progress.record(results)
|
|
245
|
+
placed = self.progress.place(self.lessons, results)
|
|
246
|
+
if not placed:
|
|
247
|
+
return "No lessons skipped. Start from Week 1 · Day 1."
|
|
248
|
+
return "Known already: " + ", ".join(f"{p.label} ({p.title})" for p in placed)
|
|
249
|
+
|
|
250
|
+
self.push_screen(LessonScreen(placement_lesson(self.lessons), on_results))
|
|
251
|
+
|
|
252
|
+
def action_endless(self) -> None:
|
|
253
|
+
self.run_step("endless", lambda: None)
|
|
254
|
+
|
|
255
|
+
def start_endless(self, start: int, done: Callable[[], None]) -> None:
|
|
256
|
+
"""Climb from `start` until the user quits, then show how it went."""
|
|
257
|
+
best = self.progress.endless_best()
|
|
258
|
+
rungs = endless.ladder(self.lessons, start)
|
|
259
|
+
pool = {t.item_id: t for t, _ in rungs}
|
|
260
|
+
with self.suspend():
|
|
261
|
+
results = run_endless(pool, [(t.item_id, v) for t, v in rungs], start, endless.RAMP, endless.GAP)
|
|
262
|
+
mouse_wheel(False)
|
|
263
|
+
run = endless.summary(results, start)
|
|
264
|
+
self.progress.record_endless(results, run)
|
|
265
|
+
note = (f"Level {run['start']} → {run['level']} · {plural(run['missed'], 'miss', 'misses')}, "
|
|
266
|
+
f"{run['fixed']} recalled afterwards")
|
|
267
|
+
if run["level"] > best and run["climbed"]:
|
|
268
|
+
note += " · a new best!"
|
|
269
|
+
|
|
270
|
+
def after(again: bool) -> None:
|
|
271
|
+
if again:
|
|
272
|
+
self.start_endless(self.progress.endless_start(endless.RESTART_BELOW), done)
|
|
273
|
+
else:
|
|
274
|
+
done()
|
|
275
|
+
|
|
276
|
+
self.push_screen(ResultsScreen(endless_lesson(start, best), results, note, f"level {run['level']}"), after)
|
|
277
|
+
|
|
278
|
+
def action_vimgolf(self) -> None:
|
|
279
|
+
self.start_vimgolf(lambda: None)
|
|
280
|
+
|
|
281
|
+
def start_vimgolf(self, done: Callable[[], None]) -> None:
|
|
282
|
+
self.notify("Fetching today's VimGolf challenge…")
|
|
283
|
+
self.run_worker(lambda: self._fetch_vimgolf(done), thread=True, exclusive=True, group="vimgolf")
|
|
284
|
+
|
|
285
|
+
def _fetch_vimgolf(self, done: Callable[[], None]) -> None:
|
|
286
|
+
# A thread of its own: sqlite connections can't cross threads, so it opens one.
|
|
287
|
+
try:
|
|
288
|
+
challenge = Progress().vimgolf_today(vimgolf.index(), vimgolf.challenge)
|
|
289
|
+
except Exception as error: # offline, or the site changed: never take the app down
|
|
290
|
+
self.call_from_thread(self.notify, f"Couldn't get a VimGolf challenge ({error}). Skipping it.", severity="warning")
|
|
291
|
+
self.call_from_thread(done)
|
|
292
|
+
return
|
|
293
|
+
if challenge is None:
|
|
294
|
+
self.call_from_thread(self.notify, "No VimGolf challenges left to try.")
|
|
295
|
+
self.call_from_thread(done)
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
def on_results(results: list[TaskResult]) -> str:
|
|
299
|
+
score = self.progress.record_vimgolf(challenge.id, results[0], vimgolf.SAVE_KEYS)
|
|
300
|
+
if score is None:
|
|
301
|
+
return f"Not solved. It stays today's challenge. ({challenge.url})"
|
|
302
|
+
best = f", best on vimgolf.com {challenge.best}" if challenge.best else ""
|
|
303
|
+
return f"Your VimGolf score: {score} (keys + ZZ){best}. Solutions: {challenge.url}"
|
|
304
|
+
|
|
305
|
+
self.call_from_thread(
|
|
306
|
+
self.push_screen, LessonScreen(vimgolf_lesson(challenge), on_results), lambda _: done()
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def action_stats(self) -> None:
|
|
310
|
+
self.push_screen(StatsScreen())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
deck: foot
|
|
2
|
+
title: foot (terminal)
|
|
3
|
+
requires: foot
|
|
4
|
+
style: combo
|
|
5
|
+
case_sensitive: false
|
|
6
|
+
# Defaults from foot.ini(5), with copy/paste as set in ~/.config/foot/foot.ini.
|
|
7
|
+
cards:
|
|
8
|
+
- {prompt: Copy the selection, answer: [CTRL SHIFT + C, CTRL + INSERT]}
|
|
9
|
+
- {prompt: Paste from the clipboard, answer: [CTRL SHIFT + V, SHIFT + INSERT]}
|
|
10
|
+
- {prompt: Scroll back one page, answer: [SHIFT + PAGE_UP]}
|
|
11
|
+
- {prompt: Scroll forward one page, answer: [SHIFT + PAGE_DOWN]}
|
|
12
|
+
- {prompt: Search the scrollback, answer: [CTRL SHIFT + R]}
|
|
13
|
+
- {prompt: Make the font bigger, answer: [CTRL + PLUS, CTRL + EQUAL]}
|
|
14
|
+
- {prompt: Make the font smaller, answer: [CTRL + MINUS]}
|
|
15
|
+
- {prompt: Reset the font size, answer: [CTRL + 0]}
|
|
16
|
+
- {prompt: Open a new terminal in the same directory, answer: [CTRL SHIFT + N]}
|
|
17
|
+
- {prompt: Pick a URL on screen with the keyboard, answer: [CTRL SHIFT + O]}
|
|
18
|
+
- {prompt: Jump to the previous shell prompt, answer: [CTRL SHIFT + Z]}
|
|
19
|
+
- {prompt: Type a Unicode character, answer: [CTRL SHIFT + U]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
deck: lazyvim
|
|
2
|
+
title: LazyVim
|
|
3
|
+
requires: lazyvim
|
|
4
|
+
style: keys
|
|
5
|
+
case_sensitive: true
|
|
6
|
+
# The core set, introduced first. After it come the keymaps in `from_config`,
|
|
7
|
+
# read from your own Neovim config so their names and keys follow your setup
|
|
8
|
+
# (any that your config doesn't have are left out).
|
|
9
|
+
# LSP keys are listed here because they only exist once a language server attaches.
|
|
10
|
+
cards:
|
|
11
|
+
- {prompt: Find files (root dir), answer: ['<leader><Space>', '<leader>ff']}
|
|
12
|
+
- {prompt: Grep the project (root dir), answer: [<leader>/, <leader>sg]}
|
|
13
|
+
- {prompt: Switch buffer, answer: ['<leader>,', '<leader>fb']}
|
|
14
|
+
- {prompt: Recent files, answer: [<leader>fr]}
|
|
15
|
+
- {prompt: File explorer (neo-tree), answer: [<leader>e]}
|
|
16
|
+
- {prompt: Next buffer, answer: [<S-l>, ']b']}
|
|
17
|
+
- {prompt: Previous buffer, answer: [<S-h>, '[b']}
|
|
18
|
+
- {prompt: Delete the buffer, answer: [<leader>bd]}
|
|
19
|
+
- {prompt: Split the window below, answer: ['<leader>-']}
|
|
20
|
+
- {prompt: Split the window to the right, answer: ['<leader>|']}
|
|
21
|
+
- {prompt: Move to the window on the left, answer: [<C-h>]}
|
|
22
|
+
- {prompt: Search keymaps, answer: [<leader>sk]}
|
|
23
|
+
- {prompt: Search help pages, answer: [<leader>sh]}
|
|
24
|
+
- {prompt: Lazygit, answer: [<leader>gg]}
|
|
25
|
+
- {prompt: Comment the line, answer: [gcc]}
|
|
26
|
+
- {prompt: Flash jump (type a few characters, then a label), answer: [s]}
|
|
27
|
+
- {prompt: Go to definition, answer: [gd]}
|
|
28
|
+
- {prompt: Show references, answer: [gr]}
|
|
29
|
+
- {prompt: Hover documentation, answer: [K]}
|
|
30
|
+
- {prompt: Code action, answer: [<leader>ca]}
|
|
31
|
+
- {prompt: Rename the symbol, answer: [<leader>cr]}
|
|
32
|
+
- {prompt: Next diagnostic, answer: [']d']}
|
|
33
|
+
- {prompt: Previous diagnostic, answer: ['[d']}
|
|
34
|
+
- {prompt: Format the file, answer: [<leader>cf]}
|
|
35
|
+
- {prompt: Diagnostics list (Trouble), answer: [<leader>xx]}
|
|
36
|
+
- {prompt: Toggle a terminal, answer: [<C-/>, <leader>ft]}
|
|
37
|
+
- {prompt: Quit everything, answer: [<leader>qq]}
|
|
38
|
+
- {prompt: 'neo-tree: open the file with the system app', answer: [O]}
|
|
39
|
+
- {prompt: 'neo-tree: add a file or folder', answer: [a]}
|
|
40
|
+
- {prompt: 'neo-tree: rename', answer: [r]}
|
|
41
|
+
- {prompt: 'neo-tree: delete', answer: [d]}
|
|
42
|
+
- {prompt: 'neo-tree: toggle hidden files', answer: [H]}
|
|
43
|
+
- {prompt: 'neo-tree: show all neo-tree keys', answer: ['?']}
|
|
44
|
+
|
|
45
|
+
# Worth learning beyond the core, in the order they're introduced.
|
|
46
|
+
from_config:
|
|
47
|
+
- '<leader>fn' # new file
|
|
48
|
+
- '<leader>fc' # find config file
|
|
49
|
+
- '<leader>sr' # search and replace
|
|
50
|
+
- '<leader>sw' # search for the word under the cursor
|
|
51
|
+
- '<leader>sR' # resume the last picker
|
|
52
|
+
- '<leader>sb' # lines in this buffer
|
|
53
|
+
- '<leader>sd' # diagnostics
|
|
54
|
+
- '<leader>su' # undotree
|
|
55
|
+
- '<leader>`' # other buffer
|
|
56
|
+
- '<leader>bo' # delete other buffers
|
|
57
|
+
- '<leader>bp' # pin buffer
|
|
58
|
+
- '<leader>wd' # delete window
|
|
59
|
+
- '<leader>gs' # git status
|
|
60
|
+
- '<leader>gb' # git blame line
|
|
61
|
+
- '<leader>gd' # git diff
|
|
62
|
+
- '<leader>gl' # git log
|
|
63
|
+
- '<leader>cd' # line diagnostics
|
|
64
|
+
- '<leader>cs' # symbols
|
|
65
|
+
- '<leader>xq' # quickfix list
|
|
66
|
+
- '<leader>ql' # restore last session
|
|
67
|
+
- '<leader>:' # command history
|
|
68
|
+
- '<leader>.' # scratch buffer
|
|
69
|
+
- '<leader>?' # buffer keymaps
|
|
70
|
+
- '<leader>n' # notification history
|
|
71
|
+
- '<leader>un' # dismiss notifications
|
|
72
|
+
- '<leader>uw' # toggle wrap
|
|
73
|
+
- '<leader>ul' # toggle line numbers
|
|
74
|
+
- '<leader>us' # toggle spelling
|
|
75
|
+
- '<leader>ud' # toggle diagnostics
|
|
76
|
+
- '<leader>uz' # zen mode
|
|
77
|
+
- '<leader>l' # Lazy (plugins)
|
|
78
|
+
- '<leader><Tab><Tab>' # new tab
|
|
79
|
+
- '<leader><Tab>]' # next tab
|
|
80
|
+
- '<leader><Tab>d' # close tab
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
deck: shell
|
|
2
|
+
title: Shell (bash)
|
|
3
|
+
style: keys
|
|
4
|
+
case_sensitive: false
|
|
5
|
+
note: Readline keys work in bash and in most terminal prompts.
|
|
6
|
+
cards:
|
|
7
|
+
- {prompt: Search back through your command history, answer: [<C-r>]}
|
|
8
|
+
- {prompt: Jump to the start of the line, answer: [<C-a>, <Home>]}
|
|
9
|
+
- {prompt: Jump to the end of the line, answer: [<C-e>, <End>]}
|
|
10
|
+
- {prompt: Delete the word before the cursor, answer: [<C-w>]}
|
|
11
|
+
- {prompt: Delete from the cursor to the start of the line, answer: [<C-u>]}
|
|
12
|
+
- {prompt: Delete from the cursor to the end of the line, answer: [<C-k>]}
|
|
13
|
+
- {prompt: Paste what you just deleted with Ctrl-w/u/k, answer: [<C-y>]}
|
|
14
|
+
- {prompt: Move one word back, answer: [<M-b>]}
|
|
15
|
+
- {prompt: Move one word forward, answer: [<M-f>]}
|
|
16
|
+
- {prompt: Clear the screen, answer: [<C-l>]}
|
|
17
|
+
- {prompt: Cancel the current line, answer: [<C-c>]}
|
|
18
|
+
- {prompt: Insert the last argument of the previous command, answer: [<M-.>, '!$']}
|
|
19
|
+
- {prompt: Run the previous command again with sudo, answer: [sudo !!]}
|
|
20
|
+
- {prompt: Open the current command in your editor, answer: [<C-x><C-e>]}
|
|
21
|
+
- {prompt: Undo the last edit on the command line, answer: [<C-_>, <C-x><C-u>]}
|
|
22
|
+
- {prompt: Go back to the previous directory, answer: [cd -]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
deck: vim
|
|
2
|
+
title: Vim
|
|
3
|
+
style: keys
|
|
4
|
+
case_sensitive: true
|
|
5
|
+
# A card comes in once you've done the lesson for its `week` and `day` (the
|
|
6
|
+
# lesson that teaches it); with no `day`, once the lessons reach its week.
|
|
7
|
+
cards:
|
|
8
|
+
# week 1: text objects, find/till, lines, counts
|
|
9
|
+
- {week: 1, day: 1, prompt: 'Change the whole word under the cursor, wherever in it the cursor is', answer: [ciw], note: 'cw only changes from the cursor to the end of the word.'}
|
|
10
|
+
- {week: 1, day: 1, prompt: Delete a word and the space after it, answer: [daw]}
|
|
11
|
+
- {week: 1, day: 1, prompt: 'Change a WORD (space to space, punctuation included)', answer: [ciW]}
|
|
12
|
+
- {week: 1, day: 1, prompt: Yank the word under the cursor, answer: [yiw]}
|
|
13
|
+
- {week: 1, day: 2, prompt: Change everything inside the double quotes, answer: ['ci"']}
|
|
14
|
+
- {week: 1, day: 2, prompt: Delete the quotes and what is inside them, answer: ['da"']}
|
|
15
|
+
- {week: 1, day: 2, prompt: Delete inside the parentheses, answer: [di(, dib, di)]}
|
|
16
|
+
- {week: 1, day: 2, prompt: Delete the parentheses and their content, answer: [da(, dab, da)]}
|
|
17
|
+
- {week: 1, day: 2, prompt: Change inside curly braces, answer: ['ci{', ciB, 'ci}']}
|
|
18
|
+
- {week: 1, day: 2, prompt: Change the text inside an HTML tag, answer: [cit]}
|
|
19
|
+
- {week: 1, day: 3, prompt: Delete the paragraph (and the blank line after it), answer: [dap]}
|
|
20
|
+
- {week: 1, day: 3, prompt: Yank the paragraph, answer: [yap]}
|
|
21
|
+
- {week: 1, day: 3, prompt: Change the sentence, answer: [cis]}
|
|
22
|
+
- {week: 1, day: 3, prompt: Indent the paragraph, answer: ['>ip']}
|
|
23
|
+
- {week: 1, day: 4, prompt: Jump onto the next comma on the line, answer: ['f,']}
|
|
24
|
+
- {week: 1, day: 4, prompt: Jump to just before the next comma, answer: ['t,']}
|
|
25
|
+
- {week: 1, day: 4, prompt: 'Repeat the last f/t/F/T', answer: [;]}
|
|
26
|
+
- {week: 1, day: 4, prompt: 'Repeat the last f/t/F/T in the other direction', answer: [',']}
|
|
27
|
+
- {week: 1, day: 4, prompt: Delete up to (not including) the next closing parenthesis, answer: [dt)]}
|
|
28
|
+
- {week: 1, day: 4, prompt: Change up to the semicolon, answer: [ct;]}
|
|
29
|
+
- {week: 1, day: 5, prompt: Delete to the end of the line, answer: [D, d$]}
|
|
30
|
+
- {week: 1, day: 5, prompt: Change to the end of the line, answer: [C, c$]}
|
|
31
|
+
- {week: 1, day: 5, prompt: Change the whole line, answer: [cc]}
|
|
32
|
+
- {week: 1, day: 5, prompt: Join the next line onto this one, answer: [J]}
|
|
33
|
+
- {week: 1, day: 5, prompt: Swap this line with the one below, answer: [ddp]}
|
|
34
|
+
- {week: 1, day: 5, prompt: Append at the end of the line, answer: [A]}
|
|
35
|
+
- {week: 1, day: 5, prompt: Insert at the first non-blank character of the line, answer: [I]}
|
|
36
|
+
- {week: 1, day: 6, prompt: Delete three lines, answer: [3dd, d2j]}
|
|
37
|
+
- {week: 1, day: 6, prompt: Change the next two words, answer: [c2w, 2cw]}
|
|
38
|
+
- {week: 1, day: 6, prompt: Go to the first line of the file, answer: [gg]}
|
|
39
|
+
- {week: 1, day: 6, prompt: Go to the last line of the file, answer: [G]}
|
|
40
|
+
- {week: 1, day: 6, prompt: Go to line 42, answer: [42G, ':42<CR>', '42gg']}
|
|
41
|
+
- {week: 1, day: 6, prompt: Move down five lines, answer: [5j]}
|
|
42
|
+
- {week: 1, day: 6, prompt: Half a page down, answer: ['<C-d>'], note: 'Ctrl-d goes down; Ctrl-u goes up.'}
|
|
43
|
+
- {week: 1, day: 6, prompt: Half a page up, answer: ['<C-u>'], note: 'Ctrl-u goes up; Ctrl-d goes down.'}
|
|
44
|
+
# week 2: dot, search, cgn, case, numbers, insert, visual
|
|
45
|
+
- {week: 2, day: 1, prompt: Repeat the last change, answer: [.]}
|
|
46
|
+
- {week: 2, day: 2, prompt: Search forward for the word under the cursor, answer: ['*']}
|
|
47
|
+
- {week: 2, day: 2, prompt: Search backward for the word under the cursor, answer: ['#']}
|
|
48
|
+
- {week: 2, day: 2, prompt: Next search match, answer: [n]}
|
|
49
|
+
- {week: 2, day: 2, prompt: Previous search match, answer: [N]}
|
|
50
|
+
- {week: 2, day: 3, prompt: 'Change the next search match (then . repeats it)', answer: [cgn]}
|
|
51
|
+
- {week: 2, day: 3, prompt: Delete the next search match, answer: [dgn]}
|
|
52
|
+
- {week: 2, day: 4, prompt: Uppercase the word under the cursor, answer: [gUiw, gUaw]}
|
|
53
|
+
- {week: 2, day: 4, prompt: Lowercase the whole line, answer: [guu, gugu]}
|
|
54
|
+
- {week: 2, day: 4, prompt: Toggle the case of one character, answer: ['~']}
|
|
55
|
+
- {week: 2, day: 4, prompt: Add one to the next number on the line, answer: [<C-a>]}
|
|
56
|
+
- {week: 2, day: 4, prompt: Subtract one from the next number on the line, answer: [<C-x>]}
|
|
57
|
+
- {week: 2, day: 5, prompt: Open a new line below and start typing, answer: [o]}
|
|
58
|
+
- {week: 2, day: 5, prompt: Open a new line above and start typing, answer: [O]}
|
|
59
|
+
- {week: 2, day: 5, prompt: Append after the end of the current word, answer: [ea]}
|
|
60
|
+
- {week: 2, day: 5, prompt: Enter replace mode (type over text), answer: [R]}
|
|
61
|
+
- {week: 2, day: 5, prompt: Replace one character with x, answer: [rx]}
|
|
62
|
+
- {week: 2, day: 5, prompt: 'In insert mode: delete the word before the cursor', answer: [<C-w>]}
|
|
63
|
+
- {week: 2, day: 5, prompt: 'In insert mode: delete back to the start of the line', answer: [<C-u>]}
|
|
64
|
+
- {week: 2, day: 6, prompt: Start visual line mode, answer: [V]}
|
|
65
|
+
- {week: 2, day: 6, prompt: Start visual block mode, answer: [<C-v>]}
|
|
66
|
+
- {week: 2, day: 6, prompt: Reselect the last visual selection, answer: [gv]}
|
|
67
|
+
- {week: 2, day: 6, prompt: Jump to the matching bracket, answer: ['%']}
|
|
68
|
+
- {week: 2, day: 6, prompt: Uppercase the visual selection, answer: [U]}
|
|
69
|
+
# week 3: macros, registers, marks, visual block, :s, :g
|
|
70
|
+
- {week: 3, day: 1, prompt: Record a macro into register a, answer: [qa]}
|
|
71
|
+
- {week: 3, day: 1, prompt: Play the macro in register a, answer: ['@a']}
|
|
72
|
+
- {week: 3, day: 2, prompt: Replay the last played macro, answer: ['@@']}
|
|
73
|
+
- {week: 3, day: 3, prompt: Yank the line into register a, answer: ['"ayy']}
|
|
74
|
+
- {week: 3, day: 3, prompt: Paste from register a, answer: ['"ap']}
|
|
75
|
+
- {week: 3, day: 3, prompt: 'Paste the last yank (not the last delete)', answer: ['"0p']}
|
|
76
|
+
- {week: 3, day: 3, prompt: 'In insert mode: paste register a', answer: [<C-r>a]}
|
|
77
|
+
- {week: 3, day: 4, prompt: Set mark a, answer: [ma]}
|
|
78
|
+
- {week: 3, day: 4, prompt: Jump to the line of mark a, answer: ["'a"]}
|
|
79
|
+
- {week: 3, day: 4, prompt: Jump to the exact position of mark a, answer: ['`a']}
|
|
80
|
+
- {week: 3, day: 4, prompt: Jump back to where you were before the last jump, answer: ['``', "''"]}
|
|
81
|
+
- {week: 3, day: 5, prompt: 'Visual block: insert the same text on every line', answer: [<C-v>jI]}
|
|
82
|
+
- {week: 3, day: 6, prompt: Replace every foo with bar in the whole file, answer: [':%s/foo/bar/g<CR>']}
|
|
83
|
+
- {week: 3, day: 6, prompt: Delete every line containing TODO, answer: [':g/TODO/d<CR>']}
|
|
84
|
+
- {week: 3, day: 6, prompt: Run the normal command A; on every selected line, answer: [":'<,'>norm A;<CR>", ":norm A;<CR>"]}
|
|
85
|
+
# week 4 and later: windows, jumps, scrolling
|
|
86
|
+
- {week: 4, prompt: Jump to the older position in the jump list, answer: [<C-o>]}
|
|
87
|
+
- {week: 4, prompt: Jump to the newer position in the jump list, answer: [<C-i>, <Tab>]}
|
|
88
|
+
- {week: 4, prompt: Go to the position of the last change, answer: ['g;']}
|
|
89
|
+
- {week: 4, prompt: Go back to where you last stopped inserting and insert there, answer: [gi]}
|
|
90
|
+
- {week: 4, prompt: Scroll so the cursor line is in the middle of the screen, answer: [zz]}
|
|
91
|
+
- {week: 4, prompt: Scroll so the cursor line is at the top, answer: [zt]}
|
|
92
|
+
- {week: 4, prompt: Move to the top of the screen, answer: [H]}
|
|
93
|
+
- {week: 4, prompt: Move to the middle of the screen, answer: [M]}
|
|
94
|
+
- {week: 4, prompt: Move to the bottom of the screen, answer: [L]}
|
|
95
|
+
- {week: 4, day: 5, prompt: Move to the next paragraph, answer: ['}']}
|