summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agents/skills/pardes-9p/SKILL.md5
-rw-r--r--docs/fs.md10
-rw-r--r--src/ninep/ctl.zig49
3 files changed, 48 insertions, 16 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 7e1c7d98..eb5a26e1 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -319,8 +319,9 @@ an open nor a `>` resets it (acme resets it on the first open): each `echo /re/
Write `0` to start from the top. The search wraps, so stop a find-all loop when
the address comes back to where it began, or set `limit`.
`$pane/ctl` reads acme's window status line — serial, tag length, body length,
-a reserved zero, the dirty flag, the width in cells, the font and the tab
-width, then `current` or `notcurrent` — and takes `get` (reload from disk),
+isdir (0), the dirty flag, the width in cells, the font, the tab width, the
+undo flag and the redo flag, as acme's fields — then `current` or
+`notcurrent` — and takes `get` (reload from disk),
`lock`/`unlock`, and any builtin that acts on a pane (`Del`, `Save f`,
`Collapse`, `Undo`/`Redo`: 256 steps; with none left they say so, and the
write succeeds). A `lock` another open holds fails at once with `file in use`
diff --git a/docs/fs.md b/docs/fs.md
index dfd887ac..8464768b 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -608,10 +608,12 @@ it was read too, and for an empty range acts on its text, which is how such
a line is done. A click in a file's body carries the offsets of the text it
took; one in a terminal's body cannot, since that body is a history
snapshot, and is also at `0 0` with its text. A click that takes no text
-sends nothing, as in acme. `ctl` reads acme's window status line — serial, tag length, body
-length, a reserved zero, the dirty flag, the width in cells, the font and the
-tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`):
-whether the pane has the keyboard. It takes the pane's builtins (below),
+sends nothing, as in acme. `ctl` reads acme's window status line, field for
+field as plan9port's — serial, tag length, body length, isdir (0), the dirty
+flag, the width in cells, the font, the tab width, whether Undo has a step
+(1) and whether Redo has one — followed by pardes's own: rio's `current` or
+`notcurrent` (rio(4), `wctl`), whether the pane has the keyboard, and a
+REPL's id. It takes the pane's builtins (below),
`get`, which reloads the buffer from the name it
carries (unsaved edits are refused once, listed in `+Unsaved` with a short
notice as Exit's are, the write failing `<name>: Modified (get again to
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 403100f2..e7274c5e 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -331,13 +331,13 @@ 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 + 48;
+pub const pane_text_cap = 9 * 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
-/// rio's word for whether the window has the keyboard, `current` or
-/// `notcurrent` (rio(4), wctl), which acme's line has no room for because
-/// acme has one window for input at a time only by the mouse.
+/// acme's winctlprint (plan9port wind.c), field for field: serial, tag
+/// length, body length, isdir (0), the dirty flag, the width in cells, the
+/// font, the tab width, whether Undo has a step, whether Redo has one. Then
+/// pardes's own: rio's word for whether the window has the keyboard,
+/// `current` or `notcurrent` (rio(4), wctl), and a REPL's id.
pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
var w = std.Io.Writer.fixed(buf);
w.print("{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} ", .{
@@ -353,7 +353,10 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
writeQuoted(&w, if (font.len == 0) "default" else font) catch return w.buffered();
// 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 {};
+ const f = pane_files.fileOf(pane);
+ const undo = if (f) |file| file.history.undo_len > 0 else false;
+ const redo = if (f) |file| file.history.redo_len > 0 else false;
+ w.print(" {d:>11} {d:>11} {d:>11} {s} ", .{ config.tab_width, @intFromBool(undo), @intFromBool(redo), 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);
@@ -1004,7 +1007,7 @@ const root_status = @intFromEnum(tree.TopFile.status);
const root_look = @intFromEnum(tree.TopFile.look);
const root_exec = @intFromEnum(tree.TopFile.exec);
-test "pane ctl read is index's five fields plus width in cells, font, tab width and whether it is current" {
+test "pane ctl read is acme's fields -- index's five, width in cells, font, tab width, undo, redo -- then whether it is current" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
defer p.deinit();
@@ -1014,8 +1017,8 @@ test "pane ctl read is index's five fields plus width in cells, font, tab width
try testing.expectEqual(Status.ok, a.reply.status);
var want: std.ArrayList(u8) = .empty;
defer want.deinit(gpa);
- try want.print(gpa, "{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {s} {d:>11} current ", .{
- pane.serial, pane_files.tagOf(p, pane).len, @as(usize, 2), 0, 0, pane.cols, "default", config.tab_width,
+ try want.print(gpa, "{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {s} {d:>11} {d:>11} {d:>11} current ", .{
+ pane.serial, pane_files.tagOf(p, pane).len, @as(usize, 2), 0, 0, pane.cols, "default", config.tab_width, 0, 0,
});
try testing.expectEqualStrings(want.items, a.bytes);
try testing.expectEqual(@as(u64, a.bytes.len), call(p, .{ .tag = 1, .op = .getattr, .node = Node.of(pane.serial, .ctl) }).reply.attr.size);
@@ -2399,6 +2402,32 @@ test "a pane's ctl takes acme's words: done by the files that replace them, or r
try testing.expectEqualStrings("0", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
}
+test "ctl reads acme's fields in acme's order, undo and redo flags included, then pardes's own" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const ctl_node = Node.of(serial, .ctl);
+ const fields = struct {
+ fn of(text: []const u8, buf: *[16][]const u8) [][]const u8 {
+ var it = std.mem.tokenizeScalar(u8, text, ' ');
+ var n: usize = 0;
+ while (it.next()) |t| : (n += 1) buf[n] = t;
+ return buf[0..n];
+ }
+ };
+ var buf: [16][]const u8 = undefined;
+ var f = fields.of(rd(p, ctl_node, 0, 4096).bytes, &buf);
+ try testing.expectEqualStrings("0", f[8]); // nothing to undo
+ try testing.expectEqualStrings("0", f[9]); // nothing to redo
+ try testing.expect(std.mem.eql(u8, f[10], "current") or std.mem.eql(u8, f[10], "notcurrent"));
+ _ = wr(p, Node.of(serial, .body), "two\n");
+ f = fields.of(rd(p, ctl_node, 0, 4096).bytes, &buf);
+ try testing.expectEqualStrings("1", f[8]);
+ _ = wr(p, ctl_node, "Undo\n");
+ f = fields.of(rd(p, ctl_node, 0, 4096).bytes, &buf);
+ try testing.expectEqualStrings("1", f[9]);
+}
+
test "every EINVAL a write gets says why, in its err record too; DEL is a control character in a line" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();