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
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
|
//! Every syntactic choice pardes makes, in one file: which key runs what,
//! which mouse button means what, how a looked-at word is SPELLED, and the
//! handful of layout numbers that are taste rather than structure.
//!
//! The point is the editing session, not the architecture: retargeting a key,
//! a chord or a piece of Look syntax is an edit HERE and nowhere else. Nothing
//! below is read at runtime from a file — this IS the config format, recompiled
//! — so a binding may be any comptime expression and a wrong one is a compile
//! error rather than a silent no-op.
//!
//! Order is deliberate. PART 1 is pardes's OWN vocabulary, the part a user
//! actually fiddles with, so it is where a reader lands. PART 2 is Look/Exec
//! syntax. PART 3 is the helix keymap, which is under a differential-testing
//! contract — see the banner there before touching it.
//!
//! These COMPILED bindings are distinct from the small startup command file
//! described in docs/config.md. That file can run builtins such as `Theme`
//! and `Font`; it does not replace or mutate this keymap at runtime.
//!
//! What is NOT a binding: a named key's own identity. Insert mode's Backspace,
//! Delete, Enter and Tab are dispatched on `Key.<name>` in handleInsert and
//! stay there — Backspace deleting backwards is what the key IS, not a choice
//! anyone remaps. `Ctrl-w` deleting a word backwards is a choice, and it is
//! here.
const std = @import("std");
const pardes = @import("pardes.zig");
const builtins = @import("builtins.zig");
const Key = pardes.Key;
const Mouse = pardes.Mouse;
const Builtin = builtins.Builtin();
/// One key press a binding matches. `shift` is only consulted when a binding
/// ASKS for it: shift is already carried in the codepoint for anything
/// printable (`A` is `A`, not shift-`a`), so the one chord that needs the flag
/// is Shift-Esc, on a key that has no shifted codepoint. pardes.zig's `hit`
/// is the matcher.
pub const Chord = struct {
cp: u21,
ctrl: bool = false,
alt: bool = false,
shift: bool = false,
};
// A binding is a LIST because most have two spellings that must reach the same
// arm — a letter and an arrow, `Ctrl-f` and PageDown. One list, one dispatch
// site; the `or` chains this replaces had the modifier logic written out three
// ways (the old is/isC/isA).
// ============================================================================
// PART 1 — pardes's own bindings. Nothing here is inherited from anywhere;
// these are the ones to fiddle with.
// ============================================================================
// ---- the SPC leader ----
/// opens the leader: a key path from here runs a BUILTIN with no arguments.
/// Body normal mode only — a tag is always insert, and a tty pane's keys
/// belong to the program.
pub const leader: []const Chord = &.{.{ .cp = ' ' }};
/// Help's key, honored at ANY depth: it lists what the prefix typed so far can
/// still reach. Also Help's own path below, so the character is spelled once.
pub const leader_help: u8 = '?';
/// what an unlisted builtin's path is until someone says otherwise: a value no
/// key path can be, so the loop at the bottom of the table can refuse it
const undecided: []const u8 = "<undecided>";
/// SPC leader: ONE key path per builtin, the whole remapping surface. A new
/// enum field is a compile error until someone has DECIDED its path — that is
/// what `undecided` and the loop under the table are for, and it used to be
/// EnumArray.init's own doing (it demands every field). It cannot be any more:
/// the gui-only builtins are not fields of this literal's type on tty or web,
/// so the literal cannot name them and a default is the only way to have both.
/// Groups are just shared first letters (f files, h docs, c columns, t
/// toggles, s session, l language, w windows).
///
/// `null` = this builtin's shortcut is not a leader path. Look and Exec are
/// the two: their shortcuts are Enter/Tab and the two mouse buttons below, and
/// a third spelling under SPC would be a key that does nothing you cannot
/// already do with the key your hand is on. The option is the honest type —
/// "every builtin has a leader path" was only ever true by accident.
pub const leader_path = paths: {
var table = std.EnumArray(Builtin, ?[]const u8).initDefault(@as(?[]const u8, undecided), .{
.Help = &[_]u8{leader_help},
// The whole LANGUAGE group lives under `l`, and pardes's own builtins keep
// the letters they always had — `SPC d` is Del, `SPC k` is Kill.
//
// Helix puts these on bare `<space>` letters, and an earlier pass followed
// it there, which cost `d`, `k`, `s`, `h` and the session group. That is
// the wrong trade: those five are pardes's most-pressed keys and predate
// the language work, whereas an LSP command is something you reach for
// deliberately and can afford one more keystroke. Each one still keeps
// HELIX'S OWN LETTER inside the group, so the mapping is `<space>X` ->
// `SPC l X` with nothing to re-learn but the prefix.
//
// The five GOTOS are untouched and remain exactly helix's — `gd` `gD` `gy`
// `gi` `gr`, plus `]d`/`[d` and `=`. Those never collided with anything, so
// there was never a reason to move them. They are in PART 3.
.Hover = "lk",
.Rename = "lr",
.CodeAction = "la",
.SelectRefs = "lh",
.Symbols = "ls",
.WsSymbols = "lS",
.Diagnostics = "ld",
.WsDiagnostics = "lD",
.Lspinfo = "li",
.Lspwhy = "lw",
.Del = "d",
.Kill = "k",
// the `f` file group (spacemacs): Save left vim's `w` to join Find and
// New here, which frees `w` for the window group (SPC w h/j/k/l).
.Save = "fs",
.New = "fn",
.Find = "ff",
.Grep = "fg",
.Tutor = "ht",
.Newcol = "cn",
.Delcol = "cd",
.Debug = "td",
.Colors = "tc",
.NextColor = "tn",
.Crt = "tr",
// the theme picker joins the toggles it belongs with; `Theme` itself takes
// a NAME, and a key path can never carry one, so it has none (the same
// reason Look and Exec have none)
.ThemeSel = "tt",
.Theme = null,
// the image toggles join the same `t` group; Palette takes `l` because
// `p` is Petscii's and `c` is Colors'.
.Petscii = "tp",
.Palette = "tl",
.Ascii = "ta",
.Dump = "sd",
.Restore = "sr",
// the `w` window group `Save` vacated: the four directional focus moves
// the Ctrl-w prefix does, spelled h/j/k/l because focus IS a motion, plus
// `t` for the file<->terminal hop.
.Left = "wh",
.Down = "wj",
.Up = "wk",
.Right = "wl",
.Toggleterm = "wt",
// the `j` JUMP group, its own letter rather than more of `w`: the window
// group moves focus by GEOMETRY (the pane left of this one), these move it
// by TIME (the pane I was in before). `o` and `i` are the letters of the
// chords that do the same thing, `jj` is the group's obvious verb, and
// `jl` is the list itself.
.Back = "jo",
.Forward = "ji",
.Last = "jj",
.Jumplist = "jl",
// the two acme verbs: keys and buttons, no leader path — see above
.Look = null,
.Exec = null,
});
// The GUI's two font builtins, in the same `t` group as the theme picker
// they mirror — but set here rather than named above, because on tty and
// web they are not builtins at all (see builtins.zig) and the literal's
// type has no field to write. `Font` takes a NAME, so it has no path, for
// the same reason `Theme` has none.
if (pardes.platform == .gui) {
table.set(.FontSel, "tf");
table.set(.Font, null);
}
// PdfFit exists only in MuPDF builds (its run declaration deliberately
// changes shape when the feature is off), so name its toggle path through
// the same comptime branch rather than making feature-off enums mention it.
if (pardes.pdf_enabled) table.set(.PdfFit, "tz");
// ...and the property EnumArray.init used to give for free: every builtin
// this build HAS is a builtin someone decided a path (or a null) for.
for (std.enums.values(Builtin)) |b| if (table.get(b)) |p| {
if (std.mem.eql(u8, p, undecided)) @compileError("builtin has no leader path decided: " ++ @tagName(b));
};
break :paths table;
};
// ---- the acme chords ----
// Look and Execute, the two verbs the whole environment is built on. Each has
// a KEY and a mouse BUTTON, and they are the same two verbs: Enter on a word
// does what a right click on it does. Swap the two `_key` lines for the vim
// reading, where Enter runs the command line. (This pair replaced a
// `swap_enter_tab` bool, which could only ever exchange them — two bindings
// can also be moved somewhere else entirely.)
pub const look_key: []const Chord = &.{.{ .cp = Key.enter }};
pub const exec_key: []const Chord = &.{.{ .cp = Key.tab }};
pub const look_button: Mouse.Button = .right;
pub const exec_button: Mouse.Button = .middle;
/// sweep, focus, place the cursor, and the left half of every acme chord
pub const select_button: Mouse.Button = .left;
/// ...and WHAT those four run. Look and Exec are ORDINARY builtins — the same
/// kind of thing Save and Grep are, executable by name wherever text lives
/// (`Look main.zig` in a tag does what a right click on `main.zig` does) — so
/// the four bindings above are bindings like any other, and these two lines
/// are the whole of what makes them special. Point `look_cmd` at `.Grep` and
/// Enter greps.
pub const look_cmd: Builtin = .Look;
pub const exec_cmd: Builtin = .Exec;
// ---- windows ----
/// helix's window prefix. It stays despite `SPC w` covering the same four
/// builtins because it reaches one place the leader cannot: a pane in raw tty
/// mode never sees SPC (the shell owns every printable key), so this is the
/// only keyboard way out of one.
pub const window_prefix: []const Chord = &.{.{ .cp = 'w', .ctrl = true }};
/// The four directional focus moves, as data: `Ctrl-w <letter|arrow>` in a
/// body, and the bare LETTER on a focused tagline, where focus IS the motion
/// (the arrows stay grapheme motion inside the tag, which is why the two
/// spellings are separate fields rather than one list). One table, two
/// readers — and the `cmd` column is what makes the chord discoverable from
/// the builtin as well as the other way round.
pub const window_keys = [_]struct { letter: Chord, arrow: Chord, cmd: Builtin }{
.{ .letter = .{ .cp = 'h' }, .arrow = .{ .cp = Key.left }, .cmd = .Left },
.{ .letter = .{ .cp = 'j' }, .arrow = .{ .cp = Key.down }, .cmd = .Down },
.{ .letter = .{ .cp = 'k' }, .arrow = .{ .cp = Key.up }, .cmd = .Up },
.{ .letter = .{ .cp = 'l' }, .arrow = .{ .cp = Key.right }, .cmd = .Right },
};
/// global window ops, live in ANY mode (which is why they are Alt-, not a
/// leader path): a new terminal below, and moving this terminal into a fresh
/// column. Alt-c is a deliberate divergence from helix's change-noyank —
/// hxdiff waives it by name.
pub const new_shell_below: []const Chord = &.{.{ .cp = 'n', .alt = true }};
pub const pane_to_new_column: []const Chord = &.{.{ .cp = 'c', .alt = true }};
// ---- jumps ----
/// vim's Ctrl-o / Ctrl-i, walking the focus history back and forward. Global
/// in any mode, like the two Alt- ops above and for the same reason: getting
/// BACK has to work from inside a pane that owns its keys.
///
/// CAREFUL, and this is why the table has a comment: **Ctrl-i is Tab**. On the
/// wire they are the same byte (0x09), so on a host that speaks only the
/// legacy encoding this binding never fires and 0x09 keeps meaning `exec_key`
/// below — which is the right way round, since Tab-executes is the older and
/// more used of the two. Where the host speaks the kitty keyboard protocol
/// (`CSI 105;5u`) the two are distinct keys and both work. Shift-Esc
/// (tty_toggle_alt) is already spelled on that same bet.
///
/// Shaped like window_keys: the `cmd` column is what makes the chord
/// discoverable from the builtin as well as the other way round, and it is
/// what keeps ONE implementation — pressing the chord and executing the word
/// `Back` are the same call.
pub const jump_keys = [_]struct { chord: Chord, cmd: Builtin }{
.{ .chord = .{ .cp = 'o', .ctrl = true }, .cmd = .Back },
.{ .chord = .{ .cp = 'i', .ctrl = true }, .cmd = .Forward },
};
/// `j` off the topbar drops back onto a tagline — the mirror of the `k` that
/// got you there (window_keys' letter, answered by tagNormalKey).
pub const topbar_down: []const Chord = &.{.{ .cp = 'j' }};
/// the topbar has no neighbouring window to walk to, so up here h/l are plain
/// grapheme motion instead
pub const topbar_left: []const Chord = &.{.{ .cp = 'h' }};
pub const topbar_right: []const Chord = &.{.{ .cp = 'l' }};
// ---- raw tty mode ----
/// Ctrl-<this> toggles raw tty mode in and out (terminals only); tty is
/// deliberately off the normal editing path. Not a Chord because the shell can
/// override it at runtime (`--tty-toggle`), so this is only Options' default.
pub const tty_toggle_default: u21 = 'b';
/// the second spelling, for hosts that report modifiers on Escape (the kitty
/// keyboard protocol). Where they don't it arrives as a plain Escape and still
/// means what Escape always means.
pub const tty_toggle_alt: []const Chord = &.{.{ .cp = Key.escape, .shift = true }};
// ---- the tag line and the topbar ----
// The topbar is a HAND-PICKED subset in a fixed order, not a derivation: row 0
// is where topbar clicks land, so its exact bytes are load-bearing (every
// snapshot golden records the column each word starts at). Comptime-checked
// against the enum in pardes.zig so a rename cannot silently rot it.
// Colors and Crt are NOT here: both are set-once display switches you flip and
// forget, and a bar you read every frame should not spend width on them now
// that `SPC t c` / `SPC t r` press them. NextColor stays — it is the one you
// cycle repeatedly, so a click beats a three-key path.
//
// Help is LAST and is the one word that has to be here. A bare `pardes` boots
// straight into tty mode (main.zig: `args.len == 1`), where every printable
// key belongs to the shell — so SPC never reaches the leader and `SPC ?`, the
// thing that would tell you the leader exists, is exactly what you cannot
// press. Row 0 is not a pane, so a middle-click on it is dispatched before any
// pane's mode is consulted: this word works in tty mode, which is the only
// reason it earns the width. Appended rather than inserted so every existing
// word keeps its column and no golden's click coordinates move.
//
// New is appended with the late additions, immediately before Help: every
// older action keeps its click column, and the roadmap's separate tagline-order
// item owns the later reshuffle. The two searches stay next to each other:
// Find matches file NAMES, Grep their CONTENTS.
pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep New Help";
/// the default editable tail of a pane's tag, per kind (an output buffer has
/// no file to Save, so it gets the plain one)
pub const pane_builtins_str = "Del";
pub const file_pane_builtins_str = "Save Del";
/// the pane's mode, as ONE character in the layout box at its top-left — live
/// chrome, not text you own. It used to be a three-letter word leading every
/// tagline; the box was already there carrying no information at all, so the
/// mode moved into it and the taglines got their four columns back.
///
/// These are NOT the initials. A badge you read at a glance every time your
/// eye crosses a pane should LOOK like what it means, and each of these is a
/// mark that already means its mode somewhere else: `^` is the proofreader's
/// caret, the mark that says text goes in HERE; `$` is the shell prompt, and a
/// pane wearing it has the keyboard wired straight to the program on the other
/// end; `•` is the pane at rest, a full stop, nothing waiting to eat what you
/// type. (The caret's true form is `‸` U+2038 and the ASCII `^` is only its
/// stand-in — but `^` is in every font ever made and `‸` is in about four, and
/// a mode badge that renders blank on someone's terminal is worse than one
/// spelled with the near-miss.)
///
/// ONE CODEPOINT each. The box prints a single cell, so a two-character string
/// here would be pushed into one cell as a single grapheme and come out wrong.
/// A font missing the glyph draws a blank box, which is exactly what the box
/// drew before there was anything in it.
pub const tag_normal = " ";
pub const tag_insert = "^";
pub const tag_tty = "$";
/// ...and `img` stays a WORD at the head of an image pane's tagline, because
/// it is not a mode: it says what the pane IS, which no amount of watching the
/// box will tell you. The box on an image pane still shows its mode.
pub const tag_image = "img";
/// on a focused tag: yank what the chord would run (the selection, else the
/// word under the cursor). The path is selectable, so this is how you copy it.
pub const tag_yank: []const Chord = &.{.{ .cp = 'y' }};
// ---- the command line and search ----
/// vim's command line with acme's vocabulary: focus the pane's own tag in
/// normal mode, parked at the tail's start, and the execute chord runs the
/// word under the cursor (`:w<Tab>` = Save).
pub const command_line: []const Chord = &.{.{ .cp = ':' }};
/// `/` types a pattern into the tag; n/N walk the results. Same keys on every
/// kind of pane — a terminal with no search armed falls back to n/N as a
/// motion over the lookable tokens in its output.
///
/// Helix's letters, but NOT helix's commands (it searches by regex and pardes
/// has no regex engine), so these three sit out here rather than under the
/// contract in PART 3 — the differential suites never press them.
pub const search: []const Chord = &.{.{ .cp = '/' }};
pub const search_next: []const Chord = &.{.{ .cp = 'n' }};
pub const search_prev: []const Chord = &.{.{ .cp = 'N' }};
/// Enter on an armed search input runs it; `escape` (PART 3) abandons it.
pub const search_submit: []const Chord = &.{.{ .cp = Key.enter }};
// ---- mouse ----
/// wheel step, in rows / in columns. The horizontal step is bigger because a
/// column is narrower than a row is tall and a wheel tick should move a
/// comparable distance either way.
pub const wheel_rows: i32 = 1;
pub const wheel_cols: i32 = 4;
/// Touchpad drift guard, in ticks. A two-finger swipe that is MEANT to be
/// vertical carries a little sideways drift, and the pad faithfully turns that
/// drift into wheel_left/wheel_right — so a plain scroll slides the view
/// sideways under you. Every vertical tick re-arms the guard to this many
/// ticks and every horizontal tick spends one instead of scrolling, which
/// makes horizontal EARN its way back: it has to land this many ticks in a row
/// with no vertical among them. 3, because drift arrives in ones and twos —
/// at 1 or 2 a doubled drift tick mid-swipe still gets through, and much
/// higher starts eating deliberate swipes. Set 0 to disable the heuristic.
///
/// It costs nothing at rest: the guard is only armed by vertical scrolling, so
/// a horizontal swipe that starts from a still view moves on its FIRST tick.
/// Note this applies to a tilt wheel too, where "recent vertical" is a much
/// weaker signal of accident — a mouse would rather not have it. Living with
/// that is deliberate: the only honest fix is a per-device flag out of the
/// shell (libinput/SDL know which is which, vaxis does not), and paying for a
/// device-detection layer to spare a tilt wheel three clicks after a scroll is
/// a worse trade than the three clicks.
pub const wheel_guard_ticks: u8 = 3;
/// The whole guard, as one state machine, so it can be tested as one thing:
/// fold a wheel tick into `guard` and answer whether it scrolls. The core owns
/// the counter (Pardes.wheel_guard) because the gesture belongs to the DEVICE,
/// not to whichever pane the pointer happens to sit over.
pub fn wheelTick(guard: *u8, vertical: bool) bool {
if (vertical) {
guard.* = wheel_guard_ticks;
return true;
}
if (guard.* == 0) return true;
guard.* -= 1;
return false;
}
// written to hold for ANY tuning of wheel_guard_ticks, since tuning it by hand
// is what this file is for — a test that pinned the number 3 would just be a
// second place to edit it
test "wheel drift guard" {
var g: u8 = 0;
// from rest, horizontal moves on the first tick — nothing to prove
try std.testing.expect(wheelTick(&g, false));
try std.testing.expect(wheelTick(&g, false));
if (wheel_guard_ticks == 0) return; // guard disabled: nothing left to check
// a vertical swipe with drift mixed in: every sideways tick is swallowed,
// because each vertical tick re-arms the guard in full
for (0..4) |_| try std.testing.expect(wheelTick(&g, true));
try std.testing.expect(!wheelTick(&g, false));
try std.testing.expect(wheelTick(&g, true));
try std.testing.expect(!wheelTick(&g, false));
// the deliberate horizontal swipe that follows pays off the rest of the
// guard tick by tick, then runs free
for (1..wheel_guard_ticks) |_| try std.testing.expect(!wheelTick(&g, false));
try std.testing.expect(wheelTick(&g, false));
try std.testing.expect(wheelTick(&g, false));
}
// ---- layout numbers that are taste ----
/// the file pane's line-number gutter, in columns
pub const PREFIX_W: u16 = 5;
/// vim 'scrolloff': keyboard cursor moves keep this many context rows visible
/// above/below the cursor (clamped at file boundaries and short panes), and
/// the same count of COLUMNS horizontally
pub const scroll_off = 3;
/// the pane's left chrome: scrollbar + the layout box in the tag row. The
/// scrollbar PAINTS only the first of these columns; the second is the pane's
/// own background, so the bar reads as one column with a column of page
/// between it and the text. Layout is untouched by that — the gutter is still
/// GUTTER columns and a click anywhere in them still scrolls; only the ink
/// narrowed. A half-block glyph in the second column was tried and rejected.
pub const GUTTER: u16 = 2;
/// a pane never shrinks past this (the h-handle can still take it to its tag
/// row alone, which is BOX_H and a structural fact, not this)
pub const MINW: u16 = 10;
pub const MINH: u16 = 3;
// ============================================================================
// PART 2 — Look/Exec syntax: how a click on text is SPELLED. What the
// resolution then DOES with it is look.zig.
// ============================================================================
/// file-ish word chars (acme's isfilec): alnum + these. This set is the whole
/// definition of "the word under the cursor" for Look, Execute, the tag chord
/// and every search result row.
pub fn isFileChar(c: u8) bool {
return std.ascii.isAlphanumeric(c) or switch (c) {
'.', '-', '+', '/', ':', '@', '_', '~' => true,
else => false,
};
}
/// `` @`ls -la` `` — a word that names a COMMAND to run rather than a file to
/// open. Both halves are named here because they are a CHOICE: the `@` marks
/// it as ours (it is already a file char, so it can never split a path) and
/// the backquotes hold a command line with spaces in it, which is the whole
/// point — a file-ish word cannot. Respell them here and nowhere else.
pub const cmd_open = "@`";
pub const cmd_close: u8 = '`';
/// The command inside `` @`...` ``, or null when `w` is not one.
pub fn commandWord(w: []const u8) ?[]const u8 {
if (w.len <= cmd_open.len or !std.mem.startsWith(u8, w, cmd_open)) return null;
if (w[w.len - 1] != cmd_close) return null;
return w[cmd_open.len .. w.len - 1];
}
/// The bounds of the word at `col` in `line` — THE expansion a no-drag
/// look/execute click and the tag chord both use.
///
/// A `` @`...` `` run is taken WHOLE and wins outright: a backtick is not a
/// file char, so the plain scan below would stop dead inside one and hand a
/// look the fragment `ls` out of `` @`ls -la` ``. Acme does exactly this for
/// its own `<`/`|`/`>` command words. Otherwise it is the file-ish word.
pub fn wordBounds(line: []const u8, col: usize) struct { lo: usize, hi: usize } {
var i: usize = 0;
while (std.mem.indexOfPos(u8, line, i, cmd_open)) |o| {
const close = std.mem.indexOfScalarPos(u8, line, o + cmd_open.len, cmd_close) orelse break;
if (col >= o and col <= close) return .{ .lo = o, .hi = close + 1 };
i = close + 1;
}
var lo = col;
while (lo > 0 and isFileChar(line[lo - 1])) lo -= 1;
var hi = col;
while (hi < line.len and isFileChar(line[hi])) hi += 1;
return .{ .lo = lo, .hi = hi };
}
/// separates a path from its LINE and COL: `main.zig:100:7`. Must be a member
/// of isFileChar or the suffix would not be part of the word in the first
/// place.
pub const line_col_sep: u8 = ':';
/// ...and separates that spot from the END of a RANGE. A look at a ranged path
/// SELECTS the span rather than just parking on its first cell, which is what
/// lets a search result carry the text it matched and `n` land ON it.
///
/// Three spellings. The long one subsumes the other two, but the short ones
/// are what a person actually types and what a grep-alike emits, so all three
/// parse:
/// main.zig:412-418 lines 412 through 418, whole
/// main.zig:412:9-21 line 412, columns 9 through 21
/// main.zig:412:9-418:1 line 412 column 9 through line 418 column 1
/// Both ends are INCLUSIVE and 1-based, like the spot they extend — `412-418`
/// reads as seven lines, not six. `main.zig:412` and `main.zig:412:9` keep
/// meaning exactly what they always did.
///
/// Must be an isFileChar member, same as the separator above, or a click would
/// expand to half a range. That is also why reading it is FUSSY (look.zig,
/// parsePathLine): ordinary paths are full of dashes, so the suffix counts as
/// a range only when a NUMBER follows the dash — `my-file:10` and `build-2`
/// stay the paths they are.
pub const range_sep: u8 = '-';
/// `@p7:10:5` — pane 7, line 10, column 5. The one look target that names a
/// live pane instead of a path, because terminals and output buffers have no
/// file for a location to point at. Both the writer (a `/` result row) and the
/// reader (look.resolve) spell it from here.
pub const pane_addr = "@p";
/// a word starting with one of these is a URL and leaves the app entirely: no
/// filesystem can answer it
pub const url_schemes = [_][]const u8{ "http://", "https://" };
/// a path ending in one of these opens an image pane instead of a file pane
pub const image_exts = [_][]const u8{ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ppm", ".pgm", ".tga" };
/// What `Ctrl-c` (comment_toggle) puts at the front of a line, by file
/// EXTENSION — which is how src/syntax.zig already tells one language from
/// another, so this is that same notion and not a second one. A pane whose
/// path matches nothing here (and every terminal, which has no path at all)
/// gets `comment_token_default`.
///
/// `#` as the default is not a guess: it is helix's own DEFAULT_COMMENT_TOKEN,
/// which is what a helix buffer with no language configured comments with —
/// and therefore what the differential oracle answers, since the harness runs
/// with zero language configs.
/// Languages with no LINE comment at all (css, html, json, ocaml) are absent
/// on purpose: helix leaves those buffers on its default too, and inventing a
/// token for them would be a divergence nothing asked for.
pub const comment_token_default = "#";
pub const comment_tokens: []const struct { exts: []const []const u8, token: []const u8 } = &.{
.{ .token = "//", .exts = &.{ ".zig", ".zon", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".rs", ".go", ".java", ".scala", ".sc", ".kt", ".kts", ".cs", ".csx", ".php", ".pas", ".pp", ".p", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".swift", ".dart" } },
.{ .token = "#", .exts = &.{ ".py", ".pyw", ".sh", ".bash", ".zsh", ".rb", ".rake", ".ex", ".exs", ".ps1", ".psm1", ".psd1", ".pl", ".pm", ".r", ".jl", ".nix", ".toml", ".yaml", ".yml", ".cmake", ".mk", ".tf" } },
.{ .token = "--", .exts = &.{ ".lua", ".hs", ".lhs", ".elm", ".sql", ".adb", ".ads", ".ada" } },
.{ .token = ";", .exts = &.{ ".clj", ".cljs", ".cljc", ".edn", ".el", ".lisp", ".scm", ".asm", ".s" } },
.{ .token = "%", .exts = &.{ ".erl", ".hrl", ".tex", ".cls", ".sty" } },
.{ .token = "!", .exts = &.{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" } },
.{ .token = "\"", .exts = &.{ ".vim", ".vimrc" } },
};
/// What an armed search writes into the tag tail — and the ONLY record of
/// which search it is: Enter reads the marker back (submitSearch) instead of
/// pardes carrying a second piece of pane state per command. The pattern is
/// everything past the `/`, so a pattern may itself contain slashes; the word
/// before it is the builtin's own name, so the armed tag reads as the command
/// it will run. The bare `/` has no name — it searches the pane itself.
pub const search_marker = " /";
pub const find_marker = " Find /";
pub const grep_marker = " Grep /";
pub const rename_marker = " Rename /";
pub const symbol_marker = " WsSymbols /";
/// helix `s` / `S`. The only two markers whose word is NOT a builtin — there
/// is no Select/Split command to run from a tag, they name the key that armed
/// the input so the tag still reads as what it is about to do. They also mark
/// the one input that previews as you type (pardes.zig, previewSelRegex).
pub const select_marker = " Select /";
pub const split_marker = " Split /";
/// Output-buffer names (acme's +Errors). Cosmetic now, and deliberately so: a
/// buffer is DERIVED from the command that opened it (output_pane.traits), and
/// nothing identifies one by matching this text any more — renaming any of
/// these changes only what you read in a tag.
pub const search_buffer = "+Search";
pub const help_buffer = "+Help";
pub const jumps_buffer = "+Jumps";
pub const themes_buffer = "+Themes";
pub const fonts_buffer = "+Fonts";
pub const hover_buffer = "+Hover";
pub const lsp_buffer = "+Lsp";
// ============================================================================
// PART 3 — THE HELIX KEYMAP. READ THIS BEFORE RETARGETING ANYTHING BELOW.
//
// These are not free choices. `zig build hxdiff` (360 cases) and
// `zig build hxparity` (440 cases) are DIFFERENTIAL suites: they drive a real
// helix and this core with the same keystrokes and compare the results, and a
// mismatch is a failure, not a diff to accept. Moving a key here therefore
// breaks the build until the divergence is written down as a waiver with a
// reason — which is the correct workflow for a DELIBERATE divergence (Alt-c
// above is one) and an alarm for an accidental one.
//
// Everything in PART 1 is outside that contract and free to move.
// ============================================================================
// ---- modal prefixes ----
// These are STORED — pane.pending / pending2 / find_op hold the codepoint
// ITSELF until the next key completes the sequence, and the continuation reads
// it back — so they are bare codepoints rather than Chords, and pardes.zig
// matches them with `isPrefix` instead of `hit`. A prefix must therefore be an
// unmodified printable key. Untyped so they compare against both the u21 and
// the u8 fields that hold them.
pub const goto_prefix = 'g';
pub const view_prefix = 'z';
pub const match_prefix = 'm';
pub const replace_prefix = 'r';
pub const next_prefix = ']';
pub const prev_prefix = '[';
// f/F/t/T: the stored byte IS the operator — `f`/`t` mean forward and `t`/`T`
// mean stop short — so the four are read back as values, not just matched.
pub const find_char_fwd = 'f';
pub const find_char_back = 'F';
pub const till_char_fwd = 't';
pub const till_char_back = 'T';
/// repeat the last f/F/t/T
pub const repeat_find: []const Chord = &.{.{ .cp = '.', .alt = true }};
// ---- motion ----
pub const move_left: []const Chord = &.{ .{ .cp = 'h' }, .{ .cp = Key.left } };
pub const move_right: []const Chord = &.{ .{ .cp = 'l' }, .{ .cp = Key.right } };
pub const move_down: []const Chord = &.{ .{ .cp = 'j' }, .{ .cp = Key.down } };
pub const move_up: []const Chord = &.{ .{ .cp = 'k' }, .{ .cp = Key.up } };
pub const next_word_start: []const Chord = &.{.{ .cp = 'w' }};
pub const prev_word_start: []const Chord = &.{.{ .cp = 'b' }};
pub const next_word_end: []const Chord = &.{.{ .cp = 'e' }};
pub const next_long_word_start: []const Chord = &.{.{ .cp = 'W' }};
pub const prev_long_word_start: []const Chord = &.{.{ .cp = 'B' }};
pub const next_long_word_end: []const Chord = &.{.{ .cp = 'E' }};
pub const line_start: []const Chord = &.{ .{ .cp = '0' }, .{ .cp = Key.home } };
pub const line_end: []const Chord = &.{ .{ .cp = '$' }, .{ .cp = Key.end } };
pub const line_first_nonws: []const Chord = &.{.{ .cp = '^' }};
/// helix goto_line: only acts WITH a count (bare G is a no-op; `ge` is
/// goto-last-line)
pub const goto_line: []const Chord = &.{.{ .cp = 'G' }};
/// grapheme motion in a ONE-LINE context (a tag, the topbar): the arrows only.
/// A tagline spends h/l on the layout (window_keys) and the topbar answers
/// them itself (topbar_left/right), so the letters are not in this vocabulary.
pub const line_move_left: []const Chord = &.{.{ .cp = Key.left }};
pub const line_move_right: []const Chord = &.{.{ .cp = Key.right }};
// ---- paging and the view ----
pub const half_page_down: []const Chord = &.{.{ .cp = 'd', .ctrl = true }};
pub const half_page_up: []const Chord = &.{.{ .cp = 'u', .ctrl = true }};
pub const page_down: []const Chord = &.{ .{ .cp = 'f', .ctrl = true }, .{ .cp = Key.page_down } };
pub const page_up: []const Chord = &.{ .{ .cp = 'b', .ctrl = true }, .{ .cp = Key.page_up } };
/// under `z`: put the cursor's line at the top / centre / bottom of the view
pub const view_top: []const Chord = &.{.{ .cp = 't' }};
pub const view_center: []const Chord = &.{ .{ .cp = 'z' }, .{ .cp = 'c' } };
pub const view_bottom: []const Chord = &.{.{ .cp = 'b' }};
/// under `z`: scroll the view one line, cursor snapped to the scrolloff edge
pub const view_scroll_down: []const Chord = &.{ .{ .cp = 'j' }, .{ .cp = Key.down } };
pub const view_scroll_up: []const Chord = &.{ .{ .cp = 'k' }, .{ .cp = Key.up } };
// ---- under `g` (goto) ----
pub const goto_file_start: []const Chord = &.{.{ .cp = 'g' }};
pub const goto_last_line: []const Chord = &.{.{ .cp = 'e' }};
pub const goto_line_start: []const Chord = &.{.{ .cp = 'h' }};
pub const goto_line_end: []const Chord = &.{.{ .cp = 'l' }};
pub const goto_first_nonws: []const Chord = &.{.{ .cp = 's' }};
pub const goto_line_down: []const Chord = &.{.{ .cp = 'j' }};
pub const goto_line_up: []const Chord = &.{.{ .cp = 'k' }};
pub const goto_column: []const Chord = &.{.{ .cp = '|' }};
/// view-relative rows (helix goto_window)
pub const goto_view_top: []const Chord = &.{.{ .cp = 't' }};
pub const goto_view_center: []const Chord = &.{.{ .cp = 'c' }};
pub const goto_view_bottom: []const Chord = &.{.{ .cp = 'b' }};
// helix's five LSP gotos, all under `g` and nowhere else. They are motions,
// not builtins — a motion has no business being a word you can middle-click,
// which is why they are not in the language group under `SPC l`.
pub const goto_definition: []const Chord = &.{.{ .cp = 'd' }};
pub const goto_declaration: []const Chord = &.{.{ .cp = 'D' }};
pub const goto_type_definition: []const Chord = &.{.{ .cp = 'y' }};
pub const goto_implementation: []const Chord = &.{.{ .cp = 'i' }};
pub const goto_references: []const Chord = &.{.{ .cp = 'r' }};
// ---- under `m` (match) ----
/// `mm` acts at once, so it is an ordinary chord
pub const match_bracket: []const Chord = &.{.{ .cp = 'm' }};
// The other five are SUB-prefixes: each waits for a textobject or surround
// character (`mi(`, `mr[{`), so pardes stores them the way it stores `m` and
// they are bare codepoints for the same reason as the block above.
pub const match_inside = 'i';
pub const match_around = 'a';
pub const surround_add = 's';
pub const surround_replace = 'r';
pub const surround_delete = 'd';
// ---- under `]` / `[` ----
pub const goto_paragraph: []const Chord = &.{.{ .cp = 'p' }};
pub const add_newline: []const Chord = &.{.{ .cp = ' ' }};
/// step the diagnostics list, asking for one if it is not up yet
pub const goto_diagnostic: []const Chord = &.{.{ .cp = 'd' }};
/// ]D / [D — the last / the first
pub const goto_diagnostic_end: []const Chord = &.{.{ .cp = 'D' }};
// ---- insert entry ----
pub const insert: []const Chord = &.{.{ .cp = 'i' }};
pub const append: []const Chord = &.{.{ .cp = 'a' }};
pub const insert_line_start: []const Chord = &.{.{ .cp = 'I' }};
pub const insert_line_end: []const Chord = &.{.{ .cp = 'A' }};
pub const open_below: []const Chord = &.{.{ .cp = 'o' }};
pub const open_above: []const Chord = &.{.{ .cp = 'O' }};
// ---- selection ----
pub const select_mode: []const Chord = &.{.{ .cp = 'v' }};
pub const select_line: []const Chord = &.{.{ .cp = 'x' }};
pub const select_line_bounds: []const Chord = &.{.{ .cp = 'X' }};
pub const shrink_to_line_bounds: []const Chord = &.{.{ .cp = 'x', .alt = true }};
pub const collapse_selection: []const Chord = &.{.{ .cp = ';' }};
pub const flip_selection: []const Chord = &.{.{ .cp = ';', .alt = true }};
pub const select_all: []const Chord = &.{.{ .cp = '%' }};
// ---- multiple cursors ----
//
// helix's Selection is a LIST of ranges with a primary index, and these ten
// keys are the ones that act on the list rather than on the text: every other
// key is replayed once per range instead (pardes.zig, replaySels). Alt-C is
// Alt-SHIFT-c and so does not collide with pane_to_new_column's Alt-c — `hit`
// compares the codepoint, and `C` is `C`.
pub const copy_sel_below: []const Chord = &.{.{ .cp = 'C' }};
pub const copy_sel_above: []const Chord = &.{.{ .cp = 'C', .alt = true }};
pub const keep_primary_sel: []const Chord = &.{.{ .cp = ',' }};
pub const remove_primary_sel: []const Chord = &.{.{ .cp = ',', .alt = true }};
pub const rotate_sel_fwd: []const Chord = &.{.{ .cp = ')' }};
pub const rotate_sel_back: []const Chord = &.{.{ .cp = '(' }};
pub const split_sel_newline: []const Chord = &.{.{ .cp = 's', .alt = true }};
pub const merge_sels: []const Chord = &.{.{ .cp = '-', .alt = true }};
pub const merge_consecutive_sels: []const Chord = &.{.{ .cp = '_', .alt = true }};
pub const trim_sels: []const Chord = &.{.{ .cp = '_' }};
// The other two list-making keys: a REGEX turns each range into many. Both
// arm the tag input above (select_marker / split_marker) instead of doing
// anything immediately, so `s` and `S` are the only normal-mode keys whose
// effect lands a keystroke later, on Enter — or live, as you type.
// `s` is free here despite `gs` (goto_first_nonws) also being `s`: a pending
// prefix is matched by the stored codepoint, never by these chords.
pub const select_regex: []const Chord = &.{.{ .cp = 's' }};
pub const split_regex: []const Chord = &.{.{ .cp = 'S' }};
// ---- edits ----
pub const delete: []const Chord = &.{.{ .cp = 'd' }};
pub const delete_noyank: []const Chord = &.{.{ .cp = 'd', .alt = true }};
pub const change: []const Chord = &.{.{ .cp = 'c' }};
pub const yank: []const Chord = &.{.{ .cp = 'y' }};
pub const replace_with_yank: []const Chord = &.{.{ .cp = 'R' }};
/// helix: the DEFAULT register, not the system clipboard — most terminals
/// refuse the OSC 52 read, so a round trip would never come back
pub const paste_after: []const Chord = &.{.{ .cp = 'p' }};
pub const paste_before: []const Chord = &.{.{ .cp = 'P' }};
pub const switch_case: []const Chord = &.{.{ .cp = '~' }};
pub const to_lowercase: []const Chord = &.{.{ .cp = '`' }};
pub const to_uppercase: []const Chord = &.{.{ .cp = '`', .alt = true }};
pub const join_lines: []const Chord = &.{.{ .cp = 'J' }};
pub const indent: []const Chord = &.{.{ .cp = '>' }};
pub const unindent: []const Chord = &.{.{ .cp = '<' }};
/// helix's format_selections — its neighbour on the keyboard and in the keymap
pub const format: []const Chord = &.{.{ .cp = '=' }};
pub const increment: []const Chord = &.{.{ .cp = 'a', .ctrl = true }};
pub const decrement: []const Chord = &.{.{ .cp = 'x', .ctrl = true }};
pub const undo: []const Chord = &.{.{ .cp = 'u' }};
pub const redo: []const Chord = &.{.{ .cp = 'U' }};
/// helix `toggle_comments`: comment or uncomment every line the selection
/// touches, with `comment_tokens` above choosing the token. Ctrl-c reaches a
/// terminal pane only in NORMAL mode — raw tty forwards it to the program,
/// where it is still SIGINT.
pub const comment_toggle: []const Chord = &.{.{ .cp = 'c', .ctrl = true }};
/// In body normal mode, clear modal residue and run Toggleterm (the same
/// builtin as `SPC w t`). Elsewhere: leave insert mode; abandon a leader path,
/// tag, armed search or the topbar; raw tty mode forwards it to the program.
pub const escape: []const Chord = &.{.{ .cp = Key.escape }};
// ---- insert mode ----
// The three helix aliases: normalized to the base key and re-dispatched, so
// they behave identically to it everywhere downstream.
pub const insert_backspace_alias: []const Chord = &.{.{ .cp = 'h', .ctrl = true }};
pub const insert_enter_alias: []const Chord = &.{.{ .cp = 'j', .ctrl = true }};
pub const insert_delete_alias: []const Chord = &.{.{ .cp = 'd', .ctrl = true }};
/// helix insert-mode kills. Ctrl-w is also the WINDOW prefix in normal/tty —
/// insert mode wins it, which is helix's own arrangement.
pub const delete_word_backward: []const Chord = &.{ .{ .cp = 'w', .ctrl = true }, .{ .cp = Key.backspace, .alt = true } };
pub const delete_word_forward: []const Chord = &.{ .{ .cp = 'd', .alt = true }, .{ .cp = Key.delete, .alt = true } };
pub const kill_to_line_start: []const Chord = &.{.{ .cp = 'u', .ctrl = true }};
pub const kill_to_line_end: []const Chord = &.{.{ .cp = 'k', .ctrl = true }};
|