summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig86
1 files changed, 76 insertions, 10 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index fadaf3cc..0725e8ab 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -71,6 +71,17 @@ const Builtin = enum {
Del,
Delcol,
Help,
+ // the window group. The DESTINATION is the name — a word, the way a tag
+ // holds Del or Save — because these names live in the same vocabulary as
+ // everything else here: `Wh` would be a leader key path leaking into the
+ // text you can middle-click. Plain English words are safe for exactly
+ // these five: focus is the cheapest thing to change by accident (nothing
+ // is edited, closed or written) and the way back is the opposite word.
+ Left,
+ Down,
+ Up,
+ Right,
+ Toggleterm,
};
/// SPC leader: ONE key path per builtin, the whole remapping surface. An
@@ -95,6 +106,14 @@ const leader_path = std.EnumArray(Builtin, []const u8).init(.{
.Crt = "tr",
.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
@@ -139,7 +158,7 @@ const leader_rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = blk: {
// snapshot golden records the column each word starts at). Comptime-checked
// against the enum so a rename cannot silently rot it.
comptime {
- @setEvalBranchQuota(4000); // the tokenizer walks the string one branch per byte
+ @setEvalBranchQuota(8000); // the tokenizer walks the string one branch per byte, stringToEnum builds a map per name
var it = std.mem.tokenizeScalar(u8, topbar_str, ' ');
while (it.next()) |w| {
if (std.meta.stringToEnum(Builtin, w) == null) @compileError("topbar word is not a builtin: " ++ w);
@@ -1377,11 +1396,14 @@ 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)
+ // 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
+ // 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.focusDir(.left) else if (is(key, 'l') or is(key, Key.right)) p.focusDir(.right) else if (is(key, 'k') or is(key, Key.up)) p.focusDir(.up) else if (is(key, 'j') or is(key, Key.down)) p.focusDir(.down);
- if (p.panes[p.active]) |nt| nt.pending = 0;
+ if (is(key, 'h') or is(key, Key.left)) p.runBuiltin(.Left, p.active, "") else if (is(key, 'l') or is(key, Key.right)) p.runBuiltin(.Right, p.active, "") else if (is(key, 'k') or is(key, Key.up)) p.runBuiltin(.Up, p.active, "") else if (is(key, 'j') or is(key, Key.down)) p.runBuiltin(.Down, p.active, "");
return;
}
// insert mode owns Ctrl-w (delete word backward, helix); the focus
@@ -1571,13 +1593,15 @@ pub const Pardes = struct {
pane.pending = 0;
}
- /// move focus to the nearest pane in `dir` (overlap-aware nearest edge)
- fn focusDir(p: *Pardes, dir: enum { left, right, up, down }) void {
- const a = p.rects[p.active];
+ /// move focus to the nearest pane in `dir` of `from` (overlap-aware
+ /// nearest edge). `from` is the pane the builtin ran on, which is the
+ /// active one for a key but the CLICKED one for a name executed in a tag.
+ fn focusDir(p: *Pardes, from: usize, dir: enum { left, right, up, down }) void {
+ const a = p.rects[from];
var best: ?usize = null;
var best_d: i32 = 0;
for (p.panes, 0..) |slot, i| {
- if (slot == null or i == p.active) continue;
+ if (slot == null or i == from) continue;
const r = p.rects[i];
const vov = a.y < r.y + r.h and r.y < a.y + a.h;
const hov = a.x < r.x + r.w and r.x < a.x + a.w;
@@ -1599,7 +1623,11 @@ pub const Pardes = struct {
best_d = d;
}
}
- if (best) |b| p.active = b;
+ if (best) |b| {
+ p.active = b;
+ // a count typed before the hop was meant for the pane you left
+ p.panes[b].?.pending = 0;
+ }
}
// ---- move-drag placement ----
@@ -4681,7 +4709,8 @@ pub const Pardes = struct {
/// Run a builtin on pane `id`. `txt` is the executed text — only Restore
/// reads it (for its optional path argument), so the leader passes "" and
/// every builtin runs with zero arguments. The topbar ones are global; the
- /// pane-scoped ones (Save, Del, Delcol) act on `id`.
+ /// pane-scoped ones (Save, Del, Delcol, and the window group, which moves
+ /// focus relative to `id`) act on `id`.
fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8) void {
const pane = p.panes[id] orelse return;
switch (b) {
@@ -4752,6 +4781,43 @@ pub const Pardes = struct {
const nt = p.openTutorView(free) catch return;
p.placeDoc(id, free, nt); // a doc like any other
},
+ .Left => p.focusDir(id, .left),
+ .Down => p.focusDir(id, .down),
+ .Up => p.focusDir(id, .up),
+ .Right => p.focusDir(id, .right),
+ // The file<->terminal hop. "Latest" is already recorded: focus_hist
+ // is the MRU sync() rebuilds every update (most recent last), the
+ // same list a closing pane hands focus back through — so this walks
+ // it instead of keeping a second one. Which side is which: only a
+ // shell is a terminal; a file, an image and an output buffer
+ // (+Search/+Help) are all DOCS you read, so isTerminal is the whole
+ // test. Landing pushes this pane to the top of that same history,
+ // which is why the hop back is the same key.
+ .Toggleterm => {
+ const want_term = !pane.isTerminal();
+ var t: ?usize = null;
+ // any live pane of the other kind: a pane you have never
+ // focused (the file you started with) is in no history at all
+ for (p.panes, 0..) |slot, k| {
+ const op = slot orelse continue;
+ if (k != id and op.isTerminal() == want_term) t = k;
+ }
+ // ...but the most recently focused one wins
+ var i = p.nfocus;
+ while (i > 0) {
+ i -= 1;
+ const hid = p.focus_hist[i];
+ const hp = p.panes[hid] orelse continue;
+ if (hid != id and hp.isTerminal() == want_term) {
+ t = hid;
+ break;
+ }
+ }
+ if (t) |target| {
+ p.active = target;
+ p.panes[target].?.pending = 0;
+ }
+ },
}
}