diff options
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 5 | ||||
| -rw-r--r-- | docs/fs.md | 10 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 49 |
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` @@ -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(); |
