summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-01 11:16:13 -0300
committerGabriel Schneider <[email protected]>2026-07-01 11:52:12 -0300
commit021e579cdd6e231cc3e9128f64a4ce7df2a5eddb (patch)
tree9f820142eb7f57ba73a0fbd34408cf0aa0e25b7a
parentc301ae3567204666c914ba4b36336d3a91af0cd1 (diff)
downloadpardes-021e579cdd6e231cc3e9128f64a4ce7df2a5eddb.tar.gz
pardes-021e579cdd6e231cc3e9128f64a4ce7df2a5eddb.zip
more heuristics for layout splitting
-rw-r--r--main.zig52
-rw-r--r--tests.zig110
2 files changed, 149 insertions, 13 deletions
diff --git a/main.zig b/main.zig
index 67904722..3db83d94 100644
--- a/main.zig
+++ b/main.zig
@@ -714,6 +714,12 @@ fn layoutRemove(l: *Layout, id: usize) void {
while (k + 1 < l.col_n[c]) : (k += 1) l.col_terms[c][k] = l.col_terms[c][k + 1];
l.col_n[c] -= 1;
if (l.col_n[c] == 0) {
+ // The column just emptied: hand its width to a neighbor (the column to its
+ // left, or right if it was the first) so the OTHER columns keep their widths
+ // and only that neighbor grows to fill the gap. Horizontal mirror of
+ // absorbVWeight; without it, wsum drops and every remaining column reflows
+ // sideways — an extra movement on delete.
+ if (l.ncol > 1) l.col_weight[if (c > 0) c - 1 else c + 1] += l.col_weight[c];
var j = c;
while (j + 1 < l.ncol) : (j += 1) {
l.col_terms[j] = l.col_terms[j + 1];
@@ -2309,21 +2315,41 @@ fn actOnSelection(
(openFileView(gpa, rp, pl.line, sw, sh) catch null);
if (opened) |nt| {
terms[free] = nt;
- // files/images live in the LEFT column (acme convention).
- // Stack at its top with a fair share of the column height,
- // then drop a leftover unused shell.
- const lc = 0;
- const ln = layout.col_n[lc];
- if (ln > 0) {
- var vsum: f32 = 0;
- for (0..ln) |i| if (terms[layout.col_terms[lc][i]]) |p| {
- vsum += p.vweight;
+ // If a doc pane (file/image) already exists, SPLIT it
+ // (acme-style, minimal reflow): drop the new pane directly
+ // below the source and shrink ONLY it — the rest of the
+ // column keeps its sizes. Same rule as the tty right-click
+ // split. Prefer the pane the look came from when it's a doc;
+ // else the first doc pane (docs live in the LEFT column).
+ var src_id: ?usize = null;
+ if (t.file != null or t.image != null) src_id = id;
+ if (src_id == null) for (terms.*, 0..) |sl, i| {
+ if (sl) |p| if ((p.file != null or p.image != null) and i != free) {
+ src_id = i;
+ break;
};
- nt.vweight = vsum / @as(f32, @floatFromInt(ln));
+ };
+ if (src_id) |sid| {
+ const sf = layoutFindTerm(layout, sid).?;
+ layoutInsert(layout, sf.col, sf.idx + 1, free);
+ splitBelow(terms[sid].?, nt, rects[sid].h);
+ active.* = free;
+ } else {
+ // No doc pane yet: stack at the top of the LEFT column
+ // with a fair share, then drop a leftover unused shell.
+ const lc = 0;
+ const ln = layout.col_n[lc];
+ if (ln > 0) {
+ var vsum: f32 = 0;
+ for (0..ln) |i| if (terms[layout.col_terms[lc][i]]) |p| {
+ vsum += p.vweight;
+ };
+ nt.vweight = vsum / @as(f32, @floatFromInt(ln));
+ }
+ layoutInsert(layout, lc, 0, free);
+ active.* = free;
+ evictLonePristineTty(layout, terms, io, gpa, lc, free, active);
}
- layoutInsert(layout, lc, 0, free);
- active.* = free;
- evictLonePristineTty(layout, terms, io, gpa, lc, free, active);
}
}
}
diff --git a/tests.zig b/tests.zig
index 9bb97f22..83e42dad 100644
--- a/tests.zig
+++ b/tests.zig
@@ -26,6 +26,16 @@ fn rowOf(text: []const u8, needle: []const u8) ?usize {
return null;
}
+// Column (0-based byte offset within its line) of the first `needle` on screen.
+// The grid is monospace ASCII here, so the byte offset is the screen column.
+fn colOf(text: []const u8, needle: []const u8) ?usize {
+ var it = std.mem.splitScalar(u8, text, '\n');
+ while (it.next()) |line| {
+ if (std.mem.indexOf(u8, line, needle)) |off| return off;
+ }
+ return null;
+}
+
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const args = try init.minimal.args.toSlice(init.arena.allocator());
@@ -479,4 +489,104 @@ pub fn main(init: std.process.Init) !void {
// grapheme slice into a stack-local cell).
try hs5.expectContains("█", "petscii image body is blank — no block glyphs rendered");
}
+
+ // 16. Opening a SECOND file SPLITS the existing file pane (acme-style minimal
+ // reflow) instead of stacking at the top of the column: the new file lands
+ // directly BELOW the source file, so A is above B. The OLD stack-at-top
+ // behavior would put B ABOVE A — so asserting rowOf(A) < rowOf(B) is a
+ // regression test for the split.
+ {
+ var hs6 = try Harness.init(gpa, exe.ptr, 24, 80);
+ defer hs6.deinit();
+ try hs6.expectWaitFor("build.zig", 8000, "fresh terminal never auto-ls'd");
+ try h.writeFile("/tmp/pardesE2Efa.txt", "ZZFILEA\nfa1\nfa2\nfa3\nfa4\nfa5\nfa6\nfa7\nfa8\nfa9\n");
+ try h.writeFile("/tmp/pardesE2Efb.txt", "ZZFILEB\nfb1\nfb2\nfb3\nfb4\nfb5\nfb6\nfb7\nfb8\nfb9\n");
+ try hs6.resize(30, 100); // clean geometry: right column starts ~col 51
+ try hs6.pump(700);
+ // open file A from the right-column terminal: the FIRST doc pane, so it
+ // takes the stack-at-top fallback (no existing doc pane to split).
+ try hs6.send("\x1b[<0;60;20M\x1b[<0;60;20m"); // pin a blank row, right column -> normal
+ try hs6.pump(400);
+ try hs6.send("i");
+ try hs6.pump(300);
+ try hs6.send("/tmp/pardesE2Efa.txt");
+ try hs6.pump(500);
+ try hs6.send("\x1b"); // -> normal
+ try hs6.pump(400);
+ try hs6.send("\x1b[<2;70;20M\x1b[<2;70;20m"); // right-click the path -> open file A
+ try hs6.expectWaitFor("ZZFILEA", 12000, "right-click did not open file A");
+ // open file B likewise: a doc pane now exists, so B SPLITS it (lands below A).
+ try hs6.send("\x1b[<0;60;22M\x1b[<0;60;22m"); // pin another blank row, right column
+ try hs6.pump(400);
+ try hs6.send("i");
+ try hs6.pump(300);
+ try hs6.send("/tmp/pardesE2Efb.txt");
+ try hs6.pump(500);
+ try hs6.send("\x1b");
+ try hs6.pump(400);
+ try hs6.send("\x1b[<2;70;22M\x1b[<2;70;22m"); // right-click the path -> open file B
+ try hs6.expectWaitFor("ZZFILEB", 12000, "right-click did not open file B");
+ // both markers sit on each file's TOP line (scroll 0). The split puts B
+ // under A -> A's row < B's row. Stack-at-top would invert this.
+ const text = try hs6.screenText();
+ defer gpa.free(text);
+ const ra = rowOf(text, "ZZFILEA") orelse return error.NoFileAOnScreen;
+ const rb = rowOf(text, "ZZFILEB") orelse return error.NoFileBOnScreen;
+ if (!(ra < rb)) {
+ hs6.dump("second file did not split BELOW the first (B not under A)");
+ return error.FileSplitOrderWrong;
+ }
+ }
+
+ // 17. Deleting a pane that EMPTIES its column hands the column's width to a
+ // neighbor, so the OTHER columns keep their widths (only the neighbor
+ // grows). Regression for the extra horizontal movement: without the
+ // col-weight handoff, wsum drops and every surviving column reflows
+ // sideways. Build 3 columns, put a marker in the rightmost, delete the
+ // MIDDLE column's shell (exit), and assert the marker's screen column is
+ // unchanged (the middle column's width flowed LEFT, not to the right one).
+ {
+ var hs7 = try Harness.init(gpa, exe.ptr, 24, 80);
+ defer hs7.deinit();
+ try hs7.expectWaitFor("build.zig", 8000, "fresh terminal never auto-ls'd");
+ try hs7.resize(30, 120); // 3 columns of ~40 each
+ try hs7.pump(700);
+ // add a 3rd column via the global-tagbar "Newcol" (cols 5-10, 0-based).
+ try hs7.send("\x1b[<1;8;1M\x1b[<1;8;1m"); // middle-click "Newcol"
+ try hs7.pump(1600); // let the new shell come up
+ // mark the rightmost column (now the active pane) with a unique token.
+ try hs7.send("\x02"); // normal -> tty
+ try hs7.pump(500);
+ try hs7.send("clear\r");
+ try hs7.pump(800);
+ // split the token so it appears ONLY in the OUTPUT (at the pane's left edge),
+ // never in the command text — else colOf would match the prompt-relative echo.
+ try hs7.send("echo ZZFAR''COL\r");
+ try hs7.expectWaitFor("ZZFARCOL", 8000, "new column's shell did not echo the marker");
+ const before = try hs7.screenText();
+ const x1 = colOf(before, "ZZFARCOL") orelse {
+ gpa.free(before);
+ return error.NoFarColBefore;
+ };
+ gpa.free(before);
+ // focus the MIDDLE column (x 40..80) and exit its shell -> the column
+ // empties and collapses. pty_eof deletes the pane and reflows.
+ try hs7.send("\x1b[<0;58;12M\x1b[<0;58;12m"); // left-click inside the middle column
+ try hs7.pump(500);
+ try hs7.send("\x02"); // -> tty
+ try hs7.pump(400);
+ try hs7.send("exit\r"); // shell EOF -> pane removed -> middle column collapses
+ try hs7.pump(2000); // let pty_eof + the reflow settle
+ try hs7.expectContains("ZZFARCOL", "marker vanished after collapsing the middle column");
+ const after = try hs7.screenText();
+ defer gpa.free(after);
+ const x2 = colOf(after, "ZZFARCOL") orelse return error.NoFarColAfter;
+ // with the width handoff the rightmost column stays put (x2 ~= x1); without
+ // it, every column reflows and x2 shrinks by ~one column width.
+ if (x2 + 2 < x1) {
+ hs7.dump("rightmost column moved when the middle column collapsed (no width handoff)");
+ std.debug.print("ZZFARCOL col: before={d} after={d}\n", .{ x1, x2 });
+ return error.ColumnCollapseReflow;
+ }
+ }
}