summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md6
-rw-r--r--src/Messages.zig49
-rw-r--r--src/builtins.zig14
-rw-r--r--src/esp32p4.zig2
-rw-r--r--src/exec.zig6
-rw-r--r--src/fs.zig6
-rw-r--r--src/gui/gui.zig2
-rw-r--r--src/look.zig7
-rw-r--r--src/ninep/ctl.zig6
-rw-r--r--src/ninep/events.zig2
-rw-r--r--src/ninep/pane.zig6
-rw-r--r--src/ninep/pty.zig28
-rw-r--r--src/pardes.zig20
-rw-r--r--test/fs.py2
14 files changed, 122 insertions, 34 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 8caaff3c..58c57867 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -186,7 +186,7 @@ the keyboard no pane does: `/focus` reads empty and every pane's `ctl` says
`notcurrent`. A write gives the keyboard and nothing else: a folded pane
stays folded (unfold it with `Collapse` on its ctl), as rio keeps `current`
apart from `unhide`. A serial no pane has fails with `no such
-window`; anything but a number, with `ill-formed control message`.
+pane`; anything but a number, with `ill-formed control message`.
A pane is made by **opening** `/pane/new`, and closed by Tremove on
`/pane/<n>` (`rmdir`), which is the only remove the tree serves; Tcreate is
@@ -500,7 +500,9 @@ for ever: cancel the read (interrupt it, or close the open) to give up;
`error not run` when the shell refused
the line without running it (a fish syntax error; the line is taken back off
the prompt); `error shell gone` when the pane closed or its shell was
-replaced; `error no prompt marks` for a shell pardes could not instrument.
+replaced; `error no prompt marks` for a shell pardes could not instrument;
+`error command done; not a shell` (or `error a command runs here, not a
+shell`) on a command pane, whose child is its command.
It relies on the OSC 133 marks pardes injects into bash and fish, tagged
`aid=pardes` so fish's own marks and a nested shell's are ignored. A second
line on an open whose command still runs fails the write. `exec zsh` or a
diff --git a/src/Messages.zig b/src/Messages.zig
index f8178629..66261eb4 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -416,23 +416,51 @@ pub fn messageLog(m: *const Messages, i: usize) ?*const LoggedMessage {
return &m.log[(first + i) % limits.message_log];
}
+/// `<operation>: <the error in words>`: FileNotFound reads "file not found".
pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void {
var buf: [256]u8 = undefined;
- const text = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch operation;
- reportFailure(p, id, text);
+ var w = std.Io.Writer.fixed(&buf);
+ w.print("{s}: ", .{operation}) catch {};
+ for (@errorName(err), 0..) |c, i| {
+ if (std.ascii.isUpper(c) and i > 0) w.writeByte(' ') catch break;
+ w.writeByte(std.ascii.toLower(c)) catch break;
+ }
+ reportFailure(p, id, w.buffered());
+}
+
+/// The longest start of `text` that fits `max` bytes and ends between
+/// words (or, with no space to cut at, between characters).
+pub fn clip(text: []const u8, max: usize) []const u8 {
+ if (text.len <= max) return text;
+ if (std.mem.lastIndexOfScalar(u8, text[0 .. max + 1], ' ')) |sp| return text[0..sp];
+ var n = max;
+ while (n > 0 and text[n] & 0xC0 == 0x80) n -= 1;
+ return text[0..n];
}
/// reportError with the words already chosen.
pub fn reportFailure(p: *Pardes, id: usize, text: []const u8) void {
- // A builtin a ctl write runs: its first error is also the write's.
+ // A builtin a ctl write runs: its first error is also the write's, cut
+ // between words.
if (p.fs.no_prompt and p.fs.failure_len == 0) {
- const n = @min(text.len, p.fs.failure.len);
- @memcpy(p.fs.failure[0..n], text[0..n]);
+ const kept = if (text.len > p.fs.failure.len) clip(text, p.fs.failure.len - 3) else text;
+ @memcpy(p.fs.failure[0..kept.len], kept);
+ var n = kept.len;
+ if (kept.len < text.len) {
+ @memcpy(p.fs.failure[n..][0..3], "...");
+ n += 3;
+ }
p.fs.failure_len = @intCast(n);
}
setMessage(p, id, text);
}
+test "an error reads as words and a failure is cut between them" {
+ try std.testing.expectEqualStrings("go", clip("go haskell", 4));
+ try std.testing.expectEqualStrings("go haskell", clip("go haskell", 10));
+ try std.testing.expectEqualStrings("\u{e9}", clip("\u{e9}\u{e9}", 3));
+}
+
/// The one place that decides which transient lines a pane is showing and
/// in what order. Everything downstream reads the answer: the body layer
/// so it does not paint under them, and the paint pass so it can stack
@@ -534,14 +562,14 @@ test "Msg writes the transient row by hand, bare or with text, and input ends it
// ...on the pane that ran it, and nowhere else
try std.testing.expectEqualStrings("", row(p, 0));
- // Bare, it reports ITSELF through reportError — the `<operation>: <Error>`
+ // Bare, it reports ITSELF through reportError — the `<operation>: <error in words>`
// every failed save and refused Look arrives in.
try std.testing.expect(p.executeBuiltinLine(2, "Msg"));
- try std.testing.expectEqualStrings("Msg: NoMessage", row(p, 2));
+ try std.testing.expectEqualStrings("Msg: no message", row(p, 2));
// Input starts its exit: it lingers, dissolves, and is gone.
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expectEqualStrings("Msg: NoMessage", row(p, 2));
+ try std.testing.expectEqualStrings("Msg: no message", row(p, 2));
for (0..200) |_| p.update(.tick);
try std.testing.expectEqualStrings("", row(p, 2));
// ...and with neither linger nor animation it is as old as your last input.
@@ -764,6 +792,11 @@ test "a builtin announces itself on the message row, and Verbose silences it" {
// for its announcement, which is correct and not what this is testing.
try std.testing.expect(p.executeBuiltinLine(p.active, "Wrap"));
try std.testing.expectEqualStrings("Wrap", pane.msg[0..pane.msg_len]);
+ // A value it does not take is said, in place of the announcement.
+ const wrapped = p.settings.wrap;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Wrap maybe"));
+ try std.testing.expectEqualStrings("Wrap: takes on or off", pane.msg[0..pane.msg_len]);
+ try std.testing.expectEqual(wrapped, p.settings.wrap);
// The notice list is what the paint pass reads, so the announcement has to
// reach it, not just the buffer.
diff --git a/src/builtins.zig b/src/builtins.zig
index 79c45325..2f34c647 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -319,8 +319,9 @@ test "Kill stops the commands pardes typed, bare all and by name those that matc
}
};
p.host = .{ .vtable = &killed.vtable };
- // Nothing running: nothing to stop, and never the shell.
+ // Nothing running: nothing to stop, and never the shell; said.
try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill"));
+ try std.testing.expectEqualStrings("Kill: nothing running", p.panes[term].?.msg[0..p.panes[term].?.msg_len]);
_ = exec.execute(p, term, "make -j8");
while (p.nextEffect()) |_| {}
// Typed but not started yet (no C mark): not running.
@@ -488,7 +489,10 @@ pub const Kill = struct {
}
if (stopped) return;
if (jobless) return c.p.reportFailure(c.id, "Kill: no job to signal (its shell runs it without job control)");
- if (names.len > 0) c.p.reportError(c.id, "Kill", error.NoSuchCommand);
+ if (names.len > 0)
+ c.p.reportFailure(c.id, "Kill: no running command has that first word")
+ else // said, but no failure: a script's Kill has nothing left to do
+ c.p.setMessage(c.id, "Kill: nothing running");
}
};
@@ -804,7 +808,7 @@ pub const Del = struct {
else if (std.mem.eql(u8, side, "j"))
below orelse above
else if (side.len > 0)
- return c.p.reportError(c.id, "Del k|j", error.InvalidArgument)
+ return c.p.reportFailure(c.id, "Del: takes k, j or nothing")
else
null;
// Someone at the keyboard with a real choice to make is asked, on the
@@ -939,9 +943,9 @@ pub const Repl = struct {
const named: ?u8 = if (manifest.byName(arg)) |i| if (grammars[i].exts.len > 0) @intCast(i) else null else null;
const lang: u8 = named orelse {
var w = std.Io.Writer.fixed(&buf);
- w.print("Repl: no language \"{s}\"; one of", .{arg[0..@min(arg.len, 32)]}) catch {};
+ w.print("Repl: no language \"{s}\"; - or one of:", .{@import("Messages.zig").clip(arg, 24)}) catch {};
for (grammars) |g| if (g.exts.len > 0) w.print(" {s}", .{g.name}) catch break;
- return c.p.reportFailure(c.id, w.buffered());
+ return c.p.reportFailure(c.id, @import("Messages.zig").clip(w.buffered(), 256));
};
if (pane.repl) |r| if (r.lang == lang)
return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl");
diff --git a/src/esp32p4.zig b/src/esp32p4.zig
index 0d532e1e..45126a74 100644
--- a/src/esp32p4.zig
+++ b/src/esp32p4.zig
@@ -610,7 +610,7 @@ export fn pardes_esp32p4_quit() callconv(.c) bool {
const pardes_host: pardes.Host.VTable = .{ .present = present, .gpio_toggle = gpioToggle };
/// The `Gpio` word's one seam to the board. Nothing here knows what a pad is; it forwards, and
-/// answers false when the firmware brought none, which is what puts "gpio: NoPads" on the message
+/// answers false when the firmware brought none, which is what puts "gpio: no pads" on the message
/// row rather than a trap.
fn gpioToggle(_: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool {
const f = host_gpio orelse return false;
diff --git a/src/exec.zig b/src/exec.zig
index 12934c3c..584d1b96 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -740,7 +740,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| {
diff --git a/src/fs.zig b/src/fs.zig
index 0b620c9f..b2e2751d 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -1261,11 +1261,11 @@ pub const Namespace = struct {
/// ctl answers a command's error (editors/acme/xfid.c:700).
no_prompt: bool = false,
refused: bool = false,
- failure: [96]u8 = undefined,
- failure_len: u8 = 0,
+ failure: [256]u8 = undefined,
+ failure_len: u16 = 0,
/// A refusal that quotes the message it refuses, as Plan 9's cmderror
/// does (kernel/misc/parse.c:82); answered at once (src/9p_io.zig).
- ename: [128]u8 = undefined,
+ ename: [320]u8 = undefined,
/// Something a held read may be waiting on changed since they were last
/// answered: a record queued, a run answered, a pane gone.
news: bool = false,
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 0cfe80f5..cf708a0e 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1660,7 +1660,7 @@ test "GUI LSP worker limit rejects before allocation and recovers after owned wo
try std.testing.expect(core.lsp_wait == null);
try std.testing.expectEqualStrings("abc", pane.file.?.content);
try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col);
- try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "WorkersBusy") != null);
+ try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "workers busy") != null);
try std.testing.expectEqual(@as(usize, 0), failing.alloc_index);
try std.testing.expect(!failing.has_induced_failure);
try std.testing.expectEqual(rejected, queue.lsp_id.?);
diff --git a/src/look.zig b/src/look.zig
index d7e763d3..136a9ede 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1162,6 +1162,13 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
pane.search_row = null;
_ = searchStep(p, id, 1);
}
+ // A path that is not there searches for its text, and a search
+ // that found nothing says so rather than leave an empty buffer.
+ const results = if (pane.search_pane) |rid| p.panes[rid] else null;
+ if (results) |r| if (r.file) |f| if (f.content.len == 0) {
+ var said: [96]u8 = undefined;
+ p.setMessage(id, std.fmt.bufPrint(&said, "Look: no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}) catch "Look: no match");
+ };
},
.pane => |t| {
if (t.id >= MAX_PANES) return;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 4e1c039b..680a09b0 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -200,7 +200,7 @@ const Builtin = builtins.registry.Builtin();
/// Refuses a control message, quoting it the way Plan 9's cmderror does
/// (kernel/misc/parse.c:82): `unknown control message "Bogus 3"`.
fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply {
- const room = p.fs.ename.len - why.len - 3;
+ const room = p.fs.ename.len -| (why.len + 3);
const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\"", .{ why, line[0..@min(line.len, room)] }) catch why;
return tree.failText(req.tag, E.INVAL, text);
}
@@ -399,7 +399,7 @@ pub fn writeFocus(p: *Pardes, req: Req) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-const e_no_pane = "no such window";
+const e_no_pane = "no such pane";
pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply {
var buf: [pane_text_cap]u8 = undefined;
@@ -666,7 +666,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
// its line; the lines before it have taken effect, as in acme.
const failed = wr(p, root_ctl, "Verbose on\nMount x\nVerbose off");
try testing.expectEqual(E.IO, failed.errno());
- try testing.expectEqualStrings("Mount name dial: MissingArgument \"Mount x\"", failed.reply.ename);
+ try testing.expectEqualStrings("Mount name dial: missing argument \"Mount x\"", failed.reply.ename);
try testing.expect(p.settings.verbose);
try testing.expect(!p.fs.no_prompt);
try testing.expectEqual(Status.ok, wr(p, root_ctl, "Verbose off").reply.status);
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index cff42c55..ffdea95b 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -877,7 +877,7 @@ test "a refused or failed write is an err record in the log, saying which file a
_ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus");
try testing.expectEqualStrings("err - ctl: unknown control message \"Bogus\"\n", next.record(p, f, frozen));
_ = wr(p, @intFromEnum(tree.TopFile.focus), "99999");
- try testing.expectEqualStrings("err - focus: no such window\n", next.record(p, f, frozen));
+ try testing.expectEqualStrings("err - focus: no such pane\n", next.record(p, f, frozen));
// A write that succeeds says nothing.
_ = wr(p, Node.of(serial, .addr), "#0");
try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status);
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 77be80c6..d2b2a791 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -507,7 +507,7 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply {
fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply {
const word = std.mem.trim(u8, req.data, " \t\r\n");
- if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return Reply.fail(req.tag, E.INVAL);
+ if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "takes 1 or 0");
const on = word[0] == '1';
const pf = &pane.fs;
switch (file) {
@@ -873,8 +873,10 @@ test "the flag files read and write the state acme kept behind ctl verbs" {
for ([_]u64{ dirty, mark, scroll }) |node| {
try testing.expectEqual(@as(u64, 2), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size);
- for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad|
+ for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| {
try testing.expectEqual(E.INVAL, wr(p, node, bad).errno());
+ try testing.expectEqualStrings("takes 1 or 0", wr(p, node, bad).reply.ename);
+ }
}
try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes);
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 88dafdfc..a2a40811 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -43,7 +43,7 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply {
while (it.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
if (line.len == 0) continue;
- if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl);
+ if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, e_bad_pty_ctl);
}
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
@@ -165,7 +165,10 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const pf = &pane.fs;
const state = pane.terminal orelse return tree.failText(req.tag, E.INVAL, e_bad_line);
const marks = &state.stream.handler;
- if (pf.unmarked) {
+ if (pane.command != null) {
+ // Its child is the command, and no shell is there to take a line.
+ answer(p, slot, "error {s}", .{if (pane.command_done) "command done; not a shell" else "a command runs here, not a shell"});
+ } else if (pf.unmarked) {
answer(p, slot, "error no prompt marks", .{});
} else if (waitingRun(p, pane) != null or (marks.prompts > 0 and (marks.phase != .input or
!pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id))))
@@ -335,6 +338,7 @@ fn waitingRun(p: *Pardes, pane: *Pane) ?*Run {
}
const e_bad_line = "bad command line";
+const e_bad_pty_ctl = tree.e_bad_ctl ++ "; takes winsize COLS ROWS, sig INT|TERM|HUP|QUIT|KILL, or exec";
pub fn readData(p: *Pardes, req: Req, pf: *pane_files.State) Reply {
if (pf.pty_out.empty()) return .{ .tag = req.tag, .status = .again };
@@ -505,6 +509,7 @@ test "every pty/ctl verb, and every refusal" {
}) |bad| {
const a = wr(p, ctl, bad);
try testing.expectEqual(E.INVAL, a.errno());
+ try testing.expectEqualStrings(e_bad_pty_ctl, a.reply.ename); // names what it takes
try testing.expect(a.winsize == null);
try testing.expect(a.signal == null);
try testing.expect(!a.spawned);
@@ -743,6 +748,25 @@ test "run answers how its command ended once the shell is back at a prompt" {
for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
+test "run on a command pane says no shell is there" {
+ const gpa = testing.allocator;
+ const p = try withTerm(gpa);
+ defer p.deinit();
+ const node = Node.of(serialOf(p), .pty_run);
+ const pane = p.panes[0].?;
+ pane.command = try gpa.dupe(u8, "make");
+ for ([_]struct { bool, []const u8 }{
+ .{ false, "error a command runs here, not a shell\n" },
+ .{ true, "error command done; not a shell\n" },
+ }) |case| {
+ pane.command_done = case[0];
+ const o = call(p, .{ .tag = 1, .op = .open, .node = node });
+ _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = o.reply.handle, .data = "ls\n" });
+ try testing.expectEqualStrings(case[1], call(p, .{ .tag = 3, .op = .read, .node = node, .handle = o.reply.handle, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 4, .op = .release, .node = node, .handle = o.reply.handle });
+ }
+}
+
test "a run's answer says cut when its output's start is gone, and reads a bounded tail" {
const gpa = testing.allocator;
const p = try withTerm(gpa);
diff --git a/src/pardes.zig b/src/pardes.zig
index c5413672..45d96f88 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -552,7 +552,7 @@ test "Detach asks the frontend to leave, and says so when there is nothing to le
p.perform(.{ .detach = asked });
const pane = p.panes[0].?;
- try std.testing.expectEqualStrings("detach: NotAttached", pane.msg[0..pane.msg_len]);
+ try std.testing.expectEqualStrings("detach: not attached", pane.msg[0..pane.msg_len]);
try std.testing.expect(!p.quit);
}
@@ -1924,14 +1924,14 @@ test "owned cwd preserves long paths aliases and failed updates" {
try std.testing.expect(p.panes[free] == null);
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);
+ try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "path too long") != null);
const current = pane.cwdSlice();
allocator.fail_index = allocator.alloc_index;
p.setCwd(0, "/replacement");
try std.testing.expectEqual(current.ptr, pane.cwdSlice().ptr);
try std.testing.expectEqualStrings(&path, pane.cwdSlice());
- try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "OutOfMemory") != null);
+ try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "out of memory") != null);
allocator.fail_index = std.math.maxInt(usize);
p.setCwd(0, "/replacement");
try std.testing.expectEqualStrings("/replacement", pane.cwdSlice());
@@ -2482,7 +2482,7 @@ test "a builtin that needs a pane reports capacity failure when every slot is fu
};
const help = std.meta.stringToEnum(Builtin, "Help") orelse return error.MissingHelpBuiltin;
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);
+ try std.testing.expect(std.mem.indexOf(u8, p.panes[0].?.msg[0..p.panes[0].?.msg_len], "help: no pane slots") != null);
}
test "Font keeps requested and acknowledged faces as separate plain state" {
@@ -6997,6 +6997,7 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" {
// A word that is not a side closes nothing.
try std.testing.expect(p.executeBuiltinLine(again, "Del x"));
+ try std.testing.expectEqualStrings("Del: takes k, j or nothing", p.panes[again].?.msg[0..p.panes[again].?.msg_len]);
try std.testing.expect(p.panes[again] != null);
// DelAbove is Del k, and a line that is not a key never asks.
try std.testing.expect(p.executeBuiltinLine(again, "DelAbove"));
@@ -7024,6 +7025,17 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" {
try std.testing.expect(p.panes[low] == null);
}
+test "a Look that finds nothing says so" {
+ if (platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ p.newScratchBelow(p.active);
+ const id = p.active;
+ p.sync();
+ look.lookAt(p, id, "/no/such/zzqq");
+ try std.testing.expectEqualStrings("Look: no match for \"/no/such/zzqq\"", p.panes[id].?.msg[0..p.panes[id].?.msg_len]);
+}
+
test "focus on a closed pane goes to the pane that takes its place, not the one focused before" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 });
diff --git a/test/fs.py b/test/fs.py
index 45c38c2e..53a9d4df 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -246,7 +246,7 @@ def discovery(binary, embedded=False):
client.write('/focus', b'99999\n')
raise AssertionError('focus took a serial no pane has')
except OSError as refused:
- assert 'no such window' in str(refused), refused
+ assert 'no such pane' in str(refused), refused
# /ctl reads the settings as a write takes them and takes the
# session's builtins; a pane's ctl takes the pane's.
settings = client.read('/ctl')