summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 07:15:20 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit03773fac517a2a26dc2f2813db9906d3bf7e91db (patch)
treea048a062c7994cec1b676f04300ce0a5e0af9ffd /src/ninep
parentd13ae04008e678ed992e6181487dbfd126a4154f (diff)
downloadpardes-03773fac517a2a26dc2f2813db9906d3bf7e91db.tar.gz
pardes-03773fac517a2a26dc2f2813db9906d3bf7e91db.zip
`size` is refused only when the panes' own minima do not fit, so it is monotonic
The check read how the weights shared the rows after the resize, not whether the rows could hold every pane's tag and two rows: a pane the proportional squeeze took short made `size` refuse, growing included, and refuse sizes it took a moment before. It now asks layout.minRows, the minimum placement asks, per column; a size they fit is taken, and a pane the resize took under its minimum gets its rows back. An out of range size says `invalid size`, which 9ns reads as EINVAL, as the doc had. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/ctl.zig55
1 files changed, 52 insertions, 3 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 02201de0..665913fc 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -521,15 +521,20 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
if (!p.unattended) return refuse(p, req, "size: a frontend is attached and owns the size", line);
if (apply) {
const was: [2]u16 = .{ p.screen_w, p.screen_h };
+ const before = p.rects;
p.update(.{ .resize = .{ .cols = size[0], .rows = size[1] } });
p.sync();
// Every pane keeps its tag and two rows, as placement
- // leaves them: a size too small for that is refused.
- if (!pardes.layout.everyPaneRoomy(p)) {
+ // leaves them (layout.minRows, the one minimum): a size
+ // too small for the panes' minima is refused, and one
+ // they fit gives back rows the resize took under them.
+ if (!pardes.layout.minimaFit(p)) {
p.update(.{ .resize = .{ .cols = was[0], .rows = was[1] } });
p.sync();
return refuse(p, req, "size: too small for the panes, each its tag and 2 rows", line);
}
+ pardes.layout.restoreMinima(p, &before);
+ p.sync();
}
continue;
}
@@ -548,7 +553,8 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
const size_min = [2]u16{ 20, 6 };
const size_max = [2]u16{ 4096, 4096 };
-const e_size = std.fmt.comptimePrint("size takes <cols> <rows>, at least {d}x{d}, at most {d}x{d}", .{ size_min[0], size_min[1], size_max[0], size_max[1] });
+// "invalid": the word 9ns reads as EINVAL (enameToErrno), as winsize's.
+const e_size = std.fmt.comptimePrint("invalid size: takes <cols> <rows>, at least {d}x{d}, at most {d}x{d}", .{ size_min[0], size_min[1], size_max[0], size_max[1] });
/// `size <cols> <rows>`, within size_min and size_max.
fn sizeOf(line: []const u8) ?[2]u16 {
@@ -1703,6 +1709,49 @@ test "size sets the screen of a session no frontend is attached to, and only of
try testing.expectEqual(@as(u16, 40), p.screen_h);
}
+test "size is monotonic: growing is never refused, and a size once taken is taken again" {
+ const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ p.unattended = true;
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ const new = @intFromEnum(tree.TopFile.new);
+ // The dogfood's repro: Newcol, three pane/new, a Tty, then pane/new
+ // until a column has no room (ENOSPC).
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, "Newcol\n").reply.status);
+ for (0..3) |_| try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(p.panes[p.active].?.serial, .ctl), "Tty\n").reply.status);
+ p.sync();
+ while (call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status == .ok) p.sync();
+ p.sync();
+ var line: [32]u8 = undefined;
+ // Down a row at a time (59, 58, 57, 56 and on) until one is refused:
+ // once refused, every smaller one is too.
+ var taken: [60]bool = @splat(false);
+ var rows: u16 = 59;
+ var refused = false;
+ while (rows >= 20) : (rows -= 1) {
+ taken[rows] = wr(p, root_ctl, try std.fmt.bufPrint(&line, "size 160 {d}\n", .{rows})).reply.status == .ok;
+ if (refused) try testing.expect(!taken[rows]);
+ if (!taken[rows]) refused = true;
+ }
+ try testing.expect(refused); // the columns are full: some size is too small
+ // Back up: each size is taken again exactly when it was on the way down.
+ rows = 20;
+ while (rows <= 59) : (rows += 1) {
+ const ok = wr(p, root_ctl, try std.fmt.bufPrint(&line, "size 160 {d}\n", .{rows})).reply.status == .ok;
+ try testing.expectEqual(taken[rows], ok);
+ }
+ for ([_]u16{ 60, 61, 64, 70, 100 }) |grown| {
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, try std.fmt.bufPrint(&line, "size 160 {d}\n", .{grown})).reply.status);
+ try testing.expectEqual(grown, p.screen_h);
+ }
+ // Out of range is EINVAL, in words 9ns reads as that.
+ const bad = wr(p, root_ctl, "size 5 2\n");
+ try testing.expectEqual(E.INVAL, bad.errno());
+ try testing.expectStringStartsWith(bad.reply.ename, "invalid size");
+}
+
test "Repl with no such language names a few whole, and where the rest are" {
const p = try th.withTerm(testing.allocator);
defer p.deinit();