summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 13:30:40 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit23d7f949b8595ed42e938a28b0d433d90853b341 (patch)
tree73b8b8b0ef981b513de05024ad0756ac4fb2c375
parent92ef244c8a24583896feb47c8c48ec40aa04ebb9 (diff)
downloadpardes-23d7f949b8595ed42e938a28b0d433d90853b341.tar.gz
pardes-23d7f949b8595ed42e938a28b0d433d90853b341.zip
A Newcol refused because the panes' tags would not fit logs its err alone and spends no column serial
Newcol tries its column before keeping it: the new column is inserted, the panes' rows are shared out again, and where a pane would be left under its tag and two rows the column is taken back. The trial showed. insertColumn logged `newcol N` and the take-back `delcol N`, so a follower saw a column come and go around the err. Laying the column out also gave it a serial, so the next column made skipped one. The trial is now unannounced, since insert and drop have unlogged forms. A refusal puts the serial counter back where it was, after first giving every existing column its serial so the trial's is the only one spent. Only a column that stays is logged, and it gets the next serial. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/layout.zig20
-rw-r--r--src/ninep/cols.zig42
-rw-r--r--src/pardes.zig12
3 files changed, 68 insertions, 6 deletions
diff --git a/src/layout.zig b/src/layout.zig
index 28f3d270..4aebcf28 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -325,8 +325,14 @@ pub fn removePane(p: *Pardes, id: usize) void {
/// Close column `c` once its panes are gone, its width to the one left of
/// it (the first column's to its right), as acme's rowclose gives it.
pub fn dropColumn(p: *Pardes, c: usize) void {
- std.debug.assert(p.col_n[c] == 0);
const serial = columnSerial(p, c);
+ dropColumnUnannounced(p, c);
+ noteColumn(p, "delcol", serial);
+}
+
+/// dropColumn with no `delcol`, for a column never announced.
+pub fn dropColumnUnannounced(p: *Pardes, c: usize) void {
+ std.debug.assert(p.col_n[c] == 0);
tagline.exitHeader(p);
if (p.ncol > 1) p.col_weight[if (c > 0) c - 1 else c + 1] +|= p.col_weight[c];
p.col_tags[c].deinit(p.gpa);
@@ -341,12 +347,11 @@ pub fn dropColumn(p: *Pardes, c: usize) void {
p.ncol -= 1;
p.col_tags[p.ncol] = .{ .what = .column, .gpa = p.gpa };
p.col_serial[p.ncol] = 0;
- noteColumn(p, "delcol", serial);
}
/// `newcol <serial>` or `delcol <serial>` in the log: the column's serial,
/// as /layout and /col name it.
-fn noteColumn(p: *Pardes, what: []const u8, serial: u32) void {
+pub fn noteColumn(p: *Pardes, what: []const u8, serial: u32) void {
var buf: [16]u8 = undefined;
pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d}", .{serial}) catch return);
}
@@ -649,6 +654,14 @@ pub fn firstColumn(p: *Pardes) ?usize {
}
pub fn insertColumn(p: *Pardes, col: usize, before: bool) ?usize {
+ const c = insertColumnUnannounced(p, col, before) orelse return null;
+ noteColumn(p, "newcol", columnSerial(p, c));
+ return c;
+}
+
+/// insertColumn with no serial given and no `newcol`: a column that may
+/// yet be taken back (Newcol's trial), which then leaves no trace.
+pub fn insertColumnUnannounced(p: *Pardes, col: usize, before: bool) ?usize {
if (!canSplitCol(p, col)) return null;
var old_weight = p.col_weight[col];
if (old_weight < 2 or old_weight % 2 != 0) {
@@ -672,7 +685,6 @@ pub fn insertColumn(p: *Pardes, col: usize, before: bool) ?usize {
p.col_serial[c] = 0;
p.col_n[c] = 0;
p.ncol += 1;
- noteColumn(p, "newcol", columnSerial(p, c));
return c;
}
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index 454cce4a..f07c7f39 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -228,6 +228,48 @@ 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 "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();
+ while (p.nextEffect()) |_| {}
+ // Long tags wrap onto more rows as their column narrows: the boot's
+ // two panes share one, and each keeps its tag and two rows.
+ const made = try th.newPane(p);
+ const pane = p.panes[p.paneBySerial(made).?].?;
+ const col = layout.findPane(p, p.paneBySerial(made).?).?.col;
+ for (p.col_panes[col][0..p.col_n[col]]) |k| {
+ const each = p.panes[k].?;
+ if (each.tag.own) |old| p.gpa.free(old);
+ each.tag.own = try p.gpa.dupe(u8, "word " ** 60);
+ }
+ try testing.expect(p.col_n[col] >= 2);
+ p.sync();
+ const ctl = @intFromEnum(tree.TopFile.ctl);
+ const focus = @intFromEnum(tree.TopFile.focus);
+ var digits: [16]u8 = undefined;
+ const own = try std.fmt.bufPrint(&digits, "{d}\n", .{pane.serial});
+ var refused = false;
+ for (0..pardes.MAX_COLS) |_| {
+ // From the long tag's column, so it is the one split narrower.
+ _ = th.wr(p, focus, own);
+ const serial_before = p.next_column_serial;
+ const log_before = try pardes.ctlfs.events.freezeLog(p);
+ defer p.gpa.free(log_before.bytes);
+ const r = th.wr(p, ctl, "Newcol\n");
+ if (r.reply.status == .ok) continue;
+ if (std.mem.indexOf(u8, r.reply.ename, "tags would not fit") == null) continue;
+ refused = true;
+ try testing.expectEqual(serial_before, p.next_column_serial);
+ const log_after = try pardes.ctlfs.events.freezeLog(p);
+ defer p.gpa.free(log_after.bytes);
+ const added = log_after.bytes[log_before.bytes.len..];
+ try testing.expect(std.mem.startsWith(u8, added, "err "));
+ try testing.expectEqual(@as(usize, 1), std.mem.count(u8, added, "\n"));
+ break;
+ }
+ try testing.expect(refused);
+}
+
test "a tagexec word that makes, acts on and focuses no pane reads back nothing" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();
diff --git a/src/pardes.zig b/src/pardes.zig
index f0d22d57..f61ba797 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4971,8 +4971,13 @@ pub const Pardes = struct {
// An empty window (Delcol took the last column): its first column.
if (layout.firstColumn(p)) |c| return tagline.enterHeader(p, c);
const col = p.keyboardColumn(from_id) orelse return;
+ // A trial: laying it out gives it a serial (layout.compute), which a
+ // refusal takes back. Every other column has its own already, so
+ // the counter as it is now is the trial's to return to.
+ for (0..p.ncol) |k| _ = layout.columnSerial(p, k);
+ const unspent = p.next_column_serial;
// Said, not silently nothing: "no space" is 9ns's word for ENOSPC.
- const c = layout.insertColumn(p, col, false) orelse {
+ const c = layout.insertColumnUnannounced(p, col, false) orelse {
var said: [96]u8 = undefined;
return p.reportFailure(from_id, if (p.ncol >= MAX_COLS)
std.fmt.bufPrint(&said, "Newcol: no space for a column: {d} max", .{MAX_COLS}) catch "Newcol: no space for a column"
@@ -4984,10 +4989,13 @@ pub const Pardes = struct {
// out again, and where they cannot hold every minimum there is no
// new column, as a size too small is refused.
for (0..p.ncol) |k| if (!layout.columnAtMinimums(p, k) and !layout.shareColumn(p, k)) {
- layout.dropColumn(p, c);
+ layout.dropColumnUnannounced(p, c);
+ p.next_column_serial = unspent;
layout.compute(p);
return p.reportFailure(from_id, "Newcol: no space for a column: the panes' tags would not fit");
};
+ // Kept: now it has a serial, and the log hears of it.
+ layout.noteColumn(p, "newcol", layout.columnSerial(p, c));
tagline.enterHeader(p, c);
}