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
|
# pardes ↔ helix keybinding plan
Living tracking doc for the helix-parity effort. Every key / table row in
`helix/book/src/keymap.md` appears in exactly one of the three sections
below. The keymap was read at UPSTREAM commit 278b24389 of the genizah
checkout (`~/05-genizah/helix`); that checkout now sits on the local
`pardes-harness` branch, whose three commits add only the harness, so
`book/src/keymap.md` is byte-identical at its tip (694e7dfd). As of phase 5
every helix-equivalent row in A and B is differentially verified against real
helix (see "Differential testing" at the bottom). Five of the six waivers are
named in the row they belong to; the sixth, `wiX-edit-drops-sel`, belongs to no
single key — it is the anchor-only divergence every insert-mode edit shares —
and is described in the Files list at the bottom instead.
Code map, by SYMBOL — line numbers rot, names do not. Body-normal key
RECOGNITION is `modal.Normal` in `src/modal.zig`: a state machine over `Role` (one
per bound command, matched against `src/config.zig`'s chord lists by
`Pardes.normalInput`), a `Prefix`, a `MatchSub` and a count, emitting a
semantic `Action` that both the text and PDF adapters consume. It knows
nothing about panes or text. `src/pardes.zig` then EXECUTES: `Key`,
`handleKey` (intercept order is load-bearing, see the comment there),
`handleNormal` (parses directly into `Pane.normal`),
`executeNormalAction`, `setPaneRange` (helix range → pane state),
`enterInsert`, `handleInsert` / `insertKey`, `insertTab`, `normalDelete`,
`normalYank`, `pasteText`, `normalChange`, `replaySels`, `multiOnce`. Undo
and redo live in `panes.File` and `panes.Terminal`. Pure text math:
`src/modal.zig` (the `hx*` family is the helix-semantics layer: gap offsets +
ranges over the flat text). Differential harness: `test/hxdiff.zig` +
`test/hxcases/*.jsonl` — see "Differential testing"
at the bottom.
## Global semantic divergences (read first)
- **FULL HELIX MOTION MODEL (phase 5 decision — overrides phase 2's
"keep point motions").** Motions select the range they traverse with
helix's landing conventions: after `w` on "alpha beta" the anchor is
(0,0) and the block cursor sits ON the space at col 5 (not vim's col
6); a following `d` deletes "alpha ". Which motions select a range and
which collapse to a point follows helix exactly (word/find/till/
paragraph-class motions select; h/j/k/l-class char and line moves
collapse) — the checked-in helix goldens are the ground truth, verified
case-by-case by `zig build hxdiff`. Motion-created ranges live in
`pane.vsel` marked IMPLICIT (`vsel.explicit = false`); `v` extend mode,
`x`/`X` and `n`/`N` (the look-ring walk, `lookWalk`) set EXPLICIT ones.
`d`/`c`/`y`/`~`/… act on the active selection either
way — that is the point of the model. The pardes-specific chords
(normal-mode Enter look / Tab execute)
act on EXPLICIT selections only and fall back to the word under the
cursor when the selection is implicit motion residue. Mode is reported
"select" ONLY while `v` extend mode is on (helix keeps mode normal for
x/%/mi-created selections).
- **Stored columns are UTF-8 byte offsets** (`cur_col`), but every modal
cursor/range endpoint is snapped to an extended-grapheme boundary. Rendering,
mouse input and vertical motion translate those offsets through terminal-cell
widths, so combining sequences and wide glyphs remain single cursor cells.
Three functions in `modal.zig` do the snapping — `graphemeStart` (repair an
offset back onto a boundary), `nextGrapheme`, `prevGrapheme` — each with an
arithmetic ASCII fast path over the UAX #29 segmenter. All three carry the
same exclusion by hand, because GB3 is the one UAX #29 rule that joins two
ASCII scalars: a CR takes a following LF into the same cluster. `nextGrapheme`
and `prevGrapheme` did not spell it out and so disagreed with `graphemeStart`
by exactly one byte on a CRLF file — a head could step to the offset between
CR and LF and be repaired straight back. Fixed; the test "GB3 keeps CR-LF one
cluster for every grapheme step" is what holds the three in agreement.
- **Terminal panes: shell output is immutable.** Edit ops only ever
drop/alter typed insertion runs. All "To implement" edit ops follow the
same rule (motion/selection parts work on the full motion surface; the
mutating half is file-pane only or run-only).
- **Soft wrap is a toggle** (`Wrap`, `SPC t w`, on by default), and the
visual/textual pair is assigned the other way round from helix: `j`/`k` are
TEXTUAL file lines, `gj`/`gk` follow the automatic breaks. A file pane's `j`
is expected to move one line of the FILE; helix makes `j` the visual one and
`gj` the textual one. With the toggle off a line is one visual row and the
two pairs are the same motion.
- **Undo is snapshot-per-edit-op / per-insert-session**, not a
transaction log. Anything requiring replayable edits (`.`,
macros) needs new machinery.
## A. Implemented today
Parity column (phase 5): **helix-verified** = matched field-by-field
(text/mode/cursor/anchor) against the helix goldens by `zig build hxdiff`;
**pardes-specific** = key helix leaves unbound (or a deliberate pardes
binding) — cannot mismatch a golden; **waived** = corpus-covered divergence
with a reason in `test/hxcases/waivers.jsonl`; **out of corpus** = helix's
own key and meaning, but the work leaves the core as an EFFECT the headless
differential has no shell to perform — the clipboard commands, the
language-backend queries, and the shell pipe.
| Key | Behavior (pardes today) | Notes / quirks vs helix | Parity |
| --- | --- | --- | --- |
| `h` `j` `k` `l`, arrows | char left/right, line up/down — collapse the selection to a point (helix) | `j`/`k` are TEXTUAL lines even under soft wrap (`gj`/`gk` are the visual pair — the reverse of helix's assignment, see the divergence above); sticky col, phantom-line-blocked | helix-verified (unwrapped) |
| `w` `b` `e` | select to next word start / prev word start / next word end | full helix model incl. landing conventions (block cursor one before the next word after `w`) and newline/punct/EOF edges | helix-verified |
| `W` `B` `E` | long-word (WORD) variants | same | helix-verified |
| `Home` / `End` | line start / line end | matches `goto_line_start` / `goto_line_end` | helix-verified |
| `0` / `$` / `^` | line start / line end / first non-ws | `0` and `^` are pardes extras — helix leaves those two unbound (it spells them `gh`/`gs`). `$` is NOT unbound in helix: it is `shell_keep_pipe` (`keymap/default.rs`), and taking it for line-end is a deliberate divergence — see the `$` row in C. It is the one shell command still unimplemented: it needs a per-selection verdict, where the runner's answer is atomic. `0` is a count digit while a count is pending | pardes-specific (`$`: deliberate) |
| `G` | bare `G` is a **no-op**; `<n>G` = goto line n | vim-ism removed (phase 5): helix `goto_line` only acts with a count; `ge` is goto-last-line | helix-verified |
| `gg` / `<n>gg` | goto first line / line n | | helix-verified |
| `ge` | goto last content line | matches `goto_last_line` (ignores the trailing empty line) | helix-verified |
| `gh` / `gl` | line start / line end (last char, not the newline) | | helix-verified |
| `Ctrl-d` / `Ctrl-u` | half page down/up, cursor follows | matches `page_cursor_half_down/up` | helix-verified |
| `Ctrl-f` | full page down | matches `page_down`; file panes only get `Ctrl-b` (see next row) | helix-verified |
| `Ctrl-b` | file panes: full page up. Terminal panes: **toggle raw tty/editor mode** (`opts.tty_toggle`, configurable; `Shift-Esc` also enters). In raw tty Shift-Esc goes to the child | tty toggle is pardes-specific and wins on terminals; harness `pane:"tty"` cases avoid `Ctrl-b` | helix-verified (file) / pardes-specific (tty) |
| `PageUp` / `PageDown` | full page | matches helix `page_up`/`page_down` (view scroll + cursor snap to the scrolloff edge) | helix-verified |
| `zt` / `zz` / `zb` | scroll current line to top / center / bottom | matches `align_view_top/center/bottom` (helix harness pins scrolloff to pardes' 3) | helix-verified |
| `i` `a` | insert at selection start / after selection end | helix semantics: `i` before the selection, `a` selects and appends after it | helix-verified |
| `I` `A` | insert at first non-ws / line end | matches `insert_at_line_start` / `insert_at_line_end` | helix-verified |
| `o` `O` | open line below / above, copying the current line's indent levels; `<n>o` opens n | matches `open_below`/`open_above` | helix-verified |
| `v` | select (extend) mode: motions extend from the fixed anchor; `v` again exits KEEPING the selection | mode "select" is reported only while this is on (helix keeps mode normal for x/%/mi selections) | helix-verified |
| `x` / `<n>x` | select current line / extend one (n) line(s) down, char-range through the newline, cursor ON the `\n` | col-0 vim-ism removed (phase 5); matches `extend_line_below`, mode stays normal | helix-verified |
| `d` | delete selection (implicit or explicit); bare cursor = the 1-wide selection | always yanks (`Alt-d` noyank in B); terminals: drops typed runs only | helix-verified |
| `c` | change (delete + insert mode); linewise selections open a fresh indented line | same shape as `d` | helix-verified |
| `y` | yank selection; bare cursor yanks the 1-wide selection (char under cursor) | line-yank vim-ism removed (phase 5); yank keeps selection AND cursor (helix). Writes the DEFAULT REGISTER and nothing else — the system clipboard is `SPC y`, which is helix's own split and so moves this row TOWARDS helix, not away: a `d` of one character can no longer clobber what the desktop was holding | helix-verified |
| `u` / `U` | undo / redo | restores the pre-edit selection (helix); snapshot granularity, no `Alt-u`/`Alt-U` history walking (skipped) | helix-verified |
| `p` (normal) | paste the core's yank register after the selection | helix default-register semantics, and only the register — nothing on this path reads or writes the system clipboard. `SPC p` is the word that does, and on a tty its read is OSC 52, which most terminals refuse: an honest no-op there rather than a paste of the wrong text | helix-verified |
| `Esc` (body normal) | clear a pending modal prefix / exit select mode, keeping the selection, then run `Last`: hop to the pane you were in before this one, whichever kind it was, exactly like `SPC j j` — so held down it alternates between two panes, two files as readily as a file and its shell. A PDF pane is the ONE exception: there Esc is the document's own cancel (drop the mouse selection and the search overlay, stay where you are reading) and `Shift-Esc` is the hop out, while raw tty only intercepts unmodified Esc at a detected shell prompt | Pardes-specific focus binding layered on helix's cleanup. A leader path, tag, topbar or search owns Esc while it is active; raw tty forwards it | pardes-specific (cleanup helix-verified) |
| `Esc` (insert) | back to normal mode, cursor right after the insertion (no vim left-step) | | helix-verified |
| `Enter` (normal) | acme **look** chord: EXPLICIT selection, else file-ish word under cursor | pardes-specific, keep (helix normal-mode Enter unbound). Covers helix `gf`. Implicit motion residue falls back to the cursor word | pardes-specific |
| `Tab` (normal) | acme **execute** chord | pardes-specific, keep; explicit-selection rule as Enter | pardes-specific |
| `:` (normal, body) | focuses the pane's tag in normal mode at its remembered cursor position (first entry starts at the commands). `h`/`l`, arrows, word motions and `0`/`$`/`^` move within the tag; `k` stays on its single line; `j` or `Esc` returns to the body. Uppercase `H`/`J`/`K`/`L` run `Left`/`Down`/`Up`/`Right` and enter the neighboring pane's tag. `K` beyond the top pane reaches the headers below. `y` yanks; Enter/Tab Look/Exec; `i`/`a`/`I`/`A` enter insert mode, where letters type normally. Explicit mouse clicks choose a new cursor position. | Each tag keeps its own cursor position during the session. Positions are UTF-8 byte offsets in the rendered prefix plus editable tail; file-name changes are staged as described in [editable tags](tags.md). | pardes-specific |
| `K` (tag normal, topmost tagline) | focuses the column tag, then another `K` reaches the workspace tag. With `ColumnTags` disabled it goes directly to the workspace. `J` walks back down. Header arrows and `h`/`l` move by grapheme; word motions and `0`/`$`/`^` work too. `j`/`Esc` returns to the panel; lowercase `k` stays put. Enter/Tab executes in normal mode; `i`/`a`/`I`/`A` enter editing, where Enter is Look and Tab is Exec. | Column commands target that column's active pane, or its first pane when coming from elsewhere. Workspace and column text are independently editable and persist in dumps. See [editable tags](tags.md). | pardes-specific |
| `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — editor normal mode only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; Raw **tty** mode forwards Ctrl-w to the child. Insert mode owns `Ctrl-w` = delete-word-back, so a tag being TYPED into swallows it; from a tag in normal mode (`:`) it moves focus to the neighbour's BODY, while the bare letters `h/j/k/l` there move to its TAGLINE (next row) | pardes-specific |
| `Alt-n` | new terminal below (outside raw tty) | shadows helix `Alt-n` TS sibling-select — skipped anyway (tree-sitter) | pardes-specific |
| `Alt-c` | move active terminal to a fresh column (outside raw tty) | helix `Alt-c` is change-noyank; the pardes window op wins (do-not-touch contract). `Alt-d` + `i` covers the behavior | waived (`alt-c-window-op`) |
| `Space` (normal, body) | starts the pardes LEADER: a key path runs the same builtin words used by tags and the topbar. The main groups are `f` files, `h` docs, `c` columns, `t` toggles/effects, `a` panel animations, `s` session, `j` jumps, `l` language, and `w` directional focus; `SPC ?` lists every path and `<prefix> ?` lists one group in `+Help`. The pending path appears on the active pane's transient body/message row. Esc or an unmapped key abandons it. Paths, Help rows, and dispatch are all generated from the builtin registry at comptime; the leader applies only to a BODY in normal mode because tags and tty programs own their input | pardes-specific; helix spends Space on pickers/LSP (section C), while pardes uses acme-style executable words |
| `SPC y` `SPC Y` `SPC p` `SPC P` `SPC R` | helix's clipboard menu on helix's own letters: yank the selection to the system clipboard (`ClipYank`) or the PRIMARY selection alone (`ClipYankMain`), paste the system clipboard after (`ClipPaste`) / before (`ClipPasteBefore`) the selection, replace the selection with it (`ClipReplace`) | the ONLY five words in pardes that touch the desktop's clipboard — `y`/`d`/`c`/`p`/`P`/`R` and the acme cut/paste chords are the internal register alone. Builtins rather than bare chords because a leader path names a builtin: they land in Help's index and are executable words like every other verb. One divergence: pardes keeps a single register VALUE where helix keeps one per range, so `SPC y` at N cursors joins them with newlines (`Pardes.setYank`, the divergence `msel-yank-paste` already waives). On a tty the write is OSC 52 out and the READ is OSC 52 back, which many terminals refuse or gate — so `SPC y` works there and `SPC p` can be a no-op | out of corpus |
| a paste from the OUTER terminal | one `Event.paste`, spliced in at the cursor | the tty shell enables bracketed paste and coalesces `paste_start`..`paste_end` into a single event; before that the bytes arrived as individual key presses and normal mode RAN them, which is how a pasted `d` deleted a line. The bytes deliberately never enter the yank register — clipboard and default register are separate stores in both directions | pardes-specific |
| `/` (any pane) | pardes' own plain-substring search into a `+Search` output buffer: the tag takes the pattern, Enter fills the buffer, and its rows are ordinary look targets. Enter also GOES to the first row — the buffer is focused and then the step `n` is and the look Enter is run in it (`Pardes.lookFirstHit`), so `/foo` lands on the first hit with the matched span selected. A pattern that matched nothing opens its empty buffer and moves nothing | KEEP, do not touch; not in the corpus (helix `/` is regex search). Find and Grep answer with OTHER files and deliberately do NOT jump. The stepping half is the next row | pardes-specific |
| `n` / `N` (any pane) | MOTION, not a jump: move the SELECTION to the next / previous look-able text and open NOTHING. Enter — the look chord — on what it leaves selected is what opens it | KEEP, do not touch; not in the corpus (helix's `n`/`N` walk regex search hits). What a step selects is the pane's GRAIN (`output_pane.Grain`, read in `Pardes.lookSpanIn`): in FREE TEXT — a terminal, a file, a PDF, a prose answer buffer — the largest whitespace-delimited run `look.resolve` can act on (`look.lookableSpan`, wrapper punctuation peeled off both ends), several to a line; in a RESULTS BUFFER one stop per ROW, the largest run its head resolves as (`look.lookableLineSpan`), because a row there IS one location and the words after it are the match rather than a second place to go; in a COMMAND list the whole line. The walk is a RING across PANES: every pane that has performed a Look, most recent first (`Pardes.look_src`), then the output buffers that have not, newest first, and only when both are empty the active pane. Exhausting a pane enters the next at its first (forward) / last (backward) span and the end wraps to the start, so `N` is the exact inverse of `n`. What it lands on becomes an EXPLICIT `vsel` with the cursor on its FIRST column, in the pane the walk focuses. ONE motion in every pane kind and every buffer kind — a PDF steps the `+Search` buffer its own search filled, `n` to select the row and Enter to jump. The single thing a buffer may change is that grain, and it changes it by BEING a kind of buffer rather than by a branch: `output_pane.Traits.steps` (a list of locations) makes a step take one row at a time, and `Traits.commands` makes it take the WHOLE LINE, because a command list (`ThemeSel`/`FontSel`) holds words to run and there is no path inside `Theme gruvbox` to pick out. Tab on what `n` selected wears the theme, which is the same middle click on the row is. `]d`/`[d` are helix's diagnostic motions, a different binding, and they do still jump to each diagnostic (`docs/lsp.md`) | pardes-specific |
| insert: printable text | file: real edit; terminal: typed run splice | | helix-verified (file) |
| insert: `Enter` | newline; keeps the current full indent levels and adds one 4-space logical tab when the text before the cursor ends in `(`, `[`, `{`, or `)` (including `})`) | plain lines match `insert_newline`; delimiter heuristic is pardes-specific | helix-verified (plain) / pardes-specific (delimiter) |
| insert: `Backspace` (+ `Shift-Backspace`) | delete prev char, joins lines at col 0 | matches `delete_char_backward`; `Ctrl-h` alias in B | helix-verified |
| insert: `Up` `Down` `Left` `Right` | move cursor | matches helix's "not recommended" insert arrows | helix-verified |
| `gd` `gD` `gy` `gi` `gr` | LSP definition / declaration / type-definition / implementation / references. ONE answer jumps straight there; several fill `+Search`, where n/N walk and Enter opens | in-process ZLS (`src/lsp/lsp_zls.zig`), `.zig` only — on a file the backend does not speak these do nothing at all, with no error row. Ctrl+left-click is the mouse spelling of `gd` | out of corpus |
| `]d` `[d` / `]D` `[D` | step the diagnostics list / go to its last or first; if no list is up, asking the backend for one is part of the press | | out of corpus |
| `=` | `format_selections` — writes a `- old` / `+ new` diff into `+Lsp` | deliberate divergence: the seam returns ROWS, not edits, so this SHOWS the formatting instead of applying it. Not in the corpus, so there is no waiver to name — the query leaves the core as an effect the headless harness has no shell to perform | out of corpus |
| `Ctrl-o` / `Ctrl-i` | jumplist back / forward (also Mouse4 / Mouse5 in SDL, macOS and web); selections remain unchanged (a selected cursor stays at its selection while the view visits the saved location); raw tty forwards both to the child — the `Back` / `Forward` builtins, also on `SPC j o` / `SPC j i`, with `SPC j l` rendering the stack as a buffer | helix binds both keys (`jump_backward` / `jump_forward`) but to a POSITION jumplist; pardes' stack is over panes and focus, so the keys agree and the semantics do not. `Ctrl-i` and Tab are the same byte under the legacy encoding; there Tab keeps meaning execute, and the pair only separates where the host speaks the kitty keyboard protocol | pardes-specific |
| `\|` | pipe every selection through `/bin/sh -c`: its bytes in on stdin, its stdout replacing them, one undo across all cursors | helix's own key and meaning; the command is typed into the pane's tag after a bare `\|` marker rather than into a popup | out of corpus |
| `A-\|` | the same, and the output is DISCARDED — the text is not touched at all | helix `shell_pipe_to`. For a command run for its effect. Marker `\|-` | out of corpus |
| `!` | run with NO stdin, insert the output BEFORE each selection | helix `shell_insert_output`. Runs ONCE and every cursor gets that one answer, as helix does — ten cursors and `date` give ten identical stamps. Marker `!` | out of corpus |
| `A-!` | the same, appended AFTER each selection | helix `shell_append_output`. Marker `!+` | out of corpus |
## B. To implement
Status: `todo` → set to `done (phase N)` as rows land. All rows verified
against keymap.md. Edit ops on terminal panes obey the immutable-output
rule (typed runs only) — same as `d`/`c` today.
`Pane.normal` holds `modal.Normal.State`: a count (accumulator,
capped 0xffff), a `Prefix` (`g` `z` `m` `f` `F` `t` `T` `r` `]` `[`), a
`MatchSub` (m-mode's `i`/`a`/`s`/`r`/`d`) and one `held_char` for `mr`'s
`<from>`. `find_op`/`find_ch` (the `Alt-.` repeat target) stay
on `Pane`, because the parser emits `repeat_find` without remembering what was
found. Pure text math in `modal.zig`: `findChar`, `matchBracket`,
`paragraphFwd`/`paragraphBwd`/`paragraphRange`, textobject/surround ranges,
`replaceRange`/`replaceChars`, `changeCase`, `joinLine`, `indentLines`,
`adjustNumber`, `deleteSpan`, `advanceBy` — each with inline tests that
`zig build unit-test` runs.
### Counts
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `<n>` digit prefix | count for motions (h/j/k/l/arrows, w/b/e/W/B/E, gj/gk), `x`, `f/t` family + `Alt-.`, `G`/`gg`, `g|`, `]p`/`[p`, `]Space`/`[Space`, `J`, `>`/`<`, `Ctrl-a`/`Ctrl-x` | `0` stays line-start when NO count is pending, count-digit otherwise. `Pane.count` accumulator, capped at 0xffff (ponytail). Digits are literal char args while a prefix waits (`f4` finds '4'). Any non-prefix key consumes the count; prefix setters carry it into their continuation | helix-verified (phase 5) |
| `<n>G`, `<n>gg` | goto line n (1-based, clamped), col 0 | plain `G` is a no-op like helix (vim-ism removed in phase 5; `ge` = last line) | helix-verified (phase 5) |
### Movement
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `f<ch>` / `F<ch>` | find next / prev char (on it) | NOT confined to current line. `pending` holds the waiting op (`f`/`F`/`t`/`T`); ASCII targets only (byte columns — ponytail); not found = no move | helix-verified (phase 5) |
| `t<ch>` / `T<ch>` | till next / prev char (one short of it) | `modal.findChar` handles all four + counts (nth occurrence, till applied after) | helix-verified (phase 5) |
| `Alt-.` | repeat last `f`/`t`/`F`/`T` motion (`Pane.find_op`/`find_ch`), takes a count | decision: repeats ONLY the find family, not `m`/`[`/`]` (helix extends it there; marginal) | helix-verified (phase 5) |
| `g|`, `<n>g|` | goto column n (1 = line start), clamped to the line | | helix-verified (phase 5) |
| `gs` | goto first non-whitespace | alias of the `^` handler | helix-verified (phase 5) |
| `gt` / `gc` / `gb` | goto screen top / center / bottom | view-relative (`pane.scroll()` + `pane.rows`), column kept (clamped) | helix-verified (phase 5) |
| `gj` / `gk` | VISUAL line down / up (+ count) | follows the wrapped body's own breaks (`file_pane.visualRow`, the same walk `fillBody` renders), keeping the goal column INSIDE the row; the last row of a line steps into the next line's first. Wrap off = one row per line, and this IS `j`/`k`. helix assigns the pair the other way round (its `j` is the visual one) | pardes-specific |
| `PageUp` / `PageDown` | FULL page (was half) | `Ctrl-u`/`Ctrl-d` stay the half-page pair | helix-verified (phase 5) |
### Changes
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `r<ch>` | replace selection/char with `<ch>`, newlines kept | terminals: replaces the typed-run byte under the cursor if any, else no-op — one code path for both kinds, since `normalReplaceChar` goes through `Pardes.editText`/`setEditText`, which route to a file's content or to `term_pane`'s typed-run overlay | helix-verified (phase 5) |
| `R` | replace selection (or cursor char) with the yank register; pasted text becomes the selection, head on its last char | uses the INTERNAL yank (`p.yank`), no clipboard round trip — `SPC R` is the system-clipboard twin; empty register = no-op; terminals: no-op | helix-verified (phase 5) |
| `~` | switch case of selection/char, selection kept | terminals: run byte only | helix-verified (phase 5) |
| `` ` `` | selection to lowercase | terminals: run byte only | helix-verified (phase 5) |
| ``Alt-` `` | selection to uppercase | terminals: run byte only | helix-verified (phase 5) |
| `J` | join lines in selection (or cur+next; helix ignores the count) | helix semantics: the newline + next line's leading whitespace collapse to ONE space (first line untrimmed); selection and cursor STAY where they were (vim's cursor-on-the-space removed in phase 5); terminals: no-op | helix-verified (phase 5) |
| `>` / `<` | indent / unindent selected lines (count times) | width = `modal.INDENT_W` = **4 spaces** (helix harness pins its no-language indent style to Spaces(4) to match); `<` also takes one leading tab as a level; empty lines never indented (helix); selection kept, positions mapped through the edit (phase 5) | helix-verified (phase 5) |
| `Ctrl-a` / `Ctrl-x` | increment / decrement the decimal int under the cursor by count | helix-style: under the cursor only (no vim forward scan), `-` handled, i64 saturating, zero-padding width preserved (phase 5); cursor to the last digit, selection kept; terminals: no-op | helix-verified (phase 5) |
| `Alt-d` | delete without yanking | `normalDelete(yank=false)` | helix-verified (phase 5) |
| `Alt-c` | change without yanking | **skipped (phase 2)**: the pardes `Alt-c` window op (move pane to fresh column, any mode) wins — it's load-bearing global UX and the parent contract forbids touching it. `Alt-d` + `i` covers the behavior. Corpus-covered and waived (`alt-c-window-op`) | waived (phase 5) |
| `P` | paste before | same yank-register path as `p` | helix-verified (phase 5) |
| `p`/`P` semantics | yank ending `\n` pastes as whole lines below/above the SELECTION's line span; else inline at the selection's outer edge. The paste (× count) becomes the selection, head on its last char (helix) | terminals keep the run-splice-at-cursor path (before/after collapses). Pinned by yankpaste.snap and the hxdiff yp cases | helix-verified (phase 5) |
### Selection manipulation
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `;` | collapse selection to cursor | drops vsel/msel, cursor stays | helix-verified (phase 5) |
| `Alt-;` | flip anchor and head | vsel: swap cursor ↔ anchor; msel: cursor to the other end (r0/r1 swapped to keep the cursor-at-r1 invariant) | helix-verified (phase 5) |
| `%` | select whole buffer | vsel anchor 0,0, cursor on the buffer's last char | helix-verified (phase 5) |
| `X` | snap selection to line bounds | vsel → msel over its row span; bare cursor → 1-line msel; msel: already line-wise, no-op | helix-verified (phase 5) |
| `Alt-x` | shrink selection to line bounds | vsel only: partial first/last lines drop out; nothing left collapses the selection; msel: no-op | helix-verified (phase 5) |
### Multiple cursors
helix's `Selection` is a LIST of ranges with a primary index. pardes keeps the
PRIMARY where it always was (`cur_row`/`cur_col` + `vsel`) and the other ranges
in `Pane.sels`; an ordinary key is REPLAYED once per range (`replaySels`), so
every motion and operator above works at every cursor without being rewritten.
With one cursor nothing is replayed and nothing changed. The result contract
grew `sels` + `primary`, emitted only when there is more than one range.
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `C` / `Alt-C` / `<n>C` | copy the selection to the next / previous line | helix `copy_selection_on_line`, incl. skipping lines too short to hold the column. **Byte columns, not visual ones** (a TAB counts as one) | helix-verified |
| `,` / `Alt-,` | keep only the primary / remove the primary | `Alt-,` on a lone cursor is a no-op (helix errors) | helix-verified |
| `)` / `(` / `<n>)` | rotate the primary forward / backward (wrapping) | ranges unchanged, only which one is primary | helix-verified |
| `Alt-s` | split the selection on newlines | helix `split_on_newline`; the newlines themselves drop out, primary becomes 0 (helix's own TODO) | helix-verified |
| `Alt-minus` / `Alt-_` | merge all ranges into one / merge the consecutive ones | helix `merge_selections` / `merge_consecutive_ranges` | helix-verified |
| `_` | trim whitespace off both ends of every range | empty and all-whitespace ranges drop; nothing left = collapse + keep primary (helix) | helix-verified |
| `<n>o` / `<n>O` | open n lines, one cursor per line | helix `open` with a count; this is why the `o-count` golden gained a `sels` field | helix-verified |
| every motion/operator | acts at every cursor | replayed last-range-first, so an edit never disturbs a range still waiting; each pass's result is remembered as a distance from the END of the text, which an earlier edit cannot move | helix-verified (56 msel-* cases) |
| `y` with several ranges | joins the ranges' text with newlines into the ONE register | helix keeps a register VALUE per range and pastes value[i] at range[i]. Waived (`msel-yank-paste`) | waived |
| `a` … `Esc` with several ranges | the primary's appended-over span is restored; the others collapse to bare cursors | `Pane.append_at` is a single field. Waived (`msel-append`) | waived |
| `&` | align selections into a column | **skipped**: needs visual (tab-expanded) columns, which nothing else in pardes measures |
| `Alt-(` / `Alt-)` | rotate the CONTENTS of the selections | skipped: a separate feature from rotating which range is primary |
Anything that reaches outside the buffer — a builtin, a language query — runs
once from the primary and drops back to a single cursor rather than firing per
range (`multiOnce`). Undo restores the primary from the snapshot and leaves the
other cursors where they are (`FileSnap` holds one range).
### Regex selection (`s` / `S`) — the interactive pair
Both arm the tag input the same way `/` does — the pattern is typed into the
tag tail after a marker, no popup — and both re-run on EVERY keystroke, from
the selection the prompt opened on (`Pane.sel_snap`). That is what makes the
selection a live preview, what makes typing a pattern one character at a time
land where pasting it whole would, and what makes Esc a plain restore. Enter
re-runs the final pattern down the same path, so a submit can never disagree
with what is on screen. Engine: **mvzr** (`build.zig.zon`), a bytecode VM that
compiles a runtime pattern with no allocator.
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `s<pat>` Enter | select every match INSIDE each range | helix `select_on_matches`; a match sitting right off a range's end is dropped (what `\b` and empty matches produce there). No match, no pattern, or one that will not compile = the selection is left alone (helix's "nothing selected") | helix-verified |
| `S<pat>` Enter | split each range on its matches | helix `split_on_matches`; the pieces BETWEEN the matches, including the empty one a leading match produces | helix-verified |
| `s`/`S` then Esc | back to the selection the prompt opened on | the empty pattern applies nothing, so cancelling IS the restore — one path, not a second one | helix-verified |
| an all-lowercase pattern | matches case-blind | helix's smart-case. mvzr has no such flag, so the surface is lowercased instead (ASCII folding is byte-for-byte, so the offsets are identical) | helix-verified |
| `^` and `$` | assert at the SCAN position, not at a line | helix compiles with `multi_line(true)`, so its `^` is every line start. Waived (`sel-regex-caret`) | waived |
| `.` | matches a newline like any other byte | the Rust regex crate excludes `\n` by default; mvzr does not. `[^\n]` is the workaround and agrees in both. Waived (`sel-regex-dot-newline`) | waived |
| more than 64 matches | the ones past `MAX_SELS` are dropped | the ceiling the whole selection model has, not this key's |
| `K` / `Alt-K` | keep / remove ranges matching a regex | **skipped**: the same prompt, filtering instead of splitting — worth adding next |
### `Ctrl-c` — toggle comments
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `Ctrl-c` | comment or uncomment every line the WHOLE selection touches | helix `toggle_comments`. One decision for the whole set — one uncommented non-blank line and everything gets commented — which is why it is a `wholeKey` and not a per-cursor replay | helix-verified |
| … at several cursors | each line once, in order | helix's `min_next_line`: two cursors on one line comment it once | helix-verified |
| … with mixed indents | the token goes in at the SHALLOWEST indent in the set | helix's `min`, quirks included: a deeper line is commented mid-whitespace | helix-verified |
| … uncommenting | one space after the token goes too, unless some line lacks it | helix's `margin` | helix-verified |
| … blank lines | skipped entirely, and they do not vote on commented-or-not | helix | helix-verified |
| which token | by file EXTENSION, from `config.comment_tokens` | the same notion of "language" `src/syntax.zig` picks grammars with. No extension (a terminal, an output buffer) or an unlisted one gets `config.comment_token_default` = `#`, which is helix's own `DEFAULT_COMMENT_TOKEN` and therefore what the oracle answers | helix-verified |
| block comments (`/* */`) | **skipped**: helix only reaches them when a language declares block tokens and no line tokens; every language in the table has a line comment |
### Match mode (`m` prefix) — plain-text, no tree-sitter
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `mm` | goto matching bracket | dumb text scan with nesting for `()[]{}<>`; ON a bracket only (no TS "nearest pair"); works on terminals via the motion surface | helix-verified (phase 5) |
| `mi<pair>` / `ma<pair>` | select inside / around textobject | pairs `( ) [ ] { } < >` nesting-aware multi-line; quotes `' " `` ` `` ` **line-scoped** (plain-text strings don't span lines); `w`/`W` word run (+trailing ws around, leading if none); `p` blank-line block (+trailing blanks around). Empty inside (`()`) = no-op. Selections work on terminals; `Pane.pending2` holds the i/a/s/r/d sub-key | helix-verified (phase 5) |
| `ms<ch>` | surround selection (or cursor char) with the `<ch>` pair; wrap incl. pair becomes the selection | either bracket names its pair; any other ASCII char wraps with itself; file panes only | helix-verified (phase 5) |
| `mr<from><to>` | replace enclosing `<from>` pair chars with `<to>`'s | `Pane.pending_ch` holds `<from>` while `<to>` pends; file panes only | helix-verified (phase 5) |
| `md<ch>` | delete the enclosing `<ch>` pair chars | file panes only | helix-verified (phase 5) |
### View mode (`z` prefix) additions
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `zj` / `zk` (+ `zdown`/`zup`) | scroll view down / up one line, cursor untouched | `pane.scrollBy(±1)` | helix-verified (phase 5) |
| `zc` | center (alias of `zz`) | | helix-verified (phase 5) |
| `z Ctrl-d` / `z Ctrl-u` | half page with cursor | aliases of the bare handlers | helix-verified (phase 5) |
| `z Ctrl-f` / `z Ctrl-b` / `z PageUp` / `z PageDown` | full page | aliases | helix-verified (phase 5) |
### Unimpaired (plain-text subset)
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `]p` / `[p` | next / prev paragraph (+ count) | helix `goto_next/prev_paragraph`: selects to the boundary (anchor at the origin), blank-line-delimited blocks, count iterates | helix-verified (phase 5) |
| `]Space` / `[Space` | add `<n>` blank lines below / above, cursor staying on its text line | file panes only (shell output immutable) | helix-verified (phase 5) |
### Insert mode
All the mutating insert keys follow the pane split: file panes edit content
(`modal.deleteSpan` for the kills), terminal panes edit ONLY the typed run at
the cursor (word = space-delimited within the run). Ctrl-h/j/d are normalized
to Backspace/Enter/Delete and re-dispatched at the top of `handleInsert`. The
text-insert path now requires no ctrl/alt so modifier combos can't leak their
text.
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `Ctrl-w` / `Alt-Backspace` | delete word backward (ws then word-class run; at col 0 = the ordinary backspace join) | resolved per proposal: focus prefix restricted to normal/tty; insert owns `Ctrl-w` | helix-verified (phase 5) |
| `Alt-d` / `Alt-Delete` | delete word forward (word run + trailing ws; at EOL eats the newline) | | helix-verified (phase 5) |
| `Ctrl-u` | kill to line start | | helix-verified (phase 5) |
| `Ctrl-k` | kill to line end | | helix-verified (phase 5) |
| `Ctrl-h` | delete prev char (Backspace alias) | | helix-verified (phase 5) |
| `Ctrl-d` / `Delete` | delete next char; at line end joins the next line up | `Key.delete` (0xF0009) added + `mapKey` in tty.zig/gui.zig (vaxis + SDLK) + `forwardKey` sends `ESC[3~` in tty mode | helix-verified (phase 5) |
| `Ctrl-j` | insert newline (Enter alias) | | helix-verified (phase 5) |
| `Home` / `End` | line start / line end past-the-last-char (`goto_line_end_newline`) | file panes (terminal insert cursor rides its run, as before) | helix-verified (phase 5) |
| `PageUp` / `PageDown` | cursor page up / down, col kept (clamped to line) | file panes | helix-verified (phase 5) |
| `Tab` | indent to the next 4-column stop with SPACES (`insertTab`, `pardes.zig`: `pad = INDENT_W - col % INDENT_W`) — no `\t` byte ever reaches the file | helix's Spaces indent style; helix smart-tab skipped. After a `.` in a file the backend speaks, Tab instead asks for completion and only indents if the answer is empty (section A) | helix-verified (phase 5) |
## C. Skipped
| Key(s) | Helix behavior | Reason |
| --- | --- | --- |
| `?`, `*`, `Alt-*` | rsearch / selection-as-pattern | search — pardes has its own `/` (plain substring into an output buffer, kept as-is) and an `n`/`N` that SELECTS the next look-able text instead of walking match hits; helix regex search machinery not wanted |
| `Space` mode: `f F e . b j g G ' w c C Alt-c / ?` | pickers, global search, palette | pickers and the command palette do not exist here — but the KEY is taken: pardes' own leader runs the acme builtins (section A). Two halves of helix's space mode DO exist and are in section A: the clipboard menu (`<space>y Y p P R`, helix's letters on helix's leader) and the LSP menu, moved one prefix deeper to `SPC l k/r/a/h/s/S/d/D` because `d`, `k`, `s` and `h` were already pardes' most-pressed keys |
| Popup `Ctrl-u`/`Ctrl-d`, Completion menu, Signature help tables | LSP popups | LSP |
| Picker table (all rows), Prompt table (all rows) | picker / prompt internals | pickers — pardes' tag line is its own one-line editor |
| `gn` `gp` `ga` `gm` | next/prev/alternate buffer | buffer nav — pardes panes aren't a buffer list |
| `gw` | word-label jump | label-jump overlay machinery, not core editing |
| `g.` | goto last modification | jumplist/history position tracking |
| Window mode table: `Ctrl-w` + `w v s t f F h j k l q o H J K L ns nv` (+ Ctrl variants) | splits/window management | window mode — pardes has its own Ctrl-w focus + Alt-n/Alt-c + mouse layout drags |
| `Alt-o`/`Alt-up`, `Alt-i`/`Alt-down`, `Alt-p`/`Alt-left`, `Alt-n`/`Alt-right`, `Alt-a`, `Alt-I`, `Alt-e`, `Alt-b` | syntax-node selection | tree-sitter |
| `]f [f ]t [t ]a [a ]c [c ]e [e ]T [T ]x [x` | TS unimpaired jumps | tree-sitter |
| `]g [g ]G [G` | git change jumps | needs VCS diff state |
| insert `Ctrl-x` | completion menu | completion exists, but not as a popup: insert-mode Tab straight after a `.` opens a buffer of candidate DECLARATIONS (section A). helix's menu itself is skipped |
| `"` `<reg>`, insert `Ctrl-r` | register select / insert | registers — one yank register and no way to name a second; the system clipboard is not spelled as a register here either, it is the five `SPC` commands in A |
| `Q` / `q` | record / replay macro | macros — needs replayable input log |
| `Ctrl-s` (normal) | save jumplist position | jumplist itself is implemented (`Ctrl-o`/`Ctrl-i`, section A); only the explicit save point is skipped |
| `Alt-u` / `Alt-U` | undo-history earlier/later | history timeline — linear snapshot u/U covers pardes |
| `K Alt-K`, `Alt-:` | regex keep/remove, ensure-forward | `K`/`Alt-K` are the same prompt `s`/`S` now have, filtering instead of splitting (`s S` moved to "Regex selection" in A, the rest of the family to "Multiple cursors") |
| `&`, `Alt-(` / `Alt-)` | align selections, rotate selection CONTENTS | see the multiple-cursors table for why |
| `Alt-J` | join + select the inserted space | marginal over `J` |
| `Alt-\|` `!` `Alt-!` `$` | shell pipe-to (output discarded), insert output, append output, keep-by-exit-status | shell — `\|` (`shell_pipe`, output replaces the selection) is implemented in section A; these four are the other members of helix's shell family. `$` is additionally taken for line-end here (section A) |
| `:` | command mode | side-effects/file-ops — pardes builtins live in the tag, and `:` is bound to focusing it (section A) |
| `gf` | goto file under selection | covered by pardes Enter-look |
| `Ctrl-z` | suspend | pardes IS the terminal multiplexer |
| insert `Ctrl-s` | commit undo checkpoint | undo is per-insert-session snapshots; no sub-session checkpoints |
| `Shift-Tab` (insert), smart-tab semantics | insert tab / smart tab | smart-tab machinery; plain Tab-inserts-tab lands in B |
| `Z` (sticky view mode) | persistent view mode | marginal; `z` one-shots suffice |
| `zm` (view) | align middle horizontally | marginal even with hscroll |
| `.` | repeat last insert | **deferred by decision**: needs recording the insert session's keystrokes and a replay path — a new subsystem; undo is whole-buffer snapshots with no edit log to piggyback on. Revisit after phases 2–5 if the log exists by then for another reason |
| Select/extend mode section (prose) | `v` turns all motions into extenders, `n`/`N` keep selections | implemented for motions (phase 5: `pane.select` + the fixed anchor, differential-verified — see the `v` row in A); helix's search-`n`/`N` extension doesn't apply (pardes' `n`/`N` is the look-ring motion, which REPLACES the selection with the span it lands on) |
## Differential testing (phase 5)
pardes is diffed key-for-key against real helix (checkout
`~/05-genizah/helix`, branch `pardes-harness`) over a shared JSON-Lines
case corpus. Both sides speak the same contract: a case is
`{"name","pane","text","keys"}` (helix key notation; `pane` is
`"file"`/`"tty"`, ignored by helix), a result is the full final buffer
text, the mode (`normal`/`insert`/`select` — select only for `v` extend
mode), and the block-cursor positions of the primary selection's head
(`cursor`) and other end (`anchor`), 0-based row + byte col. With more than
one selection the result line grows two more fields — `sels`, every range in
document order measured the same way, and `primary`, the index of the one
`cursor`/`anchor` describe. Both are omitted at one selection, which is why
every golden written before multiple cursors existed is still byte-for-byte
valid.
Files (all in `test/hxcases/`):
- `cases.jsonl` — the corpus: 481 cases (360 file + 121 tty). Every
helix-equivalent row of sections A and B has at least a typical and an
edge case; pure navigation/selection bindings get a `pane:"tty"` twin
(same text/keys — helix on that text IS the oracle for tty navigation
parity). No tty twins for content-mutating ops (shell output is
immutable) or doc-marked terminal no-ops. tty case texts keep motions
inside the content (the tty motion surface trims trailing blank rows,
so ge/G-to-last-line style assertions stay off tty).
- `goldens.jsonl` — checked-in helix results, regenerated by `zig build hxdiff-update`
(runs the `hx-harness` binary from the helix checkout; override with
`$HX_HARNESS`). Only needed when cases change — the diff itself runs
offline.
- `waivers.jsonl` — named exemptions, each with a reason. Six live ones:
`wiX-edit-drops-sel` and `msel-append` (helix maps selections through
insert-mode edits, pardes does not), `alt-c-window-op` (the pardes
window op deliberately shadows helix change-noyank), `msel-yank-paste`
(one yank register, not one value per range), and `sel-regex-caret` /
`sel-regex-dot-newline` (mvzr is not the Rust regex crate: no
multi-line `^`/`$`, and `.` matches a newline).
- `parity.jsonl` — 80 further cases, used only by the parity gate below.
- `parity-waivers.jsonl` — 13 named exemptions for the parity gate, in three
classes: one deliberate pardes binding (`ctrl-b-page`, since `Ctrl-b` IS the
tty toggle), eight pty VIEWPORT divergences (a terminal's view cannot scroll
below the vt's live grid bottom, so the cursor snaps into a different
scrolloff band — no text differs), and four case texts a pty cannot hold
verbatim (a literal TAB the emulator expands, a file with no trailing
newline, an all-whitespace last row the dump trims).
- `smoke.jsonl` — 20 cases referenced by nothing in the tree: no build step,
no script. Either wire it up or delete it.
The pardes half is `test/hxdiff.zig`, which builds `pardes-hxdiff` and drives
the core headlessly at 80x24 (22 body rows, matching helix's 22 text rows).
Run it:
zig build hxdiff # cases + goldens + waivers, field-by-field;
# unwaivered mismatch = per-case report + exit 1
zig build hxdiff -- test/hxcases/cases.jsonl # results to stdout, no diff
zig build hxparity # the SAME binary in --parity mode: cases.jsonl
# + parity.jsonl, each case run twice over the
# same text and keys (once in a file pane, once
# in a pty pane), both result lines must match.
# No goldens — the file pane IS the oracle, so
# "editing a shell pane behaves like editing a
# file" cannot drift. The case's own "pane" field
# is ignored; exemptions in parity-waivers.jsonl
zig build hxdiff-live # compare a fresh reference without changing goldens
zig build hxdiff-update # update goldens only after the live comparison passes
Set `-Dhelix-harness=/path/to/hx-harness` or `HX_HARNESS`; otherwise these
steps look for `hx-harness` on PATH.
The helix half (`hx-harness`) lives on the `pardes-harness` branch:
a full headless `Application` with LSP/tree-sitter/auto-pairs/word-
completion off, scrolloff pinned to pardes' 3, no-language indent style
pinned to Spaces(4), smart-tab off, and the buffer-setup transaction
committed as its own undo revision. Build:
`cargo build --release -p helix-term --features helix-term/integration --bin hx-harness`.
Raw TTY input keeps Ctrl-b for `Togglettymode` and unmodified Escape at a detected shell
prompt for `Last`. Other Ctrl/Alt chords, modified Escape, and clipboard shortcut
keys go to the child. Use the `Togglettymode` tag to leave raw input in place.
Completed mouse rectangles keep their source text when jump navigation scrolls
the pane. Wrapped pieces stay separate copied rows. Changing file content or
reflowing a terminal to a new width clears these rectangles.
File clicks place the editing cursor at the nearest valid insertion position:
blank columns stop at the line end, rows below the file stop on its last row,
and wide glyphs or tabs stay on their character boundary. Clicking while
inserting keeps insert mode. Terminal overlays still support virtual spaces.
In a raw TTY pane, finishing a mouse drag keeps the exact rectangular text
as the Pardes selection and internal yank register. Switch to editor mode
with Ctrl-B to inspect or yank it with `y`; returning to TTY preserves it
without moving the shell's input cursor. In another TTY pane, hold select
(left) and tap look (right) to paste it using the existing chord.
|