summaryrefslogtreecommitdiff
path: root/src/config.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 00:26:12 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitea228d1b832401db8efe33a5db4099ba41d42509 (patch)
tree10a5a7514eca150ba90c188a109c3c604607ef9b /src/config.zig
parent19f7322100062b7c1adcde3376063ce6c1d8c72d (diff)
downloadpardes-ea228d1b832401db8efe33a5db4099ba41d42509.tar.gz
pardes-ea228d1b832401db8efe33a5db4099ba41d42509.zip
config.zig: every binding and every piece of Look syntax in one file
Retargeting a key, a mouse chord or a spelling is now an edit to src/config.zig and nothing else. Three parts in reading order: pardes's own bindings (where a reader lands), the Look/Exec syntax, then the helix keymap under a banner saying hxdiff/hxparity are differential suites against real helix, so a key moved there is a divergence and not a tweak. The bindings are data a comptime loop can walk, because the builtin index is going to walk them. Duplication this cut: is/isC/isA became one hit(key, chords) over ~132 call sites, and a binding being a LIST collapses the letter-or-arrow chains; swap_enter_tab is gone, replaced by look_key/exec_key that can point anywhere; the four focus builtins' h/j/k/l lived hardcoded in two places and is now one table. No golden moved.
Diffstat (limited to 'src/config.zig')
-rw-r--r--src/config.zig498
1 files changed, 498 insertions, 0 deletions
diff --git a/src/config.zig b/src/config.zig
new file mode 100644
index 00000000..141d8376
--- /dev/null
+++ b/src/config.zig
@@ -0,0 +1,498 @@
+//! 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.
+//!
+//! 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 = '?';
+
+/// SPC leader: ONE key path per builtin, the whole remapping surface. An
+/// EnumArray, so a new enum field is a compile error until it has a path.
+/// Groups are just shared first letters (f files, h docs, c columns, t
+/// toggles, s session, l language, w windows).
+pub const leader_path = std.EnumArray(Builtin, []const u8).init(.{
+ .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 here,
+ // which frees `w` for the window group (SPC w h/j/k/l) to move into.
+ .Save = "fs",
+ .Find = "ff",
+ .Grep = "fg",
+ .Tutor = "ht",
+ .Newcol = "cn",
+ .Delcol = "cd",
+ .Debug = "td",
+ .Colors = "tc",
+ .NextColor = "tn",
+ .Crt = "tr",
+ // 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 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;
+
+// ---- 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 }};
+
+/// `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.
+//
+// The two searches go LAST (before the optional `Restore <path>`), and next to
+// each other: they are one pair — Find matches file NAMES, Grep their CONTENTS.
+pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep 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 tag prefix's mode word — live chrome, not text you own
+pub const tag_normal = "NOR";
+pub const tag_insert = "INS";
+pub const tag_tty = "TTY";
+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;
+
+// ---- 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
+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,
+ };
+}
+
+/// 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 = ':';
+
+/// `@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 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 /";
+
+/// Output-buffer names (acme's +Errors). Load-bearing beyond cosmetics: n/N
+/// finds its results buffer by name, so the writer and the stepper must agree.
+pub const search_buffer = "+Search";
+pub const help_buffer = "+Help";
+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 = '%' }};
+
+// ---- 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' }};
+
+/// Exit select mode / drop pending prefixes and the count; leave insert mode;
+/// abandon a tag, an armed search or the topbar. On an OUTPUT buffer there is
+/// nothing worth keeping, so Esc there is the Del builtin instead.
+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 }};