diff options
| -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 |
