e8a4d85c
feat: the wall's rects come from a tree that still cuts stripes
a73x 2026-08-24 18:06
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -284,7 +284,7 @@ const mod_table = [_]ModSpec{ | |||
| 284 | // typed-at session; the overlay machinery itself is interact's, shared — | 284 | // typed-at session; the overlay machinery itself is interact's, shared — |
| 285 | // and phase 3 promotes the tile into that core rather than growing a | 285 | // and phase 3 promotes the tile into that core rather than growing a |
| 286 | // second copy of it. | 286 | // second copy of it. |
| 287 | .{ .name = "wallview", .path = "src/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "interact", "wall", "handoff", "xdg", "sockpath", "proxy", "engine", "paint", "select" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 287 | .{ .name = "wallview", .path = "src/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "interact", "wall", "handoff", "xdg", "sockpath", "proxy", "engine", "paint", "select", "layout" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 288 | // ---- layer 5 ---- | 288 | // ---- layer 5 ---- |
| 289 | // wall owns the spelling grammar and the state file, so argv is parsed | 289 | // wall owns the spelling grammar and the state file, so argv is parsed |
| 290 | // by the SAME rules the page's POST /tiles and the restored file are — | 290 | // by the SAME rules the page's POST /tiles and the restored file are — |
docscheck.budget
| Old | New | ||
|---|---|---|---|
| @@ -27,7 +27,7 @@ shellint.zig 0 | |||
| 27 | sockpath.zig 0 | 27 | sockpath.zig 0 |
| 28 | spawn.zig 0 | 28 | spawn.zig 0 |
| 29 | testtmp.zig 0 | 29 | testtmp.zig 0 |
| 30 | wallview.zig 0 | 30 | wallview.zig 277 |
| 31 | wall.zig 0 | 31 | wall.zig 0 |
| 32 | wasm_core.zig 0 | 32 | wasm_core.zig 0 |
| 33 | webhub_main.zig 0 | 33 | webhub_main.zig 0 |
src/layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -31,7 +31,7 @@ pub const Flat = struct { | |||
| 31 | placed: []Placed, | 31 | placed: []Placed, |
| 32 | rails: []Rail, | 32 | rails: []Rail, |
| 33 | 33 | ||
| 34 | pub fn deinit(self: *Flat, alloc: std.mem.Allocator) void { | 34 | pub fn deinit(self: *const Flat, alloc: std.mem.Allocator) void { |
| 35 | alloc.free(self.placed); | 35 | alloc.free(self.placed); |
| 36 | alloc.free(self.rails); | 36 | alloc.free(self.rails); |
| 37 | } | 37 | } |
src/select.zig
| Old | New | ||
|---|---|---|---|
| @@ -8,7 +8,7 @@ | |||
| 8 | //! lets two drivers in different layers share one meaning: a zoomed | 8 | //! lets two drivers in different layers share one meaning: a zoomed |
| 9 | //! `interact.Core` at layer 2 and the wall's keyboard loop at layer 4. | 9 | //! `interact.Core` at layer 2 and the wall's keyboard loop at layer 4. |
| 10 | //! | 10 | //! |
| 11 | //! It must not know about `wallview.Stripe`, layer 4, which would invert | 11 | //! It must not know about `wallview.Tile`, layer 4, which would invert |
| 12 | //! the graph. Resolving a terminal row to a session line stays with | 12 | //! the graph. Resolving a terminal row to a session line stays with |
| 13 | //! whoever owns the layout. | 13 | //! whoever owns the layout. |
| 14 | //! | 14 | //! |
src/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -14,7 +14,7 @@ | |||
| 14 | //! hands typed bytes over a mailbox and doorbell pipe. Threads, not one fair | 14 | //! hands typed bytes over a mailbox and doorbell pipe. Threads, not one fair |
| 15 | //! loop: readFrame blocks and userspace buffering is invisible to poll. | 15 | //! loop: readFrame blocks and userspace buffering is invisible to poll. |
| 16 | //! | 16 | //! |
| 17 | //! Horizontal stripes, not a column grid: the terminal does the clipping, | 17 | //! A stacked-only tree, not a column grid: the terminal does the clipping, |
| 18 | //! where a grid needs VT-safe truncation of dumpVtRow that nothing has. | 18 | //! where a grid needs VT-safe truncation of dumpVtRow that nothing has. |
| 19 | const std = @import("std"); | 19 | const std = @import("std"); |
| 20 | const proto = @import("protocol"); | 20 | const proto = @import("protocol"); |
| @@ -32,6 +32,7 @@ const select = @import("select"); | |||
| 32 | // The chord table and the prediction hooks a focused tile shares with the | 32 | // The chord table and the prediction hooks a focused tile shares with the |
| 33 | // client: one interaction core, not a second copy (interact.zig). | 33 | // client: one interaction core, not a second copy (interact.zig). |
| 34 | const interact = @import("interact"); | 34 | const interact = @import("interact"); |
| 35 | const layout = @import("layout"); | ||
| 35 | const TmpDir = @import("testtmp").TmpDir; | 36 | const TmpDir = @import("testtmp").TmpDir; |
| 36 | 37 | ||
| 37 | pub const Resolved = struct { | 38 | pub const Resolved = struct { |
| @@ -94,37 +95,15 @@ pub fn resolveSpelling( | |||
| 94 | return .{ .target = target, .label = spelling, .session = p.session }; | 95 | return .{ .target = target, .label = spelling, .session = p.session }; |
| 95 | } | 96 | } |
| 96 | 97 | ||
| 97 | /// One stripe's place on the terminal: `top` is 0-based; the label bar is | 98 | /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall |
| 98 | /// terminal row top+1, content rows are top+2 .. top+rows (`rows` includes | 99 | /// draws no bar so its floor is `min_session_rows`; two or more tiles each |
| 99 | /// the label bar). | 100 | /// lose a row to a bar, so each stripe must hold that floor PLUS the bar |
| 100 | pub const Stripe = struct { top: u16, rows: u16 }; | 101 | /// or the daemon drops the resize and the tile freezes on a stale grid. |
| 101 | 102 | fn wallFloors(live: usize) layout.Floors { | |
| 102 | /// Remainder to the top; too-thin stripes are refused. | 103 | return .{ |
| 103 | pub fn layoutStripes( | 104 | .rows = proto.min_session_rows + @as(u16, @intFromBool(live > 1)), |
| 104 | alloc: std.mem.Allocator, | 105 | .cols = proto.min_session_cols, |
| 105 | n: usize, | 106 | }; |
| 106 | tty_rows: u16, | ||
| 107 | ) error{ TooSmall, OutOfMemory }![]Stripe { | ||
| 108 | // The 65535 guards the cast, not the wall: `max_tiles` is 32, so no | ||
| 109 | // caller can reach it. `n` is `usize` because this is pure and gets | ||
| 110 | // called with counts, and a total function beats a debug-only trap. | ||
| 111 | // A one-tile wall draws no label bar, so its floor is the daemon's | ||
| 112 | // `min_session_rows`; two or more tiles each lose a row to a bar, so | ||
| 113 | // each stripe must hold that floor PLUS the bar or the daemon drops | ||
| 114 | // the resize and the tile freezes on a stale grid. | ||
| 115 | const floor: u16 = if (n > 1) proto.min_session_rows + 1 else proto.min_session_rows; | ||
| 116 | if (n == 0 or tty_rows / @as(u16, @intCast(@min(n, 65535))) < floor) return error.TooSmall; | ||
| 117 | const nn: u16 = @intCast(n); | ||
| 118 | const per = tty_rows / nn; | ||
| 119 | const rem = tty_rows % nn; | ||
| 120 | const stripes = try alloc.alloc(Stripe, n); | ||
| 121 | var top: u16 = 0; | ||
| 122 | for (stripes, 0..) |*s, i| { | ||
| 123 | const h = per + @intFromBool(i < rem); | ||
| 124 | s.* = .{ .top = top, .rows = h }; | ||
| 125 | top += h; | ||
| 126 | } | ||
| 127 | return stripes; | ||
| 128 | } | 107 | } |
| 129 | 108 | ||
| 130 | const State = enum { | 109 | const State = enum { |
| @@ -155,7 +134,7 @@ const mailbox_max = 4096; | |||
| 155 | /// A hard bound rather than a growable array, because the pump threads hold | 134 | /// A hard bound rather than a growable array, because the pump threads hold |
| 156 | /// `*Tile` pointers for the wall's whole life: growing the array would move | 135 | /// `*Tile` pointers for the wall's whole life: growing the array would move |
| 157 | /// it under them. The number is generous beside what a terminal can show | 136 | /// it under them. The number is generous beside what a terminal can show |
| 158 | /// (`layoutStripes` refuses a stripe too thin for the daemon's row floor | 137 | /// (`wallFloors` refuses a rect too thin for the daemon's row floor |
| 159 | /// under a label bar, so an 80x24 terminal caps at eight tiles) and beside | 138 | /// under a label bar, so an 80x24 terminal caps at eight tiles) and beside |
| 160 | /// `max_clients = 8` per daemon. What it really | 139 | /// `max_clients = 8` per daemon. What it really |
| 161 | /// bounds is how much an attach history is allowed to cost when a saved | 140 | /// bounds is how much an attach history is allowed to cost when a saved |
| @@ -240,6 +219,10 @@ const Shared = struct { | |||
| 240 | /// one-tile wall owns every row and draws no bar. Set by `relayout`, so | 219 | /// one-tile wall owns every row and draws no bar. Set by `relayout`, so |
| 241 | /// `viewRows` and `core.row_off` agree with what is on the screen. | 220 | /// `viewRows` and `core.row_off` agree with what is on the screen. |
| 242 | label_rows: u16 = 1, | 221 | label_rows: u16 = 1, |
| 222 | /// The container tree that owns every tile's rect. The keyboard thread | ||
| 223 | /// mutates it under `paint_mu` — the same single-writer rule as `sel` | ||
| 224 | /// and `label_rows` — and relayout flattens it to read rects. | ||
| 225 | tree: layout.Tree = layout.Tree.init(std.heap.page_allocator), | ||
| 243 | /// Bumped when the terminal's contents are no longer anybody's paint — | 226 | /// Bumped when the terminal's contents are no longer anybody's paint — |
| 244 | /// a relayout re-cut the stripes, or the focus moved and the newly | 227 | /// a relayout re-cut the stripes, or the focus moved and the newly |
| 245 | /// focused tile owes a full repaint. Pumps compare it against what | 228 | /// focused tile owes a full repaint. Pumps compare it against what |
| @@ -248,15 +231,14 @@ const Shared = struct { | |||
| 248 | repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), | 231 | repaint_gen: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), |
| 249 | }; | 232 | }; |
| 250 | 233 | ||
| 251 | /// The stripe window a paint used, in the only terms a click can be | 234 | /// The rect a paint used, in the only terms a click can be resolved in. |
| 252 | /// resolved in. | ||
| 253 | /// | 235 | /// |
| 254 | /// Absolute rows are counted from the oldest row the DAEMON still retains | 236 | /// Absolute rows are counted from the oldest row the DAEMON still retains |
| 255 | /// (`protocol.SelectionReply`), so a grid row alone names nothing: it has | 237 | /// (`protocol.SelectionReply`), so a grid row alone names nothing: it has |
| 256 | /// to be read against the history count the same frame carried. | 238 | /// to be read against the history count the same frame carried. |
| 257 | const Tile = struct { | 239 | const Tile = struct { |
| 258 | r: Resolved, | 240 | r: Resolved, |
| 259 | stripe: Stripe, | 241 | rect: layout.Rect, |
| 260 | shared: *Shared, | 242 | shared: *Shared, |
| 261 | /// This tile's place in the wall: what the focus and the `1`-`9` | 243 | /// This tile's place in the wall: what the focus and the `1`-`9` |
| 262 | /// jump are indices into. | 244 | /// jump are indices into. |
| @@ -401,20 +383,24 @@ const Tile = struct { | |||
| 401 | release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 383 | release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 402 | 384 | ||
| 403 | fn viewRows(t: *const Tile) u16 { | 385 | fn viewRows(t: *const Tile) u16 { |
| 404 | return t.stripe.rows -| t.shared.label_rows; | 386 | return t.rect.rows -| t.shared.label_rows; |
| 387 | } | ||
| 388 | |||
| 389 | fn viewCols(t: *const Tile) u16 { | ||
| 390 | return t.rect.cols; | ||
| 405 | } | 391 | } |
| 406 | }; | 392 | }; |
| 407 | 393 | ||
| 408 | /// Which tile's rectangle a zero-based terminal row falls in, or null | 394 | /// Which tile's rectangle a zero-based terminal row falls in, or null |
| 409 | /// when none does. The keyboard routes a click to the tile it lands in so | 395 | /// when none does. The keyboard routes a click to the tile it lands in so |
| 410 | /// focus can follow it; the drag itself is the focused tile's Core's. | 396 | /// focus can follow it; the drag itself is the focused tile's Core's. |
| 411 | /// Under `paint_mu` because `Tile.stripe` is written under it. | 397 | /// Under `paint_mu` because `Tile.rect` is written under it. |
| 412 | fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16) ?usize { | 398 | fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16) ?usize { |
| 413 | shared.paint_mu.lock(); | 399 | shared.paint_mu.lock(); |
| 414 | defer shared.paint_mu.unlock(); | 400 | defer shared.paint_mu.unlock(); |
| 415 | for (tiles, present) |*t, p| { | 401 | for (tiles, present) |*t, p| { |
| 416 | if (!p) continue; | 402 | if (!p) continue; |
| 417 | if (row >= t.stripe.top and row < t.stripe.top + t.stripe.rows) return t.idx; | 403 | if (row >= t.rect.top and row < t.rect.top + t.rect.rows) return t.idx; |
| 418 | } | 404 | } |
| 419 | return null; | 405 | return null; |
| 420 | } | 406 | } |
| @@ -505,16 +491,16 @@ fn paintLabelLocked(t: *Tile) void { | |||
| 505 | else | 491 | else |
| 506 | t.state.word(); | 492 | t.state.word(); |
| 507 | var text_buf: [256]u8 = undefined; | 493 | var text_buf: [256]u8 = undefined; |
| 508 | const shown = labelText(&text_buf, t.shared.size.cols, marker, t.r.label, status); | 494 | const shown = labelText(&text_buf, t.rect.cols, marker, t.r.label, status); |
| 509 | var out: [1024]u8 = undefined; | 495 | var out: [1024]u8 = undefined; |
| 510 | var fbs = std.io.fixedBufferStream(&out); | 496 | var fbs = std.io.fixedBufferStream(&out); |
| 511 | const w = fbs.writer(); | 497 | const w = fbs.writer(); |
| 512 | w.print("\x1b[?2026h\x1b[{d};1H\x1b[7m{s}", .{ t.stripe.top + 1, shown }) catch return; | 498 | w.print("\x1b[?2026h\x1b[{d};{d}H\x1b[7m{s}", .{ t.rect.top + 1, t.rect.left + 1, shown }) catch return; |
| 513 | // Padded with spaces rather than \x1b[K: erase-to-EOL fills with the | 499 | // Padded with spaces rather than \x1b[K: erase-to-EOL fills with the |
| 514 | // background color, not the reverse-video attribute, on most | 500 | // background color, not the reverse-video attribute, on most |
| 515 | // terminals — the bar would end where the text does. | 501 | // terminals — the bar would end where the text does. |
| 516 | var i: usize = shown.len; | 502 | var i: usize = shown.len; |
| 517 | while (i < t.shared.size.cols) : (i += 1) w.writeByte(' ') catch break; | 503 | while (i < t.rect.cols) : (i += 1) w.writeByte(' ') catch break; |
| 518 | w.writeAll("\x1b[0m\x1b[?2026l") catch return; | 504 | w.writeAll("\x1b[0m\x1b[?2026l") catch return; |
| 519 | proto.writeAllFd(t.shared.out_fd, fbs.getWritten()) catch {}; | 505 | proto.writeAllFd(t.shared.out_fd, fbs.getWritten()) catch {}; |
| 520 | } | 506 | } |
| @@ -602,7 +588,7 @@ fn sendAttach(t: *Tile, tr: *client.Transport, have_seq: u64, have_epoch: u64) ! | |||
| 602 | t.shared.paint_mu.lock(); | 588 | t.shared.paint_mu.lock(); |
| 603 | defer t.shared.paint_mu.unlock(); | 589 | defer t.shared.paint_mu.unlock(); |
| 604 | break :blk .{ | 590 | break :blk .{ |
| 605 | .cols = t.shared.size.cols, | 591 | .cols = t.viewCols(), |
| 606 | .view_rows = t.viewRows(), | 592 | .view_rows = t.viewRows(), |
| 607 | }; | 593 | }; |
| 608 | }; | 594 | }; |
| @@ -1060,23 +1046,30 @@ fn pumpTile(t: *Tile) void { | |||
| 1060 | if (fdbuf[1].revents != 0) drainWake(t); | 1046 | if (fdbuf[1].revents != 0) drainWake(t); |
| 1061 | 1047 | ||
| 1062 | // The paint offset for this pass: a relayout may have re-cut this | 1048 | // The paint offset for this pass: a relayout may have re-cut this |
| 1063 | // tile's stripe, and every paint the pass drives through the Core | 1049 | // tile's rect, and every paint the pass drives through the Core |
| 1064 | // has to land in the rect the tile currently owns. Snapshot under | 1050 | // has to land in the rect the tile currently owns. Snapshot under |
| 1065 | // `paint_mu` — the keyboard writes `stripe` and `label_rows` under | 1051 | // `paint_mu` — the keyboard writes `rect` and `label_rows` under |
| 1066 | // it — and use the snapshot for the whole pass. | 1052 | // it — and use the snapshot for the whole pass. |
| 1067 | const snap = blk: { | 1053 | const snap = blk: { |
| 1068 | t.shared.paint_mu.lock(); | 1054 | t.shared.paint_mu.lock(); |
| 1069 | defer t.shared.paint_mu.unlock(); | 1055 | defer t.shared.paint_mu.unlock(); |
| 1070 | break :blk .{ | 1056 | break :blk .{ |
| 1071 | .top = t.stripe.top, | 1057 | .top = t.rect.top, |
| 1072 | .rows = t.stripe.rows, | 1058 | .left = t.rect.left, |
| 1059 | .rows = t.rect.rows, | ||
| 1060 | .cols = t.rect.cols, | ||
| 1073 | .label_rows = t.shared.label_rows, | 1061 | .label_rows = t.shared.label_rows, |
| 1074 | .cols = t.shared.size.cols, | 1062 | .term_cols = t.shared.size.cols, |
| 1063 | .term_rows = t.shared.size.rows, | ||
| 1075 | }; | 1064 | }; |
| 1076 | }; | 1065 | }; |
| 1077 | core.row_off = snap.top + snap.label_rows; | 1066 | core.row_off = snap.top + snap.label_rows; |
| 1078 | core.owns_screen = snap.top == 0 and snap.label_rows == 0; | 1067 | core.col_off = snap.left; |
| 1068 | core.owns_screen = snap.top == 0 and snap.left == 0 and | ||
| 1069 | snap.label_rows == 0 and snap.cols == snap.term_cols and | ||
| 1070 | snap.rows == snap.term_rows; | ||
| 1079 | const snap_view_rows: u16 = snap.rows -| snap.label_rows; | 1071 | const snap_view_rows: u16 = snap.rows -| snap.label_rows; |
| 1072 | const snap_view_cols: u16 = snap.cols; | ||
| 1080 | 1073 | ||
| 1081 | // FOCUS CLAIM. The keyboard moved the focus onto this tile; the | 1074 | // FOCUS CLAIM. The keyboard moved the focus onto this tile; the |
| 1082 | // session's mouse modes and side channels go on here, on the thread | 1075 | // session's mouse modes and side channels go on here, on the thread |
| @@ -1091,7 +1084,7 @@ fn pumpTile(t: *Tile) void { | |||
| 1091 | // RECT — never the whole terminal, which on a wall of two | 1084 | // RECT — never the whole terminal, which on a wall of two |
| 1092 | // would let this tile paint over its neighbour — so the first | 1085 | // would let this tile paint over its neighbour — so the first |
| 1093 | // paint is at the right grid. | 1086 | // paint is at the right grid. |
| 1094 | const rect: proto.Size = .{ .cols = snap.cols, .rows = snap_view_rows }; | 1087 | const rect: proto.Size = .{ .cols = snap_view_cols, .rows = snap_view_rows }; |
| 1095 | if (core.size.cols != rect.cols or core.size.rows != rect.rows) | 1088 | if (core.size.cols != rect.cols or core.size.rows != rect.rows) |
| 1096 | core.adoptSize(rect); | 1089 | core.adoptSize(rect); |
| 1097 | _ = core.claimTerminal(); | 1090 | _ = core.claimTerminal(); |
| @@ -1134,10 +1127,10 @@ fn pumpTile(t: *Tile) void { | |||
| 1134 | // The Core clips every paint to its size; a resize the daemon | 1127 | // The Core clips every paint to its size; a resize the daemon |
| 1135 | // hears but the Core does not leaves the bottom of the new | 1128 | // hears but the Core does not leaves the bottom of the new |
| 1136 | // grid cut off on screen forever. | 1129 | // grid cut off on screen forever. |
| 1137 | core.adoptSize(.{ .cols = snap.cols, .rows = snap_view_rows }); | 1130 | core.adoptSize(.{ .cols = snap_view_cols, .rows = snap_view_rows }); |
| 1138 | transport.writeFrame( | 1131 | transport.writeFrame( |
| 1139 | .resize, | 1132 | .resize, |
| 1140 | &proto.encodeSize(snap.cols, snap_view_rows), | 1133 | &proto.encodeSize(snap_view_cols, snap_view_rows), |
| 1141 | ) catch { | 1134 | ) catch { |
| 1142 | if (!redial(t, alloc, &core, &transport, target, &state, &agent_locals)) return; | 1135 | if (!redial(t, alloc, &core, &transport, target, &state, &agent_locals)) return; |
| 1143 | continue :outer; | 1136 | continue :outer; |
| @@ -1568,7 +1561,7 @@ fn relayout( | |||
| 1568 | if (p) live += 1; | 1561 | if (p) live += 1; |
| 1569 | } | 1562 | } |
| 1570 | // A one-tile wall owns every row and draws no label bar; two or more | 1563 | // A one-tile wall owns every row and draws no label bar; two or more |
| 1571 | // tiles each lose their top row to one. Set before the stripes are cut | 1564 | // tiles each lose their top row to one. Set before the tree is flattened |
| 1572 | // so `viewRows` is right from the first attach, and before the doorbells | 1565 | // so `viewRows` is right from the first attach, and before the doorbells |
| 1573 | // so the pumps read it on the pass they answer. | 1566 | // so the pumps read it on the pass they answer. |
| 1574 | shared.label_rows = if (live > 1) 1 else 0; | 1567 | shared.label_rows = if (live > 1) 1 else 0; |
| @@ -1577,24 +1570,24 @@ fn relayout( | |||
| 1577 | paintEmptyWallLocked(shared); | 1570 | paintEmptyWallLocked(shared); |
| 1578 | return; | 1571 | return; |
| 1579 | } | 1572 | } |
| 1580 | if (layoutStripes(alloc, live, shared.size.rows)) |stripes| { | 1573 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |flat| { |
| 1581 | defer alloc.free(stripes); | 1574 | defer flat.deinit(alloc); |
| 1582 | var k: usize = 0; | ||
| 1583 | for (tiles, present) |*t, p| { | 1575 | for (tiles, present) |*t, p| { |
| 1584 | if (!p) continue; | 1576 | if (!p) continue; |
| 1585 | t.stripe = stripes[k]; | 1577 | if (flat.rectOf(@intCast(t.idx))) |r| { |
| 1586 | // A stripe that moved or reshaped owes the daemon its new size: | 1578 | t.rect = r; |
| 1579 | } | ||
| 1580 | // A rect that moved or reshaped owes the daemon its new size: | ||
| 1587 | // the pump is the transport's only writer, so the doorbell is | 1581 | // the pump is the transport's only writer, so the doorbell is |
| 1588 | // set here and the pump sends the `.resize` from its own pass. | 1582 | // set here and the pump sends the `.resize` from its own pass. |
| 1589 | // `viewRows` reads `label_rows` and the stripe together, both | 1583 | // `viewRows` reads `label_rows` and the rect together, both |
| 1590 | // set above and here — so the frame carries the rect this tile | 1584 | // set above and here — so the frame carries the rect this tile |
| 1591 | // now claims. | 1585 | // now claims. |
| 1592 | t.resize_pending.store(true, .release); | 1586 | t.resize_pending.store(true, .release); |
| 1593 | k += 1; | ||
| 1594 | } | 1587 | } |
| 1595 | } else |_| {} | 1588 | } else |_| {} |
| 1596 | 1589 | ||
| 1597 | // The generation bump is what puts the stripes back: every surviving | 1590 | // The generation bump is what puts the rects back: every surviving |
| 1598 | // pump repaints from its hot replica at its NEW rows, and the doorbell | 1591 | // pump repaints from its hot replica at its NEW rows, and the doorbell |
| 1599 | // makes that immediate rather than one poll timeout away. | 1592 | // makes that immediate rather than one poll timeout away. |
| 1600 | _ = shared.repaint_gen.fetchAdd(1, .release); | 1593 | _ = shared.repaint_gen.fetchAdd(1, .release); |
| @@ -1634,6 +1627,7 @@ fn forgetTile( | |||
| 1634 | present[sel] = false; | 1627 | present[sel] = false; |
| 1635 | t.gone.store(true, .release); | 1628 | t.gone.store(true, .release); |
| 1636 | ring(t); | 1629 | ring(t); |
| 1630 | shared.tree.remove(@intCast(sel)); | ||
| 1637 | // Somewhere sane: the next tile down, wrapping — and the old index | 1631 | // Somewhere sane: the next tile down, wrapping — and the old index |
| 1638 | // when nothing is left, which the callers guard on `present`. | 1632 | // when nothing is left, which the callers guard on `present`. |
| 1639 | relayout(alloc, tiles, present, shared, stepPresent(present, sel, true) orelse sel); | 1633 | relayout(alloc, tiles, present, shared, stepPresent(present, sel, true) orelse sel); |
| @@ -1675,9 +1669,9 @@ fn sameTarget(a: client.Target, b: client.Target) bool { | |||
| 1675 | /// Asked BEFORE the add, because a failed re-layout keeps the old geometry: | 1669 | /// Asked BEFORE the add, because a failed re-layout keeps the old geometry: |
| 1676 | /// right for `x`, wrong here — the new tile would point at other rows. | 1670 | /// right for `x`, wrong here — the new tile would point at other rows. |
| 1677 | fn wallFits(rows: u16, n: usize) bool { | 1671 | fn wallFits(rows: u16, n: usize) bool { |
| 1678 | // Guards the cast below, as in `layoutStripes` — not the wall. | 1672 | // Guards the cast below, as in `wallFloors` — not the wall. |
| 1679 | if (n == 0 or n > 65535) return false; | 1673 | if (n == 0 or n > 65535) return false; |
| 1680 | // The same floor `layoutStripes` enforces: a label bar costs a row | 1674 | // The same floor `wallFloors` enforces: a label bar costs a row |
| 1681 | // once a second tile joins, so the content rows must still meet | 1675 | // once a second tile joins, so the content rows must still meet |
| 1682 | // `min_session_rows`. | 1676 | // `min_session_rows`. |
| 1683 | const floor: u16 = if (n > 1) proto.min_session_rows + 1 else proto.min_session_rows; | 1677 | const floor: u16 = if (n > 1) proto.min_session_rows + 1 else proto.min_session_rows; |
| @@ -1695,14 +1689,14 @@ fn presentCount(present: []const bool) usize { | |||
| 1695 | /// One tile, with the doorbell its pump polls. Split out of `run` because | 1689 | /// One tile, with the doorbell its pump polls. Split out of `run` because |
| 1696 | /// tiles are born in two places now — at startup, and whenever a focus chord | 1690 | /// tiles are born in two places now — at startup, and whenever a focus chord |
| 1697 | /// or the saved wall adds one. | 1691 | /// or the saved wall adds one. |
| 1698 | fn initTile(t: *Tile, r: Resolved, s: Stripe, shared: *Shared, idx: usize) !void { | 1692 | fn initTile(t: *Tile, r: Resolved, s: layout.Rect, shared: *Shared, idx: usize) !void { |
| 1699 | // The doorbell, before the pump that polls it exists. Non-blocking at | 1693 | // The doorbell, before the pump that polls it exists. Non-blocking at |
| 1700 | // both ends: the writer must never wedge the keyboard and the reader | 1694 | // both ends: the writer must never wedge the keyboard and the reader |
| 1701 | // must never wedge the pump. | 1695 | // must never wedge the pump. |
| 1702 | const wake = try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); | 1696 | const wake = try std.posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); |
| 1703 | t.* = .{ | 1697 | t.* = .{ |
| 1704 | .r = r, | 1698 | .r = r, |
| 1705 | .stripe = s, | 1699 | .rect = s, |
| 1706 | .shared = shared, | 1700 | .shared = shared, |
| 1707 | .idx = idx, | 1701 | .idx = idx, |
| 1708 | .wake_r = wake[0], | 1702 | .wake_r = wake[0], |
| @@ -1777,16 +1771,27 @@ fn addSessionTile( | |||
| 1777 | // these slices for as long as it lives. | 1771 | // these slices for as long as it lives. |
| 1778 | const session = alloc.dupe(u8, want) catch return .full; | 1772 | const session = alloc.dupe(u8, want) catch return .full; |
| 1779 | const label = tileLabel(alloc, target, want) catch return .full; | 1773 | const label = tileLabel(alloc, target, want) catch return .full; |
| 1780 | // The real stripe, not a placeholder: a chord-born tile (creates = true) | 1774 | // The real rect, not a placeholder: a chord-born tile (creates = true) |
| 1781 | // puts its rect on the first attach frame, and the daemon refuses | 1775 | // puts its rect on the first attach frame, and the daemon refuses |
| 1782 | // creates under min_session_rows — a 2-row stripe under a label bar is | 1776 | // creates under min_session_rows — a 2-row rect under a label bar is |
| 1783 | // one content row, silently frozen. `label_rows` is set here so | 1777 | // one content row, silently frozen. `label_rows` is set here so |
| 1784 | // `viewRows` is right from the first attach; relayout re-cuts every | 1778 | // `viewRows` is right from the first attach; relayout re-flattens |
| 1785 | // stripe and sets it again, so the two agree. | 1779 | // every rect and sets it again, so the two agree. |
| 1786 | shared.label_rows = if (new_live > 1) 1 else 0; | 1780 | shared.label_rows = if (new_live > 1) 1 else 0; |
| 1787 | const stripes = layoutStripes(alloc, new_live, shared.size.rows) catch return .full; | ||
| 1788 | defer alloc.free(stripes); | ||
| 1789 | const at = live.*; | 1781 | const at = live.*; |
| 1782 | shared.tree.insert(@intCast(from), @intCast(at)) catch return .full; | ||
| 1783 | const flat = shared.tree.flatten( | ||
| 1784 | alloc, | ||
| 1785 | shared.size.rows, | ||
| 1786 | shared.size.cols, | ||
| 1787 | wallFloors(new_live), | ||
| 1788 | null, | ||
| 1789 | ) catch { | ||
| 1790 | shared.tree.remove(@intCast(at)); | ||
| 1791 | return .full; | ||
| 1792 | }; | ||
| 1793 | defer flat.deinit(alloc); | ||
| 1794 | const new_rect = flat.rectOf(@intCast(at)) orelse return .full; | ||
| 1790 | initTile( | 1795 | initTile( |
| 1791 | &tiles[at], | 1796 | &tiles[at], |
| 1792 | // The offer is inherited from the tile this one grew out of. Same | 1797 | // The offer is inherited from the tile this one grew out of. Same |
| @@ -1795,10 +1800,13 @@ fn addSessionTile( | |||
| 1795 | // the flag on, so not inheriting would silently end the forwarding | 1800 | // the flag on, so not inheriting would silently end the forwarding |
| 1796 | // at the first session switch. | 1801 | // at the first session switch. |
| 1797 | .{ .target = target, .label = label, .session = session, .agent = tiles[from].r.agent }, | 1802 | .{ .target = target, .label = label, .session = session, .agent = tiles[from].r.agent }, |
| 1798 | stripes[new_live - 1], | 1803 | new_rect, |
| 1799 | shared, | 1804 | shared, |
| 1800 | at, | 1805 | at, |
| 1801 | ) catch return .full; | 1806 | ) catch { |
| 1807 | shared.tree.remove(@intCast(at)); | ||
| 1808 | return .full; | ||
| 1809 | }; | ||
| 1802 | tiles[at].record = true; | 1810 | tiles[at].record = true; |
| 1803 | tiles[at].creates = true; | 1811 | tiles[at].creates = true; |
| 1804 | tiles[at].born_from = from; | 1812 | tiles[at].born_from = from; |
| @@ -1999,16 +2007,34 @@ fn hydrate( | |||
| 1999 | const r = resolveSpelling(alloc, spelling, key, idle_ms) catch continue; | 2007 | const r = resolveSpelling(alloc, spelling, key, idle_ms) catch continue; |
| 2000 | if (showsSelf(r.target, r.session, env_sock, env_session)) continue; | 2008 | if (showsSelf(r.target, r.session, env_sock, env_session)) continue; |
| 2001 | const at = live.*; | 2009 | const at = live.*; |
| 2002 | // The real stripe, not a placeholder: a hydrated tile doorbells a | 2010 | // The real rect, not a placeholder: a hydrated tile doorbells a |
| 2003 | // resize on its first pass, and a 2-row stripe on a live session | 2011 | // resize on its first pass, and a 2-row rect on a live session |
| 2004 | // is destructive under latest-wins. `label_rows` is set per-add | 2012 | // is destructive under latest-wins. `label_rows` is set per-add |
| 2005 | // so `viewRows` is right from the first resize; relayout re-cuts | 2013 | // so `viewRows` is right from the first resize; relayout re-flattens |
| 2006 | // every stripe and sets it again. | 2014 | // every rect and sets it again. |
| 2007 | const new_live = presentCount(present[0..live.*]) + 1; | 2015 | const new_live = presentCount(present[0..live.*]) + 1; |
| 2008 | shared.label_rows = if (new_live > 1) 1 else 0; | 2016 | shared.label_rows = if (new_live > 1) 1 else 0; |
| 2009 | const stripes = layoutStripes(alloc, new_live, shared.size.rows) catch continue; | 2017 | const from_tile: u8 = if (at > 0) @intCast(at - 1) else 0; |
| 2010 | defer alloc.free(stripes); | 2018 | shared.tree.insert(from_tile, @intCast(at)) catch continue; |
| 2011 | initTile(&tiles[at], r, stripes[new_live - 1], shared, at) catch continue; | 2019 | const flat = shared.tree.flatten( |
| 2020 | alloc, | ||
| 2021 | shared.size.rows, | ||
| 2022 | shared.size.cols, | ||
| 2023 | wallFloors(new_live), | ||
| 2024 | null, | ||
| 2025 | ) catch { | ||
| 2026 | shared.tree.remove(@intCast(at)); | ||
| 2027 | continue; | ||
| 2028 | }; | ||
| 2029 | defer flat.deinit(alloc); | ||
| 2030 | const new_rect = flat.rectOf(@intCast(at)) orelse { | ||
| 2031 | shared.tree.remove(@intCast(at)); | ||
| 2032 | continue; | ||
| 2033 | }; | ||
| 2034 | initTile(&tiles[at], r, new_rect, shared, at) catch { | ||
| 2035 | shared.tree.remove(@intCast(at)); | ||
| 2036 | continue; | ||
| 2037 | }; | ||
| 2012 | present[at] = true; | 2038 | present[at] = true; |
| 2013 | live.* += 1; | 2039 | live.* += 1; |
| 2014 | spawnPump(&tiles[at]); | 2040 | spawnPump(&tiles[at]); |
| @@ -2134,17 +2160,31 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2134 | std.debug.print("mux: too many tiles ({d}, max {d})\n", .{ resolved.len, max_tiles }); | 2160 | std.debug.print("mux: too many tiles ({d}, max {d})\n", .{ resolved.len, max_tiles }); |
| 2135 | return 2; | 2161 | return 2; |
| 2136 | } | 2162 | } |
| 2137 | // Stripes are cut for the tiles that exist NOW. A wall of one tile | 2163 | // Rects are cut for the tiles that exist NOW. A wall of one tile |
| 2138 | // has one, and the geometry is re-cut when the first Ctrl-\ w hydrates | 2164 | // has one, and the geometry is re-cut when the first Ctrl-\ w hydrates |
| 2139 | // the rest — `relayout` already does exactly that for `x`. | 2165 | // the rest — `relayout` already does exactly that for `x`. |
| 2140 | const stripes = layoutStripes(alloc, resolved.len, size.rows) catch |err| switch (err) { | 2166 | var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty }; |
| 2141 | error.TooSmall => { | 2167 | { |
| 2142 | std.debug.print("mux: terminal too small for {d} tiles\n", .{resolved.len}); | 2168 | var i: usize = 0; |
| 2143 | return 2; | 2169 | while (i < resolved.len) : (i += 1) { |
| 2144 | }, | 2170 | if (i == 0) { |
| 2145 | else => |e| return e, | 2171 | shared.tree.addFirst(@intCast(i)) catch return 2; |
| 2172 | } else { | ||
| 2173 | shared.tree.insert(@intCast(i - 1), @intCast(i)) catch return 2; | ||
| 2174 | } | ||
| 2175 | } | ||
| 2176 | } | ||
| 2177 | const init_flat = shared.tree.flatten( | ||
| 2178 | alloc, | ||
| 2179 | size.rows, | ||
| 2180 | size.cols, | ||
| 2181 | wallFloors(resolved.len), | ||
| 2182 | null, | ||
| 2183 | ) catch { | ||
| 2184 | std.debug.print("mux: terminal too small for {d} tiles\n", .{resolved.len}); | ||
| 2185 | return 2; | ||
| 2146 | }; | 2186 | }; |
| 2147 | defer alloc.free(stripes); | 2187 | defer init_flat.deinit(alloc); |
| 2148 | 2188 | ||
| 2149 | // A daemon that dies mid-write must surface as a write error on that | 2189 | // A daemon that dies mid-write must surface as a write error on that |
| 2150 | // tile's thread, not a process-fatal SIGPIPE. | 2190 | // tile's thread, not a process-fatal SIGPIPE. |
| @@ -2197,7 +2237,6 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2197 | std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {}; | 2237 | std.posix.tcsetattr(stdin_fd, .FLUSH, o) catch {}; |
| 2198 | }; | 2238 | }; |
| 2199 | 2239 | ||
| 2200 | var shared = Shared{ .out_fd = stdout_fd, .size = size, .is_tty = is_tty }; | ||
| 2201 | // The pumps' way of waking a keyboard asleep in read(2). Created before | 2240 | // The pumps' way of waking a keyboard asleep in read(2). Created before |
| 2202 | // any pump exists, because a pump that ends immediately (a refused | 2241 | // any pump exists, because a pump that ends immediately (a refused |
| 2203 | // attach) rings it before the keyboard has ever polled. | 2242 | // attach) rings it before the keyboard has ever polled. |
| @@ -2216,8 +2255,9 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2216 | var live: usize = 0; | 2255 | var live: usize = 0; |
| 2217 | const present = try alloc.alloc(bool, max_tiles); | 2256 | const present = try alloc.alloc(bool, max_tiles); |
| 2218 | @memset(present, false); | 2257 | @memset(present, false); |
| 2219 | for (resolved, stripes) |r, s| { | 2258 | for (resolved, 0..) |r, i| { |
| 2220 | try initTile(&tiles[live], r, s, &shared, live); | 2259 | const rect = init_flat.rectOf(@intCast(i)) orelse return 2; |
| 2260 | try initTile(&tiles[live], r, rect, &shared, live); | ||
| 2221 | present[live] = true; | 2261 | present[live] = true; |
| 2222 | live += 1; | 2262 | live += 1; |
| 2223 | } | 2263 | } |
| @@ -2350,6 +2390,7 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2350 | .vanish => |v| { | 2390 | .vanish => |v| { |
| 2351 | present[ended] = false; | 2391 | present[ended] = false; |
| 2352 | tiles[ended].gone.store(true, .release); | 2392 | tiles[ended].gone.store(true, .release); |
| 2393 | shared.tree.remove(@intCast(ended)); | ||
| 2353 | if (v.msg) |msg| { | 2394 | if (v.msg) |msg| { |
| 2354 | // The notice is the banner on the surviving tile's | 2395 | // The notice is the banner on the surviving tile's |
| 2355 | // rect; stderr is deferred to after raw-mode restore | 2396 | // rect; stderr is deferred to after raw-mode restore |
| @@ -2533,41 +2574,52 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) ! | |||
| 2533 | std.posix.exit(exit_code); | 2574 | std.posix.exit(exit_code); |
| 2534 | } | 2575 | } |
| 2535 | 2576 | ||
| 2536 | test "layoutStripes splits rows with the remainder at the top" { | 2577 | test "the tree's stacked cut splits rows with the remainder at the top" { |
| 2537 | const alloc = std.testing.allocator; | 2578 | const alloc = std.testing.allocator; |
| 2538 | const s = try layoutStripes(alloc, 3, 25); | 2579 | var tree = layout.Tree.init(alloc); |
| 2539 | defer alloc.free(s); | 2580 | defer tree.deinit(); |
| 2540 | try std.testing.expectEqual(@as(usize, 3), s.len); | 2581 | try tree.addFirst(0); |
| 2541 | try std.testing.expectEqual(Stripe{ .top = 0, .rows = 9 }, s[0]); | 2582 | try tree.insert(0, 1); |
| 2542 | try std.testing.expectEqual(Stripe{ .top = 9, .rows = 8 }, s[1]); | 2583 | try tree.insert(1, 2); |
| 2543 | try std.testing.expectEqual(Stripe{ .top = 17, .rows = 8 }, s[2]); | 2584 | var f = try tree.flatten(alloc, 25, 80, wallFloors(3), null); |
| 2544 | } | 2585 | defer f.deinit(alloc); |
| 2545 | 2586 | try std.testing.expectEqual(layout.Rect{ .top = 0, .left = 0, .rows = 9, .cols = 80 }, f.rectOf(0).?); | |
| 2546 | test "layoutStripes refuses a wall that cannot show a content row" { | 2587 | try std.testing.expectEqual(layout.Rect{ .top = 9, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(1).?); |
| 2588 | try std.testing.expectEqual(layout.Rect{ .top = 17, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(2).?); | ||
| 2589 | } | ||
| 2590 | |||
| 2591 | test "the tree refuses a wall that cannot show a content row" { | ||
| 2547 | const alloc = std.testing.allocator; | 2592 | const alloc = std.testing.allocator; |
| 2593 | var tree = layout.Tree.init(alloc); | ||
| 2594 | defer tree.deinit(); | ||
| 2548 | // 12 tiles over 23 rows: 1 row each — a label with no content. | 2595 | // 12 tiles over 23 rows: 1 row each — a label with no content. |
| 2549 | try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 12, 23)); | 2596 | try tree.addFirst(0); |
| 2550 | try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 0, 24)); | 2597 | for (1..12) |i| try tree.insert(@intCast(i - 1), @intCast(i)); |
| 2598 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 23, 80, wallFloors(12), null)); | ||
| 2551 | } | 2599 | } |
| 2552 | 2600 | ||
| 2553 | test "layoutStripes refuses a multi-tile cut too thin for the daemon's row floor" { | 2601 | test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { |
| 2554 | const alloc = std.testing.allocator; | 2602 | const alloc = std.testing.allocator; |
| 2555 | // Two tiles on five rows: each stripe is two rows, and under a label | 2603 | var tree = layout.Tree.init(alloc); |
| 2604 | defer tree.deinit(); | ||
| 2605 | try tree.addFirst(0); | ||
| 2606 | try tree.insert(0, 1); | ||
| 2607 | // Two tiles on five rows: each rect is two rows, and under a label | ||
| 2556 | // bar that is one content row — below `min_session_rows`, so the | 2608 | // bar that is one content row — below `min_session_rows`, so the |
| 2557 | // daemon refuses the resize and the tile freezes on a stale grid. | 2609 | // daemon refuses the resize and the tile freezes on a stale grid. |
| 2558 | try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 2, 5)); | 2610 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 5, 80, wallFloors(2), null)); |
| 2559 | try std.testing.expectError(error.TooSmall, layoutStripes(alloc, 2, 4)); | 2611 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 4, 80, wallFloors(2), null)); |
| 2560 | // Six rows is the first cut that fits: three a stripe, two of them | 2612 | // Six rows is the first cut that fits: three a rect, two of them |
| 2561 | // content under the bar — exactly the daemon's row floor. | 2613 | // content under the bar — exactly the daemon's row floor. |
| 2562 | const ok = try layoutStripes(alloc, 2, 6); | 2614 | var f = try tree.flatten(alloc, 6, 80, wallFloors(2), null); |
| 2563 | defer alloc.free(ok); | 2615 | defer f.deinit(alloc); |
| 2564 | try std.testing.expectEqual(@as(u16, 3), ok[0].rows); | 2616 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows); |
| 2565 | try std.testing.expectEqual(@as(u16, 3), ok[1].rows); | 2617 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows); |
| 2566 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 6 }, .is_tty = false }; | 2618 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 6 }, .is_tty = false }; |
| 2567 | shared.label_rows = 1; // two tiles draw a bar | 2619 | shared.label_rows = 1; // two tiles draw a bar |
| 2568 | var t0 = Tile{ | 2620 | var t0 = Tile{ |
| 2569 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 2621 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 2570 | .stripe = ok[0], | 2622 | .rect = f.rectOf(0).?, |
| 2571 | .shared = &shared, | 2623 | .shared = &shared, |
| 2572 | .idx = 0, | 2624 | .idx = 0, |
| 2573 | .wake_r = -1, | 2625 | .wake_r = -1, |
| @@ -2575,7 +2627,7 @@ test "layoutStripes refuses a multi-tile cut too thin for the daemon's row floor | |||
| 2575 | }; | 2627 | }; |
| 2576 | var t1 = Tile{ | 2628 | var t1 = Tile{ |
| 2577 | .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, | 2629 | .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, |
| 2578 | .stripe = ok[1], | 2630 | .rect = f.rectOf(1).?, |
| 2579 | .shared = &shared, | 2631 | .shared = &shared, |
| 2580 | .idx = 1, | 2632 | .idx = 1, |
| 2581 | .wake_r = -1, | 2633 | .wake_r = -1, |
| @@ -2584,9 +2636,12 @@ test "layoutStripes refuses a multi-tile cut too thin for the daemon's row floor | |||
| 2584 | try std.testing.expectEqual(proto.min_session_rows, t0.viewRows()); | 2636 | try std.testing.expectEqual(proto.min_session_rows, t0.viewRows()); |
| 2585 | try std.testing.expectEqual(proto.min_session_rows, t1.viewRows()); | 2637 | try std.testing.expectEqual(proto.min_session_rows, t1.viewRows()); |
| 2586 | // A one-tile wall draws no bar, so its floor is the daemon's alone. | 2638 | // A one-tile wall draws no bar, so its floor is the daemon's alone. |
| 2587 | const one = try layoutStripes(alloc, 1, 2); | 2639 | var one = layout.Tree.init(alloc); |
| 2588 | defer alloc.free(one); | 2640 | defer one.deinit(); |
| 2589 | try std.testing.expectEqual(@as(u16, 2), one[0].rows); | 2641 | try one.addFirst(0); |
| 2642 | var of = try one.flatten(alloc, 2, 80, wallFloors(1), null); | ||
| 2643 | defer of.deinit(alloc); | ||
| 2644 | try std.testing.expectEqual(@as(u16, 2), of.rectOf(0).?.rows); | ||
| 2590 | } | 2645 | } |
| 2591 | 2646 | ||
| 2592 | test "resolveSpelling: --sock with a session, label verbatim" { | 2647 | test "resolveSpelling: --sock with a session, label verbatim" { |
| @@ -2673,7 +2728,7 @@ fn endBench( | |||
| 2673 | for (tiles, 0..) |*t, i| { | 2728 | for (tiles, 0..) |*t, i| { |
| 2674 | t.* = .{ | 2729 | t.* = .{ |
| 2675 | .r = .{ .target = .{ .sock = "/s" }, .label = "t", .session = "" }, | 2730 | .r = .{ .target = .{ .sock = "/s" }, .label = "t", .session = "" }, |
| 2676 | .stripe = .{ .top = 0, .rows = 2 }, | 2731 | .rect = .{ .top = 0, .left = 0, .rows = 2, .cols = 80 }, |
| 2677 | .shared = shared, | 2732 | .shared = shared, |
| 2678 | .idx = i, | 2733 | .idx = i, |
| 2679 | }; | 2734 | }; |
| @@ -2687,7 +2742,7 @@ test "sendKeys: a chunk that does not fit is dropped whole, and says so" { | |||
| 2687 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 2742 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 2688 | var t = Tile{ | 2743 | var t = Tile{ |
| 2689 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 2744 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 2690 | .stripe = .{ .top = 0, .rows = 4 }, | 2745 | .rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 }, |
| 2691 | .shared = &shared, | 2746 | .shared = &shared, |
| 2692 | .idx = 0, | 2747 | .idx = 0, |
| 2693 | // -1 both ends: `ring` writes to the doorbell and ignores the | 2748 | // -1 both ends: `ring` writes to the doorbell and ignores the |
| @@ -2892,7 +2947,7 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab | |||
| 2892 | shared.label_rows = 0; | 2947 | shared.label_rows = 0; |
| 2893 | var t0 = Tile{ | 2948 | var t0 = Tile{ |
| 2894 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 2949 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 2895 | .stripe = .{ .top = 0, .rows = 24 }, | 2950 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 2896 | .shared = &shared, | 2951 | .shared = &shared, |
| 2897 | .idx = 0, | 2952 | .idx = 0, |
| 2898 | .wake_r = -1, | 2953 | .wake_r = -1, |
| @@ -2904,7 +2959,7 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab | |||
| 2904 | shared.label_rows = 1; | 2959 | shared.label_rows = 1; |
| 2905 | var t1 = Tile{ | 2960 | var t1 = Tile{ |
| 2906 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 2961 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 2907 | .stripe = .{ .top = 0, .rows = 12 }, | 2962 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| 2908 | .shared = &shared, | 2963 | .shared = &shared, |
| 2909 | .idx = 0, | 2964 | .idx = 0, |
| 2910 | .wake_r = -1, | 2965 | .wake_r = -1, |
| @@ -2912,7 +2967,7 @@ test "viewRows: a one-tile wall keeps every row, a multi-tile wall loses the lab | |||
| 2912 | }; | 2967 | }; |
| 2913 | var t2 = Tile{ | 2968 | var t2 = Tile{ |
| 2914 | .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, | 2969 | .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, |
| 2915 | .stripe = .{ .top = 12, .rows = 12 }, | 2970 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 2916 | .shared = &shared, | 2971 | .shared = &shared, |
| 2917 | .idx = 1, | 2972 | .idx = 1, |
| 2918 | .wake_r = -1, | 2973 | .wake_r = -1, |
| @@ -2934,7 +2989,7 @@ test "relayout sets resize_pending on every live tile" { | |||
| 2934 | for (tiles, 0..) |*t, i| { | 2989 | for (tiles, 0..) |*t, i| { |
| 2935 | t.* = Tile{ | 2990 | t.* = Tile{ |
| 2936 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 2991 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 2937 | .stripe = .{ .top = 0, .rows = 24 }, | 2992 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 2938 | .shared = &shared, | 2993 | .shared = &shared, |
| 2939 | .idx = i, | 2994 | .idx = i, |
| 2940 | .wake_r = -1, | 2995 | .wake_r = -1, |
| @@ -2961,7 +3016,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" | |||
| 2961 | for (tiles, 0..) |*t, i| { | 3016 | for (tiles, 0..) |*t, i| { |
| 2962 | t.* = Tile{ | 3017 | t.* = Tile{ |
| 2963 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 3018 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 2964 | .stripe = .{ .top = 0, .rows = 24 }, | 3019 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 2965 | .shared = &shared, | 3020 | .shared = &shared, |
| 2966 | .idx = i, | 3021 | .idx = i, |
| 2967 | .wake_r = -1, | 3022 | .wake_r = -1, |
| @@ -2997,7 +3052,7 @@ test "a one-tile wall draws no label bar and paints row 1" { | |||
| 2997 | present[0] = true; | 3052 | present[0] = true; |
| 2998 | tiles[0] = Tile{ | 3053 | tiles[0] = Tile{ |
| 2999 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 3054 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 3000 | .stripe = .{ .top = 0, .rows = 24 }, | 3055 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 3001 | .shared = &shared, | 3056 | .shared = &shared, |
| 3002 | .idx = 0, | 3057 | .idx = 0, |
| 3003 | .wake_r = -1, | 3058 | .wake_r = -1, |
| @@ -3029,7 +3084,7 @@ test "focus follows a click: rectHit picks the tile under a row" { | |||
| 3029 | var tiles: [2]Tile = undefined; | 3084 | var tiles: [2]Tile = undefined; |
| 3030 | tiles[0] = Tile{ | 3085 | tiles[0] = Tile{ |
| 3031 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 3086 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 3032 | .stripe = .{ .top = 0, .rows = 12 }, | 3087 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| 3033 | .shared = &shared, | 3088 | .shared = &shared, |
| 3034 | .idx = 0, | 3089 | .idx = 0, |
| 3035 | .wake_r = -1, | 3090 | .wake_r = -1, |
| @@ -3037,7 +3092,7 @@ test "focus follows a click: rectHit picks the tile under a row" { | |||
| 3037 | }; | 3092 | }; |
| 3038 | tiles[1] = Tile{ | 3093 | tiles[1] = Tile{ |
| 3039 | .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, | 3094 | .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" }, |
| 3040 | .stripe = .{ .top = 12, .rows = 12 }, | 3095 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 3041 | .shared = &shared, | 3096 | .shared = &shared, |
| 3042 | .idx = 1, | 3097 | .idx = 1, |
| 3043 | .wake_r = -1, | 3098 | .wake_r = -1, |
| @@ -3087,7 +3142,7 @@ test "a copy too big for OSC 52 is said out loud rather than dropped" { | |||
| 3087 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | 3142 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 3088 | var t = Tile{ | 3143 | var t = Tile{ |
| 3089 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, | 3144 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, |
| 3090 | .stripe = .{ .top = 0, .rows = 24 }, | 3145 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 3091 | .shared = &shared, | 3146 | .shared = &shared, |
| 3092 | .idx = 0, | 3147 | .idx = 0, |
| 3093 | .wake_r = -1, | 3148 | .wake_r = -1, |
| @@ -3139,7 +3194,7 @@ test "setFocus writes the outgoing tile's release before the store that lets the | |||
| 3139 | var tiles: [2]Tile = undefined; | 3194 | var tiles: [2]Tile = undefined; |
| 3140 | tiles[0] = Tile{ | 3195 | tiles[0] = Tile{ |
| 3141 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, | 3196 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, |
| 3142 | .stripe = .{ .top = 0, .rows = 12 }, | 3197 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| 3143 | .shared = &shared, | 3198 | .shared = &shared, |
| 3144 | .idx = 0, | 3199 | .idx = 0, |
| 3145 | .wake_r = -1, | 3200 | .wake_r = -1, |
| @@ -3147,7 +3202,7 @@ test "setFocus writes the outgoing tile's release before the store that lets the | |||
| 3147 | }; | 3202 | }; |
| 3148 | tiles[1] = Tile{ | 3203 | tiles[1] = Tile{ |
| 3149 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, | 3204 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, |
| 3150 | .stripe = .{ .top = 12, .rows = 12 }, | 3205 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 3151 | .shared = &shared, | 3206 | .shared = &shared, |
| 3152 | .idx = 1, | 3207 | .idx = 1, |
| 3153 | .wake_r = -1, | 3208 | .wake_r = -1, |
| @@ -3198,7 +3253,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it | |||
| 3198 | for (tiles, 0..) |*t, i| { | 3253 | for (tiles, 0..) |*t, i| { |
| 3199 | t.* = Tile{ | 3254 | t.* = Tile{ |
| 3200 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 3255 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 3201 | .stripe = .{ .top = 0, .rows = 24 }, | 3256 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 3202 | .shared = &shared, | 3257 | .shared = &shared, |
| 3203 | .idx = i, | 3258 | .idx = i, |
| 3204 | .wake_r = -1, | 3259 | .wake_r = -1, |
| @@ -3222,7 +3277,7 @@ test "a reconnect drops the highlight over its own tile, and only its own" { | |||
| 3222 | var tiles: [2]Tile = undefined; | 3277 | var tiles: [2]Tile = undefined; |
| 3223 | tiles[0] = Tile{ | 3278 | tiles[0] = Tile{ |
| 3224 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, | 3279 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, |
| 3225 | .stripe = .{ .top = 0, .rows = 12 }, | 3280 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| 3226 | .shared = &shared, | 3281 | .shared = &shared, |
| 3227 | .idx = 0, | 3282 | .idx = 0, |
| 3228 | .wake_r = -1, | 3283 | .wake_r = -1, |
| @@ -3230,7 +3285,7 @@ test "a reconnect drops the highlight over its own tile, and only its own" { | |||
| 3230 | }; | 3285 | }; |
| 3231 | tiles[1] = Tile{ | 3286 | tiles[1] = Tile{ |
| 3232 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, | 3287 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, |
| 3233 | .stripe = .{ .top = 12, .rows = 12 }, | 3288 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 3234 | .shared = &shared, | 3289 | .shared = &shared, |
| 3235 | .idx = 1, | 3290 | .idx = 1, |
| 3236 | .wake_r = -1, | 3291 | .wake_r = -1, |
| @@ -3270,7 +3325,7 @@ test "a focus move drops what the wall's input filters are holding" { | |||
| 3270 | var tiles: [2]Tile = undefined; | 3325 | var tiles: [2]Tile = undefined; |
| 3271 | tiles[0] = Tile{ | 3326 | tiles[0] = Tile{ |
| 3272 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, | 3327 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, |
| 3273 | .stripe = .{ .top = 0, .rows = 12 }, | 3328 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| 3274 | .shared = &shared, | 3329 | .shared = &shared, |
| 3275 | .idx = 0, | 3330 | .idx = 0, |
| 3276 | .wake_r = -1, | 3331 | .wake_r = -1, |
| @@ -3278,7 +3333,7 @@ test "a focus move drops what the wall's input filters are holding" { | |||
| 3278 | }; | 3333 | }; |
| 3279 | tiles[1] = Tile{ | 3334 | tiles[1] = Tile{ |
| 3280 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, | 3335 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, |
| 3281 | .stripe = .{ .top = 12, .rows = 12 }, | 3336 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 3282 | .shared = &shared, | 3337 | .shared = &shared, |
| 3283 | .idx = 1, | 3338 | .idx = 1, |
| 3284 | .wake_r = -1, | 3339 | .wake_r = -1, |
| @@ -3469,7 +3524,7 @@ test "agent forwarding is per tile: no -A offers nothing and opens nothing" { | |||
| 3469 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 3524 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 3470 | var t = Tile{ | 3525 | var t = Tile{ |
| 3471 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, | 3526 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, |
| 3472 | .stripe = .{ .top = 0, .rows = 4 }, | 3527 | .rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 }, |
| 3473 | .shared = &shared, | 3528 | .shared = &shared, |
| 3474 | .idx = 0, | 3529 | .idx = 0, |
| 3475 | .wake_r = -1, | 3530 | .wake_r = -1, |
| @@ -3514,7 +3569,7 @@ test "a view tile's attach makes no size claim; a tile the user asked for does" | |||
| 3514 | shared.label_rows = 0; | 3569 | shared.label_rows = 0; |
| 3515 | var t = Tile{ | 3570 | var t = Tile{ |
| 3516 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, | 3571 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, |
| 3517 | .stripe = .{ .top = 0, .rows = 24 }, | 3572 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| 3518 | .shared = &shared, | 3573 | .shared = &shared, |
| 3519 | .idx = 0, | 3574 | .idx = 0, |
| 3520 | .wake_r = -1, | 3575 | .wake_r = -1, |
| @@ -3565,7 +3620,7 @@ test "a newborn tile's first claim is its real stripe, not the placeholder" { | |||
| 3565 | // Real stripe for the second tile of a 2-tile wall on 24 rows. | 3620 | // Real stripe for the second tile of a 2-tile wall on 24 rows. |
| 3566 | var t = Tile{ | 3621 | var t = Tile{ |
| 3567 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, | 3622 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, |
| 3568 | .stripe = .{ .top = 12, .rows = 12 }, | 3623 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 3569 | .shared = &shared, | 3624 | .shared = &shared, |
| 3570 | .idx = 1, | 3625 | .idx = 1, |
| 3571 | .wake_r = -1, | 3626 | .wake_r = -1, |
| @@ -3602,7 +3657,7 @@ test "the cursor sleeps in the focused tile, whoever painted last" { | |||
| 3602 | var tiles: [2]Tile = undefined; | 3657 | var tiles: [2]Tile = undefined; |
| 3603 | tiles[0] = Tile{ | 3658 | tiles[0] = Tile{ |
| 3604 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, | 3659 | .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" }, |
| 3605 | .stripe = .{ .top = 0, .rows = 12 }, | 3660 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| 3606 | .shared = &shared, | 3661 | .shared = &shared, |
| 3607 | .idx = 0, | 3662 | .idx = 0, |
| 3608 | .wake_r = -1, | 3663 | .wake_r = -1, |
| @@ -3610,7 +3665,7 @@ test "the cursor sleeps in the focused tile, whoever painted last" { | |||
| 3610 | }; | 3665 | }; |
| 3611 | tiles[1] = Tile{ | 3666 | tiles[1] = Tile{ |
| 3612 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, | 3667 | .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" }, |
| 3613 | .stripe = .{ .top = 12, .rows = 12 }, | 3668 | .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 }, |
| 3614 | .shared = &shared, | 3669 | .shared = &shared, |
| 3615 | .idx = 1, | 3670 | .idx = 1, |
| 3616 | .wake_r = -1, | 3671 | .wake_r = -1, |