summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 22:30:58 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit52049344c1992850ea712e029eb5c4a88a7dfe19 (patch)
tree24d12755e9545470dda8021542299f0f72fd0baf /src/pardes.zig
parent82551f77833a09b313bd3e58371582abd1aedcc4 (diff)
downloadpardes-52049344c1992850ea712e029eb5c4a88a7dfe19.tar.gz
pardes-52049344c1992850ea712e029eb5c4a88a7dfe19.zip
Move exec out of pardes.zig into exec.zig
Pure move, no behaviour change (acme keeps this in exec.c): execute, commandText, max_exec_depth, executeBuiltinLine, applyStartupConfig, runBuiltin and applySettingBuiltin; the getarg-style operand code (withArg, PointerOperand, pointerOperand, heldSelection, chordEachSel); takesCommandLine and the terminals commands run in (spawnTty, spawnV9fsTty, spawnTtyWithMount, evictLonePristineTty, replaceStillborn, ttyForDir); placeDoc; and the save path (submitSave, saveFile, saveTo, askWrite), with the acme-chords test, go verbatim to exec.zig. The methods become free functions taking `p: *Pardes`. executeBuiltinLine is called from ~170 places as `p.executeBuiltinLine(..)`, so Pardes keeps one declaration alias for it and those call sites stay; the other 104 calls change from `p.execute(..)` to `exec.execute(p, ..)`. ninep/ctl.zig has a local named exec, so it writes `pardes.exec.execute` instead of importing the file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig653
1 files changed, 58 insertions, 595 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index bdde1455..75766827 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -11,6 +11,7 @@ pub const syntax = @import("syntax.zig");
pub const locations_config = @import("locations_config.zig");
const tracy = @import("tracy.zig");
pub const panes = @import("panes.zig");
+pub const exec = @import("exec.zig");
pub const mouse = @import("mouse.zig");
pub const normal = @import("normal.zig");
pub const edit = @import("edit.zig");
@@ -435,6 +436,7 @@ pub const Loc = struct {
};
test {
+ _ = @import("exec.zig");
_ = @import("look.zig");
_ = @import("mouse.zig");
_ = @import("normal.zig");
@@ -497,7 +499,7 @@ pub const test_api = if (@import("builtin").is_test) struct {
arg: ?[]const u8,
) bool {
const command = std.meta.stringToEnum(Builtin, name) orelse return false;
- p.runBuiltin(command, id, txt, arg);
+ exec.runBuiltin(p, command, id, txt, arg);
return true;
}
@@ -510,7 +512,7 @@ pub const test_api = if (@import("builtin").is_test) struct {
}
pub fn heldSelection(p: *Pardes, first: usize) ?[]const u8 {
- return p.heldSelection(first);
+ return exec.heldSelection(p, first);
}
} else struct {};
@@ -581,30 +583,6 @@ test "Detach asks the frontend to leave, and says so when there is nothing to le
try std.testing.expect(!p.quit);
}
-test "the acme chords act once per selection, not once on the primary" {
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 });
- defer p.deinit();
- while (p.nextEffect()) |_| {}
- // Two selections, each naming a DIFFERENT builtin, so what ran is visible
- // in the layout rather than in a shell nobody can read from a test.
- const pane = try p.setTestFile("Newcol\nNewcol\n");
- const ranges = [_]modal.Selection{
- .{ .anchor = 0, .head = 6 },
- .{ .anchor = 7, .head = 13 },
- };
- pane.setRanges(pane.file.?.content, &ranges, &.{}, 0, true);
- try std.testing.expectEqual(@as(u8, 1), pane.nsel);
-
- const before = p.ncol;
- p.update(.{ .key = .{ .cp = Key.tab } }); // config.exec_key: Exec
- // BOTH ran. Before this the chord read the primary range and dropped the
- // other cursor, so one keystroke over two cursors made one column.
- try std.testing.expectEqual(before + 2, p.ncol);
- // ...and the chord consumed the selection exactly as it does with one.
- try std.testing.expectEqual(@as(u8, 0), p.panes[0].?.nsel);
-}
-
test "startup config runs builtin lines in order and isolates bad lines" {
const p = try Pardes.init(std.testing.allocator, .{
.startup_config =
@@ -1188,11 +1166,11 @@ test "Tty9p marks only the new Linux terminal for a mounted shell" {
try std.testing.expect(pane.isTerminal() and pane.mode == .tty);
try std.testing.expect(pane.v9fs_on_spawn);
try std.testing.expect(!pane.greet);
- try std.testing.expect(!p.takesCommandLine(p.active));
+ try std.testing.expect(!exec.takesCommandLine(p, p.active));
panes.Terminal.feedOutput(p, pane, "\x1b]133;A\x07$ \x1b]133;B\x07");
- try std.testing.expect(p.takesCommandLine(p.active));
+ try std.testing.expect(exec.takesCommandLine(p, p.active));
panes.Terminal.feedOutput(p, pane, "\x1b]133;C\x07");
- try std.testing.expect(!p.takesCommandLine(p.active));
+ try std.testing.expect(!exec.takesCommandLine(p, p.active));
}
test "first document fallback splits like Tty from the originating pane" {
@@ -1215,8 +1193,8 @@ test "first document fallback splits like Tty from the originating pane" {
p.active = (from + 1) % 3;
const free = p.freeSlot().?;
const doc = try p.openTutorView(free);
- p.placeDoc(from, free, doc);
- tty.spawnTty(from);
+ exec.placeDoc(p, from, free, doc);
+ exec.spawnTty(tty, from);
p.sync();
tty.sync();
try std.testing.expectEqual(@as(usize, 2), p.ncol);
@@ -1240,7 +1218,7 @@ test "first document keeps its originating shell and only opens a column when it
defer p.deinit();
const shell = p.panes[0].?;
const doc = try p.openTutorView(1);
- p.placeDoc(0, 1, doc);
+ exec.placeDoc(p, 0, 1, doc);
p.sync();
try std.testing.expectEqual(shell, p.panes[0].?);
try std.testing.expectEqual(@as(usize, 1), p.active);
@@ -1318,7 +1296,7 @@ test "unknown Exec from an image writes to a terminal in the image directory" {
const image_doc = try p.newDocPane(1);
image_doc.image = .{ .path = try gpa.dupe(u8, "/tmp/pardes-image-dir/pic.ppm") };
try std.testing.expectEqualStrings("/tmp/pardes-image-dir", Pardes.paneDir(image_doc));
- try std.testing.expectEqual(@as(?usize, 0), p.execute(1, "echo image-fallback"));
+ try std.testing.expectEqual(@as(?usize, 0), exec.execute(p, 1, "echo image-fallback"));
var sent: [256]u8 = undefined;
var sent_len: usize = 0;
@@ -1364,7 +1342,7 @@ test "Exec in a terminal whose tty is taken spawns a shell instead of typing at
var host: FakeTtyQuery = .{};
host.install(p);
host.taken[0] = true;
- const dst = p.execute(0, "echo taken-fallback") orelse return error.ExecFoundNowhereToRun;
+ const dst = exec.execute(p, 0, "echo taken-fallback") orelse return error.ExecFoundNowhereToRun;
// somewhere ELSE — and still without moving focus, which is execute's
// contract and the whole difference between it and a look
try std.testing.expect(dst != 0);
@@ -1466,7 +1444,7 @@ test "Exec from a document pane skips an occupied terminal in its directory and
host.install(p);
host.taken[0] = true;
- const dst = p.execute(1, "echo image-fallback") orelse return error.ExecFoundNowhereToRun;
+ const dst = exec.execute(p, 1, "echo image-fallback") orelse return error.ExecFoundNowhereToRun;
try std.testing.expect(dst != 0 and dst != 1);
var spawned: ?Effect = null;
@@ -1551,13 +1529,13 @@ test "a terminal its program gave back takes command lines again" {
var host: FakeTtyQuery = .{};
host.install(p);
host.taken[0] = true;
- try std.testing.expect(p.execute(0, "echo while-taken") != 0);
+ try std.testing.expect(exec.execute(p, 0, "echo while-taken") != 0);
while (p.nextEffect()) |_| {}
// the program exited, so the host's answer changes — and the core asks
// again, because it never cached the old one
host.taken[0] = false;
- try std.testing.expectEqual(@as(?usize, 0), p.execute(0, "echo after"));
+ try std.testing.expectEqual(@as(?usize, 0), exec.execute(p, 0, "echo after"));
var sent: [64]u8 = undefined;
var sent_len: usize = 0;
while (p.nextEffect()) |effect| switch (effect) {
@@ -1595,13 +1573,13 @@ test "the host is asked about a tty only where a command line is about to go" {
try std.testing.expectEqual(@as(usize, 0), host.asked);
// The pane an Exec is typed into is one question, asked once...
- _ = p.execute(0, "echo lazy");
+ _ = exec.execute(p, 0, "echo lazy");
while (p.nextEffect()) |_| {}
try std.testing.expectEqual(@as(usize, 1), host.asked);
host.taken[0] = true;
host.asked = 0;
- _ = p.execute(0, "echo lazy-again");
+ _ = exec.execute(p, 0, "echo lazy-again");
while (p.nextEffect()) |_| {}
try std.testing.expectEqual(@as(usize, 2), host.asked);
}
@@ -1654,7 +1632,7 @@ test "owned cwd preserves long paths aliases and failed updates" {
const free = p.freeSlot().?;
try std.testing.expectError(error.PathTooLong, p.newShell(free, path[0 .. effect_path_cap + 1]));
try std.testing.expect(p.panes[free] == null);
- p.saveTo(0, "file.txt");
+ exec.saveTo(p, 0, "file.txt");
try std.testing.expectEqual(effects, p.effects_len);
try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "PathTooLong") != null);
@@ -1820,7 +1798,7 @@ test "owned cwd Save promotion releases the former directory" {
const id = p.active;
const pane = p.panes[id].?;
try pane.setOwnedCwd("/old/directory");
- p.saveTo(id, "saved.txt");
+ exec.saveTo(p, id, "saved.txt");
try std.testing.expect(pane.cwd == .none);
try std.testing.expect(pane.file.?.output == null);
try std.testing.expectEqualStrings("/old/directory/saved.txt", pane.file.?.path);
@@ -1847,7 +1825,7 @@ test "Save on a scratch asks for a path in its inherited dir and makes it a file
// typing the filename and submitting converts it into an ordinary file
try std.testing.expect(np.appendTag("note.txt"));
- p.submitSave(id);
+ exec.submitSave(p, id);
try std.testing.expect(np.file.?.output == null);
try std.testing.expectEqualStrings("/tmp/pardes-save-dir/note.txt", np.file.?.path);
var saved = false;
@@ -1872,7 +1850,7 @@ test "Save on a terminal writes its plaintext scrollback and stays a terminal" {
try std.testing.expect(p.executeBuiltinLine(0, "Save"));
try std.testing.expect(pane.prompt == .save);
try std.testing.expect(pane.appendTag("log.txt"));
- p.submitSave(0);
+ exec.submitSave(p, 0);
// the pane is untouched: still a terminal, no file behind it
try std.testing.expect(pane.isTerminal());
@@ -1921,7 +1899,7 @@ test "Save on an output buffer writes its rows out and leaves the buffer alone"
try std.testing.expect(p.executeBuiltinLine(id, "Save"));
try std.testing.expect(out.prompt == .save);
try std.testing.expect(out.appendTag("help.txt"));
- p.submitSave(id);
+ exec.submitSave(p, id);
// the buffer itself is untouched: same origin, same name, still refillable
// and still walked by n/N, because a saved copy of a list is only a copy
@@ -2156,7 +2134,7 @@ test "a save path that names no file is refused before anything is rewritten" {
try std.testing.expect(p.executeBuiltinLine(id, "Save"));
try std.testing.expect(scratch.prompt == .save);
- p.submitSave(id);
+ exec.submitSave(p, id);
var buf: [256]u8 = undefined;
try std.testing.expect(drainForSavePath(p, &buf) == null);
try std.testing.expect(scratch.file.?.output != null);
@@ -2213,7 +2191,7 @@ test "a builtin that needs a pane reports capacity failure when every slot is fu
_ = try p.newDocPane(id);
};
const help = std.meta.stringToEnum(Builtin, "Help") orelse return error.MissingHelpBuiltin;
- p.runBuiltin(help, 0, "", null);
+ exec.runBuiltin(p, help, 0, "", null);
try std.testing.expect(std.mem.indexOf(u8, p.panes[0].?.msg[0..p.panes[0].?.msg_len], "help: NoPaneSlots") != null);
}
@@ -2760,7 +2738,7 @@ test "column tags follow their column through split and close" {
defer p.deinit();
_ = try p.setTestFile("left\n");
try std.testing.expect(p.col_tags[0].set("Left Find"));
- _ = p.execute(0, "Newcol");
+ _ = exec.execute(p, 0, "Newcol");
try std.testing.expectEqual(@as(usize, 2), p.ncol);
try std.testing.expectEqualStrings("Left Find", p.columnTag(0));
try std.testing.expect(p.col_tags[1].set("Right Grep"));
@@ -3362,7 +3340,7 @@ test "canonical fallback and transition toggle retire unpresented tracks" {
const moving = try p.render(frame.allocator());
p.acknowledgePanelPresentation(moving.panelTracks());
try std.testing.expect(moving.panelTracks().len > 0);
- p.applySettingBuiltin(config.Runtime.find("PanelZoom").?, null);
+ exec.applySettingBuiltin(p, config.Runtime.find("PanelZoom").?, null);
try std.testing.expectEqual(layout.Transition.zoom, p.settings.panel_transition);
try std.testing.expect(p.presentation.pending);
try std.testing.expect(p.presentation.pointer(p.screen_w, p.screen_h, 10, 4) == null);
@@ -3379,11 +3357,11 @@ test "canonical fallback and transition toggle retire unpresented tracks" {
const old_tagline_percent = p.settings.font.tagline_percent;
var percent_buf: [3]u8 = undefined;
const same_percent = try std.fmt.bufPrint(&percent_buf, "{d}", .{old_tagline_percent});
- p.applySettingBuiltin(config.Runtime.find("TaglineSize").?, same_percent);
+ exec.applySettingBuiltin(p, config.Runtime.find("TaglineSize").?, same_percent);
try std.testing.expect(p.animationActive());
const changed_percent: u8 = if (old_tagline_percent == 73) 74 else 73;
const changed_text = try std.fmt.bufPrint(&percent_buf, "{d}", .{changed_percent});
- p.applySettingBuiltin(config.Runtime.find("TaglineSize").?, changed_text);
+ exec.applySettingBuiltin(p, config.Runtime.find("TaglineSize").?, changed_text);
try std.testing.expectEqual(changed_percent, p.settings.font.tagline_percent);
try std.testing.expect(p.presentation.pending);
for (p.presentation.tracks) |track| try std.testing.expect(track == null);
@@ -3839,6 +3817,10 @@ pub const Pardes = struct {
pub const setMessage = Messages.setMessage;
pub const reportError = Messages.reportError;
+ /// Running a builtin line, called from everywhere as
+ /// `p.executeBuiltinLine(...)`; it lives with the rest of exec in exec.zig.
+ pub const executeBuiltinLine = exec.executeBuiltinLine;
+
pub fn init(gpa: std.mem.Allocator, opts: Options) !*Pardes {
const image_gpa = opts.image_allocator orelse gpa;
const pdf_gpa = opts.pdf_allocator orelse gpa;
@@ -3936,7 +3918,7 @@ pub const Pardes = struct {
},
}
p.sync();
- p.applyStartupConfig();
+ exec.applyStartupConfig(p);
colors.finishThemeInitialization(p);
p.sync();
p.presentation.enabled = true;
@@ -4272,16 +4254,6 @@ pub const Pardes = struct {
if (look.lookHoverPane(p) == id) look.cancelLookHover(p);
}
- pub fn takesCommandLine(p: *const Pardes, id: usize) bool {
- const pane = p.panes[id] orelse return false;
- if (!pane.isTerminal()) return false;
- // A mounted shell sits behind sudo's process supervisor. Its OSC 133
- // prompt marks identify input readiness; the launcher's executable is
- // not the shell executable used by the ordinary process heuristic.
- if (pane.v9fs_on_spawn) return panes.Terminal.promptInputReady(pane);
- return !p.hostTtyTaken(id);
- }
-
pub fn shellBin(p: *const Pardes) []const u8 {
const selected = p.settings.shell.requested.get();
return if (selected.len == 0) config.default_shell else selected;
@@ -4631,7 +4603,7 @@ pub const Pardes = struct {
}
pub fn update(p: *Pardes, ev: Event) void {
- defer p.replaceStillborn();
+ defer exec.replaceStillborn(p);
// A tick with nothing animating is the one event that cannot change
// the screen.
p.needs_frame = p.needs_frame or ev != .tick;
@@ -4805,7 +4777,7 @@ pub const Pardes = struct {
return defaultTail(pane);
}
- fn defaultTail(pane: *const Pane) []const u8 {
+ pub fn defaultTail(pane: *const Pane) []const u8 {
if (comptime pdf_enabled) if (pane.pdf != null) return pdf_pane_tail;
if (pane.file) |file| if (file.output) |out|
if (panes.Output.usesLocationsConfig(out.from)) return locations_pane_tail;
@@ -5029,7 +5001,7 @@ pub const Pardes = struct {
}
if (p.tagChordText(pane)) |txt| {
pane.tag_sel = false;
- p.runBuiltin(if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd, p.active, "", txt);
+ exec.runBuiltin(p, if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd, p.active, "", txt);
}
return;
}
@@ -5086,7 +5058,7 @@ pub const Pardes = struct {
const body_mode = pane.tag_mode;
exitTagEdit(pane); // leave the tag FIRST: Del frees the pane
pane.mode = body_mode;
- p.runBuiltin(cmd, id, "", txt);
+ exec.runBuiltin(p, cmd, id, "", txt);
return;
}
if (hit(key, config.tag_yank)) {
@@ -5116,7 +5088,7 @@ pub const Pardes = struct {
} else null;
if (dir) |d| {
const from = p.active;
- p.runBuiltin(d, from, "", null);
+ exec.runBuiltin(p, d, from, "", null);
if (p.active == from) {
if (d == .Up) {
exitTagEdit(pane);
@@ -5208,8 +5180,8 @@ pub const Pardes = struct {
if (word.len > 0) {
if (!p.header_insert) {
// Preserve the workspace tag's keyboard command semantics.
- _ = p.execute(p.active, word);
- } else p.runBuiltin(if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd, p.active, "", word);
+ _ = exec.execute(p, p.active, word);
+ } else exec.runBuiltin(p, if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd, p.active, "", word);
}
return;
}
@@ -5295,185 +5267,6 @@ pub const Pardes = struct {
if (col) |c| p.topbar_col = @intCast(@min(c, modal.prevGrapheme(bar, bar.len)));
}
- fn chordEachSel(p: *Pardes, pane: *Pane, cmd: Builtin) bool {
- if (pane.nsel == 0) return false;
- const text = edit.flatSurface(p, pane) catch return false;
- var ranges: [Pane.max_selections]modal.Selection = undefined;
- const got = pane.ranges(text, 0, &ranges);
- if (got.n < 2) return false;
-
- var texts: [Pane.max_selections][]u8 = undefined;
- var made: usize = 0;
- defer for (texts[0..made]) |t| p.gpa.free(t);
- for (ranges[0..got.n]) |range| {
- const lo = @min(range.anchor, range.head);
- const hi = @max(range.anchor, range.head);
- if (hi > text.len) break;
- texts[made] = p.gpa.dupe(u8, text[lo..hi]) catch break;
- made += 1;
- }
- // All or nothing, like the pipe: half a chord is not a chord.
- if (made != got.n) return false;
-
- const id = p.active;
- const serial = pane.serial;
- pane.vsel.active = false;
- pane.msel.active = false;
- pane.select = false;
- pane.nsel = 0; // the chord consumed them, exactly as it consumes one
-
- var primary_active: ?usize = null;
- for (texts[0..made], 0..) |txt, i| {
- p.runBuiltin(cmd, id, "", txt);
- if (i == got.pri) primary_active = p.active;
- const still = p.panes[id] orelse break;
- if (still.serial != serial) break;
- }
- if (primary_active) |a| if (p.panes[a] != null) {
- p.active = a;
- };
- return true;
- }
-
- pub const PointerOperand = struct {
- /// Absolute body position corresponding to the pointed screen cell.
- row: i32,
- col: i32,
- /// Exactly what a no-drag middle/right click will dispatch.
- text: ?[]const u8 = null,
- /// What hover paints. Null names the pane's live modal selection.
- preview: ?Pane.Sel = null,
- /// File words stay logical so a soft-wrapped operand is not cut into
- /// unrelated rendered fragments. Null for tags and non-file panes.
- file_word: ?look.FileWordSpan = null,
- /// A newly expanded word is installed in the gesture's button slot;
- /// an existing selection is only borrowed and must not replace it.
- expanded: ?Pane.Sel = null,
- };
-
- pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand {
- const visible = clicked.r0 - @as(i32, BOX_H);
- const wrapped = pane.wrapAt(visible);
- const raw = pane.isTerminal() and pane.mode == .tty and clicked.r0 >= BOX_H;
- const row = if (raw) panes.Terminal.gridOffset(pane) + visible else wrapped.line;
- const raw_line = if (raw) modal.lineSlice(body_layer.bodyText(p, p.scratch.allocator(), pane) catch "", @intCast(@max(0, visible))) else "";
- const col = if (raw)
- @as(i32, @intCast(panes.File.rawAtDisplay(raw_line, @intCast(@max(0, clicked.c0)))))
- else if (clicked.r0 >= BOX_H)
- edit.paneByteAtDisplay(
- p,
- pane,
- wrapped.line,
- wrapped.at,
- clicked.c0 - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0),
- )
- else
- clicked.c0;
- var result: PointerOperand = .{ .row = row, .col = col };
-
- const kept = pane.sel[sel_slot];
- const inside_kept = if (pane.pointerSelection(kept)) |rows| inside: {
- if (clicked.r0 < BOX_H) break :inside false;
- if (pane.file != null and clicked.c0 < panes.File.gutterWidth(pane)) break :inside false;
- const source_col: usize = @intCast(@max(0, col));
- for (rows) |span| {
- const target = if (span.raw_terminal)
- (if (raw) span.row else pane.surfRow(span.row))
- else
- (if (raw) pane.gridRow(span.row) else span.row);
- var lo = span.lo;
- var hi = span.hi;
- if (span.raw_terminal and !raw) {
- lo -|= span.prompt_bytes;
- hi -|= span.prompt_bytes;
- } else if (!span.raw_terminal and raw) {
- const prefix = panes.Terminal.promptPrefixBytes(pane, row, raw_line);
- lo += prefix;
- hi += prefix;
- }
- if (target == row and source_col >= lo and source_col < hi)
- break :inside true;
- }
- break :inside false;
- } else edit.spanHas(clicked.r0, clicked.c0, kept.r0, kept.c0, kept.r1, kept.c1);
- if (kept.state == .done and inside_kept) {
- result.text = edit.selectionText(p, pane, kept) catch null;
- result.preview = kept;
- return result;
- }
- if (pane.vsel.active and pane.vsel.explicit and
- edit.spanHas(row, col, pane.vsel.row, pane.vsel.col, pane.cur_row, pane.cur_col))
- {
- result.text = edit.currentSelText(p, pane);
- return result;
- }
- if (pane.msel.active and row >= @min(pane.msel.r0, pane.msel.r1) and
- row <= @max(pane.msel.r0, pane.msel.r1))
- {
- result.text = edit.currentSelText(p, pane);
- return result;
- }
- if (pane.file != null and clicked.r0 >= BOX_H and clicked.r0 == clicked.r1 and clicked.c0 == clicked.c1) {
- const line = panes.File.sourceLine(pane, row);
- const source_col: usize = @intCast(@max(0, col));
- const b = look.expandedWord(line, source_col) orelse return result;
- const lo = std.math.cast(i32, b.lo) orelse return result;
- const hi = std.math.cast(i32, b.hi) orelse return result;
- result.text = line[b.lo..b.hi];
- result.file_word = .{ .row = row, .lo = lo, .hi = hi };
-
- var expanded = clicked;
- const first = pane.wrapRow(row, lo);
- const last = pane.wrapRow(row, hi - 1);
- if (first.row >= 0 and last.row >= 0) {
- expanded.r0 = first.row + @as(i32, BOX_H);
- expanded.c0 = @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, row, first.at, lo);
- expanded.r1 = last.row + @as(i32, BOX_H);
- expanded.c1 = @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayEndOffset(pane, row, last.at, hi - 1);
- }
- result.preview = expanded;
- result.expanded = expanded;
- return result;
- }
-
- const expanded = look.expandedSel(p, pane, clicked) orelse return result;
- result.text = edit.selectionText(p, pane, expanded) catch null;
- if (result.text == null or result.text.?.len == 0) return result;
- result.preview = expanded;
- result.expanded = expanded;
- return result;
- }
-
- pub fn heldSelection(p: *Pardes, first: usize) ?[]const u8 {
- var k: usize = 0;
- while (k < p.panes.len + 2) : (k += 1) {
- const i = switch (k) {
- 0 => first,
- 1 => p.active,
- else => k - 2,
- };
- if (k > 0 and (i == first or (k > 1 and i == p.active))) continue;
- const t = p.panes[i] orelse continue;
- if (comptime pdf_enabled) if (p.native_images) if (t.pdf) |pv| {
- if (pv.selection != null and pv.selection_text.len > 0)
- return pv.selection_text;
- };
- if (t.sel[sel_slot].state == .done) {
- if (edit.selectionText(p, t, t.sel[sel_slot]) catch null) |tx| return tx;
- } else if ((t.vsel.active and t.vsel.explicit) or t.msel.active) {
- if (edit.currentSelText(p, t)) |tx| return tx;
- }
- }
- return null;
- }
-
- pub fn withArg(p: *Pardes, txt: []const u8, arg: ?[]const u8) []const u8 {
- const a = std.mem.trim(u8, arg orelse return txt, " \t\r\n");
- if (a.len == 0) return txt;
- const t = std.mem.trim(u8, txt, " \t\r\n");
- return std.fmt.allocPrint(p.scratch.allocator(), "{s} {s}", .{ t, a }) catch txt;
- }
-
/// Cycle only the body modes supported by this pane.
pub fn cycleMode(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
@@ -5516,8 +5309,8 @@ pub const Pardes = struct {
const asked = slot orelse continue;
if (asked.prompt != .del_side) continue;
asked.prompt = .none;
- if (hit(key, config.del_above)) p.runBuiltin(.Del, id, "", "k");
- if (hit(key, config.del_below)) p.runBuiltin(.Del, id, "", "j");
+ if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k");
+ if (hit(key, config.del_below)) exec.runBuiltin(p, .Del, id, "", "j");
return;
}
if (p.topbar_col != null) return p.topbarKey(key);
@@ -5530,9 +5323,9 @@ pub const Pardes = struct {
// Shift-Escape is the unconditional way out, even mid-application;
// bare Escape only leaves at a shell prompt.
if (hit(key, config.tty_toggle_alt))
- return p.runBuiltin(.Last, p.active, "", null);
- if (key.cp == Key.escape and !key.ctrl and !key.alt and !key.shift and p.takesCommandLine(p.active))
- return p.runBuiltin(.Last, p.active, "", null);
+ return exec.runBuiltin(p, .Last, p.active, "", null);
+ if (key.cp == Key.escape and !key.ctrl and !key.alt and !key.shift and exec.takesCommandLine(p, p.active))
+ return exec.runBuiltin(p, .Last, p.active, "", null);
panes.Terminal.followOutput(pane); // typing snaps back to live output
// Paste stays the window's, the one exception to forwarding a raw
// tty's keys: Ctrl-V types the register at the program and
@@ -5554,7 +5347,7 @@ pub const Pardes = struct {
if (p.ctrl_w_pending) {
p.ctrl_w_pending = false;
for (config.window_keys) |wk| {
- if (hit(key, &.{ wk.letter, wk.arrow })) return p.runBuiltin(wk.cmd, p.active, "", null);
+ if (hit(key, &.{ wk.letter, wk.arrow })) return exec.runBuiltin(p, wk.cmd, p.active, "", null);
}
return;
}
@@ -5579,7 +5372,7 @@ pub const Pardes = struct {
return;
}
for (config.jump_keys) |jk| {
- if (hit(key, &.{jk.chord})) return p.runBuiltin(jk.cmd, p.active, "", null);
+ if (hit(key, &.{jk.chord})) return exec.runBuiltin(p, jk.cmd, p.active, "", null);
}
if (hit(key, config.pane_to_new_column)) {
const f = layout.findPane(p, p.active).?;
@@ -5601,7 +5394,7 @@ pub const Pardes = struct {
pane.tag_tail_len = @min(pipe.at, pane.tag_tail_len);
},
.save => |at| {
- if (submit) p.submitSave(p.active) else pane.tag_tail_len = @min(at, pane.tag_tail_len);
+ if (submit) exec.submitSave(p, p.active) else pane.tag_tail_len = @min(at, pane.tag_tail_len);
},
.search => |at| {
if (submit)
@@ -5629,18 +5422,18 @@ pub const Pardes = struct {
const explicit = (p.native_images and pane.hasPdfSelection()) or
(pane.vsel.active and pane.vsel.explicit) or pane.msel.active;
if (explicit) {
- if (p.chordEachSel(pane, cmd)) return;
+ if (exec.chordEachSel(p, pane, cmd)) return;
if (edit.currentSelText(p, pane)) |txt| {
pane.vsel.active = false;
pane.msel.active = false;
pane.select = false;
- p.runBuiltin(cmd, p.active, "", txt);
+ exec.runBuiltin(p, cmd, p.active, "", txt);
return;
}
}
const sel = look.expandedSel(p, pane, look.cursorWordSel(p, pane)) orelse return;
const word = edit.selectionText(p, pane, sel) catch return;
- p.runBuiltin(cmd, p.active, "", word);
+ exec.runBuiltin(p, cmd, p.active, "", word);
return;
}
if (pane.mode == .normal and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key);
@@ -5672,7 +5465,7 @@ pub const Pardes = struct {
// a path-less builtin is a row here too (it is the same index Help
// reads), and SPC cannot reach one — so both scans skip it
for (builtin_rows) |row| {
- if (std.mem.eql(u8, row.path orelse continue, pfx)) return p.runBuiltin(row.cmd, p.active, "", null);
+ if (std.mem.eql(u8, row.path orelse continue, pfx)) return exec.runBuiltin(p, row.cmd, p.active, "", null);
}
for (builtin_rows) |row| {
if (std.mem.startsWith(u8, row.path orelse continue, pfx)) {
@@ -5744,73 +5537,7 @@ pub const Pardes = struct {
pane.tag_col = @intCast(prefix_len + pane.tag_tail_len);
}
- /// Enter on a save input: the path is everything past the marker.
- fn submitSave(p: *Pardes, id: usize) void {
- const pane = p.panes[id] orelse return;
- const at = switch (pane.prompt) {
- .save => |a| a,
- else => return,
- };
- const armed = pane.tag_tail[at..pane.tag_tail_len];
- pane.tag_tail_len = at; // drop the prompt back to the seeded tail
- if (!std.mem.startsWith(u8, armed, config.save_marker)) return;
- const path = std.mem.trim(u8, armed[config.save_marker.len..], " \t\r\n");
- if (path.len == 0) return;
- p.saveTo(id, path);
- }
-
- /// Write the file this pane already has: acme's Put, and the one Save that
- /// needs no argument because the path was never a question.
- pub fn saveFile(p: *Pardes, id: usize) void {
- const pane = p.panes[id] orelse return;
- const f = if (pane.file) |*file| file else return;
- if (f.output != null) return; // nothing behind it yet: saveTo, with a path
- p.emit(.{ .save_file = .{ .pane = @intCast(id) } });
- }
-
- pub fn saveTo(p: *Pardes, id: usize, path: []const u8) void {
- const pane = p.panes[id] orelse return;
- if (path.len == 0 or path[path.len - 1] == '/' or
- std.mem.indexOfScalar(u8, path, '\n') != null)
- return p.reportError(id, "save", error.NoFilename);
- const full = std.fs.path.resolvePosix(
- p.scratch.allocator(),
- &.{ paneDir(pane), path },
- ) catch return;
- if (!std.fs.path.isAbsolute(full)) return p.reportError(id, "save", error.NoDirectory);
- if (full.len > SavePath.cap) return p.reportError(id, "save", error.PathTooLong);
- const f = if (pane.file) |*file| file else {
- // a terminal writes its scrollback; an image and a PDF have nothing
- // of their own that is unwritten, so they have nothing to write
- if (pane.isTerminal()) p.askWrite(id, pane.serial, full);
- return;
- };
- if (f.output != null and panes.Output.fileTraits(f.output).saves) {
- const owned = p.gpa.dupe(u8, full) catch return;
- p.gpa.free(f.path);
- f.path = owned;
- f.output = null; // an ordinary file pane from here on
- f.watch_after_save = true;
- pane.clearCwd();
- pane.tag_init = false; // re-derive the tag as a plain file
- pane.tag_tail_len = 0;
- p.emit(.{ .save_file = .{ .pane = @intCast(id) } });
- return;
- }
- // its own path, spelled out: the in-place write, so the pane comes clean
- if (f.output == null and std.mem.eql(u8, f.path, full)) return p.saveFile(id);
- p.askWrite(id, pane.serial, full);
- }
-
- const SavePath = Effect.Buf(effect_path_cap);
-
- fn askWrite(p: *Pardes, id: usize, serial: u32, path: []const u8) void {
- p.emit(.{ .save_text = .{
- .pane = @intCast(id),
- .serial = serial,
- .path = SavePath.from(path),
- } });
- }
+ pub const SavePath = Effect.Buf(effect_path_cap);
pub fn lspRequest(p: *Pardes, id: usize, kind: lsp.Kind, arg: []const u8) void {
if (!normal.multiOnce(p)) return; // one question per keystroke, from the primary
@@ -6054,104 +5781,6 @@ pub const Pardes = struct {
p.settings.scene_effects = .{};
}
- /// Tty: a shell in the caller's directory, raw from the first frame,
- /// stacked below the caller like Alt-n's new shell.
- pub fn spawnTty(p: *Pardes, from: usize) void {
- p.spawnTtyWithMount(from, false);
- }
-
- pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
- if (comptime !hosted or @import("builtin").os.tag != .linux)
- return p.reportError(from, "Tty9p", error.LinuxRequired);
- if (p.fs.socket_path.len == 0)
- return p.reportError(from, "Tty9p", error.SessionSocketRequired);
- p.spawnTtyWithMount(from, true);
- }
-
- fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void {
- const src = p.panes[from] orelse return;
- const free = p.freeSlot() orelse return;
- const nt = p.newShell(free, paneDir(src)) catch return;
- nt.greet = !v9fs;
- nt.v9fs_on_spawn = v9fs;
- nt.mode = .tty;
- const parent = layout.splitParent(p, from);
- const f = layout.findPane(p, parent).?;
- layout.insert(p, f.col, f.idx + 1, free);
- layout.splitBelow(p, parent, nt);
- p.active = free;
- p.stillborn_joiner = nt.serial;
- }
-
- /// when a doc lands in `col`, a lone pristine shell there is clutter — drop
- /// it; absorbVWeight hands its space to the doc
- pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void {
- var n_tty: usize = 0;
- var tty_id: usize = 0;
- for (0..p.col_n[col]) |k| {
- const cid = p.col_panes[col][k];
- if (p.panes[cid]) |ct| if (ct.isTerminal()) {
- n_tty += 1;
- tty_id = cid;
- };
- }
- if (n_tty != 1) return;
- const tt = p.panes[tty_id] orelse return;
- // No typing, cursor on the first prompt line, no scrollback: this is
- // the throwaway boot placeholder a document may replace.
- if (tt.ovl != null or panes.Terminal.gridCursor(tt).y != 0 or
- panes.Terminal.scrollbar(tt).total > tt.rows) return;
- p.deinitPane(tt) catch |err| return p.reportError(tty_id, "close", err);
- layout.compute(p); // a just-stacked doc has no rect yet; absorb snaps to rows
- layout.absorbVWeight(p, tty_id, null);
- layout.removePane(p, tty_id);
- p.panes[tty_id] = null;
- if (p.active == tty_id) p.active = keep_id;
- }
-
- /// A pane that just joined a column whose only other pane is stillborn —
- /// an empty scratch nobody typed into, body or tag (what Newcol makes and
- /// what a closed column's last pane leaves behind) — takes the column
- /// whole: the placeholder was only holding the space for it. Run at the
- /// end of a step: the pane that asked for the joiner is often the
- /// placeholder itself, and its caller is still holding it.
- fn replaceStillborn(p: *Pardes) void {
- const serial = p.stillborn_joiner orelse return;
- p.stillborn_joiner = null;
- const new_id = p.paneBySerial(serial) orelse return;
- const f = layout.findPane(p, new_id) orelse return;
- if (p.col_n[f.col] != 2) return;
- const old_id = p.col_panes[f.col][1 - f.idx];
- const old = p.panes[old_id] orelse return;
- const file = old.file orelse return;
- const out = file.output orelse return;
- if (out.from != .cmd or out.from.cmd != .New) return;
- if (file.revision != 0 or file.content.len != 0) return;
- if (old.tag_init and !std.mem.eql(u8, old.tagSlice(), defaultTail(old))) return;
- const was_active = p.active == old_id;
- layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows
- p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err);
- if (was_active) p.active = new_id;
- }
-
- fn ttyForDir(p: *Pardes, dir: []const u8) ?usize {
- for (p.panes, 0..) |slot, i| if (slot) |tt| {
- if (!std.mem.eql(u8, tt.cwdSlice(), dir)) continue;
- if (p.takesCommandLine(i)) return i;
- };
- const free = p.freeSlot() orelse return null;
- const nt = p.newShell(free, dir) catch return null;
- nt.greet = false;
- const rc = if (p.ncol > 0) p.ncol - 1 else 0;
- if (p.col_n[rc] > 0) {
- const src = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]);
- const f = layout.findPane(p, src).?;
- layout.splitBelow(p, src, nt);
- layout.insert(p, f.col, f.idx + 1, free);
- } else layout.insert(p, rc, p.col_n[rc], free);
- return free;
- }
-
// ---- doc panes ----
pub fn openTutorView(p: *Pardes, id: usize) !*Pane {
@@ -6188,172 +5817,6 @@ pub const Pardes = struct {
// ---- the ONE dispatcher: look (right/Enter) and execute (middle/Tab) ----
- const max_exec_depth = 8;
-
- pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
- const pane = p.panes[id] orelse return null;
- const cmd = commandText(txt);
- if (cmd.len == 0) return null;
- if (p.exec_depth == 0) p.noteHaptic(.exec);
- if (p.executeBuiltinLine(id, cmd)) return null;
- if (p.exec_depth >= max_exec_depth) return null;
- p.exec_depth += 1;
- defer p.exec_depth -= 1;
- const dst = (if (p.takesCommandLine(id)) id else p.ttyForDir(paneDir(pane))) orelse return null;
- panes.Terminal.padOutputBelowEdits(p, dst);
- if (panes.Terminal.queuePendingCommand(p.panes[dst].?, cmd) catch |err| {
- p.reportError(id, "queue command", err);
- return dst;
- }) return dst;
- p.emitWrite(dst, cmd);
- p.emitWrite(dst, "\r");
- return dst;
- }
-
- /// Trim and unwrap clickable command-word notation once for every command
- /// consumer, so startup lines and interactive Exec speak the same syntax.
- pub fn commandText(txt: []const u8) []const u8 {
- var cmd = std.mem.trim(u8, txt, " \t\r\n");
- while (config.commandWord(cmd)) |inner| cmd = std.mem.trim(u8, inner, " \t\r\n");
- return cmd;
- }
-
- pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool {
- const cmd = commandText(txt);
- if (cmd.len == 0 or p.exec_depth >= max_exec_depth) return false;
- const sp = std.mem.indexOfAny(u8, cmd, " \t");
- const bi: ?Builtin = std.meta.stringToEnum(Builtin, cmd) orelse blk: {
- const head = std.meta.stringToEnum(Builtin, cmd[0 .. sp orelse break :blk null]) orelse break :blk null;
- break :blk if (builtins.registry.takesArg(head)) head else null;
- };
- const b = bi orelse return false;
- p.exec_depth += 1;
- defer p.exec_depth -= 1;
- const typed = if (sp) |s| std.mem.trim(u8, cmd[s + 1 ..], " \t") else "";
- p.runBuiltin(b, id, cmd, if (typed.len > 0) typed else null);
- return true;
- }
-
- fn applyStartupConfig(p: *Pardes) void {
- p.announce = false;
- defer p.announce = true;
- var lines = std.mem.splitScalar(u8, p.opts.startup_config orelse return, '\n');
- while (lines.next()) |line| _ = p.executeBuiltinLine(p.active, line);
- }
-
- pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
- if (!normal.multiOnce(p)) return; // a builtin is per-keystroke, never per-cursor
- const pane = p.panes[id] orelse return;
- // A builtin says its own name before it runs, so a tagline click is
- // legible even when what it did is invisible. Whatever the builtin
- // then says for itself replaces the announcement.
- if (p.announce and p.settings.verbose and builtins.registry.announces(b)) {
- p.setMessage(id, @tagName(b));
- pane.msg_announcement = true;
- }
- const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg };
- builtins.registry.dispatch(b, c);
- }
-
- pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]const u8) void {
- const previous_colors = p.settings.colors;
- const previous_context_style = p.settings.tree_context_tag_style;
- const previous_transition = p.settings.panel_transition;
- const previous_tagline_percent = p.settings.font.tagline_percent;
- const previous_column_tags = p.settings.column_tags;
- switch (setting.action) {
- .theme => {
- const want = std.mem.trim(u8, arg orelse return, " \t\r\n");
- for (themes, 0..) |t, i| if (std.mem.eql(u8, t.name, want)) {
- colors.setThemeIndex(p, i);
- return;
- };
- },
- .font => {
- if (comptime !font_picker) return;
- const spec = config.Runtime.FontSpec.parse(arg orelse return) orelse return;
- const matches = fonts.list(p.scratch.allocator(), spec.name);
- if (matches.len == 0) return;
- if (!p.settings.requestFont(matches[0].path, matches[0].name, spec.size_hundredths)) return;
- p.font_request_taken = false;
- },
- else => _ = p.settings.apply(setting, arg),
- }
- if (p.settings.colors != previous_colors) {
- for (p.panes) |slot| {
- const pane = slot orelse continue;
- if (pane.file) |*file| file.syntax_dirty = true;
- }
- }
- if (p.settings.tree_context_tag_style != previous_context_style) {
- for (p.panes, 0..) |slot, id| {
- p.surface.body_layers[id].rows = 0;
- const pane = slot orelse continue;
- pane.body_rows = 0;
- pane.body_visible_rows = 0;
- pane.context_row_limit = null;
- pane.wrap_n = 0;
- }
- p.abandonPanelAnimations();
- }
- const transition_changed = setting.action == .transition and
- p.settings.panel_transition != previous_transition;
- const tagline_metrics_changed = setting.action == .tagline_size and
- p.settings.font.tagline_percent != previous_tagline_percent;
- if (p.settings.column_tags != previous_column_tags) {
- if (p.header_column != null) p.exitHeader();
- }
- if (transition_changed or tagline_metrics_changed or p.settings.column_tags != previous_column_tags) {
- p.abandonPanelAnimations();
- }
- }
-
- pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void {
- p.stillborn_joiner = nt.serial;
- const doc = if (nt.file) |f| panes.Output.fileTraits(f.output).doc else true; // an image is a doc
- var src_id: ?usize = null;
- if (p.panes[from_id]) |src| if (if (src.file) |f| panes.Output.fileTraits(f.output).doc else src.image != null or src.hasPdf()) {
- src_id = from_id;
- };
- if (doc and src_id == null) {
- var n = p.njumps;
- while (n > 0) : (n -= 1) {
- const j = p.jumps[n - 1];
- const pp = p.panes[j.pane] orelse continue;
- if (pp.serial != j.serial) continue;
- if (if (pp.file) |f| panes.Output.fileTraits(f.output).doc else pp.image != null or pp.hasPdf()) {
- src_id = j.pane;
- break;
- }
- }
- }
- if (doc and src_id == null) for (p.panes, 0..) |sl, i| {
- if (sl) |pp| if (i != free and (if (pp.file) |f| panes.Output.fileTraits(f.output).doc else pp.image != null or pp.hasPdf())) {
- src_id = i;
- break;
- };
- };
- if (!doc) if (layout.findPane(p, from_id)) |sf| {
- layout.insert(p, sf.col, sf.idx + 1, free);
- layout.splitBelow(p, from_id, nt); // NOT splitParent: no bystander pays
- p.active = free;
- return;
- };
- if (src_id == null and doc and p.ncol < MAX_COLS and
- layout.columnFitsHalves(p, from_id, 100) and layout.splitColumn(p, from_id, free, true))
- {
- p.active = free;
- return;
- }
- // Without room for a first-document column, split the Look origin
- // just like Tty. Never displace (or evict) a top-left bystander.
- const src = layout.splitParent(p, src_id orelse from_id);
- const sf = layout.findPane(p, src).?;
- layout.insert(p, sf.col, sf.idx + 1, free);
- layout.splitBelow(p, src, nt);
- p.active = free;
- }
-
// ---- dump / load (acme-style: `pardes -l state.zon` restores a session) ----
pub fn dumpState(p: *Pardes) !void {
@@ -6562,7 +6025,7 @@ pub const Pardes = struct {
}
p.active = @min(st.active, MAX_PANES - 1);
p.sync();
- p.applyStartupConfig();
+ exec.applyStartupConfig(p);
colors.finishThemeInitialization(p);
p.sync();
p.presentation.enabled = true;
@@ -7725,7 +7188,7 @@ test "Esc alternates between two panes of the SAME kind" {
defer p.deinit();
p.update(.{ .resize = .{ .cols = 80, .rows = 40 } });
const a = p.active;
- p.runBuiltin(.Look, a, "", "build.zig");
+ exec.runBuiltin(p, .Look, a, "", "build.zig");
p.sync();
const b = p.active;
try std.testing.expect(b != a);
@@ -8229,7 +7692,7 @@ test "raw tty Escape only leaves at a shell prompt" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24, .file = "mise.toml" });
defer p.deinit();
- p.spawnTty(0);
+ exec.spawnTty(p, 0);
const shell = p.active;
const pane = p.panes[shell].?;
var buf: [256]u8 = undefined;
@@ -8260,7 +7723,7 @@ test "raw tty Ctrl-B switches to editor mode without sending child input" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .file = "mise.toml" });
defer p.deinit();
- p.spawnTty(0);
+ exec.spawnTty(p, 0);
const shell = p.active;
p.sync();
var buf: [256]u8 = undefined;
@@ -8313,7 +7776,7 @@ test "hopping between two panes does not grow the jump stack" {
try std.testing.expectEqual(depth, p.njumps);
const before = p.active;
- p.runBuiltin(.Back, p.active, "", null);
+ exec.runBuiltin(p, .Back, p.active, "", null);
p.sync();
try std.testing.expect(p.active != before);
try std.testing.expectEqual(depth, p.njumps);