From ea228d1b832401db8efe33a5db4099ba41d42509 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 31 Jul 2026 00:26:12 -0300 Subject: 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. --- src/config.zig | 498 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 src/config.zig (limited to 'src/config.zig') 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.` 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 `` 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 `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 ` 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- 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 `), 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` = 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 }}; -- cgit v1.3