summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 03:39:43 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit84d23aef86c0bcb6ee14bb69b1e4d6d65205e2f6 (patch)
treef45ab1d07c16ceec51d8c164ad210aa220efe484
parent98e94b86a7c38e2bf4d66e57144fc9329906bb3b (diff)
downloadpardes-84d23aef86c0bcb6ee14bb69b1e4d6d65205e2f6.tar.gz
pardes-84d23aef86c0bcb6ee14bb69b1e4d6d65205e2f6.zip
size says its limits, keeps every pane its rows, and ptys follow their panes
size refused an out-of-range line without saying the range, took a size too small for the panes (leaving bodies of no rows), and a terminal squeezed to its tag kept a pty of its old size. The refusal now names 20x6 to 4096x4096 (a bare size is EINVAL like any malformed line), a size that would leave any pane less than its tag and 2 rows is refused and undone, and a terminal's pty is resized with its pane down to one row (only a folded or unplaced one keeps its grid). Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md4
-rw-r--r--src/layout.zig14
-rw-r--r--src/ninep/ctl.zig32
-rw-r--r--src/pardes.zig5
-rw-r--r--test/panes.zig26
5 files changed, 74 insertions, 7 deletions
diff --git a/docs/fs.md b/docs/fs.md
index cf6acb60..03851d8d 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -106,7 +106,9 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
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;
`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)
+ (`--detach`, 80x24 until then; refused while a frontend owns the size),
+ from 20x6 to 4096x4096, and refused when the panes it has would not each
+ keep their tag and 2 rows; a terminal's pty follows its pane on every resize
/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/layout.zig b/src/layout.zig
index 4c974fb7..bdef9675 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -433,6 +433,20 @@ pub fn placedRoomy(p: *Pardes, id: usize, before: *const [MAX_PANES]Rect) bool {
return true;
}
+/// Whether every unfolded pane sharing its column has its tag and
+/// `min_body_rows` (one alone in its column always fits).
+pub fn everyPaneRoomy(p: *Pardes) bool {
+ compute(p);
+ for (0..p.ncol) |c| {
+ if (p.col_n[c] < 2) continue;
+ for (p.col_panes[c][0..p.col_n[c]]) |k| {
+ const pane = p.panes[k] orelse continue;
+ if (!pane.collapsed and p.rects[k].h < minRows(p, k)) return false;
+ }
+ }
+ return true;
+}
+
/// Places `id` in column `c` by halving its tallest unfolded pane, when
/// that leaves both their rows; false, and `id` in no column, when not.
pub fn splitTallest(p: *Pardes, c: usize, id: usize) bool {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index dd2d9ed0..dd382290 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -473,11 +473,22 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
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);
+ const size = sizeOf(line) orelse return refuse(p, req, e_size, 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] } });
+ if (apply) {
+ const was: [2]u16 = .{ p.screen_w, p.screen_h };
+ 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)) {
+ 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);
+ }
+ }
continue;
}
if (!apply) {
@@ -493,12 +504,16 @@ 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.
+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] });
+
+/// `size <cols> <rows>`, within size_min and size_max.
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;
+ if (it.next() != null or cols < size_min[0] or rows < size_min[1] or cols > size_max[0] or rows > size_max[1]) return null;
return .{ cols, rows };
}
@@ -1490,5 +1505,12 @@ test "size sets the screen of a session no frontend is attached to, and only of
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());
+ try testing.expectEqual(E.INVAL, wr(p, root_ctl, "size\n").errno());
+ try testing.expect(std.mem.indexOf(u8, wr(p, root_ctl, "size 5 2\n").reply.ename, "at least 20x6, at most 4096x4096") != null);
+ // Too small for the panes it has, each its tag and two rows: refused,
+ // the size as it was.
+ for (0..4) |_| p.newScratchBelow(p.active);
+ p.sync();
+ try testing.expect(std.mem.indexOf(u8, wr(p, root_ctl, "size 80 8\n").reply.ename, "too small for the panes") != null);
+ try testing.expectEqual(@as(u16, 40), p.screen_h);
}
diff --git a/src/pardes.zig b/src/pardes.zig
index 72fd4d46..c103b9d7 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -6527,7 +6527,10 @@ pub const Pardes = struct {
} else pane.tag_top = 0;
const cols = @max(1, r.w -| config.GUTTER);
const rows = @max(1, r.h -| pane.tag_rows); // the tag steals the top rows
- if ((cols != pane.cols or rows != pane.rows) and r.h > pane.tag_rows) {
+ // A terminal's pty follows its pane down to a row, whatever the
+ // resize; only a folded one keeps the size it had, and one not
+ // placed yet the grid it was made with.
+ if ((cols != pane.cols or rows != pane.rows) and (r.h > pane.tag_rows or (pane.isTerminal() and !pane.collapsed and r.h > 0))) {
// doc panes have no pty/emulator grid to reflow; just record
// the size so bodyText renders the right number of rows
if (pane.isTerminal()) {
diff --git a/test/panes.zig b/test/panes.zig
index 25f0f6db..ca53af3d 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -4890,3 +4890,29 @@ const LookResetTests = struct {
test {
_ = LookResetTests;
}
+
+test "a terminal's pty follows its pane down to its last row" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ p.presentation.enabled = false;
+ p.sync();
+ while (p.nextEffect()) |_| {}
+ const term = for (p.panes, 0..) |slot, i| {
+ if (slot) |pane| if (pane.isTerminal()) break i;
+ } else return error.NoTerminal;
+ p.newScratchBelow(term);
+ p.sync();
+ while (p.nextEffect()) |_| {}
+ // The terminal squeezed to its tag alone, folded by nobody.
+ p.panes[term].?.vweight = 0.01;
+ for (p.col_panes[0][0..p.col_n[0]]) |k| if (k != term) {
+ p.panes[k].?.vweight = 1000;
+ };
+ p.sync();
+ try std.testing.expect(p.rects[term].h <= p.panes[term].?.tag_rows);
+ var rows: ?u16 = null;
+ while (p.nextEffect()) |effect| if (effect == .resize_pty and effect.resize_pty.pane == term) {
+ rows = effect.resize_pty.rows;
+ };
+ try std.testing.expectEqual(@as(?u16, 1), rows);
+}