shoin 0.2.1

A no-clutter terminal Markdown editor with modal editing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
//! In-app help, shown by `:help [topic]` in a scrollable overlay.

/// A resolved help page: a title and its lines, plus the current scroll.
pub struct Help {
    pub title: String,
    pub lines: Vec<String>,
    pub scroll: usize,
}

impl Help {
    /// Build the page for a topic (empty = overview). Unknown topics fall back
    /// to a short "no such topic" page listing the real ones.
    pub fn open(topic: &str) -> Help {
        let key = topic.trim().to_ascii_lowercase();
        let (title, lines): (&str, Vec<&str>) = match key.as_str() {
            "" | "overview" | "help" => ("help", OVERVIEW.to_vec()),
            "shortcuts" | "reference" | "cheatsheet" | "cheat" => ("shortcuts", SHORTCUTS.to_vec()),
            "bindings" | "keys" | "keymap" | "keymaps" | "motions" => ("bindings", BINDINGS.to_vec()),
            "commands" | "command" | "ex" => ("commands", COMMANDS.to_vec()),
            "writer" | "verbs" | "markdown" | "format" => ("writer verbs", WRITER.to_vec()),
            "config" | "configuration" | "settings" | "theme" => ("config", CONFIG.to_vec()),
            _ => ("help", unknown(topic)),
        };
        Help {
            title: title.to_string(),
            lines: lines.into_iter().map(str::to_string).collect(),
            scroll: 0,
        }
    }
}

fn unknown(topic: &str) -> Vec<&'static str> {
    // Leak a tiny owned string so the rest can stay &'static.
    let msg: &'static str = Box::leak(format!("No help topic {topic:?}.").into_boxed_str());
    let mut v = vec![msg, ""];
    v.extend_from_slice(&[
        "Available topics:",
        "  :help              this overview",
        "  :help shortcuts    every keyboard interaction, by category",
        "  :help bindings     keys, motions, operators",
        "  :help commands     the : (ex) commands",
        "  :help writer       bold / italic / headings / tasks",
        "  :help config       the config file and theme",
    ]);
    v
}

const OVERVIEW: &[&str] = &[
    "Shoin — a no-clutter terminal Markdown editor.",
    "",
    "The line the cursor is on shows raw Markdown; every other line hides",
    "its markers and shows the result (live preview). Editing is modal,",
    "with Vim-style keys.",
    "",
    "MODES",
    "  Normal    move around and run operators. Esc always returns here.",
    "  Insert    type text — enter with  i a I A o O  (Esc to leave).",
    "  Visual    select text —  v  (chars),  V  (lines), or drag the mouse.",
    "            Dragging while inserting gives (insert) visual: the caret",
    "            takes the accent color, and finishing hands typing back.",
    "  Command   the  :  line for ex commands.",
    "  Search    /  forward,  ?  backward;  n / N  repeat.",
    "",
    "GETTING AROUND THE BASICS",
    "  h j k l / arrows   move          i / a        insert / append",
    "  w b e              words         o / O        open line below/above",
    "  0 ^ $              line          x            delete char",
    "  gg / G             top / bottom  dd / yy / p  delete / yank / paste",
    "  u / Ctrl-r         undo / redo   :w  :q       save / close",
    "",
    "FILE TREE (left pane)",
    "  <leader>fe / fE    open in this file's folder / all of $HOME",
    "  j k  l/Enter  h    move · open file or expand dir · collapse",
    "  - / =              move the ROOT up a level / into this directory",
    "  H                  show / hide dotfiles",
    "  q                  close the tree   (<leader> is Space by default)",
    "  Ctrl-w  h / l / w  move between panes: tree / editor / cycle",
    "",
    "SPLITS (panes)",
    "  <leader>sv / ss    split beside / below — again to close it",
    "  Ctrl-w  v s        split           Ctrl-w  q   close this pane",
    "  Ctrl-w  h j k l    move between panes      w   cycle    o  only",
    "  Ctrl-w  > < + -    wider / narrower / taller / shorter  (3<C-w>> x3)",
    "  Ctrl-w  =          equal shares again",
    "  :sp  :vs  :close  :only",
    "",
    "BUFFERS (several files open at once)",
    "  <leader>fb         switch buffer (the finder, over open buffers)",
    "  :ls                list them    :b <name|n>   switch to one",
    "  :bn  :bp           next / previous          :bd   close this one",
    "  :e <path>          open another file",
    "",
    "FIND FILES (fuzzy finder)",
    "  <leader>ff / fF    search this file's directory / all of $HOME",
    "  type               narrow the list; Ctrl-u clears the query",
    "  Ctrl-n / Ctrl-p    next / previous match (arrows work too)",
    "  Enter  Esc         open the selected file · close the finder",
    "",
    "MORE HELP",
    "  <leader>hl         every keyboard interaction, by category",
    "  :help bindings     the full key list",
    "  :help commands     every : command",
    "  :help writer       Markdown formatting verbs (g...)",
    "  :help config       config file location and settings",
    "",
    "Scroll with j/k or the arrows. Press q or Esc to close.",
];

/// Every keyboard interaction shoin answers to, grouped by KIND rather than by
/// what it is used for — one page to scan top to bottom, opened with
/// `<leader>hl` or `:help shortcuts`. `:help bindings` covers the Vim grammar
/// (motions, operators, text objects) in more depth; this page is the map of
/// where everything lives.
const SHORTCUTS: &[&str] = &[
    "SHORTCUTS — every keyboard interaction, by kind",
    "",
    "<leader> KEYS  (leader is Space by default — [input].leader)",
    "  <leader>hl     this page",
    "  <leader>w      save                    <leader>q      quit",
    "  <leader>F      cycle focus mode        <leader>t      typewriter scroll",
    "  <leader>z      toggle zen (all chrome) <leader>c      toggle conceal",
    "  <leader>n      toggle line numbers      <leader>a      cycle text align",
    "  <leader>fe     file tree, this folder  <leader>fE     file tree, $HOME",
    "  <leader>ff     find file, this folder  <leader>fF     find file, $HOME",
    "  <leader>fb     switch buffer (finder)",
    "  <leader>sv     split beside / close    <leader>ss     split below / close",
    "  <leader>so     keep only this pane",
    "  <leader>b      toggle **bold**  (Visual mode only)",
    "  <leader>i      toggle *italic*  (Visual mode only)",
    "  These are built in (the shipped shoin/keys.conf) — remap any of them, add",
    "  more under [keys.normal] / [keys.visual], or set one to \"none\" to remove",
    "  it. See :help config.",
    "",
    ": COMMANDS  (type : then the command, Enter to run — :help commands for all)",
    "  :w  :q  :wq          save / close / both        :Q  :qa      quit all",
    "  :e <path>            open a file                :help [topic] this help",
    "  :sp  :vs  :close  :only                          split / close panes",
    "  :ls  :b <name>  :bn  :bp  :bd                     buffers",
    "  :diff  :revert  :export [fmt]                     merge / reload / export",
    "  :set <key> [value]   settings, e.g. measure, autosave, line_spacing",
    "  :embed [mode]        how much of ![[...]] to expand in place",
    "  :number [mode]       line-number gutter: off/relative/absolute",
    "  :align [mode]        text align: left/center/right/justified (no arg cycles)",
    "  :u  :redo            undo / redo — the : spellings of  u  and  Ctrl-r",
    "",
    "NAVIGATION  (motions — take a count, e.g. 3w; also see :help bindings)",
    "  h j k l        left / down / up / right (arrows work too)",
    "  w b e  W B E   word forward / back / end (W/B/E = whitespace-delimited)",
    "  0 ^ $          line start / first non-blank / end",
    "  { }            previous / next paragraph        gg G   top / bottom",
    "  f{c} t{c}      find / till char (F/T backward);  ; ,   repeat / reverse",
    "  H M L          top / middle / bottom of the visible screen",
    "  Ctrl-d/u/f/b   half page / full page down / up",
    "  gf  gx         follow a link · open it externally",
    "",
    "KEYBINDINGS  (single/doubled keys — the Vim grammar, :help bindings for all)",
    "  i a I A o O    insert / append / line start / line end / open below/above",
    "  d c y  dd cc yy  operators — delete / change / yank, doubled = whole line",
    "  x X D C Y      delete char / before / to-eol / change-to-eol / yank-line",
    "  p P            paste after / before             u  Ctrl-r  undo / redo",
    "  v V            Visual charwise / linewise        .   repeat last change",
    "  drag           Visual charwise, from where the button went down",
    "  drag in Insert (insert) visual — an operator or Esc resumes typing",
    "  / ?  n N       search forward / back / repeat    :   command line",
    "  g1..g6 g0      set / clear heading level         gb gi gh gk  bold/italic/",
    "                                                    highlight/code (writer)",
    "  Ctrl-w h j k l v s q o  window: move / split / close / cycle pane",
    "",
    "Scroll with j/k or the arrows. Press q or Esc to close.",
];

const BINDINGS: &[&str] = &[
    "KEY BINDINGS",
    "",
    "MOTIONS (also take a count, e.g. 3w)",
    "  h j k l      left / down / up / right (arrows work too)",
    "  w W b B      next / prev word (W/B = whitespace-delimited)",
    "  e E          end of word",
    "  0 ^ $        line start / first non-blank / line end",
    "  { }          previous / next paragraph",
    "  gg G         first / last line   (G with a count: go to that line)",
    "  f{c} t{c}    find / till char forward;  F / T backward",
    "  ; ,          repeat / reverse the last f / t",
    "  *            search the word under the cursor",
    "  H M L        top / middle / bottom of the visible screen",
    "  Ctrl-d/u     half page down / up   Ctrl-f/b  full page",
    "",
    "OPERATORS  (operator + motion, or doubled for the whole line)",
    "  d  delete      c  change      y  yank",
    "  dd cc yy       whole line     d$  dw  dj  ...  over a motion",
    "  >  <           indent / outdent lines   (>> << ,  >ip)",
    "  >al <al        indent / outdent a list item with its children",
    "                 (a numbered list it leaves or joins is renumbered)",
    "  >aL <aL        ... every item at that level",
    "                 (refused when it would break the list: a first item,",
    "                 a top-level outdent, a top-level list 4 columns in)",
    "  gu gU          lowercase / uppercase    (guiw, gUU)",
    "  D  C  Y        to end of line",
    "  x  X           delete char under / before the cursor",
    "  r{c}           replace the char under the cursor",
    "  ~  J           toggle case / join with the next line",
    "  p  P           paste after / before",
    "  2d3w           counts multiply — this deletes six words",
    "",
    "TEXT OBJECTS  (after an operator, or in Visual: diw, ca(, vip)",
    "  iw aw          inner / around word      iW aW  whitespace-delimited",
    "  is as          sentence                 ip ap  paragraph (linewise)",
    "  i\" i\' i`       inside quotes            a\"     with the quotes",
    "  i( i[ i{ i<    inside brackets          a( ...  with the brackets",
    "                 (b = (), B = {}; brackets may span lines)",
    "  il al          list item without / with its children (linewise)",
    "  aL             every item at the same level, with children",
    "",
    "LISTS",
    "  Alt-j Alt-k    move the item past the next / previous one, with its",
    "                 sub-items; a count moves it past that many. 1. 2. 3.",
    "                 lists are renumbered. Never leaves its list (<al does).",
    "  >al <al        indent / outdent it      il al aL  the list objects",
    "  Tab S-Tab      the same, in Insert on an item's line (list_tab)",
    "                 In tmux, a quick Esc j can arrive as Alt-j: set",
    "                 escape-time to 10 or less. On macOS the terminal must",
    "                 send Option as Meta (Esc+).",
    "",
    "REGISTERS",
    "  \"a{cmd}        use register a:  \"ayy  \"ap  \"add",
    "  \"A{cmd}        uppercase name APPENDS to that register",
    "  \"0             the last yank — a delete never overwrites it",
    "  \"1 - \"9        the delete ring, newest first",
    "  \"+ \"*          the system clipboard:  \"+yy  \"+p",
    "                 (:set clipboard sends plain y d p there too)",
    "                 Over SSH a copy reaches YOUR clipboard by OSC 52 (the",
    "                 terminal must support it; in tmux, set-clipboard on).",
    "                 Paste from elsewhere with the terminal's own paste,",
    "                 Ctrl+Shift+V / Cmd+V: \"+p there has only Shoin's copy.",
    "",
    "VISUAL",
    "  v  V           start charwise / linewise select",
    "  d  c  y  x     operate on the selection    o  swap ends",
    "  > <  u U       indent / outdent, lowercase / uppercase",
    "  i{obj} a{obj}  select a text object (viw, vap)",
    "",
    "STARTING",
    "  shoin              a blank page, with these hints on it until you type",
    "                     ([splash] art draws your own picture, or none)",
    "  shoin notes.md     open (or create on first :w) a file",
    "  shoin -R notes.md  open it read-only (see READ-ONLY below)",
    "  shoin -p notes.md  print it as the editor shows it, and exit;",
    "                     --pager pages a long one through $PAGER",
    "  cat notes.md | shoin   piped text, in an unnamed buffer: :w name.md",
    "                     saves it, :q just quits  (shoin - reads stdin too)",
    "  cat notes.md | shoin -p   print piped text",
    "",
    "READ-ONLY  (shoin -R, or :view to turn it on and off)",
    "  Every line concealed, no caret. Edits are refused with a message.",
    "  j k            scroll a line          Ctrl-d / Ctrl-u  half a page",
    "  gg G           top / bottom           / ?  n N         search",
    "  q              quit                   i                switch to editing",
    "  Yanking, panels, gf and :w still work.",
    "",
    "FILE TREE  (<leader>fe in this file's folder, <leader>fE over $HOME)",
    "  j k  h l       move / collapse / expand — Enter or o opens a file",
    "  g G  R  q      top / bottom, refresh, close",
    "  -  =           re-root one level up / into the selected directory",
    "  H              show / hide dotfiles (off by default, then remembered)",
    "  a              new file in this directory  (end with / for a folder)",
    "  r              rename          m   move (a path, relative to the root)",
    "  d              delete — asks first, and says how much a folder takes",
    "  <C-w> h j k l  or the arrows: back to the editor and between panes",
    "",
    "  A prompt takes Ctrl-u to clear it, Esc to abandon it. Renaming or",
    "  moving a file you have open carries the buffer with it.",
    "",
    "INSERT MODE",
    "  i a I A o O    insert / append / line start / line end / open below,",
    "                 above",
    "  jk             leave Insert (input.escape_alias; \"\" turns it off)",
    "  Esc  Ctrl-c    leave Insert",
    "  Ctrl-s         save, and keep typing (in Normal mode too, as :w)",
    "  Enter          continues a list marker or quote prefix; on an empty",
    "                 item it ends the list  (editor.auto_indent)",
    "",
    "OTHER",
    "  u  Ctrl-r      undo / redo        .   repeat last change",
    "  /  ?  n  N     search / repeat",
    "  :              command line",
    "",
    "Bindings are configurable under [keys.*]; see :help config.",
];

const COMMANDS: &[&str] = &[
    "COMMANDS  (type : then the command, Enter to run)",
    "",
    "  :w  :write [path]     save (optionally to a path)",
    "  :w!                   save over a file that changed on disk, or",
    "                        over an existing file :w <path> would not touch",
    "  :set autosave [on|off]  write modified NAMED buffers on a timer — off",
    "                        by default, and it keeps the guard above",
    "  :set autosave_interval=3   minutes between autosaves, 1-5",
    "  :diff                 merge against the file on disk. n/p move between",
    "                        differences, Tab cycles this one (live | file |",
    "                        both), l/f/b set them all, w commits, q aborts.",
    "                        Nothing is written until w; then it is one undo.",
    "  :revert  :revert!     re-read the file from disk (! discards unsaved",
    "                        changes). The way to say \"take theirs\".",
    "  :set autoreload [on|off]  take a file's changes back automatically when",
    "                        something else edits it. On by default, and only",
    "                        ever on a buffer with nothing unsaved.",
    "  :set clipboard [on|off]  plain y, d and p use the system clipboard too,",
    "                        as vim's clipboard=unnamedplus. Off by default;",
    "                        \"+ and \"* reach it either way.  (:set cb)",
    "  :q                    close this buffer; the LAST one quits the editor",
    "  :q!                   the same, discarding changes",
    "  :Q   :qa              quit outright — every buffer, in one step",
    "  :Q!  :qa!             quit outright, discarding changes",
    "  :wq   :x              save and quit       :wq!  even if it changed",
    "  :e! / :reload         re-read the config file",
    "  :e <path>             open a file in a new buffer",
    "  :u  :un  :und  :undo  take back the last change — the same as  u",
    "  :red  :redo           put it back again — the same as  Ctrl-r",
    "",
    "BUFFERS",
    "  :ls  :buffers         list open buffers (▸ marks the current one)",
    "  :b <name>  :b <n>     switch by name fragment or number",
    "  :bn  :bp              next / previous buffer (the list is a ring)",
    "  <C-^>  <C-6>          back to the previous buffer — the way back out of",
    "                        a gf; press it twice to land where you started",
    "  :bd   :bd!            close this buffer (! discards changes)",
    "",
    "PANES",
    "  :sp  :vs              split horizontally / vertically",
    "  :close                close the focused pane",
    "  :only                 close every pane but this one",
    "",
    "TRANSCLUSION  (![[note]] embeds another file; [[note]] is just a link)",
    "  :embed [mode]         how much of an ![[...]] to expand in place:",
    "                          none   leave the link as written (default)",
    "                          short  the target's own text, in a box",
    "                          rec    …and whatever IT embeds, recursively",
    "                          full   rec with no box — reads as one document",
    "                        a bare :embed toggles off and back to your last mode",
    "  :export [md|txt|      write the finished document out — opens a dialog",
    "          html|pdf]     html is one self-contained page; pdf needs pandoc",
    "          [justify]     with a suggested name you can change (default md)",
    "                        pdf needs pandoc + a layout engine (typst is the",
    "                        smallest); the error names whichever is missing",
    "                        justify stretches html/txt to fill the measure —",
    "                        md and pdf have no layout of their own to stretch",
    "",
    "  ![[note]]             the whole file      ![[note#Heading]]  one section",
    "  ![[note#^blockid]]    one block",
    "",
    "FOLLOWING LINKS",
    "  gf                    open what the cursor is on — [[note]], ![[note]],",
    "                        or the path in [text](note.md). A #Heading or",
    "                        #^blockid lands on it rather than at the top.",
    "                        A note that does not exist yet is CREATED empty,",
    "                        so a link can be written before its note is.",
    "                        A name several notes answer to opens the finder.",
    "  gx                    hand it to the desktop instead — a URL, a picture,",
    "                        a PDF. Never creates anything.",
    "",
    "IMAGES  (![[photo.png]] — png, jpg, gif, webp, bmp)",
    "  An image embed reserves a box the shape of the picture, captioned with",
    "  its name and pixel size. In a terminal that can draw pixels (kitty,",
    "  Ghostty, WezTerm, iTerm2) the picture itself is drawn there; everywhere",
    "  else the box and caption ARE the picture. :export html carries it along",
    "  inside the page, so the file stays self-contained.",
    "  SHOIN_IMAGE_PROTOCOL=kitty|iterm|none overrides the detection.",
    "",
    "  An embed on the cursor's line shows its raw source, like **bold** does.",
    "  Embedded text belongs to another file, so it cannot be edited here.",
    "",
    "VIEW",
    "  :focus [mode]         off | paragraph | sentence (no arg = cycle)",
    "  :number [mode]        line-number gutter: relative | absolute — a bare",
    "                        :number toggles off and back to your last mode",
    "  :align [mode]         left | center | right | justified — how each line",
    "                        sits WITHIN the column; no arg cycles through all",
    "                        four. The column's own placement on screen is the",
    "                        separate layout.align setting (center | left).",
    "  :typewriter  :tw      toggle typewriter scrolling",
    "  :zen                  toggle all chrome at once",
    "  :view                 read-only: all concealed, q quits, i edits",
    "  :set <k> [on|off]     conceal/typewriter/mouse/focus/numbers/code_syntax",
    "  :set measure=72       text width in columns (:set measure to read it)",
    "  :set line_spacing=1   blank rows between lines, 0-4",
    "",
    "HELP",
    "  :help [topic]         this help — topics: shortcuts, bindings,",
    "                        commands, writer, config",
];

const WRITER: &[&str] = &[
    "WRITER VERBS  (the g prefix — act on the word, or the selection)",
    "",
    "  gb    toggle **bold**        around the word or selection",
    "  gi    toggle *italic*",
    "  gh    toggle ==highlight==",
    "  gk    toggle `code`",
    "",
    "  gt    toggle a task checkbox  - [ ]  <->  - [x]",
    "        (on a plain list item, adds an empty checkbox)",
    "  ga    align the table under the cursor: pad every cell",
    "",
    "  gl    wrap as a [link](url)         cursor lands in the URL slot",
    "  gf    follow the link under the cursor (:help commands)",
    "  gx    open it with the desktop instead",
    "",
    "  g1 .. g6   set the heading level of the current line",
    "  g0         strip the heading",
    "  gp         start the next paragraph (jump down + Insert)",
    "",
    "In Normal mode these act on the word under the cursor; in Visual",
    "mode they wrap the selection. Each is a single undo step.",
];


const CONFIG: &[&str] = &[
    "CONFIGURATION",
    "",
    "Config lives in a directory whose *.conf files are merged (conf.d style),",
    "so you can split it by category: theme.conf, keys.conf, layout.conf, ...",
    "",
    "Searched in order (first that exists wins; file or directory):",
    "  1. --config <path>",
    "  2. $SHOIN_CONFIG",
    "  3. ./shoin.conf        (single file)",
    "  4. ./shoin/            (directory of *.conf)",
    "  5. $XDG_CONFIG_HOME/shoin/",
    "  6. ~/.config/shoin/    (the usual home)",
    "",
    "Any missing key falls back to its default. Edits hot-reload live; a parse",
    "error keeps the running config. See it all with:  shoin --print-config",
    "",
    "COMMON SETTINGS",
    "  [layout]  measure (the text width), focus, numbers, typewriter, conceal,",
    "            hanging_indent, line_spacing, scroll_hint",
    "  [theme]   background, text, heading_1..6, ...  (default: Tokyo Night)",
    "  [input]   leader, mouse, escape_alias",
    "  [editor]  auto_indent, auto_pair, scroll_off, undo_coalesce_ms,",
    "            trim_on_save, final_newline, autosave, autosave_interval,",
    "            autoreload, clipboard, clipboard_background (run copies on a",
    "            background thread, if yanks feel slow with clipboard on),",
    "            list_renumber (keep 1. 2. 3. in sequence after >al <al),",
    "            list_tab (Tab / Shift-Tab in Insert indent a list item),",
    "            table_align (auto: Esc lines up a table you edited; manual: ga)",
    "  [tree]    show_hidden — dotfiles in the file tree (H toggles it live)",
    "  [transclude]  embed, root, max_depth, heading_offset, border",
    "  [status]  enabled, flash_ms, show",
    "  [cursor]  normal, insert, visual, blink",
    "  [markdown]  tags, wiki_links, highlight, tables, code_syntax",
    "  [glyphs]  nerd_fonts, bullet, quote_bar, indent_guide, conflict, ...",
    "  [splash]  art — \"\" built-in, \"none\" off, or a path to your own",
    "  [keys.normal] / [keys.insert] / [keys.visual]",
    "     e.g.  \"<leader>w\" = \"save\"     (leader defaults to space)",
    "",
    "No config is needed: the shipped keys.conf is built in, and your [keys.*]",
    "merge over it. A binding you write replaces that key's; the rest stay.",
    "     \"<leader>z\" = \"none\"     removes a shipped binding",
    "     \"<C-j>\" = \"list_item_down\"  Ctrl-j as well as Alt-j (list_item_up: Alt-k)",
    "",
    "Bindings name any VERB, not just a discrete action, so the Vim grammar",
    "itself is remappable and keeps working:",
    "     \"s\" = \"operator_delete\"    then  ss  siw  2s3w  all behave",
    "     \"n\" = \"move_down\"         motions, objects and counts included",
    "",
    "A malformed [keys.*] binding is skipped (with a warning), never fatal.",
    "An exact match fires as soon as it is complete, so a binding that is also",
    "the start of a longer one shadows it — that is warned about too.",
    "[keys.insert] takes SINGLE keys only.",
];
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn resolves_topics_and_aliases() {
        assert_eq!(Help::open("").title, "help");
        assert_eq!(Help::open("shortcuts").title, "shortcuts");
        assert_eq!(Help::open("cheatsheet").title, "shortcuts"); // alias
        assert_eq!(Help::open("bindings").title, "bindings");
        assert_eq!(Help::open("keys").title, "bindings"); // alias
        assert_eq!(Help::open("commands").title, "commands");
        assert_eq!(Help::open("writer").title, "writer verbs");
        assert_eq!(Help::open("config").title, "config");
        assert!(!Help::open("bindings").lines.is_empty());
        assert!(!Help::open("shortcuts").lines.is_empty());
    }

    /// The shortcuts page is the one place all four kinds of interaction are
    /// listed together — the thing `<leader>hl` exists to open.
    #[test]
    fn shortcuts_page_covers_every_kind() {
        let h = Help::open("shortcuts");
        assert!(h.lines.iter().any(|l| l.contains("<leader> KEYS")));
        assert!(h.lines.iter().any(|l| l.contains(": COMMANDS")));
        assert!(h.lines.iter().any(|l| l.contains("NAVIGATION")));
        assert!(h.lines.iter().any(|l| l.contains("KEYBINDINGS")));
        assert!(h.lines.iter().any(|l| l.contains("<leader>hl")));
    }

    #[test]
    fn unknown_topic_lists_the_real_ones() {
        let h = Help::open("wat");
        assert_eq!(h.title, "help");
        assert!(h.lines.iter().any(|l| l.contains("No help topic")));
        assert!(h.lines.iter().any(|l| l.contains(":help bindings")));
    }
}