summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md8
-rw-r--r--src/layout.zig77
-rw-r--r--src/ninep/ctl.zig55
3 files changed, 120 insertions, 20 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 2149fbcf..23c87216 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -109,8 +109,12 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
/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),
- 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
+ from 20x6 to 4096x4096 (outside that, `invalid size`, EINVAL), and refused
+ when a column has not the rows for its panes' minima, each its tag and 2
+ rows, the minimum placement keeps; so a size once taken is taken again,
+ and growing is never refused. A pane the resize took under its minimum
+ gets its rows back from its column's others; a terminal's pty follows its
+ pane on every resize
/commands every builtin: word, `arg` if it takes one, `root`, `pane` or `both` (the ctl that takes it:
Edit is both, at the active pane from the root; a pane's word such as Undo or Msg
is refused at the root), a setting's values, then ` -- ` and what it does
diff --git a/src/layout.zig b/src/layout.zig
index cbf0fd96..09cf23b3 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -416,10 +416,71 @@ pub fn acmePlace(p: *Pardes, c: usize, origin: ?usize, id: usize) void {
/// its tag (acme's minht keeps one).
pub const min_body_rows = 2;
-fn minRows(p: *Pardes, id: usize) u16 {
+/// The rows a placement, and a `size`, leave a pane: its tag at its width
+/// and `min_body_rows`. The one minimum both ask.
+pub fn minRows(p: *Pardes, id: usize) u16 {
return p.wantedTagRows(id) + min_body_rows;
}
+/// Whether every column has the rows for each of its unfolded panes'
+/// `minRows` (a folded one its tag row): the panes' own minima, not how
+/// the weights happen to share the rows, so a size that fits keeps fitting
+/// when it grows. One pane alone in its column always fits.
+pub fn minimaFit(p: *Pardes) bool {
+ compute(p);
+ const avail = p.screen_h -| (p.topBarHeight() + p.columnBarHeight());
+ for (0..p.ncol) |c| {
+ if (p.col_n[c] < 2) continue;
+ var need: u32 = 0;
+ for (p.col_panes[c][0..p.col_n[c]]) |k| {
+ const pane = p.panes[k] orelse continue;
+ need += if (pane.collapsed) BOX_H else minRows(p, k);
+ }
+ if (need > avail) return false;
+ }
+ return true;
+}
+
+/// After a resize the minima fit: each unfolded pane the resize took below
+/// its `minRows` (it had them in `before`) gets them back, a row at a time
+/// from whichever pane of its column has the most above its own. A pane
+/// dragged short before keeps what it had.
+pub fn restoreMinima(p: *Pardes, before: *const [MAX_PANES]Rect) void {
+ compute(p);
+ for (0..p.ncol) |c| {
+ const n = p.col_n[c];
+ if (n < 2) continue;
+ var hs: [MAX_PANES]u16 = undefined;
+ var mins: [MAX_PANES]u16 = undefined;
+ var short = false;
+ for (p.col_panes[c][0..n], 0..) |k, i| {
+ const pane = p.panes[k] orelse return;
+ if (pane.collapsed) break;
+ hs[i] = p.rects[k].h;
+ mins[i] = minRows(p, k);
+ if (hs[i] < mins[i] and before[k].h >= mins[i]) short = true;
+ } else {
+ if (!short) continue;
+ for (p.col_panes[c][0..n], 0..) |k, i| {
+ if (hs[i] >= mins[i] or before[k].h < mins[i]) continue;
+ while (hs[i] < mins[i]) {
+ var giver: ?usize = null;
+ var spare: u16 = 0;
+ for (0..n) |j| if (j != i and hs[j] -| mins[j] > spare) {
+ spare = hs[j] - mins[j];
+ giver = j;
+ };
+ const g = giver orelse break;
+ hs[g] -= 1;
+ hs[i] += 1;
+ }
+ }
+ for (p.col_panes[c][0..n], 0..) |k, i| p.panes[k].?.vweight = @floatFromInt(@max(1, hs[i]));
+ }
+ }
+ compute(p);
+}
+
/// After placing `id`: whether it, and every pane of its column that the
/// placement shrank, still has its tag and `min_body_rows` (from `before`,
/// the rects as they were). A pane alone in its column always has room: the
@@ -437,20 +498,6 @@ 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` last in column `c`, where acme's coladd puts a new window,
/// with half the rows of the column's tallest unfolded pane, when that
/// leaves both their rows; false, and `id` in no column, when not.
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();