summaryrefslogtreecommitdiff
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
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]>
-rw-r--r--src/Mini.zig3
-rw-r--r--src/Output.zig7
-rw-r--r--src/builtins.zig15
-rw-r--r--src/exec.zig571
-rw-r--r--src/fs.zig1
-rw-r--r--src/look.zig17
-rw-r--r--src/mouse.zig25
-rw-r--r--src/ninep/ctl.zig2
-rw-r--r--src/ninep/events.zig3
-rw-r--r--src/ninep/pane.zig3
-rw-r--r--src/normal.zig7
-rw-r--r--src/pardes.zig653
-rw-r--r--src/pdf_view.zig3
-rw-r--r--test/panes.zig6
-rw-r--r--test/pdf.zig2
15 files changed, 681 insertions, 637 deletions
diff --git a/src/Mini.zig b/src/Mini.zig
index bc93f678..52e26a37 100644
--- a/src/Mini.zig
+++ b/src/Mini.zig
@@ -2,6 +2,7 @@
const panes = @import("panes.zig");
const std = @import("std");
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const config = @import("config.zig");
const Pardes = pardes.Pardes;
const modal = @import("modal.zig");
@@ -187,7 +188,7 @@ pub fn open(p: *Pardes, id: usize, argument: []const u8) !void {
.syntax_dirty = false,
};
pane.cur_pinned = true;
- p.placeDoc(id, free, pane);
+ exec.placeDoc(p, id, free, pane);
p.active = free;
}
diff --git a/src/Output.zig b/src/Output.zig
index 674fcbc8..b216c0cf 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -3,6 +3,7 @@
const panes = @import("panes.zig");
const std = @import("std");
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const Messages = @import("Messages.zig");
const config = @import("config.zig");
const Pardes = pardes.Pardes;
@@ -479,7 +480,7 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []
const free = p.freeSlot() orelse return error.NoPaneSlots;
const np = try open(p, free, dir, from, arg, result_content);
np.file.?.location_rows = result_rows;
- p.placeDoc(id, free, np);
+ exec.placeDoc(p, id, free, np);
p.active = id;
if (traits(from).steps) {
pane.search_pane = free;
@@ -559,7 +560,7 @@ fn openStepped(p: *Pardes, id: usize, from: Origin, content: []u8) !void {
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
const free = p.freeSlot() orelse return error.NoPaneSlots;
const np = try open(p, free, dir, from, "", content);
- p.placeDoc(id, free, np);
+ exec.placeDoc(p, id, free, np);
p.active = id;
pane.search_pane = free;
pane.search_row = null;
@@ -778,6 +779,6 @@ fn openRead(p: *Pardes, id: usize, from: Origin, arg: []const u8, content: []u8)
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
const free = p.freeSlot() orelse return error.NoPaneSlots;
const np = try open(p, free, dir, from, arg, content);
- p.placeDoc(id, free, np);
+ exec.placeDoc(p, id, free, np);
p.active = free;
}
diff --git a/src/builtins.zig b/src/builtins.zig
index 21dd4fa9..19be62e2 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1,6 +1,7 @@
//! Command structs and runtime settings form the builtin registry; bindings live in config.zig.
const std = @import("std");
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const look = @import("look.zig");
const edit = @import("edit.zig");
const colors = @import("colors.zig");
@@ -155,7 +156,7 @@ pub const registry = struct {
inline for (manualBuiltinList(), 0..) |T, i|
if (@intFromEnum(b) == i) return T.run(c);
inline for (comptime settingList(), manualBuiltinCount()..) |setting, i|
- if (@intFromEnum(b) == i) return c.p.applySettingBuiltin(setting, c.arg);
+ if (@intFromEnum(b) == i) return exec.applySettingBuiltin(c.p, setting, c.arg);
unreachable;
}
};
@@ -223,7 +224,7 @@ pub const Exec = struct {
pub fn run(c: Ctx) void {
// the destination pane is Look's business (it focuses what answered);
// an execute deliberately leaves you where you were
- _ = c.p.execute(c.id, c.arg orelse return);
+ _ = exec.execute(c.p, c.id, c.arg orelse return);
}
};
@@ -478,8 +479,8 @@ pub const Save = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
const path = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (path.len > 0) return c.p.saveTo(c.id, path);
- if (c.pane.file) |file| if (file.output == null) return c.p.saveFile(c.id);
+ if (path.len > 0) return exec.saveTo(c.p, c.id, path);
+ if (c.pane.file) |file| if (file.output == null) return exec.saveFile(c.p, c.id);
if (c.pane.file != null or c.pane.isTerminal()) c.p.startPrompt(c.pane, .save);
}
};
@@ -585,7 +586,7 @@ pub const DelBelow = struct {
/// pane's tagline: the fast path from wherever you are to a prompt there.
pub const Tty = struct {
pub fn run(c: Ctx) void {
- c.p.spawnTty(c.id);
+ exec.spawnTty(c.p, c.id);
}
};
@@ -593,7 +594,7 @@ pub const Tty = struct {
pub const Tty9p = struct {
pub const enabled = pardes.hosted and @import("builtin").os.tag == .linux;
pub fn run(c: Ctx) void {
- c.p.spawnV9fsTty(c.id);
+ exec.spawnV9fsTty(c.p, c.id);
}
};
@@ -609,7 +610,7 @@ pub const Tutor = struct {
pub fn run(c: Ctx) void {
const free = c.p.freeSlot() orelse return;
const nt = c.p.openTutorView(free) catch return;
- c.p.placeDoc(c.id, free, nt); // a doc like any other
+ exec.placeDoc(c.p, c.id, free, nt); // a doc like any other
}
};
diff --git a/src/exec.zig b/src/exec.zig
new file mode 100644
index 00000000..75d07441
--- /dev/null
+++ b/src/exec.zig
@@ -0,0 +1,571 @@
+//! Executing a word (acme's exec.c): a builtin line or a command typed into
+//! a terminal, the builtin dispatch and settings, the operand a click or
+//! chord hands over, the terminals commands run in, placing a document, and
+//! saving a pane.
+const pardes = @import("pardes.zig");
+const std = @import("std");
+const layout = @import("layout.zig");
+const modal = @import("modal.zig");
+const look = @import("look.zig");
+const panes = @import("panes.zig");
+const normal = @import("normal.zig");
+const edit = @import("edit.zig");
+const body_layer = @import("body_layer.zig");
+const colors = @import("colors.zig");
+const themes = colors.themes;
+const builtins = @import("builtins.zig");
+const config = @import("config.zig");
+const pdf_enabled = panes.Pdf.enabled;
+const font_picker = pardes.font_picker;
+const hosted = pardes.hosted;
+const fonts = pardes.fonts;
+const Pane = panes.Pane;
+const MAX_COLS = pardes.MAX_COLS;
+const BOX_H = pardes.BOX_H;
+const Builtin = pardes.Builtin;
+const Key = pardes.Key;
+const sel_slot = pardes.sel_slot;
+const Pardes = pardes.Pardes;
+
+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 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| {
+ runBuiltin(p, 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;
+}
+
+/// Enter on a save input: the path is everything past the marker.
+pub 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;
+ saveTo(p, 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(),
+ &.{ Pardes.paneDir(pane), path },
+ ) catch return;
+ if (!std.fs.path.isAbsolute(full)) return p.reportError(id, "save", error.NoDirectory);
+ if (full.len > Pardes.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()) askWrite(p, 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 saveFile(p, id);
+ askWrite(p, id, pane.serial, full);
+}
+
+fn askWrite(p: *Pardes, id: usize, serial: u32, path: []const u8) void {
+ p.emit(.{ .save_text = .{
+ .pane = @intCast(id),
+ .serial = serial,
+ .path = Pardes.SavePath.from(path),
+ } });
+}
+
+/// 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 {
+ spawnTtyWithMount(p, 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);
+ spawnTtyWithMount(p, 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, Pardes.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.
+pub 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(), Pardes.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 (takesCommandLine(p, 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;
+}
+
+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 (executeBuiltinLine(p, 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 (takesCommandLine(p, id)) id else ttyForDir(p, Pardes.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 "";
+ runBuiltin(p, b, id, cmd, if (typed.len > 0) typed else null);
+ return true;
+}
+
+pub 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| _ = executeBuiltinLine(p, 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;
+}
+
+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);
+}
diff --git a/src/fs.zig b/src/fs.zig
index 13fce858..825e63bb 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -43,6 +43,7 @@ const source_files = [_]Source{
.{ .path = "src/edit.zig", .contents = @embedFile("edit.zig") },
.{ .path = "src/normal.zig", .contents = @embedFile("normal.zig") },
.{ .path = "src/mouse.zig", .contents = @embedFile("mouse.zig") },
+ .{ .path = "src/exec.zig", .contents = @embedFile("exec.zig") },
.{ .path = "src/File.zig", .contents = @embedFile("File.zig") },
.{ .path = "src/Output.zig", .contents = @embedFile("Output.zig") },
.{ .path = "src/Mini.zig", .contents = @embedFile("Mini.zig") },
diff --git a/src/look.zig b/src/look.zig
index af0905a4..1534d332 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -2,6 +2,7 @@ const std = @import("std");
const builtin = @import("builtin");
const libc = std.c;
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const config = @import("config.zig");
const pdf_enabled = @import("pardes_config").mupdf;
const fs = @import("fs.zig");
@@ -528,7 +529,7 @@ fn lookFirstHit(p: *Pardes, id: usize) void {
rp.vsel.active = false;
rp.msel.active = false;
rp.select = false;
- p.runBuiltin(config.look_cmd, rid, "", txt);
+ exec.runBuiltin(p, config.look_cmd, rid, "", txt);
}
pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start: SearchStart) !void {
@@ -686,7 +687,7 @@ pub fn jumpResult(p: *Pardes, id: usize, row: usize) bool {
rp.look_at = null;
}
if (tr.commands) {
- p.runBuiltin(config.exec_cmd, rid, "", std.mem.trim(u8, ln, " \t\r"));
+ exec.runBuiltin(p, config.exec_cmd, rid, "", std.mem.trim(u8, ln, " \t\r"));
} else if (span) |selected| {
lookAt(p, rid, ln[selected.start..selected.end]);
// This is the newly visited result, not a selection carried into
@@ -922,7 +923,7 @@ pub fn advanceLookHover(p: *Pardes) void {
pane.pdf.?.invalidateRaster(probe.page);
return;
};
- const operand = p.pointerOperand(pane, pointed.sel);
+ const operand = exec.pointerOperand(p, pane, pointed.sel);
if (operand.text == null or operand.text.?.len == 0) return cancelLookHover(p);
var preview = operand.preview;
if (preview) |*sel| sel.state = .done;
@@ -1136,7 +1137,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
noteLookSource(p, id);
const command = std.mem.trim(u8, operand, " \t\r\n");
if (config.commandWord(command)) |cmd| {
- if (p.execute(id, cmd)) |dst| p.active = dst;
+ if (exec.execute(p, id, cmd)) |dst| p.active = dst;
return;
}
// The operand may borrow selected terminal/PDF text released below.
@@ -1171,7 +1172,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
.url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }),
.dir => |dir| {
for (p.panes, 0..) |slot, i| {
- if (slot) |tt| if (std.mem.eql(u8, tt.cwdSlice(), dir) and p.takesCommandLine(i)) {
+ if (slot) |tt| if (std.mem.eql(u8, tt.cwdSlice(), dir) and exec.takesCommandLine(p, i)) {
clearNavigationSelection(p, tt);
p.active = i;
p.emitWrite(i, "ls\r");
@@ -1194,7 +1195,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
const nt = panes.Pdf.openPane(p, free, target.path, target.at.line) catch |err|
return p.reportError(id, "look", err);
- p.placeDoc(id, free, nt);
+ exec.placeDoc(p, id, free, nt);
return;
};
// focus an existing pane on this path (rescrolled), else open
@@ -1203,7 +1204,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
return p.reportError(id, "look", err);
if (target.at.col > 0) nt.cur_col = @intCast(target.at.col - 1);
- p.placeDoc(id, free, nt);
+ exec.placeDoc(p, id, free, nt);
layout.compute(p);
nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2;
if (target.at.end_line != 0) {
@@ -1217,7 +1218,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
const nt = panes.Image.create(p, free, target.path, &.{}) catch |err|
return p.reportError(id, "look", err);
- p.placeDoc(id, free, nt);
+ exec.placeDoc(p, id, free, nt);
},
}
}
diff --git a/src/mouse.zig b/src/mouse.zig
index 27d5dd9c..2a00bded 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -2,6 +2,7 @@
//! releases (acme's chords, border and seam drags, gestures), and the
//! chrome targets a shell asks about.
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const look = @import("look.zig");
const std = @import("std");
const layout = @import("layout.zig");
@@ -341,9 +342,9 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
if (word.len > 0) {
if (m.button == config.exec_button) {
const named = std.meta.stringToEnum(Builtin, word);
- const held = if (named) |b| (if (builtins.registry.takesArg(b)) p.heldSelection(p.active) else null) else p.heldSelection(p.active);
- _ = p.execute(p.active, p.withArg(word, held));
- } else p.runBuiltin(config.look_cmd, p.active, "", word);
+ const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active);
+ _ = exec.execute(p, p.active, exec.withArg(p, word, held));
+ } else exec.runBuiltin(p, config.look_cmd, p.active, "", word);
}
}
return;
@@ -389,7 +390,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
config.select_button => switch (m.kind) {
.press => {
if (p.drag == .select and p.drag.select.button == config.exec_button) {
- if (p.heldSelection(p.drag.select.id)) |tx| {
+ if (exec.heldSelection(p, p.drag.select.id)) |tx| {
if (p.chord_arg) |old| p.gpa.free(old);
p.chord_arg = p.gpa.dupe(u8, tx) catch null;
}
@@ -629,7 +630,7 @@ fn reportGesture(
cmd: Builtin,
text: []const u8,
on_tag: bool,
- operand: Pardes.PointerOperand,
+ operand: exec.PointerOperand,
chorded: bool,
) bool {
const scripted = p.panes[id] orelse return false;
@@ -639,7 +640,7 @@ fn reportGesture(
(if (on_tag) .tag_look else .body_look)
else
(if (on_tag) .tag_exec else .body_exec);
- const named = std.meta.stringToEnum(Builtin, Pardes.commandText(text)) != null;
+ const named = std.meta.stringToEnum(Builtin, exec.commandText(text)) != null;
const range = gestureRange(p, id, text, on_tag, operand);
var flag: u32 = if (named) ctlfs.events.flag_builtin else 0;
if (range.q0 == range.q1 and text.len > 0) flag |= ctlfs.events.flag_expansion;
@@ -649,7 +650,7 @@ fn reportGesture(
return ctlfs.events.noteAction(p, id, action, range.q0, range.q1, flag, text);
}
-fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: Pardes.PointerOperand) ctlfs.pane.State.Range {
+fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: exec.PointerOperand) ctlfs.pane.State.Range {
const pane = p.panes[id] orelse return .{};
if (on_tag) {
const tag = p.tagText(p.scratch.allocator(), pane) catch return .{};
@@ -697,7 +698,7 @@ fn dispatchPointerBuiltin(
id: usize,
cmd: Builtin,
text: ?[]const u8,
- gesture: ?struct { on_tag: bool, operand: Pardes.PointerOperand },
+ gesture: ?struct { on_tag: bool, operand: exec.PointerOperand },
) void {
const arg = p.chord_arg;
p.chord_arg = null;
@@ -707,7 +708,7 @@ fn dispatchPointerBuiltin(
return;
};
if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg != null)) return;
- p.runBuiltin(cmd, id, "", p.withArg(operand, arg));
+ exec.runBuiltin(p, cmd, id, "", exec.withArg(p, operand, arg));
}
fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
@@ -769,7 +770,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
// a file moved into the left column evicts a lone unused shell
if (p.panes[d.id]) |mt| if (mt.file != null) {
if (layout.findPane(p, d.id)) |f| if (f.col == 0)
- p.evictLonePristineTty(0, d.id);
+ exec.evictLonePristineTty(p, 0, d.id);
};
},
.select => |s| {
@@ -848,11 +849,11 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
pane.sel[b].state = .none;
var buf: [48]u8 = undefined;
const target = std.fmt.bufPrint(&buf, "@p{d}:{d}", .{ s.id, @as(usize, @intCast(line)) + 1 }) catch return;
- p.runBuiltin(config.look_cmd, s.id, "", target);
+ exec.runBuiltin(p, config.look_cmd, s.id, "", target);
return;
}
};
- var operand = p.pointerOperand(pane, clk);
+ var operand = exec.pointerOperand(p, pane, clk);
if (operand.expanded) |expanded|
pane.sel[b] = expanded
else if (operand.text == null)
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 25fe4037..645b1c33 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -59,7 +59,7 @@ fn activeSerial(p: *Pardes) ?u32 {
fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
const context = p.panes[id].?.serial;
const before = Serials.snapshot(p);
- if (exec) _ = p.execute(id, text) else look.lookAt(p, id, text);
+ if (exec) _ = pardes.exec.execute(p, id, text) else look.lookAt(p, id, text);
const after = Serials.snapshot(p);
var created = false;
for (after.list[0..after.len]) |serial| if (!before.has(serial)) {
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index d4d7fcad..49e852b3 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -2,6 +2,7 @@
//! bounded queues that park a read until something happens.
const std = @import("std");
const pardes = @import("../pardes.zig");
+const exec = @import("../exec.zig");
const look = @import("../look.zig");
const tree = @import("tree.zig");
const pane_files = @import("pane.zig");
@@ -461,7 +462,7 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
const hi = @max(lo, @min(@as(usize, r.q1), whole.len));
const text = p.scratch.allocator().dupe(u8, whole[lo..hi]) catch continue;
switch (r.action) {
- .body_exec, .tag_exec => _ = p.execute(live, text),
+ .body_exec, .tag_exec => _ = exec.execute(p, live, text),
.body_look, .tag_look => look.lookAt(p, live, text),
else => unreachable,
}
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 4110380a..76854dbc 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -3,6 +3,7 @@
//! mark and scroll, errors, event, look and exec. Terminals add pty/ (pty.zig).
const std = @import("std");
const pardes = @import("../pardes.zig");
+const exec = @import("../exec.zig");
const look = @import("../look.zig");
const panes = @import("../panes.zig");
const modal = @import("../modal.zig");
@@ -589,7 +590,7 @@ pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize {
p.gpa.free(content);
return null;
};
- p.placeDoc(id, free, np);
+ exec.placeDoc(p, id, free, np);
return text.len;
}
diff --git a/src/normal.zig b/src/normal.zig
index e273f7f1..6647dcca 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -2,6 +2,7 @@
//! modal actions, replayed across every selection, and each action is done
//! to the pane; PDF panes get their own normal keys.
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const look = @import("look.zig");
const std = @import("std");
const mvzr = @import("mvzr");
@@ -302,7 +303,7 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v
switch (semantic) {
.escape => {
pane.select = false;
- return p.runBuiltin(.Last, p.active, "", null);
+ return exec.runBuiltin(p, .Last, p.active, "", null);
},
.goto => |go| switch (go.target) {
.file_start => {
@@ -566,11 +567,11 @@ pub fn handlePdfNormal(p: *Pardes, pane: *Pane, key: Key) void {
if (hit(key, config.leave_pane)) {
pane.normal.clear();
pane.select = false;
- return p.runBuiltin(.Last, p.active, "", null);
+ return exec.runBuiltin(p, .Last, p.active, "", null);
}
if (pane.normal.prefix == .none and isPrefix(key, 'f')) {
pane.normal.clear();
- return p.runBuiltin(.PdfSections, p.active, "", null);
+ return exec.runBuiltin(p, .PdfSections, p.active, "", null);
}
const parsed = modal.Normal.parse(&pane.normal, normalInput(key));
switch (parsed) {
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);
diff --git a/src/pdf_view.zig b/src/pdf_view.zig
index 9b623183..721dac5a 100644
--- a/src/pdf_view.zig
+++ b/src/pdf_view.zig
@@ -3,6 +3,7 @@
const panes = @import("panes.zig");
const std = @import("std");
const pardes = @import("pardes.zig");
+const exec = @import("exec.zig");
const layout = @import("layout.zig");
const config = @import("config.zig");
const modal = @import("modal.zig");
@@ -2044,7 +2045,7 @@ pub fn openSections(core: *pardes.Pardes, id: usize) void {
.serial = result.serial,
.revision = result.file.?.revision,
};
- core.placeDoc(id, free, result);
+ exec.placeDoc(core, id, free, result);
layout.compute(core);
core.active = id;
pane.search_pane = free;
diff --git a/test/panes.zig b/test/panes.zig
index f45d42b2..f99f9d56 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -721,7 +721,7 @@ const TtySelectionTests = struct {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 24 });
defer p.deinit();
p.presentation.enabled = false;
- p.spawnTty(0);
+ pardes.exec.spawnTty(p, 0);
for (p.panes[0..2]) |slot| {
slot.?.mode = .tty;
slot.?.tty_filter = false;
@@ -3111,8 +3111,8 @@ const TerminalTests = struct {
defer p.deinit();
while (p.nextEffect()) |_| {} // the host has not acknowledged spawn yet
- try std.testing.expectEqual(@as(?usize, 0), p.execute(0, "echo first"));
- try std.testing.expectEqual(@as(?usize, 0), p.execute(0, "echo second"));
+ try std.testing.expectEqual(@as(?usize, 0), pardes.exec.execute(p, 0, "echo first"));
+ try std.testing.expectEqual(@as(?usize, 0), pardes.exec.execute(p, 0, "echo second"));
while (p.nextEffect()) |effect| switch (effect) {
.write => return error.CommandEscapedBeforeFork,
else => {},
diff --git a/test/pdf.zig b/test/pdf.zig
index 69a82123..dde58df8 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -342,7 +342,7 @@ test "PdfSections Look follows the exact owning PDF, not an equal path" {
const duplicate_id = p.freeSlot() orelse return error.NoDuplicatePdfSlot;
const duplicate = try panes.Pdf.openPane(p, duplicate_id, path, 0);
- p.placeDoc(0, duplicate_id, duplicate);
+ pardes.exec.placeDoc(p, 0, duplicate_id, duplicate);
layout.compute(p);
panes.Pdf.openSections(p, duplicate_id);
const duplicate_output_id = duplicate.search_pane orelse return error.MissingDuplicatePdfSections;