From 69c5e62fe45a3768edff14993789f2aadfafadec Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 14:17:56 -0300 Subject: pane/new takes rows from a pane in another column before refusing ENOSPC, as acme's placement does A pane/new whose column had no room was refused "no space for a pane in that column", even with another column holding a tall pane that could give rows. acme takes rows wherever a window has them before saying there are none. pane/new now tries the other columns as +Errors already did: an empty one, else its tallest pane's bottom half, last column first. It is refused only when no pane anywhere can give the rows. Co-Authored-By: Claude Opus 5.5 --- docs/fs.md | 5 ++++- src/exec.zig | 14 +++++++++++--- src/ninep/cols.zig | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/fs.md b/docs/fs.md index 33cfd581..b37511dd 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -367,7 +367,10 @@ answer the same serial. It goes where acme's makenewwindow puts a window if empty, else the bottom half of its last pane. A session holds 64 panes (`no space for a pane: 64 max`), and every pane keeps its tag and 2 rows (`no space for a pane in that column: each keeps its tag and 2 rows`); both -are ENOSPC, for this open and for a look, exec, `New` or `Tty` alike. +are ENOSPC, for this open and for a look, exec, `New` or `Tty` alike. A +`pane/new` whose column is full takes its rows from a pane in another column +that has them, last column first, as acme does, and is refused only when +no pane anywhere can give them. `rmdir /pane/` closes the pane, unsaved or not. **`name`** reads the file name (a terminal's directory); a write renames the diff --git a/src/exec.zig b/src/exec.zig index 8356db75..7d682d74 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -1045,9 +1045,12 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan // No pane to halve, but the column holds every pane's minimum // with the new one's: the rows are shared out again, so what is // refused is only what the documented arithmetic cannot hold. - // +Errors goes wherever there is room, last column first; with - // none, its maker says the text some other way (appendErrors). - if (layout.shareColumn(p, f.col)) {} else if (kind == .errors) { + // +Errors and 9P's pane/new go wherever there is room, last + // column first, as acme takes rows from a window that has them + // before saying there are none; with none, +Errors's maker says + // the text some other way (appendErrors), and pane/new is + // refused. + if (layout.shareColumn(p, f.col)) {} else if (kind == .errors or kind == .fs) { layout.removePane(p, id); var c = p.ncol; while (c > 0) { @@ -1064,6 +1067,11 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan } else { layout.insert(p, f.col, p.col_n[f.col], id); p.unplaced.set(id); + // No pane anywhere can give it rows: pane/new is refused. + if (kind == .fs) { + p.panes[id].?.vweight = 0; + p.reportError(from, "placement", error.NoPaneRoom); + } return; } } else { diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 9185a2bc..c4ee83eb 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -228,6 +228,27 @@ test "New written to a column's exec makes its scratch in the session's director try testing.expectEqualStrings("/project/src/+New", p.panes[p.active].?.file.?.path); } +test "pane/new takes rows from a pane in another column before refusing ENOSPC" { + const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 16 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const first = p.panes[p.active].?.serial; + // A second column holding one tall pane. + try testing.expectEqual(tree.Status.ok, th.wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n").reply.status); + const tall = try th.newPane(p); + const tall_col = layout.findPane(p, p.paneBySerial(tall).?).?.col; + const focus = @intFromEnum(tree.TopFile.focus); + var digits: [16]u8 = undefined; + // From the first column, until pane/new is refused. + var moved = false; + for (0..pardes.MAX_PANES) |_| { + _ = th.wr(p, focus, try std.fmt.bufPrint(&digits, "{d}\n", .{first})); + const made = th.newPane(p) catch break; + if (layout.findPane(p, p.paneBySerial(made).?).?.col == tall_col) moved = true; + } + try testing.expect(moved); +} + test "a Newcol refused because the tags would not fit logs its err alone and spends no column serial" { const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 14 }); defer p.deinit(); -- cgit v1.3