summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig764
1 files changed, 354 insertions, 410 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index f6168449..5412569b 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -12,6 +12,12 @@
//! src/ layout: the core lies flat at src/, and every SUBDIRECTORY is one
//! backend (tty/ gui/ lsp/) — so a file being in no directory at all is what
//! says it is core, and nothing needs a header to claim it.
+//!
+//! Not one key, button or piece of Look syntax is spelled in this file: every
+//! one of them is a named binding in config.zig, and `hit`/`isPrefix` below are
+//! the only two matchers. That is so retargeting anything is an edit in one
+//! file, and so a later builtin can enumerate the bindings the way Help already
+//! enumerates the leader.
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
const uucode = @import("uucode");
@@ -23,6 +29,7 @@ const term_pane = @import("term_pane.zig");
const file_pane = @import("file_pane.zig");
const output_pane = @import("output_pane.zig");
const builtins = @import("builtins.zig");
+pub const config = @import("config.zig");
pub const image = @import("image.zig");
pub const dump = @import("dump.zig");
pub const lsp = @import("lsp/lsp.zig");
@@ -30,32 +37,18 @@ pub const lsp = @import("lsp/lsp.zig");
pub const Platform = enum { tty, gui, web };
pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config").platform));
+// The capacities and the two heights that are STRUCTURE, not taste: the
+// fixed-size pane/column arrays, and the fact that the topbar and a tag are
+// one row each (nothing here works at any other value). The layout numbers
+// that ARE taste — the gutter, the line-number prefix, scrolloff, the pane
+// minimums — live in config.zig with everything else a user retargets.
pub const MAX_PANES = 16;
pub const MAX_COLS = 6;
pub const TOPBAR_H: u16 = 1;
-pub const GUTTER: u16 = 2;
pub const BOX_H: u16 = 1;
-pub const MINW: u16 = 10;
-pub const MINH: u16 = 3;
-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)
-const scroll_off = 3;
-/// which key runs which acme chord — everywhere it exists: normal mode, the
-/// tag, and the `:` command line. false (the default) is the original binding,
-/// Enter looks / Tab executes (so a command runs with `:w<Tab>`); true swaps
-/// them for the vim reading.
-pub const swap_enter_tab = false;
-// 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
-// — and every word's start column is what a topbar click dispatches on, so
-// appending is the edit that leaves the bar people already know where it was.
-pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep Help";
-const pane_builtins_str = "Del";
-const file_pane_builtins_str = "Save Del";
-const pane_tail = " " ++ pane_builtins_str;
-const file_pane_tail = " " ++ file_pane_builtins_str;
+const pane_tail = " " ++ config.pane_builtins_str;
+const file_pane_tail = " " ++ config.file_pane_builtins_str;
// Builtins: executing the name (middle-click / Tab) runs it through the ONE
// dispatcher (runBuiltin, reached from actOnSelection), no matter where the
@@ -69,68 +62,6 @@ const file_pane_tail = " " ++ file_pane_builtins_str;
// sits with the other two comptime folds (leader_rows, the topbar check).
const Builtin = builtins.Builtin();
-/// 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); `?` is Help's path and is honored at ANY
-/// depth — it lists what the prefix typed so far can still reach.
-const leader_path = std.EnumArray(Builtin, []const u8).init(.{
- .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.
- .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 leader tree, flattened: one row per builtin SORTED BY PATH, so every
/// prefix's subtree is a contiguous run — which is all the two runtime
/// readers need. The matcher asks "exact hit? still a prefix of something?"
@@ -145,9 +76,9 @@ pub const leader_rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = blk: {
var rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = undefined;
// the widest spaced-out path ("h t" = 3), so the names line up
var keyw: usize = 0;
- for (std.enums.values(Builtin)) |b| keyw = @max(keyw, 2 * leader_path.get(b).len - 1);
+ for (std.enums.values(Builtin)) |b| keyw = @max(keyw, 2 * config.leader_path.get(b).len - 1);
for (std.enums.values(Builtin), 0..) |b, i| {
- const path = leader_path.get(b);
+ const path = config.leader_path.get(b);
var keys: []const u8 = "";
for (path, 0..) |c, k| {
if (k > 0) keys = keys ++ " ";
@@ -171,26 +102,12 @@ pub const leader_rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = blk: {
break :blk rows;
};
-// 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 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.
+// config.topbar_str is a HAND-PICKED subset of the builtins in a fixed order,
+// not a derivation — see it there for why each word is in or out. This is the
+// check that a rename cannot silently rot it.
comptime {
@setEvalBranchQuota(20000); // the tokenizer walks the string one branch per byte, stringToEnum builds a map per name, and both sit behind the decl walk that folds Builtin
- var it = std.mem.tokenizeScalar(u8, topbar_str, ' ');
+ var it = std.mem.tokenizeScalar(u8, config.topbar_str, ' ');
while (it.next()) |w| {
if (std.meta.stringToEnum(Builtin, w) == null) @compileError("topbar word is not a builtin: " ++ w);
}
@@ -496,8 +413,8 @@ pub const Key = struct {
ctrl: bool = false,
alt: bool = false,
/// Only consulted for keys whose codepoint does NOT already carry the
- /// shift (Escape and friends) — `A` is `A`, not shift-`a`, so `is()`
- /// ignores this field on purpose.
+ /// shift (Escape and friends) — `A` is `A`, not shift-`a`, so `hit()`
+ /// ignores this field unless a binding explicitly asks for it.
shift: bool = false,
pub const enter: u21 = 0x0D;
@@ -516,16 +433,40 @@ pub const Key = struct {
pub const delete: u21 = 0xF0009;
};
-fn is(key: Key, cp: u21) bool {
- return key.cp == cp and !key.ctrl and !key.alt;
+/// Does this press match a binding? Every keymap test in the core goes through
+/// here, so the modifier rules are written once instead of once per modifier
+/// (the three is/isC/isA predicates this replaces each spelled out the same
+/// comparison with a different pair of negations).
+///
+/// A binding is a LIST: most have two spellings that must reach the same arm —
+/// `h` and Left, `Ctrl-f` and PageDown — and the `or` chain that used to do
+/// that at every call site is now one loop here.
+fn hit(key: Key, binding: []const config.Chord) bool {
+ for (binding) |c| {
+ if (key.cp != c.cp or key.ctrl != c.ctrl or key.alt != c.alt) continue;
+ // shift is carried in the codepoint for anything printable (`A` is
+ // `A`, not shift-`a`), so it is consulted ONLY when a binding asks for
+ // it — otherwise every letter binding would newly demand shift be up
+ if (c.shift and !key.shift) continue;
+ return true;
+ }
+ return false;
}
-fn isC(key: Key, cp: u21) bool {
- return key.cp == cp and key.ctrl and !key.alt;
-}
+/// `Pane.sel` is indexed by @intFromEnum(button), so this is the SELECT
+/// button's slot: where a sweep lands and where the left half of every acme
+/// chord reads its selection from. Named rather than the bare 0 it used to be
+/// so that config.select_button is genuinely the only place the choice is
+/// made.
+const sel_slot = @intFromEnum(config.select_button);
-fn isA(key: Key, cp: u21) bool {
- return key.cp == cp and !key.ctrl and key.alt;
+/// The same test for a MODAL PREFIX (`g`, `z`, `m`, `mi`, `]`...). config
+/// spells those as bare codepoints rather than Chords because pardes stores
+/// the codepoint itself in pane.pending until the next key completes the
+/// sequence — so this is the one place the two shapes meet, and it is the same
+/// comparison the stored byte gets a few lines later.
+fn isPrefix(key: Key, cp: u21) bool {
+ return key.cp == cp and !key.ctrl and !key.alt;
}
pub const Event = union(enum) {
@@ -845,7 +786,7 @@ pub const Pane = struct {
pub fn ensureCursorVisible(pane: *Pane) void {
// scrolloff margin, shrunk on short panes so the band stays non-empty
- const margin: i32 = @min(scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
+ const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
const off = pane.scroll();
const last = off + @as(i32, pane.rows) - 1;
if (pane.cur_row < off + margin) {
@@ -861,8 +802,8 @@ pub const Pane = struct {
// context around the cursor (wheel-driven hscroll is exempt — it
// never moves the cursor, and a cursor move pulls the view back)
if (pane.file != null) {
- const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, PREFIX_W));
- const hmargin: i32 = @min(scroll_off, @divTrunc(w - 1, 2));
+ const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, config.PREFIX_W));
+ const hmargin: i32 = @min(config.scroll_off, @divTrunc(w - 1, 2));
if (pane.cur_col < pane.hscroll + hmargin)
pane.hscroll = @max(0, pane.cur_col - hmargin)
else if (pane.cur_col > pane.hscroll + w - 1 - hmargin)
@@ -914,7 +855,7 @@ pub const Options = struct {
/// the left column, a terminal on the launch directory in the right
file: ?[]const u8 = null,
file_line: usize = 0,
- tty_toggle: u21 = 'b',
+ tty_toggle: u21 = config.tty_toggle_default,
/// load a dump of another instance instead of spawning shells (acme -l)
load_path: ?[]const u8 = null,
/// Initial grid. Shell contract: for LIVE sessions leave these at the
@@ -1095,8 +1036,8 @@ pub const Pardes = struct {
/// exists — render and click dispatch must agree on this exact string
fn topbar(p: *Pardes, buf: []u8) []const u8 {
if (p.last_dump) |d|
- return std.fmt.bufPrint(buf, topbar_str ++ " Restore {s}", .{d}) catch topbar_str;
- return topbar_str;
+ return std.fmt.bufPrint(buf, config.topbar_str ++ " Restore {s}", .{d}) catch config.topbar_str;
+ return config.topbar_str;
}
pub fn deinitPane(p: *Pardes, pane: *Pane) void {
@@ -1262,15 +1203,15 @@ pub const Pardes = struct {
/// renderer toggles it used to spell out are builtins now, under SPC t)
fn tagPrefix(p: *Pardes, pane: *Pane) ![]u8 {
const arena = p.scratch.allocator();
- if (pane.image) |iv| return std.fmt.allocPrint(arena, "img {s}", .{iv.path});
+ if (pane.image) |iv| return std.fmt.allocPrint(arena, config.tag_image ++ " {s}", .{iv.path});
if (pane.file) |f| {
- const fmode = if (pane.mode == .insert) "INS" else "NOR";
+ const fmode = if (pane.mode == .insert) config.tag_insert else config.tag_normal;
return std.fmt.allocPrint(arena, "{s} {s}", .{ fmode, f.path });
}
const mode = switch (pane.mode) {
- .normal => "NOR",
- .insert => "INS",
- .tty => "TTY",
+ .normal => config.tag_normal,
+ .insert => config.tag_insert,
+ .tty => config.tag_tty,
};
return std.fmt.allocPrint(arena, "{s} {s}", .{ mode, pane.cwdSlice() });
}
@@ -1341,9 +1282,9 @@ pub const Pardes = struct {
}
const col: usize = @min(@as(usize, pane.tag_col), text.len);
var lo = col;
- while (lo > 0 and look.isFileChar(text[lo - 1])) lo -= 1;
+ while (lo > 0 and config.isFileChar(text[lo - 1])) lo -= 1;
var hi = col;
- while (hi < text.len and look.isFileChar(text[hi])) hi += 1;
+ while (hi < text.len and config.isFileChar(text[hi])) hi += 1;
return if (hi > lo) text[lo..hi] else null;
}
@@ -1351,15 +1292,15 @@ pub const Pardes = struct {
/// sharing the pane's mode — the cursor moves over all of it, edits reach
/// only the tail. Newlines are always dropped.
fn tagInsertKey(p: *Pardes, pane: *Pane, key: Key) void {
- if (is(key, Key.escape)) {
+ if (hit(key, config.escape)) {
exitTagEdit(pane); // the tag is ALWAYS insert; Esc leaves it
return;
}
// the acme chords on the selection or the word at the cursor
- if (is(key, Key.enter) or is(key, Key.tab)) {
+ if (hit(key, config.look_key) or hit(key, config.exec_key)) {
if (p.tagChordText(pane)) |txt| {
pane.tag_sel = false;
- const btn: Mouse.Button = if (is(key, Key.enter) == swap_enter_tab) .middle else .right;
+ const btn: Mouse.Button = if (hit(key, config.look_key)) config.look_button else config.exec_button;
p.actOnSelection(btn, p.active, txt, null);
}
return;
@@ -1414,13 +1355,14 @@ pub const Pardes = struct {
const text = p.tagText(p.scratch.allocator(), pane) catch return;
const cur: usize = @min(@as(usize, pane.tag_col), text.len);
// Esc abandons the command line: back to the body, tail kept as text
- if (is(key, Key.escape)) return exitTagEdit(pane);
+ if (hit(key, config.escape)) return exitTagEdit(pane);
// the chord: run the selection (or the word under the cursor) and drop
- // back into the body — the whole point of `:`. Same keys as everywhere
- // else (swap_enter_tab), so the command line is `:w<Tab>` by default.
+ // back into the body — the whole point of `:`. The same two bindings as
+ // everywhere else (config.look_key / exec_key), so the command line is
+ // `:w<Tab>` by default.
// Nothing under the cursor means nothing ran: the tag keeps focus.
- if (is(key, Key.enter) or is(key, Key.tab)) {
- const btn: Mouse.Button = if (is(key, Key.enter) == swap_enter_tab) .middle else .right;
+ if (hit(key, config.look_key) or hit(key, config.exec_key)) {
+ const btn: Mouse.Button = if (hit(key, config.look_key)) config.look_button else config.exec_button;
const txt = p.tagChordText(pane) orelse return;
const id = p.active;
exitTagEdit(pane); // leave the tag FIRST: Del frees the pane
@@ -1429,21 +1371,25 @@ pub const Pardes = struct {
}
// y — 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 out.
- if (is(key, 'y')) {
+ if (hit(key, config.tag_yank)) {
if (p.tagChordText(pane)) |txt| p.setYank(txt);
return;
}
// insert entry (one line, so I/A are the tail's ends). The prefix is
// read-only, so entering insert inside it parks at the first editable
// column instead — you can never be typing into the path.
- if (is(key, 'i') or is(key, 'a') or is(key, 'I') or is(key, 'A')) {
+ if (hit(key, config.insert) or hit(key, config.append) or
+ hit(key, config.insert_line_start) or hit(key, config.insert_line_end))
+ {
const edit0: usize = (p.tagPrefix(pane) catch return).len;
- pane.tag_col = @intCast(@max(edit0, switch (key.cp) {
- 'a' => modal.nextGrapheme(text, cur),
- 'I' => 0,
- 'A' => text.len,
- else => cur,
- }));
+ pane.tag_col = @intCast(@max(edit0, if (hit(key, config.append))
+ modal.nextGrapheme(text, cur)
+ else if (hit(key, config.insert_line_start))
+ 0
+ else if (hit(key, config.insert_line_end))
+ text.len
+ else
+ cur));
pane.tag_sel = false;
pane.mode = .insert;
return;
@@ -1451,21 +1397,15 @@ pub const Pardes = struct {
// h/j/k/l — a tagline is a place in the LAYOUT, so the four letters walk
// it: focus the neighbour and land on ITS tagline, still in normal mode,
// so tag-to-tag navigation never drops through a body. Runs the SAME
- // Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w` run — one
- // focusDir. The ARROWS below keep the in-tag grapheme motion, so the
- // letters cost nothing. Nothing in that direction = stay put, tag and
- // all: focusDir left `active` alone, so there is nothing to undo —
+ // Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w` run, off the
+ // SAME table (config.window_keys) — one focusDir, one binding. Only the
+ // LETTER column: the arrows keep the in-tag grapheme motion below, so
+ // the letters cost nothing. Nothing in that direction = stay put, tag
+ // and all: focusDir left `active` alone, so there is nothing to undo —
// EXCEPT upwards, where "nothing" is still something (see below).
- const dir: ?Builtin = if (is(key, 'h'))
- .Left
- else if (is(key, 'j'))
- .Down
- else if (is(key, 'k'))
- .Up
- else if (is(key, 'l'))
- .Right
- else
- null;
+ const dir: ?Builtin = for (config.window_keys) |wk| {
+ if (hit(key, &.{wk.letter})) break wk.cmd;
+ } else null;
if (dir) |d| {
const from = p.active;
p.runBuiltin(d, from, "", null);
@@ -1498,30 +1438,30 @@ pub const Pardes = struct {
/// one place; only `h`/`l` differ — a tagline spends them on the layout,
/// the topbar has no layout — and each caller answers those itself.
fn lineMotion(text: []const u8, cur: usize, key: Key) ?modal.HxRange {
- const target: ?usize = if (is(key, Key.left))
+ const target: ?usize = if (hit(key, config.line_move_left))
modal.prevGrapheme(text, cur)
- else if (is(key, Key.right))
+ else if (hit(key, config.line_move_right))
modal.nextGrapheme(text, cur)
- else if (is(key, '0') or is(key, Key.home))
+ else if (hit(key, config.line_start))
0
- else if (is(key, '$') or is(key, Key.end))
+ else if (hit(key, config.line_end))
modal.prevGrapheme(text, text.len)
- else if (is(key, '^'))
+ else if (hit(key, config.line_first_nonws))
modal.firstNonWs(text)
else
null;
if (target) |t| return .{ .anchor = t, .head = t };
- const wt: ?modal.WordTarget = if (is(key, 'w'))
+ const wt: ?modal.WordTarget = if (hit(key, config.next_word_start))
.next_word_start
- else if (is(key, 'b'))
+ else if (hit(key, config.prev_word_start))
.prev_word_start
- else if (is(key, 'e'))
+ else if (hit(key, config.next_word_end))
.next_word_end
- else if (is(key, 'W'))
+ else if (hit(key, config.next_long_word_start))
.next_long_word_start
- else if (is(key, 'B'))
+ else if (hit(key, config.prev_long_word_start))
.prev_long_word_start
- else if (is(key, 'E'))
+ else if (hit(key, config.next_long_word_end))
.next_long_word_end
else
null;
@@ -1542,24 +1482,24 @@ pub const Pardes = struct {
// the line GROWS a `Restore <path>` word the moment a dump lands, so
// the column is clamped against the live line on every use
const cur: usize = @min(@as(usize, p.topbar_col orelse return), bar.len);
- if (is(key, Key.escape)) {
+ if (hit(key, config.escape)) {
p.topbar_col = null; // the active pane still has its body focus
return;
}
// the chord: run the word under the cursor, exactly as a middle click
// on it does. Leave the bar FIRST — `Kill` lives up here and tears the
// session down, the same hazard the pane-tag chord has with `Del`.
- if (is(key, Key.enter) or is(key, Key.tab)) {
+ if (hit(key, config.look_key) or hit(key, config.exec_key)) {
const word = wordAtCol(bar, @intCast(cur));
p.topbar_col = null;
- if (word.len > 0) p.actOnSelection(.middle, p.active, word, null);
+ if (word.len > 0) p.actOnSelection(config.exec_button, p.active, word, null);
return;
}
// j — back down onto a tagline, the mirror of the k that got you here:
// the pane you came from if it still holds the top row, else the
// leftmost pane that does. Recomputed, never remembered, so a pane
// deleted while the bar had focus strands nobody.
- if (is(key, 'j')) {
+ if (hit(key, config.topbar_down)) {
var dest: ?usize = null;
for (p.panes, 0..) |slot, i| {
if (slot == null or p.rects[i].y != TOPBAR_H) continue;
@@ -1577,9 +1517,9 @@ pub const Pardes = struct {
if (pane.tag_edit) pane.mode = .normal;
return;
}
- const col: ?usize = if (is(key, 'h'))
+ const col: ?usize = if (hit(key, config.topbar_left))
modal.prevGrapheme(bar, cur)
- else if (is(key, 'l'))
+ else if (hit(key, config.topbar_right))
modal.nextGrapheme(bar, cur)
else if (lineMotion(bar, cur, key)) |r|
modal.hxCursor(bar, r)
@@ -1631,9 +1571,9 @@ pub const Pardes = struct {
const col: usize = @intCast(@max(0, sl.c0));
if (col > line.len) return;
var hi = col;
- while (hi < line.len and look.isFileChar(line[hi])) hi += 1;
+ while (hi < line.len and config.isFileChar(line[hi])) hi += 1;
var lo = col;
- while (lo > 0 and look.isFileChar(line[lo - 1])) lo -= 1;
+ while (lo > 0 and config.isFileChar(line[lo - 1])) lo -= 1;
if (hi > lo) {
sl.c0 = @intCast(lo);
sl.c1 = @intCast(hi - 1);
@@ -1646,7 +1586,7 @@ pub const Pardes = struct {
/// coords: row 0 is the tag; file panes carry the line-number prefix)
fn cursorWordSel(pane: *Pane) Sel {
const vrow = (pane.cur_row - pane.scroll()) + @as(i32, BOX_H);
- const vcol = if (pane.file != null) pane.cur_col - pane.hscroll + @as(i32, PREFIX_W) else pane.cur_col;
+ const vcol = if (pane.file != null) pane.cur_col - pane.hscroll + @as(i32, config.PREFIX_W) else pane.cur_col;
return .{ .state = .done, .c0 = vcol, .c1 = vcol, .r0 = vrow, .r1 = vrow };
}
@@ -1674,8 +1614,8 @@ pub const Pardes = struct {
};
if (k > 0 and (i == first or (k > 1 and i == p.active))) continue;
const t = p.panes[i] orelse continue;
- if (t.sel[0].state == .done) {
- if (p.selectionText(t, t.sel[0]) catch null) |tx| return tx;
+ if (t.sel[sel_slot].state == .done) {
+ if (p.selectionText(t, t.sel[sel_slot]) catch null) |tx| return tx;
} else if ((t.vsel.active and t.vsel.explicit) or t.msel.active) {
if (p.currentSelText(t)) |tx| return tx;
}
@@ -1693,25 +1633,28 @@ pub const Pardes = struct {
// before Ctrl-w, so a modified key abandons the sequence instead of
// arming a second prefix on top of it
if (p.leader_on) return p.leaderKey(key);
- // Ctrl-w prefix: helix-style directional pane focus (h/j/k/l), running
- // the SAME builtins `SPC w h/j/k/l` runs. It stays despite the leader
+ // Ctrl-w prefix: helix-style directional pane focus (h/j/k/l or the
+ // arrows), running the SAME builtins `SPC w h/j/k/l` runs off the SAME
+ // table a tagline's own h/j/k/l reads. It stays despite the leader
// covering it 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.
if (p.ctrl_w_pending) {
p.ctrl_w_pending = false;
- if (is(key, 'h') or is(key, Key.left)) p.runBuiltin(.Left, p.active, "", null) else if (is(key, 'l') or is(key, Key.right)) p.runBuiltin(.Right, p.active, "", null) else if (is(key, 'k') or is(key, Key.up)) p.runBuiltin(.Up, p.active, "", null) else if (is(key, 'j') or is(key, Key.down)) p.runBuiltin(.Down, p.active, "", null);
+ for (config.window_keys) |wk| {
+ if (hit(key, &.{ wk.letter, wk.arrow })) return p.runBuiltin(wk.cmd, p.active, "", null);
+ }
return;
}
// insert mode owns Ctrl-w (delete word backward, helix); the focus
// prefix keeps normal/tty
- if (isC(key, 'w') and pane.mode != .insert) {
+ if (hit(key, config.window_prefix) and pane.mode != .insert) {
p.ctrl_w_pending = true;
return;
}
// global window ops (any mode): Alt-n new terminal below, Alt-c move
// the active terminal into a fresh column
- if (key.alt and !key.ctrl and key.cp == 'n') {
+ if (hit(key, config.new_shell_below)) {
const free = p.freeSlot() orelse return;
const nt = p.newShell(free, "") catch return;
nt.greet = true;
@@ -1722,7 +1665,7 @@ pub const Pardes = struct {
p.active = free;
return;
}
- if (key.alt and !key.ctrl and key.cp == 'c') {
+ if (hit(key, config.pane_to_new_column)) {
const f = p.layoutFindTerm(p.active).?;
if (p.ncol < MAX_COLS and p.col_n[f.col] > 1) {
p.absorbVWeight(p.active);
@@ -1737,7 +1680,7 @@ pub const Pardes = struct {
// kitty keyboard protocol); where it doesn't it arrives as a plain
// Escape and still means what Escape always means.
const tty_toggle = (key.ctrl and key.cp == p.opts.tty_toggle) or
- (key.shift and !key.ctrl and !key.alt and key.cp == Key.escape);
+ hit(key, config.tty_toggle_alt);
if (pane.isTerminal() and tty_toggle) {
if (pane.mode == .tty) {
pane.mode = .normal;
@@ -1747,8 +1690,8 @@ pub const Pardes = struct {
}
// a search input in flight (`/` or Find): Enter searches, Esc abandons;
// both restore the tag tail and hand focus back to the body.
- if (pane.search_edit and (is(key, Key.enter) or is(key, Key.escape))) {
- if (is(key, Key.enter)) p.submitSearch(p.active);
+ if (pane.search_edit and (hit(key, config.search_submit) or hit(key, config.escape))) {
+ if (hit(key, config.search_submit)) p.submitSearch(p.active);
pane.tag_tail.shrinkRetainingCapacity(@min(pane.search_at, pane.tag_tail.items.len));
exitTagEdit(pane);
pane.mode = .normal;
@@ -1762,12 +1705,12 @@ pub const Pardes = struct {
if (pane.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key);
return;
}
- // normal-mode Enter/Tab: the acme chords — look at / execute (which
- // key is which is swap_enter_tab) the EXPLICIT modal selection (v/x/X,
- // terminal n/N, search n/N); implicit motion residue falls back to the
- // file-ish word under the cursor.
- if (pane.mode == .normal and (is(key, Key.enter) or is(key, Key.tab))) {
- const btn: Mouse.Button = if (is(key, Key.enter) == swap_enter_tab) .middle else .right;
+ // the acme chords in the body — look at / execute (config.look_key and
+ // exec_key, Enter and Tab by default) the EXPLICIT modal selection
+ // (v/x/X, terminal n/N, search n/N); implicit motion residue falls back
+ // to the file-ish word under the cursor.
+ if (pane.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
+ const btn: Mouse.Button = if (hit(key, config.look_key)) config.look_button else config.exec_button;
pane.pinCursor();
const explicit = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active;
if (explicit) {
@@ -1790,14 +1733,14 @@ pub const Pardes = struct {
// refuse the OSC 52 read, so a round trip would never come back).
// Skipped while a prefix waits for its char argument (f/r/m and
// friends take `p` literally).
- if (pane.mode == .normal and pane.pending == 0 and (is(key, 'p') or is(key, 'P'))) {
- p.normalPaste(pane, is(key, 'P'));
+ if (pane.mode == .normal and pane.pending == 0 and (hit(key, config.paste_after) or hit(key, config.paste_before))) {
+ p.normalPaste(pane, hit(key, config.paste_before));
return;
}
switch (pane.mode) {
.normal => p.handleNormal(pane, key),
.insert => {
- if (key.cp == Key.escape and !key.ctrl and !key.alt) {
+ if (hit(key, config.escape)) {
pane.mode = .normal;
pane.msel.active = false;
pane.pending = 0;
@@ -1837,7 +1780,7 @@ pub const Pardes = struct {
p.leader_on = false; // only "still a prefix" below re-arms it
if (key.ctrl or key.alt or key.cp < 0x20 or key.cp > 0x7e) return;
const c: u8 = @intCast(key.cp);
- if (c == '?') return output_pane.openHelp(p, p.active, p.leader_keys[0..p.leader_n]);
+ if (c == config.leader_help) return output_pane.openHelp(p, p.active, p.leader_keys[0..p.leader_n]);
if (p.leader_n >= p.leader_keys.len) return;
p.leader_keys[p.leader_n] = c;
p.leader_n += 1;
@@ -1950,7 +1893,7 @@ pub const Pardes = struct {
fn splitRowForExtent(y: u16, h: u16, cur_y: u16) ?u16 {
if (h < 2) return null;
- const min_each: u16 = if (h >= MINH * 2) MINH else 1;
+ const min_each: u16 = if (h >= config.MINH * 2) config.MINH else 1;
const lo = y +| min_each;
const hi = y + h - min_each;
if (lo > hi) return y + h / 2;
@@ -2209,7 +2152,7 @@ pub const Pardes = struct {
/// helix `scroll` without cursor sync (Ctrl-f/b, PgUp/PgDn, zj/zk): shift
/// the view, then snap a fallen-out cursor to the near scrolloff edge, col 0
fn scrollViewMove(pane: *Pane, pl: PaneLines, text: []const u8, range: modal.HxRange, delta: i32) void {
- const margin: i32 = @min(scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
+ const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
pane.scrollBy(delta);
const top = pane.scroll();
const last_row: i32 = @intCast(modal.hxLineCount(text) - 1);
@@ -2230,16 +2173,16 @@ pub const Pardes = struct {
}
/// gt/gc/gb: view-relative rows, col 0, scrolloff clamped (helix goto_window)
- fn gotoWindow(pane: *Pane, pl: PaneLines, text: []const u8, range: modal.HxRange, which: u21, cnt: usize) void {
- const margin: i32 = @min(scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
+ fn gotoWindow(pane: *Pane, pl: PaneLines, text: []const u8, range: modal.HxRange, which: enum { top, center, bottom }, cnt: usize) void {
+ const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
const top = pane.scroll();
const last_row: i32 = @intCast(modal.hxLineCount(text) - 1);
const last_vis: i32 = @min(@as(i32, pane.rows) - 1, last_row - top);
const n: i32 = @intCast(cnt - 1);
var vline: i32 = switch (which) {
- 't' => top + margin + n,
- 'c' => top + @divTrunc(last_vis, 2),
- else => top + last_vis - (margin + n),
+ .top => top + margin + n,
+ .center => top + @divTrunc(last_vis, 2),
+ .bottom => top + last_vis - (margin + n),
};
vline = @max(vline, top + margin);
vline = @min(vline, top + last_vis - margin);
@@ -2312,7 +2255,7 @@ pub const Pardes = struct {
// replace it anyway. On an OUTPUT BUFFER (+Search, +Help) there is no
// file behind the pane and nothing worth keeping, so "never mind"
// means dismiss it: Esc IS Del, the same builtin its tag offers.
- if (is(key, Key.escape)) {
+ if (hit(key, config.escape)) {
if (pane.file) |f| if (f.output) return p.runBuiltin(.Del, p.active, "", null);
pane.select = false;
pane.pending = 0;
@@ -2342,20 +2285,20 @@ pub const Pardes = struct {
};
// prefix continuations
- if (pane.pending == 'g') {
+ if (pane.pending == config.goto_prefix) {
pane.pending = 0;
- if (is(key, 'g')) {
+ if (hit(key, config.goto_file_start)) {
const line = if (has_count) @min(cnt - 1, max_line) else 0;
return pointMove(pane, pl, text, range, modal.lineStartOffset(text, line));
}
- if (is(key, 'e')) return pointMove(pane, pl, text, range, modal.lineStartOffset(text, max_line));
- if (is(key, 'h')) return pointMove(pane, pl, text, range, modal.lineStartOffset(text, modal.hxLineOf(text, cur)));
- if (is(key, 'l')) {
+ if (hit(key, config.goto_last_line)) return pointMove(pane, pl, text, range, modal.lineStartOffset(text, max_line));
+ if (hit(key, config.goto_line_start)) return pointMove(pane, pl, text, range, modal.lineStartOffset(text, modal.hxLineOf(text, cur)));
+ if (hit(key, config.goto_line_end)) {
const line = modal.hxLineOf(text, cur);
const ls = modal.lineStartOffset(text, line);
return pointMove(pane, pl, text, range, @max(ls, modal.prevGrapheme(text, modal.hxLineEndIdx(text, line))));
}
- if (is(key, 's')) {
+ if (hit(key, config.goto_first_nonws)) {
const line = modal.hxLineOf(text, cur);
const ls = modal.lineStartOffset(text, line);
const slice = text[ls..modal.hxLineEndIdx(text, line)];
@@ -2363,70 +2306,77 @@ pub const Pardes = struct {
if (nw == slice.len) return; // all-whitespace line: no move (helix)
return pointMove(pane, pl, text, range, ls + nw);
}
- if (is(key, 'j')) return verticalMove(pane, pl, text, range, true, cnt);
- if (is(key, 'k')) return verticalMove(pane, pl, text, range, false, cnt);
- if (is(key, '|')) {
+ if (hit(key, config.goto_line_down)) return verticalMove(pane, pl, text, range, true, cnt);
+ if (hit(key, config.goto_line_up)) return verticalMove(pane, pl, text, range, false, cnt);
+ if (hit(key, config.goto_column)) {
const line = modal.hxLineOf(text, cur);
const ls = modal.lineStartOffset(text, line);
return pointMove(pane, pl, text, range, @min(ls + (cnt - 1), modal.hxLineEndIdx(text, line)));
}
- if (is(key, 't') or is(key, 'c') or is(key, 'b')) return gotoWindow(pane, pl, text, range, key.cp, cnt);
+ if (hit(key, config.goto_view_top)) return gotoWindow(pane, pl, text, range, .top, cnt);
+ if (hit(key, config.goto_view_center)) return gotoWindow(pane, pl, text, range, .center, cnt);
+ if (hit(key, config.goto_view_bottom)) return gotoWindow(pane, pl, text, range, .bottom, cnt);
// helix's five LSP gotos, all under `g` and nowhere else. They ask
// and return; the answer arrives later as an lsp_resp Event and
// either jumps or opens a list (lspResponse).
- if (is(key, 'd')) return p.lspRequest(p.active, .definition, "");
- if (key.cp == 'D' and !key.ctrl and !key.alt) return p.lspRequest(p.active, .declaration, "");
- if (is(key, 'y')) return p.lspRequest(p.active, .type_definition, "");
- if (is(key, 'i')) return p.lspRequest(p.active, .implementation, "");
- if (is(key, 'r')) return p.lspRequest(p.active, .references, "");
+ if (hit(key, config.goto_definition)) return p.lspRequest(p.active, .definition, "");
+ if (hit(key, config.goto_declaration)) return p.lspRequest(p.active, .declaration, "");
+ if (hit(key, config.goto_type_definition)) return p.lspRequest(p.active, .type_definition, "");
+ if (hit(key, config.goto_implementation)) return p.lspRequest(p.active, .implementation, "");
+ if (hit(key, config.goto_references)) return p.lspRequest(p.active, .references, "");
return;
}
- if (pane.pending == 'z') {
+ if (pane.pending == config.view_prefix) {
pane.pending = 0;
- if (is(key, 't')) {
+ if (hit(key, config.view_top)) {
pane.scrollBy(pane.cur_row - pane.scroll());
pane.ensureCursorVisible();
return;
}
- if (is(key, 'z') or is(key, 'c')) {
+ if (hit(key, config.view_center)) {
pane.scrollBy(pane.cur_row - (pane.scroll() + @divTrunc(@as(i32, pane.rows), 2) - 1));
pane.ensureCursorVisible();
return;
}
- if (is(key, 'b')) {
+ if (hit(key, config.view_bottom)) {
pane.scrollBy(pane.cur_row - (pane.scroll() + @as(i32, pane.rows) - 1));
pane.ensureCursorVisible();
return;
}
- if (is(key, 'j') or is(key, Key.down)) return scrollViewMove(pane, pl, text, range, 1);
- if (is(key, 'k') or is(key, Key.up)) return scrollViewMove(pane, pl, text, range, -1);
- if (isC(key, 'd')) return halfPageMove(pane, pl, text, range, true);
- if (isC(key, 'u')) return halfPageMove(pane, pl, text, range, false);
- if (isC(key, 'f') or is(key, Key.page_down)) return scrollViewMove(pane, pl, text, range, @as(i32, pane.rows));
- if (isC(key, 'b') or is(key, Key.page_up)) return scrollViewMove(pane, pl, text, range, -@as(i32, pane.rows));
+ if (hit(key, config.view_scroll_down)) return scrollViewMove(pane, pl, text, range, 1);
+ if (hit(key, config.view_scroll_up)) return scrollViewMove(pane, pl, text, range, -1);
+ if (hit(key, config.half_page_down)) return halfPageMove(pane, pl, text, range, true);
+ if (hit(key, config.half_page_up)) return halfPageMove(pane, pl, text, range, false);
+ if (hit(key, config.page_down)) return scrollViewMove(pane, pl, text, range, @as(i32, pane.rows));
+ if (hit(key, config.page_up)) return scrollViewMove(pane, pl, text, range, -@as(i32, pane.rows));
return;
}
- if (pane.pending == 'f' or pane.pending == 'F' or pane.pending == 't' or pane.pending == 'T') {
+ if (pane.pending == config.find_char_fwd or pane.pending == config.find_char_back or
+ pane.pending == config.till_char_fwd or pane.pending == config.till_char_back)
+ {
const op: u8 = @intCast(pane.pending);
pane.pending = 0;
if (key.ctrl or key.alt or key.cp >= 0xF0000) return; // not a char
pane.find_op = op;
pane.find_ch = key.cp;
- return findMove(pane, pl, text, range, key.cp, op == 'f' or op == 't', op == 't' or op == 'T', cnt);
+ return findMove(pane, pl, text, range, key.cp, op == config.find_char_fwd or op == config.till_char_fwd, op == config.till_char_fwd or op == config.till_char_back, cnt);
}
- if (pane.pending == 'r') {
+ if (pane.pending == config.replace_prefix) {
pane.pending = 0;
if (key.ctrl or key.alt or key.cp >= 0xF0000 or key.cp > 0x7f) return;
return p.normalReplaceChar(pane, @intCast(key.cp));
}
- if (pane.pending == 'm') {
+ if (pane.pending == config.match_prefix) {
if (pane.pending2 == 0) {
- if (is(key, 'm')) {
+ if (hit(key, config.match_bracket)) {
pane.pending = 0;
const mc = modal.matchBracket(lines, modal.hxPos(text, cur)) orelse return;
return pointMove(pane, pl, text, range, modal.hxOff(text, mc));
}
- if (is(key, 'i') or is(key, 'a') or is(key, 's') or is(key, 'r') or is(key, 'd')) {
+ if (isPrefix(key, config.match_inside) or isPrefix(key, config.match_around) or
+ isPrefix(key, config.surround_add) or isPrefix(key, config.surround_replace) or
+ isPrefix(key, config.surround_delete))
+ {
pane.pending2 = key.cp; // pending stays 'm': the char arg is next
return;
}
@@ -2440,7 +2390,7 @@ pub const Pardes = struct {
pane.pending_ch = 0;
return;
}
- if (sub == 'r' and pane.pending_ch == 0) {
+ if (sub == config.surround_replace and pane.pending_ch == 0) {
pane.pending_ch = key.cp; // mr<from><to>: hold <from>, wait for <to>
return;
}
@@ -2449,30 +2399,30 @@ pub const Pardes = struct {
const from = pane.pending_ch;
pane.pending_ch = 0;
switch (sub) {
- 'i', 'a' => return p.textobjectSelect(pane, pl, key.cp, sub == 'a'),
- 's' => return p.surroundAdd(pane, key.cp),
- 'd' => return p.surroundDelete(pane, pl, key.cp),
- 'r' => return p.surroundReplace(pane, pl, from, key.cp),
+ config.match_inside, config.match_around => return p.textobjectSelect(pane, pl, key.cp, sub == config.match_around),
+ config.surround_add => return p.surroundAdd(pane, key.cp),
+ config.surround_delete => return p.surroundDelete(pane, pl, key.cp),
+ config.surround_replace => return p.surroundReplace(pane, pl, from, key.cp),
else => return,
}
}
- if (pane.pending == ']' or pane.pending == '[') {
- const fwd = pane.pending == ']';
+ if (pane.pending == config.next_prefix or pane.pending == config.prev_prefix) {
+ const fwd = pane.pending == config.next_prefix;
pane.pending = 0;
- if (is(key, 'p')) {
+ if (hit(key, config.goto_paragraph)) {
const r2 = modal.hxParaMove(text, range, cnt, fwd, pane.select);
return setPaneRange(pane, pl, text, r2, false);
}
- if (is(key, ' ')) return p.addNewline(pane, fwd, cnt);
+ if (hit(key, config.add_newline)) return p.addNewline(pane, fwd, cnt);
// ]d / [d: step the diagnostics list, asking for one if it is not
// up yet. The list is an ordinary results buffer, so "step" is the
// n/N stepper — `d` differs from `n` only in what it guarantees is
// showing. ]D / [D jump to the last/first, helix's pair.
- if (is(key, 'd')) {
+ if (hit(key, config.goto_diagnostic)) {
if (pane.search_kind == .diagnostics and p.searchStep(p.active, if (fwd) 1 else -1)) return;
return p.lspRequest(p.active, .diagnostics, "");
}
- if (key.cp == 'D' and !key.ctrl and !key.alt) {
+ if (hit(key, config.goto_diagnostic_end)) {
if (pane.search_kind != .diagnostics) return p.lspRequest(p.active, .diagnostics, "");
if (!fwd) { // [D = first
pane.search_row = null;
@@ -2488,41 +2438,46 @@ pub const Pardes = struct {
return;
}
// prefix setters (the count survives into the continuation)
- if (is(key, 'g') or is(key, 'z') or is(key, 'f') or is(key, 'F') or is(key, 't') or is(key, 'T') or is(key, 'r') or is(key, ']') or is(key, '[')) {
+ if (isPrefix(key, config.goto_prefix) or isPrefix(key, config.view_prefix) or
+ isPrefix(key, config.find_char_fwd) or isPrefix(key, config.find_char_back) or
+ isPrefix(key, config.till_char_fwd) or isPrefix(key, config.till_char_back) or
+ isPrefix(key, config.replace_prefix) or isPrefix(key, config.next_prefix) or
+ isPrefix(key, config.prev_prefix))
+ {
pane.pending = key.cp;
pane.count = saved_count;
return;
}
- if (is(key, 'm')) {
- pane.pending = 'm';
+ if (isPrefix(key, config.match_prefix)) {
+ pane.pending = config.match_prefix;
pane.pending2 = 0;
pane.pending_ch = 0;
return;
}
// movement
- if (is(key, 'h') or is(key, Key.left)) {
+ if (hit(key, config.move_left)) {
var t = cur;
for (0..cnt) |_| t = modal.prevGrapheme(text, t);
return pointMove(pane, pl, text, range, t);
}
- if (is(key, 'l') or is(key, Key.right)) {
+ if (hit(key, config.move_right)) {
var t = cur;
for (0..cnt) |_| t = modal.nextGrapheme(text, t);
return pointMove(pane, pl, text, range, t);
}
- if (is(key, 'j') or is(key, Key.down)) return verticalMove(pane, pl, text, range, true, cnt);
- if (is(key, 'k') or is(key, Key.up)) return verticalMove(pane, pl, text, range, false, cnt);
- if (is(key, 'w')) return wordMove(pane, pl, text, range, cnt, .next_word_start);
- if (is(key, 'b')) return wordMove(pane, pl, text, range, cnt, .prev_word_start);
- if (is(key, 'e')) return wordMove(pane, pl, text, range, cnt, .next_word_end);
- if (is(key, 'W')) return wordMove(pane, pl, text, range, cnt, .next_long_word_start);
- if (is(key, 'B')) return wordMove(pane, pl, text, range, cnt, .prev_long_word_start);
- if (is(key, 'E')) return wordMove(pane, pl, text, range, cnt, .next_long_word_end);
- if (isA(key, '.')) {
+ if (hit(key, config.move_down)) return verticalMove(pane, pl, text, range, true, cnt);
+ if (hit(key, config.move_up)) return verticalMove(pane, pl, text, range, false, cnt);
+ if (hit(key, config.next_word_start)) return wordMove(pane, pl, text, range, cnt, .next_word_start);
+ if (hit(key, config.prev_word_start)) return wordMove(pane, pl, text, range, cnt, .prev_word_start);
+ if (hit(key, config.next_word_end)) return wordMove(pane, pl, text, range, cnt, .next_word_end);
+ if (hit(key, config.next_long_word_start)) return wordMove(pane, pl, text, range, cnt, .next_long_word_start);
+ if (hit(key, config.prev_long_word_start)) return wordMove(pane, pl, text, range, cnt, .prev_long_word_start);
+ if (hit(key, config.next_long_word_end)) return wordMove(pane, pl, text, range, cnt, .next_long_word_end);
+ if (hit(key, config.repeat_find)) {
// repeat the last f/F/t/T; a count repeats the motion N times
if (pane.find_op == 0 or pane.find_ch > 0x7f) return;
- const fwd = pane.find_op == 'f' or pane.find_op == 't';
- const till = pane.find_op == 't' or pane.find_op == 'T';
+ const fwd = pane.find_op == config.find_char_fwd or pane.find_op == config.till_char_fwd;
+ const till = pane.find_op == config.till_char_fwd or pane.find_op == config.till_char_back;
var r = range;
var moved = false;
for (0..cnt) |_| {
@@ -2537,14 +2492,14 @@ pub const Pardes = struct {
if (!moved) return;
return setPaneRange(pane, pl, text, r, false);
}
- if (is(key, '0') or is(key, Key.home))
+ if (hit(key, config.line_start))
return pointMove(pane, pl, text, range, modal.lineStartOffset(text, modal.hxLineOf(text, cur)));
- if (is(key, '$') or is(key, Key.end)) {
+ if (hit(key, config.line_end)) {
const line = modal.hxLineOf(text, cur);
const ls = modal.lineStartOffset(text, line);
return pointMove(pane, pl, text, range, @max(ls, modal.prevGrapheme(text, modal.hxLineEndIdx(text, line))));
}
- if (is(key, '^')) {
+ if (hit(key, config.line_first_nonws)) {
const line = modal.hxLineOf(text, cur);
const ls = modal.lineStartOffset(text, line);
const slice = text[ls..modal.hxLineEndIdx(text, line)];
@@ -2552,26 +2507,26 @@ pub const Pardes = struct {
if (nw == slice.len) return;
return pointMove(pane, pl, text, range, ls + nw);
}
- if (is(key, 'G')) {
+ if (hit(key, config.goto_line)) {
// helix goto_line: only acts WITH a count (bare G is a no-op;
// ge is goto-last-line)
if (!has_count) return;
const line = @min(cnt - 1, max_line);
return pointMove(pane, pl, text, range, modal.lineStartOffset(text, line));
}
- if (isC(key, 'd')) return halfPageMove(pane, pl, text, range, true);
- if (isC(key, 'u')) return halfPageMove(pane, pl, text, range, false);
- if (isC(key, 'f') or is(key, Key.page_down)) return scrollViewMove(pane, pl, text, range, @as(i32, pane.rows));
- if (isC(key, 'b') or is(key, Key.page_up)) return scrollViewMove(pane, pl, text, range, -@as(i32, pane.rows));
+ if (hit(key, config.half_page_down)) return halfPageMove(pane, pl, text, range, true);
+ if (hit(key, config.half_page_up)) return halfPageMove(pane, pl, text, range, false);
+ if (hit(key, config.page_down)) return scrollViewMove(pane, pl, text, range, @as(i32, pane.rows));
+ if (hit(key, config.page_up)) return scrollViewMove(pane, pl, text, range, -@as(i32, pane.rows));
// insert entry
- if (is(key, 'i')) return p.enterInsert(pane, .at, cnt);
- if (is(key, 'a')) return p.enterInsert(pane, .append, cnt);
- if (is(key, 'I')) return p.enterInsert(pane, .line_start, cnt);
- if (is(key, 'A')) return p.enterInsert(pane, .line_end, cnt);
- if (is(key, 'o')) return p.enterInsert(pane, .open_below, cnt);
- if (is(key, 'O')) return p.enterInsert(pane, .open_above, cnt);
+ if (hit(key, config.insert)) return p.enterInsert(pane, .at, cnt);
+ if (hit(key, config.append)) return p.enterInsert(pane, .append, cnt);
+ if (hit(key, config.insert_line_start)) return p.enterInsert(pane, .line_start, cnt);
+ if (hit(key, config.insert_line_end)) return p.enterInsert(pane, .line_end, cnt);
+ if (hit(key, config.open_below)) return p.enterInsert(pane, .open_below, cnt);
+ if (hit(key, config.open_above)) return p.enterInsert(pane, .open_above, cnt);
// selections
- if (is(key, 'v')) {
+ if (hit(key, config.select_mode)) {
if (pane.select) {
pane.select = false; // exit select mode, selection kept (helix)
} else {
@@ -2589,41 +2544,41 @@ pub const Pardes = struct {
pane.pending = 0;
return;
}
- if (is(key, 'x')) return lineSelect(pane, pl, text, range, cnt);
- if (is(key, 'X')) return lineBoundsSelect(pane, pl, text, range);
- if (isA(key, 'x')) return shrinkSelToLineBounds(pane, pl, text, range);
- if (is(key, ';')) return setPaneRange(pane, pl, text, .{ .anchor = cur, .head = cur }, false);
- if (isA(key, ';')) {
+ if (hit(key, config.select_line)) return lineSelect(pane, pl, text, range, cnt);
+ if (hit(key, config.select_line_bounds)) return lineBoundsSelect(pane, pl, text, range);
+ if (hit(key, config.shrink_to_line_bounds)) return shrinkSelToLineBounds(pane, pl, text, range);
+ if (hit(key, config.collapse_selection)) return setPaneRange(pane, pl, text, .{ .anchor = cur, .head = cur }, false);
+ if (hit(key, config.flip_selection)) {
const expl = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active;
return setPaneRange(pane, pl, text, .{ .anchor = range.head, .head = range.anchor }, expl);
}
- if (is(key, '%')) return setPaneRange(pane, pl, text, .{ .anchor = 0, .head = text.len }, false);
+ if (hit(key, config.select_all)) return setPaneRange(pane, pl, text, .{ .anchor = 0, .head = text.len }, false);
// edits
- if (is(key, 'd')) return p.normalDelete(pane, true);
- if (isA(key, 'd')) return p.normalDelete(pane, false);
- if (is(key, 'c')) return p.normalChange(pane);
- if (is(key, 'y')) return p.normalYank(pane);
- if (is(key, 'R')) return p.normalReplaceYank(pane);
- if (is(key, '~')) return p.normalCase(pane, .toggle);
- if (is(key, '`')) return p.normalCase(pane, .lower);
- if (isA(key, '`')) return p.normalCase(pane, .upper);
- if (is(key, 'J')) return p.normalJoin(pane);
- if (is(key, '>')) return p.normalIndent(pane, cnt, true);
- if (is(key, '<')) return p.normalIndent(pane, cnt, false);
+ if (hit(key, config.delete)) return p.normalDelete(pane, true);
+ if (hit(key, config.delete_noyank)) return p.normalDelete(pane, false);
+ if (hit(key, config.change)) return p.normalChange(pane);
+ if (hit(key, config.yank)) return p.normalYank(pane);
+ if (hit(key, config.replace_with_yank)) return p.normalReplaceYank(pane);
+ if (hit(key, config.switch_case)) return p.normalCase(pane, .toggle);
+ if (hit(key, config.to_lowercase)) return p.normalCase(pane, .lower);
+ if (hit(key, config.to_uppercase)) return p.normalCase(pane, .upper);
+ if (hit(key, config.join_lines)) return p.normalJoin(pane);
+ if (hit(key, config.indent)) return p.normalIndent(pane, cnt, true);
+ if (hit(key, config.unindent)) return p.normalIndent(pane, cnt, false);
// helix's `=` (format_selections) — its neighbour on the keyboard and
// in the keymap. Unbound in pardes until now.
- if (is(key, '=')) return p.lspRequest(p.active, .format, "");
- if (isC(key, 'a')) return p.normalAdjustNumber(pane, @intCast(cnt));
- if (isC(key, 'x')) return p.normalAdjustNumber(pane, -@as(i64, @intCast(cnt)));
- if (is(key, 'u')) return p.doUndo(pane);
- if (is(key, 'U')) return p.doRedo(pane);
+ if (hit(key, config.format)) return p.lspRequest(p.active, .format, "");
+ if (hit(key, config.increment)) return p.normalAdjustNumber(pane, @intCast(cnt));
+ if (hit(key, config.decrement)) return p.normalAdjustNumber(pane, -@as(i64, @intCast(cnt)));
+ if (hit(key, config.undo)) return p.doUndo(pane);
+ if (hit(key, config.redo)) return p.doRedo(pane);
// SPC — the leader: a key path from here runs a BUILTIN with no
// arguments, the same builtins the topbar and the tags hold (`?` at
// any depth lists what the prefix can still reach). Helix spends Space
// on pickers, which pardes does not have; acme's builtins are exactly
// what a leader is for. Body normal mode only — a tag is always
// insert, and a tty pane's keys belong to the program.
- if (is(key, ' ')) {
+ if (hit(key, config.leader)) {
p.leader_on = true;
p.leader_n = 0;
return;
@@ -2633,7 +2588,7 @@ pub const Pardes = struct {
// words and the execute chord runs the one under the cursor, then
// hands the body back (`:w<Tab>` = Save). Always the start, not the
// remembered tag column, so `:w` is the same keystrokes every time.
- if (is(key, ':')) {
+ if (hit(key, config.command_line)) {
p.enterTagEdit(pane, -1);
if (pane.tag_edit) pane.mode = .normal;
return;
@@ -2641,9 +2596,9 @@ pub const Pardes = struct {
// search: `/` 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.
- if (is(key, '/')) return p.startSearch(pane, search_marker);
- if (is(key, 'n') or is(key, 'N')) {
- const delta: i32 = if (is(key, 'n')) 1 else -1;
+ if (hit(key, config.search)) return p.startSearch(pane, config.search_marker);
+ if (hit(key, config.search_next) or hit(key, config.search_prev)) {
+ const delta: i32 = if (hit(key, config.search_next)) 1 else -1;
if (p.searchStep(p.active, delta)) return;
if (pane.isTerminal()) return p.lookStep(pane, pl, delta);
return;
@@ -2652,17 +2607,6 @@ pub const Pardes = struct {
// ---- search (`/`, Find, n/N): results into an output buffer, look the rows ----
- /// 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 for two commands that
- /// differ in nothing but where the rows come from.
- const search_marker = " /";
- pub const find_marker = " Find /";
- pub const grep_marker = " Grep /";
- // the two language queries that need a word typed: same tag input, same
- // Enter, and the marker is still what says which one is armed
- pub const rename_marker = " Rename /";
- pub const symbol_marker = " WsSymbols /";
const Search = enum { text, find, grep };
/// `/` (and the Find builtin) on any pane: type the pattern into the tag
@@ -2695,11 +2639,11 @@ pub const Pardes = struct {
const tail = pane.tag_tail.items;
const armed = tail[@min(pane.search_at, tail.len)..];
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return;
- if (std.mem.startsWith(u8, armed, rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]);
- if (std.mem.startsWith(u8, armed, symbol_marker)) return p.lspRequest(id, .workspace_symbols, armed[slash + 1 ..]);
- const kind: Search = if (std.mem.startsWith(u8, armed, find_marker))
+ if (std.mem.startsWith(u8, armed, config.rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]);
+ if (std.mem.startsWith(u8, armed, config.symbol_marker)) return p.lspRequest(id, .workspace_symbols, armed[slash + 1 ..]);
+ const kind: Search = if (std.mem.startsWith(u8, armed, config.find_marker))
.find
- else if (std.mem.startsWith(u8, armed, grep_marker))
+ else if (std.mem.startsWith(u8, armed, config.grep_marker))
.grep
else
.text;
@@ -2769,7 +2713,7 @@ pub const Pardes = struct {
const loc: []const u8 = if (has_path)
std.fs.path.basename(pane.file.?.path)
else
- std.fmt.bufPrint(&idbuf, "@p{d}", .{id}) catch return;
+ std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{id}) catch return;
for (pl.lines, 0..) |ln, i| {
const at = std.ascii.indexOfIgnoreCase(ln, pat) orelse continue;
const row = std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{
@@ -2788,7 +2732,7 @@ pub const Pardes = struct {
p.gpa.free(content);
return;
};
- const np = output_pane.open(p, free, dir, "+Search", content) catch {
+ const np = output_pane.open(p, free, dir, config.search_buffer, content) catch {
p.gpa.free(content);
return;
};
@@ -2810,7 +2754,7 @@ pub const Pardes = struct {
const rp = p.panes[rid] orelse return false;
const rf = if (rp.file) |*f| f else return false;
// a freed slot can be reused by an unrelated pane: the name says ours
- if (!rf.output or !std.mem.endsWith(u8, rf.path, "/+Search")) return false;
+ if (!rf.output or !std.mem.endsWith(u8, rf.path, "/" ++ config.search_buffer)) return false;
// fresh results: n starts at the first row, N has nothing behind it
const nrows: i64 = @intCast(std.mem.count(u8, rf.content, "\n"));
const step: i64 = if (pane.search_row) |c| @as(i64, @intCast(c)) + delta else if (delta > 0) 0 else -1;
@@ -2826,8 +2770,8 @@ pub const Pardes = struct {
rp.ensureCursorVisible();
const ln = modal.lineSlice(rf.content, @intCast(step));
var hi: usize = 0;
- while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1;
- p.actOnSelection(.right, rid, ln[0..hi], null);
+ while (hi < ln.len and config.isFileChar(ln[hi])) hi += 1;
+ p.actOnSelection(config.look_button, rid, ln[0..hi], null);
// the look may focus what it opened — a Find row opens a whole new
// file pane every time — so focus comes back to the pane that owns the
// search and the next n keeps stepping. A `/` row looks at the
@@ -2886,13 +2830,13 @@ pub const Pardes = struct {
if (w.kind.jumpsWhenSingle() and nrows == 1) {
const ln = std.mem.trimEnd(u8, rows, "\n");
var hi: usize = 0;
- while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1;
+ while (hi < ln.len and config.isFileChar(ln[hi])) hi += 1;
// exactly what a `/` result row does when n steps onto it: look the
// `path:LINE:COL` token from the pane that asked, so placement,
// dedup-onto-an-open-pane and centering are the ONE look path.
// (helix would also push its jumplist here; pardes has none, so
// there is nothing to push — do not read this as one.)
- return p.actOnSelection(.right, w.pane, ln[0..hi], null);
+ return p.actOnSelection(config.look_button, w.pane, ln[0..hi], null);
}
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
@@ -2909,7 +2853,7 @@ pub const Pardes = struct {
p.active = w.pane;
// prose is not a list of locations: n/N over a hover blurb would step
// to nowhere, so only location buffers arm the stepper.
- if (std.mem.eql(u8, w.kind.bufferName(), "+Search")) {
+ if (std.mem.eql(u8, w.kind.bufferName(), config.search_buffer)) {
pane.search_pane = free;
pane.search_row = null;
pane.search_kind = w.kind;
@@ -3149,9 +3093,9 @@ pub const Pardes = struct {
/// mean exactly the same thing in a shell pane as in a document.
fn handleInsert(p: *Pardes, pane: *Pane, key: Key) void {
// helix aliases: normalize to the base key and re-dispatch
- if (isC(key, 'h')) return p.handleInsert(pane, .{ .cp = Key.backspace });
- if (isC(key, 'j')) return p.handleInsert(pane, .{ .cp = Key.enter });
- if (isC(key, 'd')) return p.handleInsert(pane, .{ .cp = Key.delete });
+ if (hit(key, config.insert_backspace_alias)) return p.handleInsert(pane, .{ .cp = Key.backspace });
+ if (hit(key, config.insert_enter_alias)) return p.handleInsert(pane, .{ .cp = Key.enter });
+ if (hit(key, config.insert_delete_alias)) return p.handleInsert(pane, .{ .cp = Key.delete });
// a selection carried into insert (i/a) survives only until the next
// key: helix maps it through every edit, pardes drops it instead —
// its only pardes use (the acme chords) needs explicit selections
@@ -3220,7 +3164,7 @@ pub const Pardes = struct {
return;
}
// helix insert-mode kills (word/line; deleteSpan is exclusive)
- if (isC(key, 'w') or (key.alt and !key.ctrl and key.cp == Key.backspace)) {
+ if (hit(key, config.delete_word_backward)) {
// helix delete_word_backward: to the previous word start —
// crossing the newline at col 0, which takes the word before it
// too, so on the buffer's first line a terminal grows up one row
@@ -3242,7 +3186,7 @@ pub const Pardes = struct {
pane.ensureCursorVisible();
return;
}
- if (key.alt and !key.ctrl and (key.cp == 'd' or key.cp == Key.delete)) {
+ if (hit(key, config.delete_word_forward)) {
// helix delete_word_forward: to the next word END (trailing
// whitespace survives), crossing newlines at line ends — at the
// buffer's last line a terminal grows down one row to allow it
@@ -3260,7 +3204,7 @@ pub const Pardes = struct {
pane.cur_pinned = true;
return;
}
- if (isC(key, 'u')) {
+ if (hit(key, config.kill_to_line_start)) {
const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = 0 }, c) catch return;
p.setEditText(pane, new);
pane.cur_col = 0;
@@ -3268,7 +3212,7 @@ pub const Pardes = struct {
pane.ensureCursorVisible();
return;
}
- if (isC(key, 'k')) {
+ if (hit(key, config.kill_to_line_end)) {
const llen = modal.lineSlice(text, c.row).len;
const new = modal.deleteSpan(p.gpa, text, c, .{ .row = c.row, .col = llen }) catch return;
p.setEditText(pane, new);
@@ -4194,10 +4138,10 @@ pub const Pardes = struct {
if (mcol >= r.x and mcol < r.x + r.w and mrow >= r.y and mrow < r.y + r.h) break i;
} else null;
- // any press that is NOT the middle+left chord abandons a pending
+ // any press that is NOT the execute+select chord abandons a pending
// chord argument
if (m.kind == .press and
- !(m.button == .left and p.drag == .select and p.drag.select.button == .middle))
+ !(m.button == config.select_button and p.drag == .select and p.drag.select.button == config.exec_button))
{
if (p.chord_arg) |a| {
p.gpa.free(a);
@@ -4214,7 +4158,7 @@ pub const Pardes = struct {
.wheel_up, .wheel_down => {
if (m.kind != .press) return;
const id = hovered orelse return;
- p.panes[id].?.scrollBy(if (m.button == .wheel_up) -1 else 1);
+ p.panes[id].?.scrollBy(if (m.button == .wheel_up) -config.wheel_rows else config.wheel_rows);
},
.wheel_left, .wheel_right => {
if (m.kind != .press) return;
@@ -4223,35 +4167,35 @@ pub const Pardes = struct {
// ponytail: no right clamp — overscroll shows blank and the
// next cursor move or left wheel pulls it back
if (pane.file != null)
- pane.hscroll = @max(0, pane.hscroll + @as(i32, if (m.button == .wheel_right) 4 else -4));
+ pane.hscroll = @max(0, pane.hscroll + (if (m.button == .wheel_right) config.wheel_cols else -config.wheel_cols));
},
- .left => switch (m.kind) {
+ config.select_button => switch (m.kind) {
.press => {
- // middle+left chord: a left press during a middle select-
- // drag captures a selection (heldSelection) as the
- // execute's argument; the middle drag keeps running to its
- // release, which consumes it.
- if (p.drag == .select and p.drag.select.button == .middle) {
+ // acme 2-1: a select press during an EXECUTE drag captures
+ // a selection (heldSelection) as the execute's argument;
+ // the execute drag keeps running to its release, which
+ // consumes it.
+ if (p.drag == .select and p.drag.select.button == config.exec_button) {
if (p.heldSelection(p.drag.select.id)) |tx| {
if (p.chord_arg) |old| p.gpa.free(old);
p.chord_arg = p.gpa.dupe(u8, tx) catch null;
}
return;
}
- // acme 3-1: a left tap during a right (look) drag cancels
- // the look; the rest of the gesture is inert
- if (p.drag == .select and p.drag.select.button == .right) {
- if (p.panes[p.drag.select.id]) |t| t.sel[@intFromEnum(Mouse.Button.right)].state = .none;
+ // acme 3-1: a select tap during a LOOK drag cancels the
+ // look; the rest of the gesture is inert
+ if (p.drag == .select and p.drag.select.button == config.look_button) {
+ if (p.panes[p.drag.select.id]) |t| t.sel[@intFromEnum(config.look_button)].state = .none;
p.drag = .none;
return;
}
- if (mrow < TOPBAR_H) return; // topbar: left click deliberately inert
+ if (mrow < TOPBAR_H) return; // topbar: a select click is deliberately inert
// the layout box (gutter cells of the tag row) wins over
// the resize handles: a tag-only pane's single row IS its
// pair's h-handle, and the box must stay grabbable to move
if (hovered) |mid| {
const mr = p.rects[mid];
- if (mcol < mr.x + GUTTER and mrow < mr.y + BOX_H) {
+ if (mcol < mr.x + config.GUTTER and mrow < mr.y + BOX_H) {
p.drag = .{ .move = .{ .id = mid, .cur_x = mcol, .cur_y = mrow } };
return;
}
@@ -4277,7 +4221,7 @@ pub const Pardes = struct {
const id = hovered orelse return;
const r = p.rects[id];
const pane = p.panes[id] orelse return;
- if (mcol < r.x + GUTTER) {
+ if (mcol < r.x + config.GUTTER) {
// gutter scrollbar: a left click scrolls UP to that row
p.active = id;
pane.scrollBy(-(@as(i32, mrow) - @as(i32, r.y + BOX_H)));
@@ -4287,35 +4231,35 @@ pub const Pardes = struct {
// agree, no clamp into the tail) and anchor a sweep, so
// dragging selects any span of it, path included
p.active = id;
- p.enterTagEdit(pane, @as(i32, mcol) - @as(i32, r.x + GUTTER));
+ p.enterTagEdit(pane, @as(i32, mcol) - @as(i32, r.x + config.GUTTER));
pane.tag_anchor = pane.tag_col;
p.drag = .{ .tag = .{ .id = id } };
} else {
p.active = id;
exitTagEdit(pane); // clicking the body leaves tag editing
- const sc: i32 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
+ const sc: i32 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
const v: i32 = @as(i32, mrow) - @as(i32, r.y);
- pane.sel[0] = .{ .state = .dragging, .c0 = sc, .c1 = sc, .r0 = v, .r1 = v };
+ pane.sel[sel_slot] = .{ .state = .dragging, .c0 = sc, .c1 = sc, .r0 = v, .r1 = v };
// Ctrl rides on the drag rather than firing here: the
// click does not place the modal cursor until RELEASE
// (dragRelease), and a goto asked at press time would
// answer about wherever the cursor happened to be.
- p.drag = .{ .select = .{ .id = id, .button = .left, .ctrl = m.ctrl } };
+ p.drag = .{ .select = .{ .id = id, .button = config.select_button, .ctrl = m.ctrl } };
}
},
.drag => p.dragUpdate(mcol, mrow),
- .release => p.dragRelease(.left),
+ .release => p.dragRelease(config.select_button),
.motion => {},
},
- .middle, .right => switch (m.kind) {
+ config.exec_button, config.look_button => switch (m.kind) {
.press => {
- // acme 1-2 / 1-3: middle (Cut) or right (Paste) tapped
- // while the left button holds a selection drag
- if (p.drag == .select and p.drag.select.button == .left) {
- p.chordCutPaste(m.button == .middle);
+ // acme 1-2 / 1-3: the execute (Cut) or look (Paste) button
+ // tapped while the select button holds a selection drag
+ if (p.drag == .select and p.drag.select.button == config.select_button) {
+ p.chordCutPaste(m.button == config.exec_button);
return;
}
- // acme 2-3 / 3-2: tapping the OTHER of middle/right during
+ // acme 2-3 / 3-2: tapping the OTHER of execute/look during
// an execute or look drag cancels it (textselect23's mask)
if (p.drag == .select) {
if (p.panes[p.drag.select.id]) |t| t.sel[@intFromEnum(p.drag.select.button)].state = .none;
@@ -4323,32 +4267,32 @@ pub const Pardes = struct {
return;
}
if (mrow < TOPBAR_H) {
- // global tagbar: MIDDLE runs the command under the click
- // (a stray left click must never Kill)
- if (m.button == .middle) {
+ // global tagbar: EXECUTE runs the command under the
+ // click (a stray select click must never Kill)
+ if (m.button == config.exec_button) {
var tb_buf: [1200]u8 = undefined;
const word = wordAtCol(p.topbar(&tb_buf), mcol);
// a topbar word runs on the PRESS — there is no
// drag to chord into, so the argument is simply
// whatever is selected right now: select a word,
// then click Grep, and it greps that word.
- if (word.len > 0) p.actOnSelection(.middle, p.active, word, p.heldSelection(p.active));
+ if (word.len > 0) p.actOnSelection(config.exec_button, p.active, word, p.heldSelection(p.active));
}
return;
}
const id = hovered orelse return;
const r = p.rects[id];
const pane = p.panes[id] orelse return;
- if (mcol < r.x + GUTTER and mrow >= r.y + BOX_H) {
+ if (mcol < r.x + config.GUTTER and mrow >= r.y + BOX_H) {
// gutter: right scrolls DOWN to here, mirroring left's up;
// middle matches left. Right focuses (look lands you
// there); middle does not.
const local = @as(i32, mrow) - @as(i32, r.y + BOX_H);
- if (m.button == .right) p.active = id;
- pane.scrollBy(if (m.button == .right) local else -local);
- } else if (mcol >= r.x + GUTTER) {
- if (m.button == .right) p.active = id;
- const c: i32 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
+ if (m.button == config.look_button) p.active = id;
+ pane.scrollBy(if (m.button == config.look_button) local else -local);
+ } else if (mcol >= r.x + config.GUTTER) {
+ if (m.button == config.look_button) p.active = id;
+ const c: i32 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
const v: i32 = @as(i32, mrow) - @as(i32, r.y);
pane.sel[@intFromEnum(m.button)] = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v };
p.drag = .{ .select = .{ .id = id, .button = m.button } };
@@ -4366,8 +4310,8 @@ pub const Pardes = struct {
.border_v => |*d| {
const c = d.left_col;
if (c + 1 < p.ncol) {
- const lo = p.col_x[c] + MINW;
- const hi = p.col_x[c] +| p.col_w[c] +| p.col_w[c + 1] -| MINW;
+ const lo = p.col_x[c] + config.MINW;
+ const hi = p.col_x[c] +| p.col_w[c] +| p.col_w[c + 1] -| config.MINW;
d.cur_x = if (lo <= hi) std.math.clamp(mcol, lo, hi) else mcol;
} else d.cur_x = mcol;
},
@@ -4391,7 +4335,7 @@ pub const Pardes = struct {
if (p.panes[s.id]) |pane| {
const r = p.rects[s.id];
const b = @intFromEnum(s.button);
- pane.sel[b].c1 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
+ pane.sel[b].c1 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
pane.sel[b].r1 = @as(i32, mrow) - @as(i32, r.y);
}
},
@@ -4401,7 +4345,7 @@ pub const Pardes = struct {
// the press put it, so this is the mouse's `v`
const pane = p.panes[d.id] orelse return;
const end: i32 = @intCast((p.tagPrefix(pane) catch return).len + pane.tag_tail.items.len);
- const c = @as(i32, mcol) - @as(i32, p.rects[d.id].x + GUTTER);
+ const c = @as(i32, mcol) - @as(i32, p.rects[d.id].x + config.GUTTER);
pane.tag_col = @intCast(std.math.clamp(c, 0, end));
pane.tag_sel = pane.tag_col != pane.tag_anchor;
},
@@ -4422,7 +4366,7 @@ pub const Pardes = struct {
// the handle is the left pane's LAST column (inclusive), so
// the new width includes it — a no-drag click changes nothing
var nl: f32 = @floatFromInt((d.cur_x + 1) -| p.col_x[c]);
- nl = std.math.clamp(nl, @as(f32, MINW), @max(@as(f32, MINW), combined - MINW));
+ nl = std.math.clamp(nl, @as(f32, config.MINW), @max(@as(f32, config.MINW), combined - config.MINW));
const pair = p.col_weight[c] + p.col_weight[c + 1];
p.col_weight[c] = pair * (nl / combined);
p.col_weight[c + 1] = pair - p.col_weight[c];
@@ -4461,18 +4405,18 @@ pub const Pardes = struct {
}
const b = @intFromEnum(s.button);
pane.sel[b].state = .done;
- if (s.button == .left) {
+ if (s.button == config.select_button) {
// keep a dragged selection highlighted; a plain click clears
// it. Either way pin the cursor; files also enter normal
// mode, terminals keep the mode they had (tty keeps the
// mouse usable, insert keeps typing where you clicked).
- const sl = pane.sel[0];
+ const sl = pane.sel[sel_slot];
const dragged = sl.c0 != sl.c1 or sl.r0 != sl.r1;
- if (!dragged) pane.sel[0].state = .none;
+ if (!dragged) pane.sel[sel_slot].state = .none;
const body_vis = sl.r1 - @as(i32, BOX_H);
if (body_vis >= 0 and pane.mode != .tty) {
pane.cur_row = body_vis + pane.scroll();
- pane.cur_col = if (pane.file != null) @max(0, sl.c1 - @as(i32, PREFIX_W)) + pane.hscroll else sl.c1;
+ pane.cur_col = if (pane.file != null) @max(0, sl.c1 - @as(i32, config.PREFIX_W)) + pane.hscroll else sl.c1;
pane.cur_pinned = true;
if (!pane.isTerminal()) pane.mode = .normal;
pane.msel.active = false;
@@ -4527,18 +4471,18 @@ pub const Pardes = struct {
// to \r like any unbracketed paste.
if (!s.chorded) {
s.chorded = true;
- pane.sel[0].state = .none; // drop the sweep highlight
+ pane.sel[sel_slot].state = .none; // drop the sweep highlight
}
if (cut) return;
const y = p.yank orelse return;
if (y.len == 0) return;
const m = &pane.vt.modes;
if (m.get(.mouse_event_normal) or m.get(.mouse_event_button) or m.get(.mouse_event_any)) {
- // dragUpdate keeps sel[0] tracking the held left button, so
+ // dragUpdate keeps sel[sel_slot] tracking the held select button, so
// the click lands where the mouse is at this tap; 1-based,
// body-relative (the tag row is ours, not the app's)
- const col: u16 = @intCast(std.math.clamp(pane.sel[0].c1 + 1, 1, 9999));
- const row: u16 = @intCast(std.math.clamp(pane.sel[0].r1 - @as(i32, BOX_H) + 1, 1, 9999));
+ const col: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].c1 + 1, 1, 9999));
+ const row: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].r1 - @as(i32, BOX_H) + 1, 1, 9999));
var mb: [32]u8 = undefined;
if (m.get(.mouse_format_sgr)) {
p.emitWrite(s.id, std.fmt.bufPrint(&mb, "\x1b[<0;{d};{d}M\x1b[<0;{d};{d}m", .{ col, row, col, row }) catch return);
@@ -4564,9 +4508,9 @@ pub const Pardes = struct {
}
if (!s.chorded) {
s.chorded = true;
- const sl = pane.sel[0];
- pane.sel[0].state = .none;
- const pfx: i32 = if (pane.file != null) PREFIX_W else 0;
+ const sl = pane.sel[sel_slot];
+ pane.sel[sel_slot].state = .none;
+ const pfx: i32 = if (pane.file != null) config.PREFIX_W else 0;
const hs: i32 = if (pane.file != null) pane.hscroll else 0;
const row0 = @max(0, sl.r0 - @as(i32, BOX_H)) + pane.scroll();
const row1 = @max(0, sl.r1 - @as(i32, BOX_H)) + pane.scroll();
@@ -4952,7 +4896,7 @@ pub const Pardes = struct {
fn actOnSelection(p: *Pardes, button: Mouse.Button, id: usize, txt: []const u8, arg: ?[]const u8) void {
const pane = p.panes[id] orelse return;
- if (button == .right) {
+ if (button == config.look_button) {
var realbuf: [4096]u8 = undefined;
// an already-loaded pane wins BEFORE any filesystem resolve: the
// web build has no fs (a look would otherwise be inert even for
@@ -4975,7 +4919,7 @@ pub const Pardes = struct {
// directory to one try; an absolute word — and `@pN`, which reads
// no directory at all — answers the same everywhere and stops
// after the first pass.
- var hit: look.Target = .none;
+ var found: look.Target = .none;
var seen: [MAX_PANES][]const u8 = undefined;
var nseen: usize = 0;
var n: usize = 0;
@@ -4992,10 +4936,10 @@ pub const Pardes = struct {
if (std.fmt.bufPrint(&joinbuf, "{s}/{s}", .{ dir, pl.path }) catch null) |j|
if (p.focusPaneByPath(j, pl.line, pl.col)) return;
}
- hit = look.resolve(txt, dir, &realbuf);
- if (hit != .none or pl.path.len == 0 or pl.path[0] == '/') break;
+ found = look.resolve(txt, dir, &realbuf);
+ if (found != .none or pl.path.len == 0 or pl.path[0] == '/') break;
}
- switch (hit) {
+ switch (found) {
// acme button-3: a word that names no file/dir is a search of
// the pane it was clicked in — exactly what `/` runs (n/N then
// walk the results). Paths (src/a/b.rs:100) still resolve above
@@ -5053,9 +4997,9 @@ pub const Pardes = struct {
}
return;
}
- // execute (middle / Tab): a builtin's name runs the builtin, anything
- // else is a command line for a shell.
- if (button != .middle) return;
+ // execute (the execute button / the execute key): a builtin's name
+ // runs the builtin, anything else is a command line for a shell.
+ if (button != config.exec_button) return;
const cmd = std.mem.trim(u8, txt, " \t\r\n");
if (cmd.len == 0) return;
// three builtins take an ARGUMENT — `Restore <path>`, `Find <pat>`,
@@ -5264,7 +5208,7 @@ pub const Pardes = struct {
const state: dump.State = .{
.screen = .{ .cols = p.screen_w, .rows = p.screen_h },
.active = slot_to_pane[p.active] orelse 0,
- .topbar = topbar_str,
+ .topbar = config.topbar_str,
.theme = p.theme().name,
.columns = columns.items,
.panes = panes.items,
@@ -5437,7 +5381,7 @@ pub const Pardes = struct {
pane.reply_len = 0;
}
const r = p.rects[id];
- const cols = @max(1, r.w -| GUTTER);
+ const cols = @max(1, r.w -| config.GUTTER);
const rows = @max(1, r.h -| BOX_H); // the tag steals the top row
// a pane shrunk to just its tag keeps its last real grid: no
// pty/vt reflow while the body is hidden, so re-enlarging brings
@@ -5648,11 +5592,11 @@ pub const Pardes = struct {
fn renderPane(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, active: bool) !void {
const tz = tracy.zone(@src(), "renderPane");
defer tz.end();
- if (r.w <= GUTTER or r.h == 0) return;
+ if (r.w <= config.GUTTER or r.h == 0) return;
const s = &p.surface;
const th = p.theme();
- const tx = r.x + GUTTER; // text area (tag + body), right of the gutter
- const tw = r.w - GUTTER;
+ const tx = r.x + config.GUTTER; // text area (tag + body), right of the gutter
+ const tw = r.w - config.GUTTER;
// text area resets to terminal-default cells (vaxis clear semantics);
// light themes paint the page over it.
@@ -5706,8 +5650,8 @@ pub const Pardes = struct {
if (pane.image != null) {
p.drawImage(pane, r, tx, tw);
const box_bg2 = if (active) th.box else th.box_dim;
- s.fill(r.x, r.y, GUTTER, BOX_H, .{ .bg = .{ .rgb = box_bg2 } });
- if (r.h > BOX_H) s.fill(r.x, r.y + BOX_H, GUTTER, r.h - BOX_H, .{ .bg = .{ .rgb = th.scroll_track } });
+ s.fill(r.x, r.y, config.GUTTER, BOX_H, .{ .bg = .{ .rgb = box_bg2 } });
+ if (r.h > BOX_H) s.fill(r.x, r.y + BOX_H, config.GUTTER, r.h - BOX_H, .{ .bg = .{ .rgb = th.scroll_track } });
return;
}
@@ -5752,7 +5696,7 @@ pub const Pardes = struct {
var row: u16 = 0;
while (row < r.h) : (row += 1) {
if (@as(i32, row) < r0 or @as(i32, row) > r1) continue;
- var col: i32 = if (pane.file != null) @max(c0, @as(i32, PREFIX_W)) else c0;
+ var col: i32 = if (pane.file != null) @max(c0, @as(i32, config.PREFIX_W)) else c0;
while (col <= c1 and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), r.y + row);
cell.default = false;
@@ -5768,7 +5712,7 @@ pub const Pardes = struct {
var row: u16 = BOX_H; // never paint the tag row
while (row < r.h) : (row += 1) {
if (@as(i32, row) < lo or @as(i32, row) > hi) continue;
- var col: u16 = if (pane.file != null) PREFIX_W else 0;
+ var col: u16 = if (pane.file != null) config.PREFIX_W else 0;
while (col < tw) : (col += 1) {
const cell = s.at(tx + col, r.y + row);
cell.default = false;
@@ -5780,7 +5724,7 @@ pub const Pardes = struct {
// modal char selection (helix `v`): stream-shaped anchor..head highlight
if (pane.mode == .normal and pane.vsel.active) {
const bnd = vselBounds(pane);
- const vpfx: i32 = if (pane.file != null) PREFIX_W else 0;
+ const vpfx: i32 = if (pane.file != null) config.PREFIX_W else 0;
const vhs: i32 = if (pane.file != null) pane.hscroll else 0;
var ar: i32 = bnd.lo_row;
while (ar <= bnd.hi_row) : (ar += 1) {
@@ -5809,7 +5753,7 @@ pub const Pardes = struct {
const prow = crow - off + @as(i32, BOX_H);
// files: cur_col is content-relative, add the lineno prefix
const cx = if (pane.file != null)
- @as(i32, PREFIX_W) + ccol - pane.hscroll
+ @as(i32, config.PREFIX_W) + ccol - pane.hscroll
else
ccol;
if (prow >= BOX_H and cx >= 0 and prow < r.h and cx < tw)
@@ -5821,9 +5765,9 @@ pub const Pardes = struct {
// gutter: move box on top, scrollbar track + thumb below
const box_bg = if (active) th.box else th.box_dim;
- s.fill(r.x, r.y, GUTTER, BOX_H, .{ .bg = .{ .rgb = box_bg } });
+ s.fill(r.x, r.y, config.GUTTER, BOX_H, .{ .bg = .{ .rgb = box_bg } });
if (r.h > BOX_H) {
- s.fill(r.x, r.y + BOX_H, GUTTER, r.h - BOX_H, .{ .bg = .{ .rgb = th.scroll_track } });
+ s.fill(r.x, r.y + BOX_H, config.GUTTER, r.h - BOX_H, .{ .bg = .{ .rgb = th.scroll_track } });
const sb: struct { total: usize, offset: usize, len: usize } = if (pane.file) |f| .{
.total = file_pane.lineCount(f.content),
.offset = f.scroll,
@@ -5838,7 +5782,7 @@ pub const Pardes = struct {
const pos = (track_h * sb.offset) / total;
var sy: usize = pos;
while (sy < track_h and sy < pos + len) : (sy += 1) {
- s.fill(r.x, r.y + BOX_H + @as(u16, @intCast(sy)), GUTTER, 1, .{ .bg = .{ .rgb = th.scroll_thumb } });
+ s.fill(r.x, r.y + BOX_H + @as(u16, @intCast(sy)), config.GUTTER, 1, .{ .bg = .{ .rgb = th.scroll_thumb } });
}
}
}