summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/ctl.zig38
-rw-r--r--src/ninep/events.zig14
-rw-r--r--src/ninep/pane.zig6
-rw-r--r--src/ninep/pty.zig32
-rw-r--r--src/ninep/screen.zig2
5 files changed, 77 insertions, 15 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index deb64565..a64521e7 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -163,7 +163,7 @@ fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void {
try w.writeByte('\'');
}
-pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len;
+pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len + 48;
/// acme's winctlprint: serial, tag length, body length, a reserved zero, the
/// dirty flag, then the width in cells, the font and the tab width. Then
@@ -186,6 +186,10 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
// While a column's or the workspace's tag has the keyboard, no pane does.
const current = p.panes[p.active] == pane and !p.header_focus;
w.print(" {d:>11} {s} ", .{ config.tab_width, if (current) "current" else "notcurrent" }) catch {};
+ // A terminal bound as a REPL ends with its id, `python-a`.
+ var idbuf: [48]u8 = undefined;
+ const id = exec_line.replId(&idbuf, pane);
+ if (id.len > 0) w.print("{s} ", .{id}) catch {};
return w.buffered();
}
@@ -196,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);
}
@@ -298,7 +302,9 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
// Bare, it names the default directory.
.dump_dir => if (s.dump_dir.len > 0) try w.print("{s} {s}\n", .{ word, s.dump_dir.get() }) else try w.print("{s}\n", .{word}),
.transition => |effect| try w.print("{s} {s}\n", .{ word, if (s.panel_transition == effect) "on" else "off" }),
- .scene => |effect| try w.print("{s} {s}\n", .{ word, if (@field(s.scene_effects, @tagName(effect))) "on" else "off" }),
+ .scene => |effect| if (s.post.level(effect) == 0) try w.print("{s} off\n", .{word}) else try w.print("{s} {d}\n", .{ word, s.post.level(effect) }),
+ .shader => for (s.post.list()) |entry| if (entry.scene == null) try w.print("{s} {s}\n", .{ word, entry.path.get() }),
+ .shader_animation => try w.print("{s} {s}\n", .{ word, @tagName(s.shader_animation) }),
}
}
try w.writeAll("LocationsConfig ");
@@ -395,7 +401,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;
@@ -662,7 +668,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);
@@ -988,3 +994,25 @@ test "Joincol with no column to the right and Theme with no such theme say so" {
const pane = p.panes[p.active].?;
try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "no theme \"drak\"") != null);
}
+
+test "a look that fills a column's stillborn scratch is answered with the scratch already gone" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "file\n" });
+ var dir: [4096]u8 = undefined;
+ const at = dir[0..try tmp.dir.realPath(testing.io, &dir)];
+ var line: [4200]u8 = undefined;
+ // Newcol makes a column of one empty +New; a file looked from it
+ // joins that column and takes the scratch's place.
+ try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n").reply.status);
+ const scratch = p.panes[p.active].?.serial;
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(scratch, .look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status);
+ try testing.expect(p.paneBySerial(scratch) == null);
+ var want: [32]u8 = undefined;
+ const gone = try std.fmt.bufPrint(&want, "{d} ", .{scratch});
+ var rows = std.mem.splitScalar(u8, rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, '\n');
+ while (rows.next()) |row| try testing.expect(!std.mem.startsWith(u8, row, gone));
+}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 9ebb4bc1..ffdea95b 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -468,7 +468,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void {
p.fs.news = true;
}
-const EventRecord = struct { action: Action, q0: u32, q1: u32, text: ?[]const u8 = null };
+const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, text: ?[]const u8 = null };
const EventReader = struct {
data: []const u8,
@@ -478,6 +478,7 @@ const EventReader = struct {
if (er.i >= er.data.len) return null;
var i = er.i;
if (i + 2 > er.data.len) return null;
+ const origin = er.data[i];
i += 1;
const action = Action.fromChar(er.data[i]) orelse return null;
i += 1;
@@ -497,7 +498,7 @@ const EventReader = struct {
}
if (i >= er.data.len or er.data[i] != '\n') return null;
er.i = i + 1;
- return .{ .action = action, .q0 = q0, .q1 = q1, .text = text };
+ return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .text = text };
}
};
@@ -539,7 +540,12 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
const said = if (r.q0 == r.q1) r.text orelse whole[lo..hi] else whole[lo..hi];
const text = p.scratch.allocator().dupe(u8, said) catch continue;
switch (r.action) {
- .body_exec, .tag_exec => _ = exec.execute(p, live, text),
+ // A body's click written back is done as the click would have
+ // been (acme's xfideventwrite, xfid.c:842-844): a REPL bound for
+ // it takes it. One that came from a write to exec (`F`) was
+ // never a click, and runs as the command it was.
+ .body_exec => _ = exec.executeFrom(p, live, text, r.origin != 'F'),
+ .tag_exec => _ = exec.execute(p, live, text),
.body_look, .tag_look => look.lookAt(p, live, text),
else => unreachable,
}
@@ -871,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..3599b612 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) };
@@ -92,7 +92,8 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
// Busy is what a run written now would be told: a command is running
// or a line is typed at the prompt. Before the shell's first prompt a
// run waits for it rather than answer busy, so that is not busy.
- const marks = if (pane.terminal) |state| &state.stream.handler else null;
+ // A build with no terminal panes (the board) has no marks to read.
+ const marks = if (comptime !pardes.panes.Terminal.enabled) null else if (pane.terminal) |state| &state.stream.handler else null;
const busy = p.hostTtyTaken(id) or (!pane.fs.unmarked and (waitingRun(p, pane) != null or
(if (marks) |m| m.prompts > 0 and (m.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane)) else false)));
const out = p.fs.stage(p.gpa);
@@ -162,10 +163,14 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
if (std.mem.trim(u8, line, " \t").len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line);
if (line.len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line);
for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, e_bad_line);
+ if (comptime !pardes.panes.Terminal.enabled) return tree.failText(req.tag, E.INVAL, e_bad_line);
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 +340,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 +511,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 +750,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/ninep/screen.zig b/src/ninep/screen.zig
index bc2788ec..88947d0d 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -245,7 +245,7 @@ test "screen snapshots preserve rendered cells and styles until their handle is
test "screen inspection preserves acknowledged presentation and the next real frame" {
const gpa = testing.allocator;
- for (std.enums.values(pardes.layout.Transition)) |transition| {
+ for (std.enums.values(pardes.animation.Transition)) |transition| {
errdefer std.debug.print("screen inspection during {s}\n", .{@tagName(transition)});
const control = try withFile(gpa, "const value = 1;\n");
defer control.deinit();