summaryrefslogtreecommitdiff
path: root/src/exec.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/exec.zig')
-rw-r--r--src/exec.zig351
1 files changed, 310 insertions, 41 deletions
diff --git a/src/exec.zig b/src/exec.zig
index ad0f7b9b..ee7c31e9 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -16,6 +16,11 @@ const colors = @import("colors.zig");
const themes = colors.themes;
const builtins = @import("builtins.zig");
const config = @import("config.zig");
+const grammars = @import("grammar_manifest.zig").all;
+const python_lang = blk: {
+ @setEvalBranchQuota(20_000);
+ break :blk @import("grammar_manifest.zig").byName("python").?;
+};
const pdf_enabled = panes.Pdf.enabled;
const font_picker = pardes.font_picker;
const hosted = pardes.hosted;
@@ -30,7 +35,8 @@ 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 command pane's child is its command, never a shell to type at.
+ if (!pane.isTerminal() or pane.command != null) 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.
@@ -67,7 +73,8 @@ pub fn chordEachSel(p: *Pardes, pane: *Pane, cmd: Builtin) bool {
var primary_active: ?usize = null;
for (texts[0..made], 0..) |txt, i| {
- runBuiltin(p, cmd, id, "", txt);
+ // Every selection is the body's, each one sent as the first is.
+ runBuiltinFrom(p, cmd, id, "", txt, true);
if (i == got.pri) primary_active = p.active;
const still = p.panes[id] orelse break;
if (still.serial != serial) break;
@@ -290,8 +297,8 @@ fn askWrite(p: *Pardes, id: usize, serial: u32, path: []const u8) void {
/// 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 spawnTty(p: *Pardes, from: usize) ?*Pane {
+ return spawnTtyWithMount(p, from, false);
}
pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
@@ -299,13 +306,19 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
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);
+ _ = 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;
+fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
+ const src = p.panes[from] orelse return null;
+ const free = p.freeSlot() orelse {
+ p.reportFailure(from, "Tty: no pane free");
+ return null;
+ };
+ const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| {
+ p.reportError(from, "Tty", err);
+ return null;
+ };
nt.greet = !v9fs;
nt.v9fs_on_spawn = v9fs;
nt.body.mode = .tty;
@@ -315,6 +328,7 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void {
layout.splitBelow(p, parent, nt);
p.active = free;
p.stillborn_joiner = nt.serial;
+ return nt;
}
/// when a doc lands in `col`, a lone pristine shell there is clutter — drop
@@ -337,7 +351,7 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void {
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.absorbVWeight(p, tty_id, null);
layout.removePane(p, tty_id);
p.panes[tty_id] = null;
if (p.active == tty_id) p.active = keep_id;
@@ -362,34 +376,156 @@ pub fn replaceStillborn(p: *Pardes) void {
if (out.from != .cmd or out.from.cmd != .New) return;
if (file.revision != 0 or file.content.len != 0) return;
if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.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;
+// ---- REPLs ----
+//
+// A terminal is bound as a language's REPL by `Repl python` (builtins.zig).
+// An exec made by a gesture on the body of a file in that language -- or on
+// the REPL's own body -- is sent to it as input rather than run: all of it,
+// a word like `ls` too. The tag's words, `Exec` by name and a command word
+// `@`...`` still run as commands.
+
+/// A file pane's language, as grammar_manifest names it, by its index.
+pub fn langOf(pane: *const Pane) ?u8 {
+ const f = pane.file orelse return null;
+ return @intCast(@import("grammar_manifest.zig").byPath(f.path) orelse return null);
+}
+
+/// A bound REPL whose program is there to take input: a command pane's
+/// while its command runs; an interactive terminal's while a program, not
+/// the shell, has its tty (the host's `tty_taken`) -- a python3 left with
+/// Ctrl-D leaves the shell, which would run `import os` as ImageMagick's.
+pub fn replLive(p: *const Pardes, id: usize) bool {
+ const pane = p.panes[id] orelse return false;
+ if (pane.repl == null) return false;
+ if (pane.command != null) return !pane.command_done;
+ return p.hostTtyTaken(id);
+}
+
+/// `python-a`.
+pub fn replId(buf: []u8, pane: *const Pane) []const u8 {
+ const r = pane.repl orelse return "";
+ return std.fmt.bufPrint(buf, "{s}-{c}", .{ grammars[r.lang].name, r.letter }) catch "";
+}
+
+const Routed = union(enum) { none, sent: usize, asked };
+
+/// The REPL bound here has no program running to take the text: said,
+/// and nothing sent or run.
+fn replGone(p: *Pardes, from: usize, repl: *const Pane) Routed {
+ var idbuf: [48]u8 = undefined;
+ var said: [128]u8 = undefined;
+ p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {s} has no program running; nothing sent", .{replId(&idbuf, repl)}) catch "Repl: no program running");
+ return .asked;
+}
+
+/// Where an exec from `from`'s body goes when a REPL is bound for it: the
+/// one there is, or a question when there are several.
+fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed {
+ const src = p.panes[from] orelse return .none;
+ if (src.repl != null) {
+ if (!replLive(p, from)) return replGone(p, from, src);
+ sendToRepl(p, from, from, text);
+ return .{ .sent = from };
+ }
+ const lang = langOf(src) orelse return .none;
+ var bound: [26]usize = undefined;
+ var n: usize = 0;
+ var gone: ?usize = null;
+ for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and n < bound.len) {
+ if (!replLive(p, i)) {
+ gone = i;
+ continue;
+ }
+ bound[n] = i;
+ n += 1;
};
- 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;
+ if (n == 0) return if (gone) |i| replGone(p, from, p.panes[i].?) else .none;
+ if (n == 1) {
+ sendToRepl(p, from, bound[0], text);
+ return .{ .sent = bound[0] };
+ }
+ // Several: asked on the pane's notice band, as Del asks, one key
+ // answering; nothing is remembered of the answer.
+ const owned = p.gpa.dupe(u8, text) catch return .none;
+ if (p.repl_ask) |old| p.gpa.free(old.text);
+ p.repl_ask = .{ .from = src.serial, .lang = lang, .text = owned };
+ for (bound[0..n]) |i| p.repl_ask.?.offered[p.panes[i].?.repl.?.letter - 'a'] = p.panes[i].?.serial;
+ var w = std.Io.Writer.fixed(&src.question);
+ w.print("send to {s}:", .{grammars[lang].name}) catch {};
+ for (bound[0..n]) |i| w.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {};
+ w.writeAll(" (Esc: none)") catch {};
+ src.question_len = @intCast(w.buffered().len);
+ src.prompt = .repl_choice;
+ p.active = from;
+ return .asked;
+}
+
+/// Types `text` into the REPL in `to`, bracketed if its program asked for
+/// that, then Enter; says so on `from`'s message row and in the log.
+pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void {
+ const dst = p.panes[to] orelse return;
+ const src = p.panes[from] orelse return;
+ var idbuf: [48]u8 = undefined;
+ const id = replId(&idbuf, dst);
+ // Lines end in \n alone (a \r\n would give Python a blank line that
+ // ends its block), and no ESC or other control byte but a tab goes: an
+ // ESC[201~ in the text would end a bracketed paste early.
+ const clean = p.scratch.allocator().alloc(u8, text.len) catch return;
+ var n: usize = 0;
+ for (text, 0..) |c, i| {
+ if (c == '\r') {
+ if (i + 1 < text.len and text[i + 1] == '\n') continue;
+ clean[n] = '\n';
+ } else if (c < ' ' and c != '\t' and c != '\n' or c == 0x7f) continue else clean[n] = c;
+ n += 1;
+ }
+ // Sent whole or not at all: a send cut short would leave the REPL in a
+ // paste, or with half a block.
+ if (n + 16 > p.writeRoom(to)) {
+ var said: [128]u8 = undefined;
+ return p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {d} bytes is more than can be sent at once; send less", .{n}) catch "Repl: too long to send");
+ }
+ edit.typeToTty(p, to, dst, clean[0..n]);
+ p.emitWrite(to, "\r");
+ // Line by line, a blank line inside a Python block ends the block.
+ if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and
+ std.mem.indexOfScalar(u8, text, '\n') != null)
+ {
+ dst.repl_warned = true;
+ var warn: [128]u8 = undefined;
+ p.setMessage(from, std.fmt.bufPrint(&warn, "Repl: {s} takes no bracketed paste; a blank line in a block ends it", .{id}) catch "Repl: no bracketed paste");
+ }
+ var said: [64]u8 = undefined;
+ p.setMessage(from, std.fmt.bufPrint(&said, "\u{2192} {s}", .{id}) catch "sent");
+ src.msg_sent = true;
+ var rec: [96]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, "send", std.fmt.bufPrint(&rec, "{d} {d} {s}", .{ src.serial, dst.serial, id }) catch return);
}
+/// The longest line a command pane runs.
+pub const command_max = 1024;
+
const max_exec_depth = 8;
+/// A word no builtin knows (acme's exec.c run()). Clicked in an interactive
+/// terminal, it is typed into that shell, whose state the click can see;
+/// from anywhere else it runs as its own command pane, a terminal whose
+/// child is `$SHELL -c` the line, which shows its output and `exit N` when
+/// done, and which Kill signals. Answers the pane it went to.
pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
+ return executeFrom(p, id, txt, false);
+}
+
+/// `execute` for text a gesture took from `id`'s body when `from_body`:
+/// such text may be a bound REPL's. Run by name (`Exec ...`, whose line
+/// comes back through `execute`) or written as a command word, never.
+pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usize {
const pane = p.panes[id] orelse return null;
+ const gesture = from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null;
const cmd = commandText(txt);
if (cmd.len == 0) return null;
if (p.exec_depth == 0) p.noteHaptic(.exec);
@@ -397,16 +533,129 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
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);
- panes.Terminal.noteCommand(p.panes[dst].?, cmd);
- if (panes.Terminal.queuePendingCommand(p.panes[dst].?, cmd) catch |err| {
+ if (gesture) switch (routeToRepl(p, id, cmd)) {
+ .none => {},
+ .sent => |to| return to,
+ .asked => return null,
+ };
+ if (!takesCommandLine(p, id)) return runCommand(p, id, cmd);
+ panes.Terminal.padOutputBelowEdits(p, id);
+ panes.Terminal.noteCommand(pane, cmd);
+ if (panes.Terminal.queuePendingCommand(pane, cmd) catch |err| {
p.reportError(id, "queue command", err);
- return dst;
- }) return dst;
- p.emitWrite(dst, cmd);
- p.emitWrite(dst, "\r");
- return dst;
+ return id;
+ }) return id;
+ p.emitWrite(id, cmd);
+ p.emitWrite(id, "\r");
+ return id;
+}
+
+/// Runs `line` in a command pane for `from`'s directory: `from` itself if
+/// it is one whose command is done, else the directory's command pane that
+/// is done, else a new one below the last pane of the rightmost column.
+/// A pane reused keeps what it showed: the new command runs below a line
+/// naming it, as acme appends to +Errors and never clears it (util.c:213).
+fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
+ // The board and the web page start no processes: a command pane there
+ // would say running for ever, and fill the slots.
+ if (comptime !hosted) {
+ p.reportFailure(from, "exec: not a builtin, and this build runs no commands");
+ return null;
+ }
+ if (line.len > command_max) {
+ p.reportFailure(from, "exec: a command line is at most 1024 bytes");
+ return null;
+ }
+ const src = p.panes[from] orelse return null;
+ const dir = Pardes.paneDir(src);
+ // Not one whose pty a job it left behind still prints to: reusing it
+ // would hang that job up.
+ const reuse: ?usize = if (src.command != null and src.command_done and !src.command_pty) from else for (p.panes, 0..) |slot, i| {
+ const other = slot orelse continue;
+ if (other.command != null and other.command_done and !other.command_pty and std.mem.eql(u8, other.cwdSlice(), dir)) break i;
+ } else null;
+ if (reuse) |id| {
+ const pane = p.panes[id].?;
+ const owned = p.gpa.dupe(u8, line) catch |err| {
+ p.reportError(from, "exec", err);
+ return null;
+ };
+ p.gpa.free(pane.command.?);
+ pane.command = owned;
+ pane.command_done = false;
+ pane.command_status = null;
+ pane.command_pty = true;
+ pane.body.mode = .tty;
+ // What the last program left the emulator in goes first: the
+ // alternate screen left (only if it is there: leaving restores a
+ // saved cursor), mouse reports and bracketed paste off, the cursor
+ // shown, colours reset. (Not DECSTR: ghostty's stream does not
+ // implement it.)
+ if (panes.Terminal.onAlternateScreen(pane)) panes.Terminal.feedOutput(p, pane, "\x1b[?1049l");
+ panes.Terminal.feedOutput(p, pane, "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?25h\x1b[0m");
+ echoCommand(p, pane, line);
+ p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } });
+ noteRun(p, pane, "run", line);
+ return id;
+ }
+ const free = p.freeSlot() orelse {
+ p.reportFailure(from, "exec: no pane free for the command");
+ return null;
+ };
+ const pane = p.newCommand(free, dir, line) catch |err| {
+ p.reportError(from, "exec", err);
+ return null;
+ };
+ echoCommand(p, pane, line);
+ const rc = if (p.ncol > 0) p.ncol - 1 else 0;
+ if (p.col_n[rc] > 0) {
+ const parent = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]);
+ const f = layout.findPane(p, parent).?;
+ layout.splitBelow(p, parent, pane);
+ layout.insert(p, f.col, f.idx + 1, free);
+ } else layout.insert(p, rc, p.col_n[rc], free);
+ noteRun(p, pane, "run", line);
+ return free;
+}
+
+/// `% line` above a command's output, every command, as a shell echoes
+/// what it was given; its newlines the terminal's, so a multi-line command
+/// does not staircase.
+fn echoCommand(p: *Pardes, pane: *Pane, line: []const u8) void {
+ var buf: [2 * command_max + 8]u8 = undefined;
+ var w = std.Io.Writer.fixed(&buf);
+ w.writeAll(if (panes.Terminal.gridCursor(pane).x != 0) "\r\n% " else "% ") catch {};
+ for (line) |c| (if (c == '\n') w.writeAll("\r\n") else w.writeByte(c)) catch {};
+ w.writeAll("\r\n") catch {};
+ panes.Terminal.feedOutput(p, pane, w.buffered());
+}
+
+/// `run <serial> <word>` or `exit <serial> <N|?>` in the log.
+/// The whole line, one record as `msg` carries its text: its control
+/// characters, a newline too, become spaces (events.pushLog). A new pane is
+/// announced first, so `new` comes before the `run` in it.
+fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void {
+ pardes.ctlfs.events.announce(p);
+ var buf: [command_max + 16]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, std.mem.trim(u8, said, " \t\r\n") }) catch return);
+}
+
+/// A command pane's child is gone: the pane stays, showing what it said
+/// and how it ended, and is the next command's for its directory.
+pub fn commandDone(p: *Pardes, id: usize, status: ?u8) void {
+ const pane = p.panes[id] orelse return;
+ pane.command_done = true;
+ // A REPL it was is gone with its command; the pane's next is not one.
+ pane.repl = null;
+ pane.command_status = status;
+ pane.body.mode = .normal;
+ var buf: [16]u8 = undefined;
+ const code = if (status) |n| std.fmt.bufPrint(&buf, "{d}", .{n}) catch "?" else "?";
+ var said: [32]u8 = undefined;
+ const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else "";
+ panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&said, "{s}exit {s}\r\n", .{ lead, code }) catch "exit ?\r\n");
+ noteRun(p, pane, "exit", code);
+ p.needs_frame = true;
}
/// Trim and unwrap clickable command-word notation once for every command
@@ -420,7 +669,12 @@ pub fn commandText(txt: []const u8) []const u8 {
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");
+ // `Word arg`, or for a word that says so, `Word+arg` (`Tty+fish`).
+ const plus = std.mem.indexOfScalar(u8, cmd, '+');
+ var sp = std.mem.indexOfAny(u8, cmd, " \t");
+ if (sp == null) if (plus) |at| if (std.meta.stringToEnum(Builtin, cmd[0..at])) |head| {
+ if (builtins.registry.plusArg(head)) sp = at;
+ };
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;
@@ -441,6 +695,11 @@ pub fn applyStartupConfig(p: *Pardes) void {
}
pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
+ runBuiltinFrom(p, b, id, txt, arg, false);
+}
+
+/// `runBuiltin` saying whether a gesture on the pane's body made it.
+pub fn runBuiltinFrom(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8, from_body: bool) 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
@@ -450,7 +709,7 @@ pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]co
p.setMessage(id, @tagName(b));
pane.msg_announcement = true;
}
- const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg };
+ const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg, .from_body = from_body };
builtins.registry.dispatch(b, c);
}
@@ -467,6 +726,12 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
colors.setThemeIndex(p, i);
return;
};
+ // A config line is not answered on screen (applyStartupConfig
+ // runs with announce off); a click or a ctl write is.
+ if (!p.announce) return;
+ var near: [96]u8 = undefined;
+ var text: [200]u8 = undefined;
+ return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Theme: no theme \"{s}\"; like it: {s}; ThemeSel lists all", .{ want[0..@min(want.len, 40)], colors.themesNear(&near, want) }) catch "Theme: no such theme");
},
.font => {
if (comptime !font_picker) return;
@@ -476,7 +741,11 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
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),
+ else => if (!p.settings.apply(setting, arg) and p.announce) {
+ var text: [96]u8 = undefined;
+ const takes = if (setting.action == .toggle) "takes on or off" else "does not take that value";
+ p.reportFailure(p.active, std.fmt.bufPrint(&text, "{s}: {s}", .{ setting.word, takes }) catch takes);
+ },
}
if (p.settings.colors != previous_colors) {
for (p.panes) |slot| {
@@ -570,7 +839,7 @@ test "the acme chords act once per selection, not once on the primary" {
.{ .anchor = 7, .head = 13 },
};
pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 0, true);
- try std.testing.expectEqual(@as(u8, 1), pane.body.nsel);
+ try std.testing.expectEqual(@as(u16, 1), pane.body.nsel);
const before = p.ncol;
p.update(.{ .key = .{ .cp = Key.tab } }); // config.exec_key: Exec
@@ -578,7 +847,7 @@ test "the acme chords act once per selection, not once on the primary" {
// 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].?.body.nsel);
+ try std.testing.expectEqual(@as(u16, 0), p.panes[0].?.body.nsel);
}
test "a switch clicked with a selection held flips unless the selection says on or off" {