From 3189900c13b98298776d0da8dcbcb4e0d88858cf Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 19:16:56 -0300 Subject: Joincol is refused, as Newcol is, when the joined column cannot give each pane its tag and 2 rows Joincol moved every pane of the keyboard's column into the next one whether they fit or not. Two columns of two panes in a 12-row window became one column of four, wanting 12 rows where 10 were free, a layout that `size` then refused as it stood. Joincol now tries the join in place first (layout.joinFits: the panes and width moved, minimaFit asked, everything put back, nothing dropped or logged). A join that does not fit is refused, "Joincol: no space: the joined column cannot give each pane its tag and 2 rows". Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 3 +++ src/layout.zig | 27 ++++++++++++++++++++++++++- src/ninep/cols.zig | 26 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/builtins.zig b/src/builtins.zig index 6688d05f..27a1d36d 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -1271,6 +1271,9 @@ pub const Joincol = struct { // The keyboard's column joins the one to its right; the last has none. const col = c.p.keyboardColumn(c.p.active) orelse return; if (col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right"); + // As Newcol is refused: a column that could not give each pane its + // tag and 2 rows would leave a layout `size` refuses as it is. + if (!layout.joinFits(c.p, col)) return c.p.reportFailure(c.id, "Joincol: no space: the joined column cannot give each pane its tag and 2 rows"); layout.joinCol(c.p, col); } }; diff --git a/src/layout.zig b/src/layout.zig index 4aebcf28..a2211dfd 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -360,13 +360,38 @@ pub fn noteColumn(p: *Pardes, what: []const u8, serial: u32) void { pub fn joinCol(p: *Pardes, c: usize) void { if (c + 1 >= p.ncol) return; const dst = c + 1; + joinPanes(p, c, dst); + dropColumn(p, c); +} + +/// Whether column `c` joined into the one on its right leaves every pane +/// its tag and 2 rows (minimaFit), tried in place and undone: nothing is +/// dropped or logged for a join that does not fit. +pub fn joinFits(p: *Pardes, c: usize) bool { + if (c + 1 >= p.ncol) return true; + const dst = c + 1; + const n = p.col_n[c]; + const had = p.col_n[dst]; + const weights = .{ p.col_weight[c], p.col_weight[dst] }; + const panes_c = p.col_panes[c]; + joinPanes(p, c, dst); + const fits = minimaFit(p); + p.col_n[dst] = had; + p.col_n[c] = n; + p.col_panes[c] = panes_c; + p.col_weight[c] = weights[0]; + p.col_weight[dst] = weights[1]; + compute(p); + return fits; +} + +fn joinPanes(p: *Pardes, c: usize, dst: usize) void { for (0..p.col_n[c]) |k| p.col_panes[dst][p.col_n[dst] + k] = p.col_panes[c][k]; p.col_n[dst] += p.col_n[c]; p.col_n[c] = 0; // The width goes right, where the panes went. p.col_weight[dst] +|= p.col_weight[c]; p.col_weight[c] = 0; - dropColumn(p, c); } /// Put new pane `id` in column `c` as acme's makenewwindow does diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index a1fe9ecc..7afcfbd8 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -232,6 +232,32 @@ 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 "Joincol is refused when the joined column cannot give each pane its tag and 2 rows" { + const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 12 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const ctl = @intFromEnum(tree.TopFile.ctl); + const focus = @intFromEnum(tree.TopFile.focus); + // Two panes in the first column, then a second column of two. + const a = try th.newPane(p); + _ = a; + try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Newcol\n").reply.status); + const b = try th.newPane(p); + var digits: [16]u8 = undefined; + _ = th.wr(p, focus, try std.fmt.bufPrint(&digits, "{d}\n", .{b})); + _ = try th.newPane(p); + try testing.expectEqual(@as(usize, 2), p.ncol); + try testing.expect(p.col_n[0] >= 2 and p.col_n[1] >= 2); + // Focus in the left column: joined, four panes want 12 rows of 10. + const left = p.panes[p.col_panes[0][0]].?.serial; + _ = th.wr(p, focus, try std.fmt.bufPrint(&digits, "{d}\n", .{left})); + const r = th.wr(p, ctl, "Joincol\n"); + try testing.expectEqual(tree.Status.err, r.reply.status); + try testing.expect(std.mem.indexOf(u8, r.reply.ename, "tag and 2 rows") != null); + try testing.expectEqual(@as(usize, 2), p.ncol); + try testing.expect(pardes.layout.minimaFit(p)); +} + 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(); -- cgit v1.3