diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 03:14:31 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | a7960117dfb83ad65a0466a96affbbb524936b7f (patch) | |
| tree | a824bff6e91e0e0e3cf84e9ac88a12201d708f81 | |
| parent | 6dfc6838c1c02978c026b650d69f87f23e3ff81c (diff) | |
| download | pardes-a7960117dfb83ad65a0466a96affbbb524936b7f.tar.gz pardes-a7960117dfb83ad65a0466a96affbbb524936b7f.zip | |
A detached session with no frontend takes its size from the root ctl
A --detach session nobody is attached to stayed 80x24, too small for an
agent's panes, and nothing could change it. The root ctl takes size <cols>
<rows> while no frontend is attached (refused while one owns the size), and
a client that attaches later still sets its own (the server compares with
the core's size). Documented with the 80x24 default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 3 | ||||
| -rw-r--r-- | docs/fs.md | 4 | ||||
| -rw-r--r-- | src/detached/server.zig | 4 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 30 | ||||
| -rw-r--r-- | test/fs.py | 6 |
5 files changed, 44 insertions, 3 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 037f2d83..38f84e77 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -72,7 +72,8 @@ $m/screen the rendered screen as JSON, frozen per open $m/listeners this session's dial addresses $m/focus the serial of the pane with the keyboard (empty while a column/workspace tag has it); echo a serial into it to move the keyboard (a folded pane stays folded) -$m/ctl the settings, one a line as a write takes them; write a setting or a session builtin +$m/ctl the settings, one a line as a write takes them; write a setting or a session builtin; + `size <cols> <rows>` sizes a --detach session no frontend is attached to (80x24 before) (Newcol makes an empty column, Dump, Theme x; Exit QUITS the editor, Kill [word...] stops the commands pardes started (command panes, lines it typed into shells), a word matching a command line's first word; Exit and Restore refuse once, @@ -104,7 +104,9 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC. /listeners the session's dial addresses /focus the serial of the pane with the keyboard; write a serial to give it the keyboard, which makes its column the active one as a click there would -/ctl the settings, one a line as a write takes them; write a setting or a session builtin +/ctl the settings, one a line as a write takes them; write a setting or a session builtin; + `size <cols> <rows>` sets the screen of a session no frontend is attached to + (`--detach`, 80x24 until then; refused while a frontend owns the size) /commands every builtin: word, `arg` if it takes one, `root` or `pane` (the ctl that takes it), a setting's values /layout one line per column, left to right: serial index x width current|notcurrent empty|full pane-serials...; then active <serial>: the active column, where diff --git a/src/detached/server.zig b/src/detached/server.zig index 8f45135a..af299051 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -1076,7 +1076,9 @@ pub const Session = struct { rows = if (rows == 0) c.rows else @min(rows, c.rows); } var regridded = false; - if (cols != 0 and (cols != s.cols or rows != s.rows)) { + // Against the core's size too: a root ctl `size` may have set it + // while nobody was attached. + if (cols != 0 and (cols != s.cols or rows != s.rows or cols != s.core.screen_w or rows != s.core.screen_h)) { s.cols = cols; s.rows = rows; for (&s.clients) |*c| c.need_full = true; diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 378dd844..26a18a12 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -472,6 +472,14 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { var it = Messages.init(p, req.data); while (it.next()) |line| { if (line.len == 0) continue; + if (std.mem.startsWith(u8, line, "size ") or std.mem.eql(u8, line, "size")) { + const size = sizeOf(line) orelse return refuse(p, req, "size takes <cols> <rows>", line); + // A frontend's window is the size; only a session none is + // attached to (--detach, unattended) takes one written. + if (!p.unattended) return refuse(p, req, "size: a frontend is attached and owns the size", line); + if (apply) p.update(.{ .resize = .{ .cols = size[0], .rows = size[1] } }); + continue; + } if (!apply) { if (checkBuiltin(p, req, line, .session)) |refusal| return refusal; continue; @@ -485,6 +493,15 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } +/// `size <cols> <rows>`, each at least a few cells and at most 4096. +fn sizeOf(line: []const u8) ?[2]u16 { + var it = std.mem.tokenizeAny(u8, line["size".len..], " \t"); + const cols = std.fmt.parseInt(u16, it.next() orelse return null, 10) catch return null; + const rows = std.fmt.parseInt(u16, it.next() orelse return null, 10) catch return null; + if (it.next() != null or cols < 20 or rows < 4 or cols > 4096 or rows > 4096) return null; + return .{ cols, rows }; +} + // ---- /commands ---- /// Every builtin the registry holds, one a line: its word, `arg` when it @@ -1460,3 +1477,16 @@ test "Edit's p and = never fail for want of room for +Errors: the text is logged for (p.panes) |slot| count += @intFromBool(slot != null); try testing.expectEqual(@as(usize, 1), count); // the +Errors that had no room is gone } + +test "size sets the screen of a session no frontend is attached to, and only of one" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const root_ctl = @intFromEnum(tree.TopFile.ctl); + try testing.expectEqual(E.INVAL, wr(p, root_ctl, "size 120 40\n").errno()); // a frontend's + p.unattended = true; + try testing.expectEqual(Status.ok, wr(p, root_ctl, "size 120 40\n").reply.status); + try testing.expectEqual(@as(u16, 120), p.screen_w); + try testing.expectEqual(@as(u16, 40), p.screen_h); + try testing.expectEqual(E.INVAL, wr(p, root_ctl, "size 120\n").errno()); + try testing.expectEqual(E.INVAL, wr(p, root_ctl, "size 5 2\n").errno()); +} @@ -556,6 +556,12 @@ def cut_writes(binary): client.write(f'/pane/{serial}/ctl', block) body = client.read(f'/pane/{serial}/body').decode().splitlines() assert body == ['line %05d of the block' % i for i in range(2500)], (len(body), body[:2]) + # A detached session with no frontend is 80x24 until told. + screen = client.screen() + assert (screen['cols'], screen['rows']) == (80, 24), (screen['cols'], screen['rows']) + client.write('/ctl', b'size 132 50\n') + screen = client.screen() + assert (screen['cols'], screen['rows']) == (132, 50), (screen['cols'], screen['rows']) def test(binary, quic=False): |
