diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 02:32:33 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 672b2643583d13feef5d47600e0f6c15f276fba2 (patch) | |
| tree | 3d556d0f30e72c8b2ed7bd4177a802e2a0697992 | |
| parent | 45a66bd1c062a5cf4d63b1df68e050b059cdfa1d (diff) | |
| download | pardes-672b2643583d13feef5d47600e0f6c15f276fba2.tar.gz pardes-672b2643583d13feef5d47600e0f6c15f276fba2.zip | |
No placement leaves a pane shorter than its tag and two rows
Placement could split a pane down to a tag and one row, or give a new one
less. Every placement now checks the pane it made and each pane it shrank
against its tag (wrapped at its width) plus two rows: where they lack the
room, the column's tallest pane is halved instead, and where none can give
it the new pane is refused with "no space for a pane in that column"
(ENOSPC over 9P) and closed at the step's end, once its maker is done with
it. splitBelow and splitParent leave two body rows (a boot layout, split
before any row exists, is as before). Re-recorded by name: lookpanes (panes
one or two rows lower) and splitparent (the 6-row squeeze now refuses);
fs.py closes the scratches and long-named panes it no longer needs, since
its 24-row session filled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/fs.md | 4 | ||||
| -rw-r--r-- | docs/tags.md | 7 | ||||
| -rw-r--r-- | src/Messages.zig | 11 | ||||
| -rw-r--r-- | src/exec.zig | 17 | ||||
| -rw-r--r-- | src/layout.zig | 63 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 26 | ||||
| -rw-r--r-- | src/pardes.zig | 58 | ||||
| -rw-r--r-- | test/fs.py | 5 | ||||
| -rw-r--r-- | test/snapshots/lookpanes.golden | 34 | ||||
| -rw-r--r-- | test/snapshots/splitparent.golden | 32 | ||||
| -rw-r--r-- | test/snapshots/splitparent.snap | 9 |
11 files changed, 200 insertions, 66 deletions
@@ -119,7 +119,9 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC. 64 panes (16 on the board); at that, every route that would open one -- this open, look, exec, New, Tty -- fails with `no space for a pane: 64 max` (ENOSPC through 9ns, which has no word for ENFILE) and an err - record, and look reads back empty + record, and look reads back empty; a column with no rows left for one + (each pane keeps its tag and 2 rows) refuses it the same way, + `no space for a pane in that column` (docs/tags.md) /pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors event look exec, plus pty/{ctl,status,data} on terminals /os/ the host filesystem diff --git a/docs/tags.md b/docs/tags.md index b8322aa9..5c4492bc 100644 --- a/docs/tags.md +++ b/docs/tags.md @@ -234,6 +234,13 @@ Every new pane goes through one placement, chosen by the `Placement` setting: it, in an init file, a tag or the root ctl; bare `Placement` flips it; `SPC c p` is its leader path, and `Config` reports it. +Under either, no placement leaves a pane, new or split, shorter than its tag +and two body rows (acme's minht keeps one). Where the place chosen has not +that room, the column's tallest pane is halved instead; where no pane of the +column can give it, the new pane is refused and closed, with `no space for a +pane in that column: each keeps its tag and 2 rows` (a 9P write or open of +`pane/new` fails with it, ENOSPC). A pane alone in its column always fits. + `Placement acme` is acme's makenewwindow (util.c:449-495). The core keeps acme's *active column* (activecol, dat.c:37): the column last typed in (acme.c:487), clicked in with the select button (acme.c:659), dropped into by diff --git a/src/Messages.zig b/src/Messages.zig index b197e715..daf195bb 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -444,18 +444,23 @@ pub fn messageLog(m: *const Messages, i: usize) ?*const LoggedMessage { return &m.log[(first + i) % limits.message_log]; } -/// `<operation>: <the error in words>`: FileNotFound reads "file not found". /// What a route that would open a pane says when every slot is taken. /// "no space": 9ns's word for ENOSPC (it has none for ENFILE). pub const no_pane_slots = "no space for a pane: {d} max"; +/// ...and when the column it would go in has no rows for one. +pub const no_pane_room = "no space for a pane in that column: each keeps its tag and {d} rows"; +/// `<operation>: <the error in words>`: FileNotFound reads "file not found". pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void { var buf: [256]u8 = undefined; var w = std.Io.Writer.fixed(&buf); w.print("{s}: ", .{operation}) catch {}; // Every route that opens a pane says the cap the same way. - if (err == error.NoPaneSlots) { - w.print(no_pane_slots, .{pardes.MAX_PANES}) catch {}; + if (err == error.NoPaneSlots or err == error.NoPaneRoom) { + if (err == error.NoPaneSlots) + w.print(no_pane_slots, .{pardes.MAX_PANES}) catch {} + else + w.print(no_pane_room, .{pardes.layout.min_body_rows}) catch {}; // A 9P look or exec that would open the pane fails with it too. p.fs.no_pane_slot = true; @memcpy(p.fs.ename[0..w.buffered().len], w.buffered()); diff --git a/src/exec.zig b/src/exec.zig index d4480e00..6de7991c 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -808,6 +808,8 @@ pub const NewPane = enum { scratch, fs, shell, doc, command, errors }; /// t->w == nil). pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPane) void { const asked = if (p.exec_column != null) null else origin; + layout.compute(p); + const before = p.rects; switch (p.settings.placement) { .acme => switch (kind) { // New: the bottom half of the last pane in its own column @@ -821,6 +823,21 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan }, .pardes => pardesPlace(p, from, id, kind), } + // No pane is made shorter than its tag and two rows: where the place + // chosen has not the room, the column's tallest pane gives it, and + // where none can, the pane is refused, and closed at the step's end + // (Pardes.unplaced) once whatever made it is done with it. + if (!layout.placedRoomy(p, id, &before)) if (layout.findPane(p, id)) |f| { + layout.removePane(p, id); + for (p.col_panes[f.col][0..p.col_n[f.col]]) |k| p.rects[k] = before[k]; + layout.snapColWeights(p, f.col); + if (!layout.splitTallest(p, f.col, id) or !layout.placedRoomy(p, id, &before)) { + if (layout.findPane(p, id) == null) layout.insert(p, f.col, p.col_n[f.col], id); + p.unplaced.set(id); + p.reportError(from, "placement", error.NoPaneRoom); + return; + } + }; if (kind == .command) return; tagline.exitHeader(p); p.active = id; diff --git a/src/layout.zig b/src/layout.zig index b9b24e79..4c974fb7 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -408,6 +408,46 @@ pub fn acmePlace(p: *Pardes, c: usize, origin: ?usize, id: usize) void { splitAt(p, c, id, y); } +/// The body rows a placement leaves every pane it makes or shrinks, under +/// its tag (acme's minht keeps one). +pub const min_body_rows = 2; + +fn minRows(p: *Pardes, id: usize) u16 { + return p.wantedTagRows(id) + min_body_rows; +} + +/// 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 +/// window can be no taller. +pub fn placedRoomy(p: *Pardes, id: usize, before: *const [MAX_PANES]Rect) bool { + compute(p); + const f = findPane(p, id) orelse return true; + if (p.col_n[f.col] == 1) return true; + for (p.col_panes[f.col][0..p.col_n[f.col]]) |k| { + const pane = p.panes[k] orelse continue; + if (pane.collapsed) continue; + if (k != id and p.rects[k].h >= before[k].h) continue; + if (p.rects[k].h < minRows(p, k)) return false; + } + return true; +} + +/// Places `id` in column `c` by halving its tallest unfolded pane, when +/// that leaves both their rows; false, and `id` in no column, when not. +pub fn splitTallest(p: *Pardes, c: usize, id: usize) bool { + compute(p); + var tall: ?usize = null; + for (p.col_panes[c][0..p.col_n[c]]) |k| { + if (k == id or p.panes[k].?.collapsed) continue; + if (tall == null or p.rects[k].h > p.rects[tall.?].h) tall = k; + } + const v = tall orelse return false; + if (p.rects[v].h < minRows(p, v) + minRows(p, id)) return false; + splitAt(p, c, id, p.rects[v].y + p.rects[v].h / 2); + return true; +} + /// acme's coladd at row `y` (cols.c:66-126): the new pane goes under the /// pane that `y` falls in, which keeps its rows above `y`. fn splitAt(p: *Pardes, c: usize, id: usize, y: u16) void { @@ -419,9 +459,9 @@ fn splitAt(p: *Pardes, c: usize, id: usize, y: u16) void { snapColWeights(p, c); insert(p, c, k + 1, id); const pane = p.panes[id].?; - // Each keeps a tag and a body row, as acme's minht does. - const lo = r.y +| BOX_H + 1; - const hi = (r.y + r.h) -| (BOX_H + 1); + // Each keeps a tag and min_body_rows, where acme's minht keeps one. + const lo = r.y +| minRows(p, v); + const hi = (r.y + r.h) -| minRows(p, id); const row = splitRowForExtent(r.y, r.h, if (lo <= hi) std.math.clamp(y, lo, hi) else r.y + r.h / 2) orelse { pane.vweight = BOX_H + 1; return; @@ -550,7 +590,7 @@ pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) ?usize { } pub fn splitParent(p: *Pardes, want: usize) usize { - const need = 2 * BOX_H + 3; + const need = 2 * (BOX_H + min_body_rows); if (p.rects[want].h >= need) return want; if (findPane(p, want)) |f| for (0..p.col_n[f.col]) |k| { if (p.rects[p.col_panes[f.col][k]].h >= need) return p.col_panes[f.col][k]; @@ -567,23 +607,28 @@ pub fn splitParent(p: *Pardes, want: usize) usize { pub fn splitBelow(p: *Pardes, src_id: usize, nw: *Pane) void { const src = p.panes[src_id] orelse return; const src_h = p.rects[src_id].h; - const body: u16 = if (src_h > BOX_H) src_h - BOX_H else 1; + // Its tag may wrap to more than one row: the rows under it are its body. + const tag_h = @max(BOX_H, src.tag_rows); + const body: u16 = if (src_h > tag_h) src_h - tag_h else 1; const cur: u16 = if (!src.isTerminal()) body / 2 else panes.Terminal.gridCursor(src).y + 1; // cap keep so a content-full source still leaves the new pane a tag + // a few body rows (an Alt-n from a full shell was born 0 rows tall) - const keep = std.math.clamp(cur, 1, @max(1, body -| (BOX_H + 3))); + // ...and the source min_body_rows of its own (exec.placeNew), when it + // has them: a boot layout splits before any row is laid out. + const least = @min(min_body_rows, body); + const keep = std.math.clamp(cur, least, @max(least, body -| (BOX_H + 3))); if (findPane(p, src_id)) |f| for (0..p.col_n[f.col]) |k| { const pid = p.col_panes[f.col][k]; if (p.panes[pid]) |pp| if (pp != nw and !pp.collapsed) { pp.vweight = @floatFromInt(@max(1, p.rects[pid].h)); }; }; - if (!src.collapsed) src.vweight = @floatFromInt(BOX_H + keep); - nw.vweight = @floatFromInt(@max(1, src_h -| (BOX_H + keep))); + if (!src.collapsed) src.vweight = @floatFromInt(tag_h + keep); + nw.vweight = @floatFromInt(@max(1, src_h -| (tag_h + keep))); if (nw.file) |f| if (!panes.Output.fileTraits(f.output).doc) { // trimmed: every row ends in a newline, and the empty line after // the last one is not a result - const want: f32 = @floatFromInt(BOX_H + panes.File.lineCount(std.mem.trimEnd(u8, f.content, "\n"))); + const want: f32 = @floatFromInt(BOX_H + @max(min_body_rows, panes.File.lineCount(std.mem.trimEnd(u8, f.content, "\n")))); if (want < nw.vweight) { src.vweight += nw.vweight - want; nw.vweight = want; diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index aec3cab7..414a0b6e 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -713,8 +713,11 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { // closing it leaves the pane alone. .new => { const slot = p.freeSlot() orelse return failText(req.tag, E.NOSPC, std.fmt.bufPrint(&p.fs.ename, "pane/new: " ++ pardes.Messages.no_pane_slots, .{pardes.MAX_PANES}) catch "no space for a pane"); + p.fs.no_pane_slot = false; p.newScratchFor(p.active, .fs); const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE); + // No room in its column: it closes at the step's end. + if (p.unplaced.isSet(slot)) return failText(req.tag, E.NOSPC, p.fs.ename[0..p.fs.no_pane_slot_len]); return .{ .tag = req.tag, .handle = made.serial }; }, .screen => .{ .snapshot = null }, @@ -1427,6 +1430,8 @@ test "editor paths resolve to the same nodes the wire serves" { test "at the pane cap, pane/new, look and New each say so, and look reads back empty" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); + // Tall enough that the column holds every pane at its tag and two rows. + p.update(.{ .resize = .{ .cols = 80, .rows = pardes.MAX_PANES * 4 } }); const new = @intFromEnum(TopFile.new); while (p.freeSlot() != null) try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status); var want: [64]u8 = undefined; @@ -1496,3 +1501,24 @@ test "look and exec read back what their own open's write touched; a fresh open try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{said}), call(p, .{ .tag = 9, .op = .read, .node = exec, .handle = fresh, .size = 64 }).bytes); for ([_]u32{ mine, theirs, fresh }) |hh| _ = call(p, .{ .tag = 10, .op = .release, .node = exec, .handle = hh, .opened = true }); } + +test "no placement leaves a pane shorter than its tag and two rows; a full column refuses the next" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const new = @intFromEnum(TopFile.new); + var made: usize = 1; + const refused = while (made < pardes.MAX_PANES) : (made += 1) { + const a = call(p, .{ .tag = 1, .op = .open, .node = new }); + if (a.reply.status == .err) break a; + } else return error.NeverRefused; + try testing.expectEqual(E.NOSPC, refused.errno()); + try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "no space for a pane in that column") != null); + p.sync(); + var count: usize = 0; + for (p.panes) |slot| count += @intFromBool(slot != null); + try testing.expectEqual(made, count); // the refused one is gone + for (0..p.ncol) |c| for (p.col_panes[c][0..p.col_n[c]]) |k| { + if (p.col_n[c] > 1 and !p.panes[k].?.collapsed) + try testing.expect(p.rects[k].h >= p.panes[k].?.tag_rows + pardes.layout.min_body_rows); + }; +} diff --git a/src/pardes.zig b/src/pardes.zig index f2791e33..ac6d52cd 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4271,6 +4271,10 @@ pub const Pardes = struct { /// The column whose tag the running command came from, while it runs: /// the column acme's commands take from their Text's col. exec_column: ?usize = null, + /// Panes made this step that found no room in their column + /// (exec.placeNew): closed at the step's end, once whatever made them + /// is done with them. + unplaced: std.StaticBitSet(MAX_PANES) = .initEmpty(), /// acme's activecol (dat.c:37), by column serial, 0 for none: the /// column last typed in (acme.c:487), clicked in with the select button /// (acme.c:659), dropped into by a grip (acme.c:640), or given a new @@ -6388,7 +6392,26 @@ pub const Pardes = struct { /// Recompute geometry, push grid-size changes to each emulator + pty, fire /// deferred greetings. The mirror of the prototype's loop epilogue. + /// The rows pane `id`'s tag takes at its width, wrapped as `sync` wraps + /// it: what a placement must leave it above its body rows. + pub fn wantedTagRows(p: *Pardes, id: usize) u16 { + const pane = p.panes[id] orelse return BOX_H; + if (pane.collapsed or !pane.tag_expanded) return BOX_H; + if (p.rects[id].w == 0) return pane.tag_rows; // never laid out + var wrap_room: [32 * 1024]u8 = undefined; + var wrap_fba = std.heap.FixedBufferAllocator.init(&wrap_room); + const wa = wrap_fba.allocator(); + const prefix = tagline.prefixIn(wa, pane) catch return pane.tag_rows; + const whole = std.mem.concat(wa, u8, &.{ prefix, tagline.curTail(pane) }) catch return pane.tag_rows; + const shown = tagline.wrapTag(wa, whole, tagline.tagWidth(p, id)) catch return pane.tag_rows; + return @intCast(@max(BOX_H, @min(shown.rows(), MAX_TAG_ROWS))); + } + pub fn sync(p: *Pardes) void { + while (p.unplaced.findFirstSet()) |id| { + p.unplaced.unset(id); + if (p.panes[id] != null) p.removePane(id, null) catch {}; + } tagline.flattenHeaders(p); if (p.header_column != null and p.columnBarHeight() == 0) { tagline.exitHeader(p); @@ -7042,7 +7065,10 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expectEqual(low, p.active); // focus follows the rows // Esc takes the question back and the pane stays; so does any other key, - // and so does a click. + // and so does a click. (Top gets rows back first: split at its cursor, + // it kept its tag and two, too few to give a pane below its own.) + p.panes[top].?.vweight = p.panes[low].?.vweight; + p.sync(); p.newScratchBelow(top); const again = p.active; p.sync(); @@ -7096,17 +7122,6 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer -\n").reply.status); try std.testing.expect(p.panes[asked] != null and p.panes[asked].?.prompt == .none); try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "answer {d} -\n", .{serial}))); - // A pane closed while its question stands ends the question in the log. - p.newScratchBelow(top); - const closing = p.active; - p.sync(); - const closing_serial = p.panes[closing].?.serial; - p.update(.{ .key = .{ .cp = ' ' } }); - p.update(.{ .key = .{ .cp = 'd' } }); - try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[closing].?.prompt); - try p.removePane(closing, null); - try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "answer {d} -\ndel {d}", .{ closing_serial, closing_serial }))); - p.active = asked; p.update(.{ .key = .{ .cp = ' ' } }); p.update(.{ .key = .{ .cp = 'd' } }); try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer k\n").reply.status); @@ -7891,3 +7906,22 @@ test "core animation keeps wall time at 60, 120 and 144 Hz and across a stall" { p.advance(p.stepped_ns + frame); try std.testing.expectEqual(@as(?u64, null), p.nextWake()); } + +test "a pane closed while its question stands ends the question in the log" { + const th = @import("ninep/testing.zig"); + const p = try th.withFile(std.testing.allocator, "x\n"); + defer p.deinit(); + p.settings.placement = .pardes; + p.newScratchBelow(0); + const closing = p.active; + p.newScratchBelow(closing); + p.sync(); + p.active = closing; // a middle pane: Del from a key asks which side + const serial = p.panes[closing].?.serial; + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[closing].?.prompt); + try p.removePane(closing, null); + var want: [64]u8 = undefined; + try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "answer {d} -\ndel {d}", .{ serial, serial }))); +} @@ -121,6 +121,10 @@ def look(client, path, source=None): client.write(f'/pane/{serial}/look', f'{path}\n'.encode()) results = [int(row) for row in client.read('/look').split()] assert len(results) == 1 and results[0] != serial, (path, results) + # The scratch it looked from goes: a column holds only so many panes, + # each its tag and two rows. + if source is None: + client.remove(f'/pane/{serial}') return results[0] @@ -936,6 +940,7 @@ def test(binary, quic=False): assert path.read_bytes() == b'written through long filename\n' opened = look(client, '/n/os' + str(path)) assert client.read(f'/pane/{opened}/body') == path.read_bytes() + client.remove(f'/pane/{opened}') # a long name's tag takes rows opened = look(client, '/n/os' + str(directory)) listing = client.read(f'/pane/{opened}/body') for path in directory.iterdir(): diff --git a/test/snapshots/lookpanes.golden b/test/snapshots/lookpanes.golden index 3c407401..6c0ddb84 100644 --- a/test/snapshots/lookpanes.golden +++ b/test/snapshots/lookpanes.golden @@ -14,18 +14,18 @@ |6: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Colla |7: pse Del /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter |8: 1 ONLY-IN-ALPHA Collapse Del -== snap clicked-wins grid=130x51 cursor=7,11 -|9: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse ls -|10: Del amark.txt both.txt pick.txt twin.txt zqonly.txt -|11: 1 BOTH-FROM-BETA +== snap clicked-wins grid=130x51 cursor=7,12 +|10: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse amark.txt both.txt pick.txt twin.txt zqonly.txt +|11: Del +|12: 1 BOTH-FROM-BETA == snap mru-alpha grid=130x51 cursor=72,14 -|12: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps +|12: 1 BOTH-FROM-BETA /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps |13: e Del |14: 1 PICK-FROM-ALPHA -== snap mru-beta grid=130x51 cursor=7,14 -|12: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps -|13: Del e Del -|14: 1 TWIN-FROM-BETA 1 PICK-FROM-ALPHA +== snap mru-beta grid=130x51 cursor=7,16 +|14: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse 1 PICK-FROM-ALPHA +|15: Del +|16: 1 TWIN-FROM-BETA == style mru-beta grid=130x51 |0: 0-70 #b2c3b8,#1d2824, 71-129 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-64 d,#26362d, 65-66 #ffffff,#887455, 67-67 d,#1d2824, 68-99 #b2c3b8,#1d2824, 100-129 d,#1d2824, @@ -36,14 +36,14 @@ |6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-39 #b2c3b8,#1d2824, 40-49 #d3b0d9,#1d2824, 50-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-129 d,#0d1410, |7: 0-2 d,#1d2824, 3-9 #b2c3b8,#1d2824, 10-64 d,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-64 d,#0d1410, 65-67 d,#1d2824, 68-80 #b2c3b8,#1d2824, 81-129 d,#1d2824, -|9: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, -|10: 0-2 d,#1d2824, 3-6 #b2c3b8,#1d2824, 7-64 d,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, -|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-129 d,#0d1410, -|12: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-64 #becfc4,#26362d, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824, -|13: 0-2 d,#26362d, 3-6 #becfc4,#26362d, 7-64 d,#26362d, 65-67 d,#1d2824, 68-72 #b2c3b8,#1d2824, 73-129 d,#1d2824, -|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410, -|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, -|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, +|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, +|10: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, +|11: 0-2 d,#1d2824, 3-6 #b2c3b8,#1d2824, 7-64 d,#1d2824, 65-65 d,#608269, 66-129 d,#0d1410, +|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824, +|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-67 d,#1d2824, 68-72 #b2c3b8,#1d2824, 73-129 d,#1d2824, +|14: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-64 #becfc4,#26362d, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410, +|15: 0-2 d,#26362d, 3-6 #becfc4,#26362d, 7-64 d,#26362d, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, +|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, |17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden index e64df572..48dcf0c8 100644 --- a/test/snapshots/splitparent.golden +++ b/test/snapshots/splitparent.golden @@ -5,35 +5,25 @@ | /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del | Mode Filter Collapse Del ls | ls -== snap swap-parent grid=100x31 cursor=4,9 -|5: -|6: /tmp/pardes-snap/splitparent/cwd Tty+bash Save -|7: Mode Filter Collapse Del -|8: ls -== snap tallest grid=100x6 cursor=54,5 +== snap swap-parent grid=100x31 cursor=4,10 +|7: /tmp/pardes-snap/splitparent/cwd Tty+bash Save +|8: Mode Filter Collapse Del +|9: ls +== snap tallest grid=100x6 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -== snap regrown grid=100x31 cursor=54,20 +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls +| e in that column: each keeps its tag and 2 rows +== snap regrown grid=100x31 cursor=4,10 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save | /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del | Mode Filter Collapse Del ls +| ls | | /tmp/pardes-snap/splitparent/cwd Tty+bash Save | Mode Filter Collapse Del -| ls -| -| -| -| -| -| -| -| -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| Mode Filter Collapse Del -| ls +| e in that column: each keeps its tag and 2 rows diff --git a/test/snapshots/splitparent.snap b/test/snapshots/splitparent.snap index b9038af2..14391467 100644 --- a/test/snapshots/splitparent.snap +++ b/test/snapshots/splitparent.snap @@ -1,6 +1,7 @@ # a new pane never opens under a too-short parent: Alt-n from a tag-only pane # splits the tall sibling in its column instead; when NO pane could leave the -# newcomer 2 body rows, the tallest one anywhere is split +# newcomer 2 body rows, the tallest one anywhere is split, and when that one +# cannot keep its own tag and 2 rows either, the pane is refused # pins the pardes placement rule (Placement pardes), which it covers config Placement pardes start 31 100 -n 3 @@ -18,8 +19,10 @@ snap shrunk key a-n stable 700 15000 snap swap-parent -# squeeze the grid until no pane is 5 rows tall, then Alt-n again: the -# tallest pane (right column) is split even though the active one is left +# squeeze the grid until no pane can give a new one its tag and 2 rows, then +# Alt-n again: the new shell is refused, and the message row says why (before +# every pane had to keep only a tag and one row, the tallest pane anywhere was +# split here) resize 6 100 stable 700 15000 key a-n |
