summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 03:14:31 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commita7960117dfb83ad65a0466a96affbbb524936b7f (patch)
treea824bff6e91e0e0e3cf84e9ac88a12201d708f81
parent6dfc6838c1c02978c026b650d69f87f23e3ff81c (diff)
downloadpardes-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.md3
-rw-r--r--docs/fs.md4
-rw-r--r--src/detached/server.zig4
-rw-r--r--src/ninep/ctl.zig30
-rw-r--r--test/fs.py6
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,
diff --git a/docs/fs.md b/docs/fs.md
index e9dfac65..80735642 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -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());
+}
diff --git a/test/fs.py b/test/fs.py
index 68da6c51..9336a53a 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -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):