//! 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.` 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.registry.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 = ""; /// 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 `` 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", // A terminal-pane tag owns this presentation switch. It deliberately // has no global leader path: executing the word beside that terminal // makes the pane-local scope visible at the point of use. .Filter = null, .Kill = "k", // THE CLIPBOARD MENU, on helix's own five letters and nowhere else. // These are the only paths in the table that keep their helix spelling // unprefixed, and they can: `y` `Y` `p` `P` `R` were free at the top // level, and moving them into a group would have made the one thing // here that IS helix's leader stop looking like it. // // Bare `y`/`p`/`P`/`R` remain the DEFAULT register — that split is the // whole design (see builtins.zig's clipboard section), and it is why // an ordinary delete no longer reaches past the editor. .ClipYank = "y", .ClipYankMain = "Y", .ClipPaste = "p", .ClipPasteBefore = "P", .ClipReplace = "R", // 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", .Newtty = "nt", .Find = "ff", .Grep = "fg", // the config FILE joins the file group: `SPC f c` says where pardes // read (or would read) its startup commands from. .Config = "fc", .Tutor = "ht", .Changelog = "hc", .Newcol = "cn", .Delcol = "cd", .Joincol = "cj", .Debug = "td", // `Msg` takes the text to post, so it has no path, for the reason // `Theme` and the two acme verbs below have none: a key path names a // builtin and can never carry an argument. Bare `Msg` still runs — it // reports itself through the error path. .Msg = null, .Colors = "tc", .Wrap = "tw", .Tagbottom = "tb", .NextColor = "tn", // 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, // ...and `Shell` takes the name of a binary, so it has none either .Shell = 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", // 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 font setting and picker, in the same `t` group as the theme // picker they mirror. Their explicit availability metadata means tty and // web Builtin enums have no fields for them. `Font` takes a NAME and // `TaglineSize` takes a percentage, so neither has a path: a leader chord // cannot carry either argument. if (builtins.capabilities.font_picker) { table.set(.FontSel, "tf"); table.set(.Font, null); table.set(.TaglineSize, null); } // Panel transitions are implemented by the fixed cell grid in TTY and by // shader-capable native GUI shells. The DOM web shell does not advertise // them until it has an equivalent renderer. if (builtins.capabilities.panel_transitions) { table.set(.PanelSlide, "as"); table.set(.PanelZoom, "az"); table.set(.PanelDissolve, "ad"); table.set(.PanelAscii, "aa"); table.set(.PanelVertical, "av"); table.set(.PanelEdges, "ae"); table.set(.PanelFall, "af"); table.set(.PanelWave, "aw"); table.set(.PanelCurtain, "ac"); table.set(.PanelScramble, "ar"); table.set(.PanelType, "at"); } if (builtins.capabilities.scene_shaders) { table.set(.Crt, "tr"); table.set(.Ripple, "tR"); table.set(.Glitch, "tg"); } // Takes the name of an effect builtin; a leader path cannot carry it. // The web build has no runnable effect argument, so it has no command, // help row, dispatcher case, or leader entry for EffectCode either. if (builtins.EffectCode.enabled) table.set(.EffectCode, null); // Native-only filesystem theme commands. ThemeFile needs an operand and // DumpThemes is intentionally occasional, so both stay word-executed // rather than spending leader chords. if (pardes.hosted) { table.set(.ThemeFile, null); table.set(.DumpThemes, null); } // ...and the one native word that DOES earn a chord. Gated on `can_attach` // and NOT on `hosted`, because this table may only name a builtin that // exists: macOS is hosted but never polls `takeAttach`, so the two words // below are compiled out there and naming them would be a compile error — // which is the good outcome, and the reason the predicate exists. if (pardes.can_attach) { // In the `s` session group beside Dump and Restore. Bare Attach means // "whichever detached session is there", which is the whole case worth // a key; the named form is typed, like every other builtin that takes // an operand. Safe to press by accident, uniquely among the three: it // connects before it swaps, so nothing to attach to costs you a // message row. table.set(.Attach, "sa"); // ...and the way back out, which is where the group runs out of // letters: `sd` has been Dump's since before there was anything to // detach from, and Detach is not worth breaking that muscle memory // for. A capital where the lowercase is taken is what this table // already does one group over (`lS` beside `ls`, `lD` beside `ld`). table.set(.Detach, "sD"); } // The bare-metal memory words. Peek, Poke and Hexdump all take an ADDRESS, // so none of them can have a leader path for the reason Theme and Msg have // none: a key path names a builtin and can never carry an operand. if (builtins.Peek.enabled) { table.set(.Peek, null); table.set(.Poke, null); table.set(.Hexdump, null); table.set(.Gpio, null); } // The 9P client word. Takes a dial AND a path, so it has no leader path // for the reason the three above have none, twice over. Its gate is the // presence of unix sockets, which is narrower than `hosted`. if (builtins.@"9p".enabled) table.set(.@"9p", null); // The pane-local PDF commands exist only in MuPDF builds through their // explicit registry availability, so name their paths inside the same // comptime branch. PdfTint/PdfFit retain their display slots and // PdfSections takes the mnemonic `s` between them. if (pardes.pdf_enabled) { table.set(.PdfTint, "ti"); table.set(.PdfSections, "ts"); 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 ` 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' }}; /// LEAVE the pane you are in — the `Last` builtin — from a pane whose own /// plain Escape already means something else. A terminal in raw tty mode has /// had it since it existed (tty_toggle_alt below, same chord, same job); a PDF /// needs it because Escape there cancels the selection and the search overlay /// without moving focus out of the document you are reading. /// /// On a host that reports no modifier on Escape it arrives as a plain Escape /// and still means what Escape always means in that pane. pub const leave_pane: []const Chord = &.{.{ .cp = Key.escape, .shift = true }}; // ---- 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 }}; /// Paste INTO the program a tty pane is running. `SPC p` and the acme 1-3 /// chord cannot be reached there — the pty owns every keystroke and every /// button — so raw tty mode needs its own pair, and these are the two a /// terminal user already has in their hands. /// /// The split is the one the whole clipboard design rests on: Ctrl-V types the /// DEFAULT register (what `y` put there, no round trip, no desktop involved), /// Ctrl-Shift-V asks for the SYSTEM clipboard. Two spellings for the second /// because a host may or may not fold the shift into the codepoint, and it /// must be tested BEFORE the first: `hit` ignores an unasked shift, so plain /// Ctrl-V matches a shifted key too. /// /// What this TAKES: forwardKey encoded both as the same byte, 0x16, so /// Ctrl-Shift-V was a duplicate ^V and costs nothing to claim. Ctrl-V was /// readline's quoted-insert, and that one is now unreachable in a tty pane — /// the trade a terminal user expects, and one line to give back. pub const tty_paste: []const Chord = &.{.{ .cp = 'v', .ctrl = true }}; pub const tty_paste_clipboard: []const Chord = &.{ .{ .cp = 'v', .ctrl = true, .shift = true }, .{ .cp = 'V', .ctrl = true }, }; /// What LEAVING raw tty mode hides on the shell's prompt rows. /// /// A prompt is CHROME. `user@host ~/src $` is redrawn on every keystroke, says /// nothing a second time, and is never what you want to select, look at or /// edit — so blanking it is most of what turns a scrollback into a readable /// document, and pardes has always done it (OSC 133 is how it knows). /// /// What it USED to take with it was the command you had typed at that prompt, /// because the two share a grid row and the row was the unit. That command is /// content: the one thing on the row worth keeping, and the thing you reach /// for `b` to get at in the first place. OSC 133 marks the two separately — /// per CELL, not just per row — so `.prompt` blanks the prompt's own cells and /// leaves the input sitting in the COLUMNS it really occupies. Those columns /// are not cosmetic: clicking the command in normal mode and pressing the /// toggle carries the click into the shell's own cursor (promptClickMove), /// which counts them. /// /// `.prompt_and_input` is the older behaviour, kept for anyone who wants a /// terminal to read as output and nothing else. pub const tty_blank: enum { prompt, prompt_and_input } = .prompt; // ---- 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 the scene/panel effects are NOT here: they are display switches // you flip and forget, and a bar read every frame should not spend width on // them now that their leader paths are discoverable through Help. NextColor // stays — it is the one you cycle repeatedly, so a click beats a three-key // path. // // Ordered by day-to-day usefulness, in stable functional groups: creation // (New/Newcol), search/navigation (Find/Grep), learning (Help/Tutor), session // persistence (Dump), appearance/diagnostics (NextColor/Debug), then the one // destructive global action (Kill) exactly last. Find and Grep stay adjacent: // the former matches file NAMES, the latter their CONTENTS. // // Help has to be here even though it is secondary. 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: Help works in tty mode, which earns its width. pub const topbar_str = "New Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill"; /// The default editable tail of a pane's tag, per kind. Save LEADS wherever the /// pane holds text of its own to write — a file, an output buffer, a terminal's /// scrollback — because `:` parks at the tail boundary and the established /// `:w` spelling walks to the first word from there. Images and PDFs get /// the plain tail: their bytes on disk already are exactly what they are, so /// there is nothing of the pane's own left to save. Terminals alone expose /// Filter, the pane-local theme-keyed colour projection. pub const pane_builtins_str = "New Newtty Del"; pub const file_pane_builtins_str = "Save New Newtty Del"; pub const terminal_pane_builtins_str = "Save New Newtty Del Filter"; /// Columns kept clear to the RIGHT of a tagline's builtins. The path stays at /// the left edge and the builtins are pushed over to end this far short of the /// pane's, which leaves somewhere to type: a word executed from the tagline is /// how you run anything here, and with the builtins hard against the edge /// there was nowhere to put one without first making room. /// /// The gap that does the pushing is made of ordinary spaces inside the tag, so /// both it and this run are editable text — see Pardes.tagGap. Widen it and /// every untouched tagline reflows on the next frame; taglines you have /// already edited keep the spacing you left them with. pub const tag_right_pad: u16 = 20; /// 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. NORMAL is a SPACE, and the empty box is the point: a pane at rest has /// nothing waiting to eat what you type, and two thirds of the screen wearing /// a bullet would be two thirds of the screen saying nothing loudly. (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` = 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' }}; /// Helix `|`: in body normal mode, pipe every file selection through one /// command typed in the pane's visible tag-tail input. pub const pipe_selection: []const Chord = &.{.{ .cp = '|' }}; /// 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; /// Enabled by default: rest the pointer over text for this many animation /// frames before showing the exact span a right-click Look would expand. /// Repeated motion inside the same cell does not restart the count; moving to /// another cell does. Set to `null` to compile the preview out while retaining /// ordinary mouse hover and resize-handle hints. pub const look_preview_delay_frames: ?u16 = 2; /// GUI shells rasterize pane-tag text at this percentage of the body face /// while retaining the same cell geometry. TTY ignores the visual role. pub const gui_tagline_font_percent: u8 = 82; comptime { if (gui_tagline_font_percent == 0 or gui_tagline_font_percent > 100) @compileError("config.gui_tagline_font_percent must be in 1...100"); } /// Physical-pixel rule between the global topbar and pane taglines in the /// SDL GUI. Their smaller font bands retain body-sized grid rows; without /// an explicit join, centering both bands leaves the two unused half-bands /// touching and makes a wide strip of the window background show through. /// Zero disables the rule and joins the two bands directly. pub const gui_topbar_pane_border_px: u8 = 1; /// Fixed RGB for that rule, or null to follow the active theme's scrollbar /// track. The themed default stays quiet across light and dark themes while a /// build that wants a deliberate accent can pin one here. pub const gui_topbar_pane_border_rgb: ?[3]u8 = null; /// 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 ---- /// What a terminal pane runs until someone says otherwise (the Shell builtin, /// or a `Shell ` line in the config file). A bare NAME, resolved against /// the usual bin directories at spawn time — so a machine without it falls /// back rather than opening a pane that dies at exec. pub const default_shell = "fish"; /// the file pane's line-number gutter, in columns pub const PREFIX_W: u16 = 5; /// Display width of a literal tab in every Surface-backed frontend. This is a /// compile-time setting: edit it and rebuild; zero cannot advance the renderer. pub const tab_width: u16 = 4; comptime { if (tab_width == 0) @compileError("config.tab_width must be greater than zero"); } /// soft wrap (the Wrap builtin): the glyph a wrapped row ends with, in the one /// column bodyText keeps free for it. A break is the one thing about a wrapped /// line you cannot see — the text simply continues, and a missing line number /// on the row below is an absence, which is a poor thing to read a document by. /// So the break says so at the point it happens, in the chrome's own colour /// because it is not in the file. pub const wrap_marker = "↩"; /// 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 { // Non-ASCII bytes belong to their UTF-8 word as a unit. Bounds are byte // offsets, so accepting every high byte keeps Unicode paths/identifiers // intact instead of returning a slice through one codepoint. return c >= 0x80 or 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", ".typ", ".typst", ".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 /"; /// Save on a pane with no file of its own yet — an output buffer or a terminal: /// the tail is the whole PATH to write (no `/` separator, since a path is made /// of them), prefilled with the pane's directory so only a filename need be /// typed. pub const save_marker = " Save "; /// 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 /"; /// The `|` prompt is not an executable tag word: the marker only makes the /// pending shell filter visible, and everything after it is preserved as the /// exact command passed to `/bin/sh -c`. pub const pipe_marker = " |"; /// 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 config_buffer = "+Config"; pub const effect_code_buffer = "+EffectCode"; pub const jumps_buffer = "+Jumps"; pub const themes_buffer = "+Themes"; pub const fonts_buffer = "+Fonts"; pub const pdf_sections_buffer = "+PdfSections"; pub const hover_buffer = "+Hover"; pub const lsp_buffer = "+Lsp"; pub const changelog_buffer = "+Changelog"; /// What `9p ` opens a remote file into. NOT the remote path: an /// output buffer's name comes off the command that filled it, and the path is /// the command's ARGUMENT, which is what makes two remote files two panes. pub const ninep_buffer = "+9p"; /// The two memory windows a bare-metal build's Peek and Hexdump render. Absent /// from every hosted build along with the builtins that name them. pub const peek_buffer = "+Peek"; pub const hexdump_buffer = "+Hexdump"; pub const gpio_buffer = "+Gpio"; /// The empty buffer New and Newcol open: no file behind it yet, so Save asks /// for a path (prefilled with the inherited directory). pub const scratch_buffer = "+New"; /// acme's own `+Errors`, and the one output buffer no keystroke opens: a /// script writes it, through a pane's `errors` file or the top-level `cons` /// (src/acmefs.zig). pub const errors_buffer = "+Errors"; // ============================================================================ // PART 3 — THE HELIX KEYMAP. READ THIS BEFORE RETARGETING ANYTHING BELOW. // // These are not free choices. `zig build hxdiff` (481 cases) and // `zig build hxparity` (561 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 ---- // // Every one of these reads or writes the DEFAULT register and only that. // The system clipboard is five separate words on `SPC y Y p P R`, which is // helix's split and the reason `d` cannot silently eat what you copied out of // a browser. See leader_path above and builtins.zig's clipboard section. 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' }}; 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 Last (the same builtin as /// `SPC j j`): the pane you were in before this one, whichever it was. Held /// down it alternates between two panes — two files, or a file and its shell. /// 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 }};