44e36df7
refactor: the pane tree and its sidecar are wall_layout.zig
a73x 2026-08-28 20:51
Commit message
docscheck.budget
| Old | New | ||
|---|---|---|---|
| @@ -30,7 +30,7 @@ sockpath.zig 0 | |||
| 30 | spawn.zig 0 | 30 | spawn.zig 0 |
| 31 | testtmp.zig 0 | 31 | testtmp.zig 0 |
| 32 | upgrade.zig 269 | 32 | upgrade.zig 269 |
| 33 | wallview.zig 1596 | 33 | wallview.zig 0 |
| 34 | wall.zig 0 | 34 | wall.zig 0 |
| 35 | wasm_core.zig 0 | 35 | wasm_core.zig 0 |
| 36 | webhub_main.zig 0 | 36 | webhub_main.zig 0 |
| @@ -51,3 +51,4 @@ server_sessions.zig 0 | |||
| 51 | wall_host.zig 0 | 51 | wall_host.zig 0 |
| 52 | wall_picker.zig 0 | 52 | wall_picker.zig 0 |
| 53 | wall_pump.zig 0 | 53 | wall_pump.zig 0 |
| 54 | wall_layout.zig 1596 | ||
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -11,6 +11,7 @@ const hosts = @import("hosts"); | |||
| 11 | const handoff = @import("handoff"); | 11 | const handoff = @import("handoff"); |
| 12 | const xdg = @import("xdg"); | 12 | const xdg = @import("xdg"); |
| 13 | const sockpath = @import("sockpath"); | 13 | const sockpath = @import("sockpath"); |
| 14 | const wall_layout = @import("wall_layout.zig"); | ||
| 14 | const wv = @import("wallview.zig"); | 15 | const wv = @import("wallview.zig"); |
| 15 | const Shared = wv.Shared; | 16 | const Shared = wv.Shared; |
| 16 | const Tile = wv.Tile; | 17 | const Tile = wv.Tile; |
| @@ -472,7 +473,7 @@ pub fn applyHostList( | |||
| 472 | // anchor, so anchoring every birth at the focus would lay a list of | 473 | // anchor, so anchoring every birth at the focus would lay a list of |
| 473 | // {b, c} out as c, b — a wall reading back-to-front against the | 474 | // {b, c} out as c, b — a wall reading back-to-front against the |
| 474 | // order its daemon reported, and against the digits the chords use. | 475 | // order its daemon reported, and against the digits the chords use. |
| 475 | var anchor = wv.anchorTile(present[0..live.*], shared.sel); | 476 | var anchor = wall_layout.anchorTile(present[0..live.*], shared.sel); |
| 476 | // Stops at the FIRST refusal rather than retrying each name: the | 477 | // Stops at the FIRST refusal rather than retrying each name: the |
| 477 | // wall refuses for a reason that holds for the whole list (no slot, | 478 | // wall refuses for a reason that holds for the whole list (no slot, |
| 478 | // no room to cut), and this list comes back every second — a | 479 | // no room to cut), and this list comes back every second — a |
| @@ -506,8 +507,8 @@ pub fn applyHostList( | |||
| 506 | } | 507 | } |
| 507 | if ((!had_focus or shared.sel >= live.* or !present[shared.sel]) and | 508 | if ((!had_focus or shared.sel >= live.* or !present[shared.sel]) and |
| 508 | wv.presentCount(present[0..live.*]) > 0) | 509 | wv.presentCount(present[0..live.*]) > 0) |
| 509 | wv.setFocus(tiles[0..live.*], shared, wv.firstPresent(present[0..live.*]) orelse 0); | 510 | wv.setFocus(tiles[0..live.*], shared, wall_layout.firstPresent(present[0..live.*]) orelse 0); |
| 510 | if (changed) wv.relayout(alloc, tiles[0..live.*], present[0..live.*], shared, shared.sel); | 511 | if (changed) wall_layout.relayout(alloc, tiles[0..live.*], present[0..live.*], shared, shared.sel); |
| 511 | } | 512 | } |
| 512 | 513 | ||
| 513 | /// The host grammar's own spelling of a target, for a wall entered by | 514 | /// The host grammar's own spelling of a target, for a wall entered by |
src/tui/wall_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,427 @@ | |||
| 1 | //! The pane tree's operations and the layout sidecar. `relayout` is the | ||
| 2 | //! single flatten point that turns the tree into tile rects and paints the | ||
| 3 | //! rails; the sidecar saves that tree on the last detach and heals it back | ||
| 4 | //! per leaf against the hosts' live lists. | ||
| 5 | const std = @import("std"); | ||
| 6 | const proto = @import("protocol"); | ||
| 7 | const wall = @import("wall"); | ||
| 8 | const interact = @import("interact"); | ||
| 9 | const layout = @import("layout"); | ||
| 10 | const wall_host = @import("wall_host.zig"); | ||
| 11 | const wv = @import("wallview.zig"); | ||
| 12 | const Resolved = wall_host.Resolved; | ||
| 13 | const Shared = wv.Shared; | ||
| 14 | const Tile = wv.Tile; | ||
| 15 | |||
| 16 | /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall | ||
| 17 | /// draws no bar so its floor is `min_session_rows`; two or more tiles each | ||
| 18 | /// lose a row to a bar, so each stripe must hold that floor PLUS the bar | ||
| 19 | /// or the daemon drops the resize and the tile freezes on a stale grid. | ||
| 20 | pub fn wallFloors(live: usize) layout.Floors { | ||
| 21 | return .{ | ||
| 22 | .rows = proto.min_session_rows + @as(u16, @intFromBool(live > 1)), | ||
| 23 | .cols = proto.min_session_cols, | ||
| 24 | }; | ||
| 25 | } | ||
| 26 | |||
| 27 | /// The root container's orientation for N tiles at a given terminal size: | ||
| 28 | /// `.beside` when the terminal is wide enough that columns are the natural | ||
| 29 | /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal | ||
| 30 | /// twice as wide as it is tall has roughly square panes side-by-side. | ||
| 31 | pub fn rootOrient(size: proto.Size) layout.Orient { | ||
| 32 | return if (size.cols >= 2 * size.rows) .beside else .stacked; | ||
| 33 | } | ||
| 34 | |||
| 35 | /// `interact.Dir` and `layout.Dir` are the same enum tags in different | ||
| 36 | /// modules; this is the one place they meet, so the switch stays explicit. | ||
| 37 | pub fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir { | ||
| 38 | return switch (d) { | ||
| 39 | .left => .left, | ||
| 40 | .down => .down, | ||
| 41 | .up => .up, | ||
| 42 | .right => .right, | ||
| 43 | }; | ||
| 44 | } | ||
| 45 | |||
| 46 | /// layout.resize always GAINS focus cells; shrink = grow a neighbor at | ||
| 47 | /// focus's expense. Fullscreen refuses — a hidden layout resizing | ||
| 48 | /// invisibly is surprise, not power. | ||
| 49 | pub fn doResize( | ||
| 50 | alloc: std.mem.Allocator, | ||
| 51 | tiles: []Tile, | ||
| 52 | present: []const bool, | ||
| 53 | shared: *Shared, | ||
| 54 | sel: usize, | ||
| 55 | d: interact.PrefixFilter.Dir, | ||
| 56 | ) bool { | ||
| 57 | if (shared.fullscreen) return false; | ||
| 58 | const ld = dirOf(d); | ||
| 59 | const grow = switch (ld) { | ||
| 60 | .right, .down => true, | ||
| 61 | .left, .up => false, | ||
| 62 | }; | ||
| 63 | const flat = shared.base_flat orelse shared.last_flat orelse return false; | ||
| 64 | const focus_tile: u8 = @intCast(sel); | ||
| 65 | var moved = false; | ||
| 66 | if (grow) { | ||
| 67 | // Focus gains from the sibling toward `ld`; if none there (edge | ||
| 68 | // pane), try the opposite side — gaining from either sibling | ||
| 69 | // widens or tallens the focus. | ||
| 70 | if (shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, ld, 1)) { | ||
| 71 | moved = true; | ||
| 72 | } else { | ||
| 73 | const opp = switch (ld) { | ||
| 74 | .right => layout.Dir.left, | ||
| 75 | .down => layout.Dir.up, | ||
| 76 | .left => layout.Dir.right, | ||
| 77 | .up => layout.Dir.down, | ||
| 78 | }; | ||
| 79 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, opp, 1); | ||
| 80 | } | ||
| 81 | } else { | ||
| 82 | // Shrink: a neighbor on the same axis gains a cell from focus. | ||
| 83 | // Try the side the key points at first, then the opposite — a | ||
| 84 | // pane pressed against one wall can still shrink toward the other. | ||
| 85 | const opp = switch (ld) { | ||
| 86 | .left => layout.Dir.right, | ||
| 87 | .up => layout.Dir.down, | ||
| 88 | .right => layout.Dir.left, | ||
| 89 | .down => layout.Dir.up, | ||
| 90 | }; | ||
| 91 | if (layout.neighbor(flat, focus_tile, ld)) |nb| { | ||
| 92 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, opp, 1); | ||
| 93 | } | ||
| 94 | if (!moved) { | ||
| 95 | if (layout.neighbor(flat, focus_tile, opp)) |nb| { | ||
| 96 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, ld, 1); | ||
| 97 | } | ||
| 98 | } | ||
| 99 | } | ||
| 100 | if (moved) relayout(alloc, tiles, present, shared, sel); | ||
| 101 | return moved; | ||
| 102 | } | ||
| 103 | |||
| 104 | /// Paint the vertical rails between `.beside` siblings. One column of | ||
| 105 | /// `\x1b[7m \x1b[0m` per rail — a reverse-video bar in the label-bar's | ||
| 106 | /// style, so a rail reads as structure and not as session output. The wall | ||
| 107 | /// owns rails; tiles never touch them (paint.zig's span-bounded clears). | ||
| 108 | fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void { | ||
| 109 | if (!shared.is_tty) return; | ||
| 110 | if (flat.rails.len == 0) return; | ||
| 111 | var buf: [128]u8 = undefined; | ||
| 112 | for (flat.rails) |rail| { | ||
| 113 | var row: u16 = rail.top; | ||
| 114 | while (row < rail.top + rail.rows) : (row += 1) { | ||
| 115 | const out = std.fmt.bufPrint(&buf, "\x1b[{d};{d}H\x1b[7m \x1b[0m", .{ row + 1, rail.col + 1 }) catch continue; | ||
| 116 | proto.writeAllFd(shared.out_fd, out) catch {}; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | /// One `paint_mu` hold: no window where a pump paints rows that just | ||
| 122 | /// changed owner. | ||
| 123 | pub fn relayout( | ||
| 124 | alloc: std.mem.Allocator, | ||
| 125 | tiles: []Tile, | ||
| 126 | present: []const bool, | ||
| 127 | shared: *Shared, | ||
| 128 | sel: usize, | ||
| 129 | ) void { | ||
| 130 | shared.paint_mu.lock(); | ||
| 131 | defer shared.paint_mu.unlock(); | ||
| 132 | shared.sel = sel; | ||
| 133 | |||
| 134 | var live: usize = 0; | ||
| 135 | for (present) |p| { | ||
| 136 | if (p) live += 1; | ||
| 137 | } | ||
| 138 | // A one-tile wall owns every row and draws no label bar; two or more | ||
| 139 | // tiles each lose their top row to one. Fullscreen is the same: one | ||
| 140 | // visible pane, no bar — the plain client's byte stream. | ||
| 141 | shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; | ||
| 142 | if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; | ||
| 143 | // The screen the popup was on has just been cleared, so the next paint | ||
| 144 | // owes it however unchanged its rows are. | ||
| 145 | if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0; | ||
| 146 | if (live == 0) { | ||
| 147 | wv.paintEmptyWallLocked(shared); | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | // The base flat (null) is always computed so `focus_dir` can read | ||
| 151 | // adjacency from the real layout while fullscreened. | ||
| 152 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| { | ||
| 153 | if (shared.base_flat) |*old| old.deinit(shared.flat_alloc); | ||
| 154 | shared.base_flat = base; | ||
| 155 | } else |_| {} | ||
| 156 | const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null; | ||
| 157 | var cut = shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg); | ||
| 158 | if (cut) |_| {} else |e| { | ||
| 159 | // A split, an insert, a resize key: those are OPERATIONS, the user | ||
| 160 | // asked, and refusing leaves the screen exactly as it was. A | ||
| 161 | // SIGWINCH is neither — the terminal has ALREADY shrunk, so | ||
| 162 | // refusing leaves every rect pointing past the bottom of a screen | ||
| 163 | // that was just cleared. Degrade instead, to the view the wall | ||
| 164 | // already has for a terminal that holds one pane: the focused tile | ||
| 165 | // whole, the rest at 0x0, which paints nothing and claims no size. | ||
| 166 | // The tree is untouched, so growing back re-cuts every pane. | ||
| 167 | if (e == error.TooSmall and fs_arg == null) { | ||
| 168 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), @intCast(sel))) |only| { | ||
| 169 | cut = only; | ||
| 170 | // One visible pane draws no bar. Left at 1, `viewRows` | ||
| 171 | // would owe the daemon a row the tile does not have. | ||
| 172 | shared.label_rows = 0; | ||
| 173 | } else |_| {} | ||
| 174 | } | ||
| 175 | } | ||
| 176 | if (cut) |flat| { | ||
| 177 | if (shared.last_flat) |*old| old.deinit(shared.flat_alloc); | ||
| 178 | shared.last_flat = flat; | ||
| 179 | for (tiles, present) |*t, p| { | ||
| 180 | if (!p) continue; | ||
| 181 | if (flat.rectOf(@intCast(t.idx))) |r| { | ||
| 182 | t.rect = r; | ||
| 183 | } | ||
| 184 | t.resize_pending = true; | ||
| 185 | } | ||
| 186 | paintRailsLocked(shared, flat); | ||
| 187 | } else |_| {} | ||
| 188 | |||
| 189 | // The generation bump is what puts the rects back: every surviving | ||
| 190 | // pump repaints from its hot replica at its NEW rows, and the doorbell | ||
| 191 | // makes that immediate rather than one poll timeout away. | ||
| 192 | _ = shared.repaint_gen.fetchAdd(1, .release); | ||
| 193 | for (tiles, present) |*t, p| { | ||
| 194 | if (p) wv.ring(t); | ||
| 195 | } | ||
| 196 | // ...except the tiles with no pump left to hear it: their bars are the | ||
| 197 | // keyboard's, the same rule `setFocus` paints a focus move by. No | ||
| 198 | // `label_rows` guard here, unlike there: the screen was just cleared, | ||
| 199 | // so on a one-tile wall of a dead tile that bar is the only thing left | ||
| 200 | // to say the target refused. | ||
| 201 | wv.paintDeadBarsLocked(tiles); | ||
| 202 | } | ||
| 203 | |||
| 204 | /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each | ||
| 205 | /// saved leaf id takes the first wall tile whose label matches its | ||
| 206 | /// spelling, unmatched saved leaves collapse out, and unmatched wall tiles | ||
| 207 | /// insert beside the first matched one. The saved root orientation wins | ||
| 208 | /// over the aspect heuristic — `setRootOrient` is never called here. | ||
| 209 | /// | ||
| 210 | /// Pure of file I/O: bytes come in, the caller decides whether to feed | ||
| 211 | /// them. False on any malformation, zero matches, or allocation failure — | ||
| 212 | /// the caller builds today's default tree and the sidecar is ignored. | ||
| 213 | pub fn restoreLayout( | ||
| 214 | alloc: std.mem.Allocator, | ||
| 215 | resolved: []const Resolved, | ||
| 216 | shared: *Shared, | ||
| 217 | bytes: []const u8, | ||
| 218 | focus_out: *?u8, | ||
| 219 | ) bool { | ||
| 220 | var parsed = layout.parse(alloc, bytes) orelse return false; | ||
| 221 | // Every early-false path deinits parsed; the success path moves the | ||
| 222 | // tree out and deinits only the spellings. | ||
| 223 | |||
| 224 | const n_saved = parsed.spellings.items.len; | ||
| 225 | const map = alloc.alloc(?u8, n_saved) catch { | ||
| 226 | parsed.deinit(alloc); | ||
| 227 | return false; | ||
| 228 | }; | ||
| 229 | defer alloc.free(map); | ||
| 230 | |||
| 231 | var taken = alloc.alloc(bool, resolved.len) catch { | ||
| 232 | parsed.deinit(alloc); | ||
| 233 | return false; | ||
| 234 | }; | ||
| 235 | defer alloc.free(taken); | ||
| 236 | @memset(taken, false); | ||
| 237 | |||
| 238 | var any_match = false; | ||
| 239 | for (0..n_saved) |i| { | ||
| 240 | const spelling = parsed.spellings.items[i]; | ||
| 241 | map[i] = null; | ||
| 242 | for (resolved, 0..) |r, j| { | ||
| 243 | if (!taken[j] and std.mem.eql(u8, r.label, spelling)) { | ||
| 244 | taken[j] = true; | ||
| 245 | map[i] = @intCast(j); | ||
| 246 | any_match = true; | ||
| 247 | break; | ||
| 248 | } | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | // Zero matches: the sidecar describes a different wall entirely. | ||
| 253 | // Deinit parsed and return false so the caller builds the default tree. | ||
| 254 | if (!any_match) { | ||
| 255 | parsed.deinit(alloc); | ||
| 256 | return false; | ||
| 257 | } | ||
| 258 | |||
| 259 | // Map the saved focus through the spelling match before remapLeaves | ||
| 260 | // consumes map. A null here means the focused leaf did not survive | ||
| 261 | // (no wall tile matched its spelling) — the caller falls back to 0. | ||
| 262 | if (parsed.focus) |k| { | ||
| 263 | if (k < map.len) focus_out.* = map[k]; | ||
| 264 | } | ||
| 265 | |||
| 266 | parsed.tree.remapLeaves(map); | ||
| 267 | |||
| 268 | // Unmatched wall indices, ascending, insert beside the first matched | ||
| 269 | // tile — the same beside-focus placement the initial build uses. | ||
| 270 | const first_tile: u8 = if (map.len > 0) blk: { | ||
| 271 | var k: usize = 0; | ||
| 272 | while (k < map.len) : (k += 1) { | ||
| 273 | if (map[k] != null) break :blk map[k].?; | ||
| 274 | } | ||
| 275 | break :blk 0; | ||
| 276 | } else 0; | ||
| 277 | |||
| 278 | var next_tile: u8 = first_tile; | ||
| 279 | for (taken, 0..) |is_taken, j| { | ||
| 280 | if (!is_taken) { | ||
| 281 | const new_id: u8 = @intCast(j); | ||
| 282 | parsed.tree.insert(next_tile, new_id) catch { | ||
| 283 | parsed.deinit(alloc); | ||
| 284 | return false; | ||
| 285 | }; | ||
| 286 | next_tile = new_id; | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | // Move the parsed tree into shared, deinit the old one first. | ||
| 291 | shared.tree.deinit(); | ||
| 292 | shared.tree = parsed.tree; | ||
| 293 | // The spellings are no longer needed — the tree holds only ids now. | ||
| 294 | for (parsed.spellings.items) |s| alloc.free(s); | ||
| 295 | parsed.spellings.deinit(alloc); | ||
| 296 | return true; | ||
| 297 | } | ||
| 298 | |||
| 299 | /// `serialize` indexes spellings by leaf ID (tile index), so the array is | ||
| 300 | /// tile-indexed: holes get "" and are never serialized (the tree dropped | ||
| 301 | /// them). A failed write prints one stderr line and returns. | ||
| 302 | pub fn saveLayoutTo( | ||
| 303 | alloc: std.mem.Allocator, | ||
| 304 | path: []const u8, | ||
| 305 | tiles: []Tile, | ||
| 306 | present: []const bool, | ||
| 307 | shared: *Shared, | ||
| 308 | ) void { | ||
| 309 | const spellings = alloc.alloc([]const u8, tiles.len) catch return; | ||
| 310 | defer alloc.free(spellings); | ||
| 311 | for (tiles, present, 0..) |*t, p, i| { | ||
| 312 | spellings[i] = if (p) t.r.label else ""; | ||
| 313 | } | ||
| 314 | var buf = std.ArrayListUnmanaged(u8){}; | ||
| 315 | defer buf.deinit(alloc); | ||
| 316 | // shared.sel is the focused tile index; serialize maps it to the | ||
| 317 | // encounter index of its leaf in the depth-first walk. | ||
| 318 | const focus: ?u8 = if (shared.sel < tiles.len and present[shared.sel]) @intCast(shared.sel) else null; | ||
| 319 | shared.tree.serialize(spellings, focus, buf.writer(alloc)) catch |err| { | ||
| 320 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | ||
| 321 | return; | ||
| 322 | }; | ||
| 323 | wall.saveLayout(path, buf.items) catch |err| { | ||
| 324 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | ||
| 325 | }; | ||
| 326 | } | ||
| 327 | |||
| 328 | /// Resolves the sidecar path from env and delegates to `saveLayoutTo`. | ||
| 329 | pub fn saveSidecar( | ||
| 330 | alloc: std.mem.Allocator, | ||
| 331 | tiles: []Tile, | ||
| 332 | present: []const bool, | ||
| 333 | shared: *Shared, | ||
| 334 | ) void { | ||
| 335 | // A pipe has no stripes, so it has no layout worth remembering — and | ||
| 336 | // a tree it saved would be a tree the next TERMINAL restores over the | ||
| 337 | // aspect rule. See `restoreSidecar` for the cost of the other half. | ||
| 338 | if (!shared.is_tty) return; | ||
| 339 | const path = wall.layoutPath(alloc) catch |err| { | ||
| 340 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | ||
| 341 | return; | ||
| 342 | }; | ||
| 343 | defer alloc.free(path); | ||
| 344 | saveLayoutTo(alloc, path, tiles, present, shared); | ||
| 345 | } | ||
| 346 | |||
| 347 | /// The saved layout over the tiles the hosts turned out to have; null | ||
| 348 | /// when nothing matched. | ||
| 349 | pub fn restoreSidecar( | ||
| 350 | alloc: std.mem.Allocator, | ||
| 351 | tiles: []Tile, | ||
| 352 | present: []bool, | ||
| 353 | live: usize, | ||
| 354 | shared: *Shared, | ||
| 355 | focus_out: *?usize, | ||
| 356 | ) ?void { | ||
| 357 | // Not on a pipe, and the cost is why. A restored tree is applied by | ||
| 358 | // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending` | ||
| 359 | // on EVERY present tile whether or not that tile's rect moved; each | ||
| 360 | // pump then sends `.resize`, and `Server.onResize` answers every one of | ||
| 361 | // them with `resyncSnapshot` — which has no "nothing changed" guard. | ||
| 362 | // Measured on a scripted `mux quic://...` whose state home already held | ||
| 363 | // a sidecar: a second full snapshot on the wire for a wall of one tile | ||
| 364 | // that was already the right shape. The CLAIM is innocent; it stopped | ||
| 365 | // sending a resize (see the pump's focus-claim block). | ||
| 366 | if (!shared.is_tty) return null; | ||
| 367 | const path = wall.layoutPath(alloc) catch return null; | ||
| 368 | defer alloc.free(path); | ||
| 369 | const bytes = wall.loadLayout(alloc, path) orelse return null; | ||
| 370 | defer alloc.free(bytes); | ||
| 371 | return restoreLayoutFrom(alloc, tiles, present, live, shared, bytes, focus_out); | ||
| 372 | } | ||
| 373 | |||
| 374 | /// Pure of file I/O like `restoreLayout`: the chained id translations — | ||
| 375 | /// saved→dense, then dense→real — are what a test must reach. | ||
| 376 | pub fn restoreLayoutFrom( | ||
| 377 | alloc: std.mem.Allocator, | ||
| 378 | tiles: []Tile, | ||
| 379 | present: []bool, | ||
| 380 | live: usize, | ||
| 381 | shared: *Shared, | ||
| 382 | bytes: []const u8, | ||
| 383 | focus_out: *?usize, | ||
| 384 | ) ?void { | ||
| 385 | const n = wv.presentCount(present[0..live]); | ||
| 386 | if (n == 0) return null; | ||
| 387 | const dense = alloc.alloc(Resolved, n) catch return null; | ||
| 388 | defer alloc.free(dense); | ||
| 389 | const remap = alloc.alloc(?u8, n) catch return null; | ||
| 390 | defer alloc.free(remap); | ||
| 391 | var di: usize = 0; | ||
| 392 | for (tiles[0..live], present[0..live], 0..) |*t, p, ti| { | ||
| 393 | if (p) { | ||
| 394 | dense[di] = t.r; | ||
| 395 | remap[di] = @intCast(ti); | ||
| 396 | di += 1; | ||
| 397 | } | ||
| 398 | } | ||
| 399 | var dense_focus: ?u8 = null; | ||
| 400 | if (restoreLayout(alloc, dense, shared, bytes, &dense_focus)) { | ||
| 401 | // The tree's leaf ids are dense indices into `dense`; remap them | ||
| 402 | // to the real tile indices relayout reads, and the saved focus | ||
| 403 | // with them. | ||
| 404 | shared.tree.remapLeaves(remap); | ||
| 405 | if (dense_focus) |d| { | ||
| 406 | if (d < remap.len) { | ||
| 407 | if (remap[d]) |real| focus_out.* = real; | ||
| 408 | } | ||
| 409 | } | ||
| 410 | return {}; | ||
| 411 | } | ||
| 412 | return null; | ||
| 413 | } | ||
| 414 | |||
| 415 | /// The leaf a birth sits beside: `birthTile` inserts against a LEAF, and | ||
| 416 | /// the focus can be a hole. | ||
| 417 | pub fn anchorTile(present: []const bool, sel: usize) usize { | ||
| 418 | if (sel < present.len and present[sel]) return sel; | ||
| 419 | return firstPresent(present) orelse 0; | ||
| 420 | } | ||
| 421 | |||
| 422 | pub fn firstPresent(present: []const bool) ?usize { | ||
| 423 | for (present, 0..) |p, i| { | ||
| 424 | if (p) return i; | ||
| 425 | } | ||
| 426 | return null; | ||
| 427 | } | ||
src/tui/wall_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -10,6 +10,7 @@ const client = @import("client"); | |||
| 10 | const hosts = @import("hosts"); | 10 | const hosts = @import("hosts"); |
| 11 | const interact = @import("interact"); | 11 | const interact = @import("interact"); |
| 12 | const wall_host = @import("wall_host.zig"); | 12 | const wall_host = @import("wall_host.zig"); |
| 13 | const wall_layout = @import("wall_layout.zig"); | ||
| 13 | const wv = @import("wallview.zig"); | 14 | const wv = @import("wallview.zig"); |
| 14 | const Host = wall_host.Host; | 15 | const Host = wall_host.Host; |
| 15 | const Shared = wv.Shared; | 16 | const Shared = wv.Shared; |
| @@ -219,7 +220,7 @@ pub fn pickBirth( | |||
| 219 | alloc.free(session); | 220 | alloc.free(session); |
| 220 | return null; | 221 | return null; |
| 221 | }; | 222 | }; |
| 222 | const anchor = wv.anchorTile(present[0..live.*], shared.sel); | 223 | const anchor = wall_layout.anchorTile(present[0..live.*], shared.sel); |
| 223 | const has_anchor = wv.presentCount(present[0..live.*]) > 0; | 224 | const has_anchor = wv.presentCount(present[0..live.*]) > 0; |
| 224 | // `-A` is inherited only within one host. A chord inherits it because | 225 | // `-A` is inherited only within one host. A chord inherits it because |
| 225 | // the new session is on the machine the offer was already made to; the | 226 | // the new session is on the machine the offer was already made to; the |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -37,6 +37,7 @@ const interact = @import("interact"); | |||
| 37 | const layout = @import("layout"); | 37 | const layout = @import("layout"); |
| 38 | const TmpDir = @import("testtmp").TmpDir; | 38 | const TmpDir = @import("testtmp").TmpDir; |
| 39 | const wall_host = @import("wall_host.zig"); | 39 | const wall_host = @import("wall_host.zig"); |
| 40 | const wall_layout = @import("wall_layout.zig"); | ||
| 40 | const wall_picker = @import("wall_picker.zig"); | 41 | const wall_picker = @import("wall_picker.zig"); |
| 41 | const wall_pump = @import("wall_pump.zig"); | 42 | const wall_pump = @import("wall_pump.zig"); |
| 42 | const AddHost = wall_host.AddHost; | 43 | const AddHost = wall_host.AddHost; |
| @@ -64,94 +65,6 @@ fn headless(out_fd: std.posix.fd_t) bool { | |||
| 64 | return interact.ttySize(out_fd) == null; | 65 | return interact.ttySize(out_fd) == null; |
| 65 | } | 66 | } |
| 66 | 67 | ||
| 67 | /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall | ||
| 68 | /// draws no bar so its floor is `min_session_rows`; two or more tiles each | ||
| 69 | /// lose a row to a bar, so each stripe must hold that floor PLUS the bar | ||
| 70 | /// or the daemon drops the resize and the tile freezes on a stale grid. | ||
| 71 | fn wallFloors(live: usize) layout.Floors { | ||
| 72 | return .{ | ||
| 73 | .rows = proto.min_session_rows + @as(u16, @intFromBool(live > 1)), | ||
| 74 | .cols = proto.min_session_cols, | ||
| 75 | }; | ||
| 76 | } | ||
| 77 | |||
| 78 | /// The root container's orientation for N tiles at a given terminal size: | ||
| 79 | /// `.beside` when the terminal is wide enough that columns are the natural | ||
| 80 | /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal | ||
| 81 | /// twice as wide as it is tall has roughly square panes side-by-side. | ||
| 82 | fn rootOrient(size: proto.Size) layout.Orient { | ||
| 83 | return if (size.cols >= 2 * size.rows) .beside else .stacked; | ||
| 84 | } | ||
| 85 | |||
| 86 | /// `interact.Dir` and `layout.Dir` are the same enum tags in different | ||
| 87 | /// modules; this is the one place they meet, so the switch stays explicit. | ||
| 88 | fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir { | ||
| 89 | return switch (d) { | ||
| 90 | .left => .left, | ||
| 91 | .down => .down, | ||
| 92 | .up => .up, | ||
| 93 | .right => .right, | ||
| 94 | }; | ||
| 95 | } | ||
| 96 | |||
| 97 | /// layout.resize always GAINS focus cells; shrink = grow a neighbor at | ||
| 98 | /// focus's expense. Fullscreen refuses — a hidden layout resizing | ||
| 99 | /// invisibly is surprise, not power. | ||
| 100 | fn doResize( | ||
| 101 | alloc: std.mem.Allocator, | ||
| 102 | tiles: []Tile, | ||
| 103 | present: []const bool, | ||
| 104 | shared: *Shared, | ||
| 105 | sel: usize, | ||
| 106 | d: interact.PrefixFilter.Dir, | ||
| 107 | ) bool { | ||
| 108 | if (shared.fullscreen) return false; | ||
| 109 | const ld = dirOf(d); | ||
| 110 | const grow = switch (ld) { | ||
| 111 | .right, .down => true, | ||
| 112 | .left, .up => false, | ||
| 113 | }; | ||
| 114 | const flat = shared.base_flat orelse shared.last_flat orelse return false; | ||
| 115 | const focus_tile: u8 = @intCast(sel); | ||
| 116 | var moved = false; | ||
| 117 | if (grow) { | ||
| 118 | // Focus gains from the sibling toward `ld`; if none there (edge | ||
| 119 | // pane), try the opposite side — gaining from either sibling | ||
| 120 | // widens or tallens the focus. | ||
| 121 | if (shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, ld, 1)) { | ||
| 122 | moved = true; | ||
| 123 | } else { | ||
| 124 | const opp = switch (ld) { | ||
| 125 | .right => layout.Dir.left, | ||
| 126 | .down => layout.Dir.up, | ||
| 127 | .left => layout.Dir.right, | ||
| 128 | .up => layout.Dir.down, | ||
| 129 | }; | ||
| 130 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, opp, 1); | ||
| 131 | } | ||
| 132 | } else { | ||
| 133 | // Shrink: a neighbor on the same axis gains a cell from focus. | ||
| 134 | // Try the side the key points at first, then the opposite — a | ||
| 135 | // pane pressed against one wall can still shrink toward the other. | ||
| 136 | const opp = switch (ld) { | ||
| 137 | .left => layout.Dir.right, | ||
| 138 | .up => layout.Dir.down, | ||
| 139 | .right => layout.Dir.left, | ||
| 140 | .down => layout.Dir.up, | ||
| 141 | }; | ||
| 142 | if (layout.neighbor(flat, focus_tile, ld)) |nb| { | ||
| 143 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, opp, 1); | ||
| 144 | } | ||
| 145 | if (!moved) { | ||
| 146 | if (layout.neighbor(flat, focus_tile, opp)) |nb| { | ||
| 147 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), nb, ld, 1); | ||
| 148 | } | ||
| 149 | } | ||
| 150 | } | ||
| 151 | if (moved) relayout(alloc, tiles, present, shared, sel); | ||
| 152 | return moved; | ||
| 153 | } | ||
| 154 | |||
| 155 | pub const State = enum { | 68 | pub const State = enum { |
| 156 | connecting, | 69 | connecting, |
| 157 | up, | 70 | up, |
| @@ -666,7 +579,7 @@ fn paintLabelLocked(t: *Tile) void { | |||
| 666 | // `gone` rather than `present` because a focus move has no `present` slice | 579 | // `gone` rather than `present` because a focus move has no `present` slice |
| 667 | // to consult; `vanishTile` stores the two together, and a vanished tile | 580 | // to consult; `vanishTile` stores the two together, and a vanished tile |
| 668 | // must not paint — its rect belongs to a neighbour now. | 581 | // must not paint — its rect belongs to a neighbour now. |
| 669 | fn paintDeadBarsLocked(tiles: []Tile) void { | 582 | pub fn paintDeadBarsLocked(tiles: []Tile) void { |
| 670 | for (tiles) |*t| { | 583 | for (tiles) |*t| { |
| 671 | if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t); | 584 | if (!t.alive.load(.acquire) and !t.gone.load(.acquire)) paintLabelLocked(t); |
| 672 | } | 585 | } |
| @@ -840,10 +753,10 @@ fn focusAnswer( | |||
| 840 | recut: bool, | 753 | recut: bool, |
| 841 | to: usize, | 754 | to: usize, |
| 842 | ) void { | 755 | ) void { |
| 843 | if (recut) relayout(alloc, tiles, present, shared, shared.sel); | 756 | if (recut) wall_layout.relayout(alloc, tiles, present, shared, shared.sel); |
| 844 | setFocus(tiles, shared, to); | 757 | setFocus(tiles, shared, to); |
| 845 | // Fullscreen re-flattens with the new focus so the full rect follows. | 758 | // Fullscreen re-flattens with the new focus so the full rect follows. |
| 846 | if (shared.fullscreen) relayout(alloc, tiles, present, shared, shared.sel); | 759 | if (shared.fullscreen) wall_layout.relayout(alloc, tiles, present, shared, shared.sel); |
| 847 | } | 760 | } |
| 848 | 761 | ||
| 849 | /// TAKEN, not read: an empty wall has no pump to paint a notice as a | 762 | /// TAKEN, not read: an empty wall has no pump to paint a notice as a |
| @@ -859,7 +772,7 @@ fn emptyWallHint(shared: *Shared) []const u8 { | |||
| 859 | /// wall is empty and the way out. A blank terminal with no cursor reads as | 772 | /// wall is empty and the way out. A blank terminal with no cursor reads as |
| 860 | /// hung, and the last `x` is precisely when the user needs to be told that | 773 | /// hung, and the last `x` is precisely when the user needs to be told that |
| 861 | /// what they see is the answer and not a crash. | 774 | /// what they see is the answer and not a crash. |
| 862 | fn paintEmptyWallLocked(shared: *Shared) void { | 775 | pub fn paintEmptyWallLocked(shared: *Shared) void { |
| 863 | if (!shared.is_tty) return; | 776 | if (!shared.is_tty) return; |
| 864 | var text_buf: [256]u8 = undefined; | 777 | var text_buf: [256]u8 = undefined; |
| 865 | const shown = labelText( | 778 | const shown = labelText( |
| @@ -875,106 +788,6 @@ fn paintEmptyWallLocked(shared: *Shared) void { | |||
| 875 | proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; | 788 | proto.writeAllFd(shared.out_fd, fbs.getWritten()) catch {}; |
| 876 | } | 789 | } |
| 877 | 790 | ||
| 878 | /// Paint the vertical rails between `.beside` siblings. One column of | ||
| 879 | /// `\x1b[7m \x1b[0m` per rail — a reverse-video bar in the label-bar's | ||
| 880 | /// style, so a rail reads as structure and not as session output. The wall | ||
| 881 | /// owns rails; tiles never touch them (paint.zig's span-bounded clears). | ||
| 882 | fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void { | ||
| 883 | if (!shared.is_tty) return; | ||
| 884 | if (flat.rails.len == 0) return; | ||
| 885 | var buf: [128]u8 = undefined; | ||
| 886 | for (flat.rails) |rail| { | ||
| 887 | var row: u16 = rail.top; | ||
| 888 | while (row < rail.top + rail.rows) : (row += 1) { | ||
| 889 | const out = std.fmt.bufPrint(&buf, "\x1b[{d};{d}H\x1b[7m \x1b[0m", .{ row + 1, rail.col + 1 }) catch continue; | ||
| 890 | proto.writeAllFd(shared.out_fd, out) catch {}; | ||
| 891 | } | ||
| 892 | } | ||
| 893 | } | ||
| 894 | |||
| 895 | /// One `paint_mu` hold: no window where a pump paints rows that just | ||
| 896 | /// changed owner. | ||
| 897 | pub fn relayout( | ||
| 898 | alloc: std.mem.Allocator, | ||
| 899 | tiles: []Tile, | ||
| 900 | present: []const bool, | ||
| 901 | shared: *Shared, | ||
| 902 | sel: usize, | ||
| 903 | ) void { | ||
| 904 | shared.paint_mu.lock(); | ||
| 905 | defer shared.paint_mu.unlock(); | ||
| 906 | shared.sel = sel; | ||
| 907 | |||
| 908 | var live: usize = 0; | ||
| 909 | for (present) |p| { | ||
| 910 | if (p) live += 1; | ||
| 911 | } | ||
| 912 | // A one-tile wall owns every row and draws no label bar; two or more | ||
| 913 | // tiles each lose their top row to one. Fullscreen is the same: one | ||
| 914 | // visible pane, no bar — the plain client's byte stream. | ||
| 915 | shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; | ||
| 916 | if (shared.is_tty) proto.writeAllFd(shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; | ||
| 917 | // The screen the popup was on has just been cleared, so the next paint | ||
| 918 | // owes it however unchanged its rows are. | ||
| 919 | if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0; | ||
| 920 | if (live == 0) { | ||
| 921 | paintEmptyWallLocked(shared); | ||
| 922 | return; | ||
| 923 | } | ||
| 924 | // The base flat (null) is always computed so `focus_dir` can read | ||
| 925 | // adjacency from the real layout while fullscreened. | ||
| 926 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| { | ||
| 927 | if (shared.base_flat) |*old| old.deinit(shared.flat_alloc); | ||
| 928 | shared.base_flat = base; | ||
| 929 | } else |_| {} | ||
| 930 | const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null; | ||
| 931 | var cut = shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg); | ||
| 932 | if (cut) |_| {} else |e| { | ||
| 933 | // A split, an insert, a resize key: those are OPERATIONS, the user | ||
| 934 | // asked, and refusing leaves the screen exactly as it was. A | ||
| 935 | // SIGWINCH is neither — the terminal has ALREADY shrunk, so | ||
| 936 | // refusing leaves every rect pointing past the bottom of a screen | ||
| 937 | // that was just cleared. Degrade instead, to the view the wall | ||
| 938 | // already has for a terminal that holds one pane: the focused tile | ||
| 939 | // whole, the rest at 0x0, which paints nothing and claims no size. | ||
| 940 | // The tree is untouched, so growing back re-cuts every pane. | ||
| 941 | if (e == error.TooSmall and fs_arg == null) { | ||
| 942 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), @intCast(sel))) |only| { | ||
| 943 | cut = only; | ||
| 944 | // One visible pane draws no bar. Left at 1, `viewRows` | ||
| 945 | // would owe the daemon a row the tile does not have. | ||
| 946 | shared.label_rows = 0; | ||
| 947 | } else |_| {} | ||
| 948 | } | ||
| 949 | } | ||
| 950 | if (cut) |flat| { | ||
| 951 | if (shared.last_flat) |*old| old.deinit(shared.flat_alloc); | ||
| 952 | shared.last_flat = flat; | ||
| 953 | for (tiles, present) |*t, p| { | ||
| 954 | if (!p) continue; | ||
| 955 | if (flat.rectOf(@intCast(t.idx))) |r| { | ||
| 956 | t.rect = r; | ||
| 957 | } | ||
| 958 | t.resize_pending = true; | ||
| 959 | } | ||
| 960 | paintRailsLocked(shared, flat); | ||
| 961 | } else |_| {} | ||
| 962 | |||
| 963 | // The generation bump is what puts the rects back: every surviving | ||
| 964 | // pump repaints from its hot replica at its NEW rows, and the doorbell | ||
| 965 | // makes that immediate rather than one poll timeout away. | ||
| 966 | _ = shared.repaint_gen.fetchAdd(1, .release); | ||
| 967 | for (tiles, present) |*t, p| { | ||
| 968 | if (p) ring(t); | ||
| 969 | } | ||
| 970 | // ...except the tiles with no pump left to hear it: their bars are the | ||
| 971 | // keyboard's, the same rule `setFocus` paints a focus move by. No | ||
| 972 | // `label_rows` guard here, unlike there: the screen was just cleared, | ||
| 973 | // so on a one-tile wall of a dead tile that bar is the only thing left | ||
| 974 | // to say the target refused. | ||
| 975 | paintDeadBarsLocked(tiles); | ||
| 976 | } | ||
| 977 | |||
| 978 | /// A tile leaves the wall: off the `present` roll, off the tree, and its | 791 | /// A tile leaves the wall: off the `present` roll, off the tree, and its |
| 979 | /// pump told to return. The ONE owner — the chord and the poll's diff both | 792 | /// pump told to return. The ONE owner — the chord and the poll's diff both |
| 980 | /// come here, which is why the focus hand-off can only be written once. | 793 | /// come here, which is why the focus hand-off can only be written once. |
| @@ -1015,149 +828,6 @@ pub fn showsSelf( | |||
| 1015 | return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); | 828 | return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); |
| 1016 | } | 829 | } |
| 1017 | 830 | ||
| 1018 | /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each | ||
| 1019 | /// saved leaf id takes the first wall tile whose label matches its | ||
| 1020 | /// spelling, unmatched saved leaves collapse out, and unmatched wall tiles | ||
| 1021 | /// insert beside the first matched one. The saved root orientation wins | ||
| 1022 | /// over the aspect heuristic — `setRootOrient` is never called here. | ||
| 1023 | /// | ||
| 1024 | /// Pure of file I/O: bytes come in, the caller decides whether to feed | ||
| 1025 | /// them. False on any malformation, zero matches, or allocation failure — | ||
| 1026 | /// the caller builds today's default tree and the sidecar is ignored. | ||
| 1027 | fn restoreLayout( | ||
| 1028 | alloc: std.mem.Allocator, | ||
| 1029 | resolved: []const Resolved, | ||
| 1030 | shared: *Shared, | ||
| 1031 | bytes: []const u8, | ||
| 1032 | focus_out: *?u8, | ||
| 1033 | ) bool { | ||
| 1034 | var parsed = layout.parse(alloc, bytes) orelse return false; | ||
| 1035 | // Every early-false path deinits parsed; the success path moves the | ||
| 1036 | // tree out and deinits only the spellings. | ||
| 1037 | |||
| 1038 | const n_saved = parsed.spellings.items.len; | ||
| 1039 | const map = alloc.alloc(?u8, n_saved) catch { | ||
| 1040 | parsed.deinit(alloc); | ||
| 1041 | return false; | ||
| 1042 | }; | ||
| 1043 | defer alloc.free(map); | ||
| 1044 | |||
| 1045 | var taken = alloc.alloc(bool, resolved.len) catch { | ||
| 1046 | parsed.deinit(alloc); | ||
| 1047 | return false; | ||
| 1048 | }; | ||
| 1049 | defer alloc.free(taken); | ||
| 1050 | @memset(taken, false); | ||
| 1051 | |||
| 1052 | var any_match = false; | ||
| 1053 | for (0..n_saved) |i| { | ||
| 1054 | const spelling = parsed.spellings.items[i]; | ||
| 1055 | map[i] = null; | ||
| 1056 | for (resolved, 0..) |r, j| { | ||
| 1057 | if (!taken[j] and std.mem.eql(u8, r.label, spelling)) { | ||
| 1058 | taken[j] = true; | ||
| 1059 | map[i] = @intCast(j); | ||
| 1060 | any_match = true; | ||
| 1061 | break; | ||
| 1062 | } | ||
| 1063 | } | ||
| 1064 | } | ||
| 1065 | |||
| 1066 | // Zero matches: the sidecar describes a different wall entirely. | ||
| 1067 | // Deinit parsed and return false so the caller builds the default tree. | ||
| 1068 | if (!any_match) { | ||
| 1069 | parsed.deinit(alloc); | ||
| 1070 | return false; | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | // Map the saved focus through the spelling match before remapLeaves | ||
| 1074 | // consumes map. A null here means the focused leaf did not survive | ||
| 1075 | // (no wall tile matched its spelling) — the caller falls back to 0. | ||
| 1076 | if (parsed.focus) |k| { | ||
| 1077 | if (k < map.len) focus_out.* = map[k]; | ||
| 1078 | } | ||
| 1079 | |||
| 1080 | parsed.tree.remapLeaves(map); | ||
| 1081 | |||
| 1082 | // Unmatched wall indices, ascending, insert beside the first matched | ||
| 1083 | // tile — the same beside-focus placement the initial build uses. | ||
| 1084 | const first_tile: u8 = if (map.len > 0) blk: { | ||
| 1085 | var k: usize = 0; | ||
| 1086 | while (k < map.len) : (k += 1) { | ||
| 1087 | if (map[k] != null) break :blk map[k].?; | ||
| 1088 | } | ||
| 1089 | break :blk 0; | ||
| 1090 | } else 0; | ||
| 1091 | |||
| 1092 | var next_tile: u8 = first_tile; | ||
| 1093 | for (taken, 0..) |is_taken, j| { | ||
| 1094 | if (!is_taken) { | ||
| 1095 | const new_id: u8 = @intCast(j); | ||
| 1096 | parsed.tree.insert(next_tile, new_id) catch { | ||
| 1097 | parsed.deinit(alloc); | ||
| 1098 | return false; | ||
| 1099 | }; | ||
| 1100 | next_tile = new_id; | ||
| 1101 | } | ||
| 1102 | } | ||
| 1103 | |||
| 1104 | // Move the parsed tree into shared, deinit the old one first. | ||
| 1105 | shared.tree.deinit(); | ||
| 1106 | shared.tree = parsed.tree; | ||
| 1107 | // The spellings are no longer needed — the tree holds only ids now. | ||
| 1108 | for (parsed.spellings.items) |s| alloc.free(s); | ||
| 1109 | parsed.spellings.deinit(alloc); | ||
| 1110 | return true; | ||
| 1111 | } | ||
| 1112 | |||
| 1113 | /// `serialize` indexes spellings by leaf ID (tile index), so the array is | ||
| 1114 | /// tile-indexed: holes get "" and are never serialized (the tree dropped | ||
| 1115 | /// them). A failed write prints one stderr line and returns. | ||
| 1116 | fn saveLayoutTo( | ||
| 1117 | alloc: std.mem.Allocator, | ||
| 1118 | path: []const u8, | ||
| 1119 | tiles: []Tile, | ||
| 1120 | present: []const bool, | ||
| 1121 | shared: *Shared, | ||
| 1122 | ) void { | ||
| 1123 | const spellings = alloc.alloc([]const u8, tiles.len) catch return; | ||
| 1124 | defer alloc.free(spellings); | ||
| 1125 | for (tiles, present, 0..) |*t, p, i| { | ||
| 1126 | spellings[i] = if (p) t.r.label else ""; | ||
| 1127 | } | ||
| 1128 | var buf = std.ArrayListUnmanaged(u8){}; | ||
| 1129 | defer buf.deinit(alloc); | ||
| 1130 | // shared.sel is the focused tile index; serialize maps it to the | ||
| 1131 | // encounter index of its leaf in the depth-first walk. | ||
| 1132 | const focus: ?u8 = if (shared.sel < tiles.len and present[shared.sel]) @intCast(shared.sel) else null; | ||
| 1133 | shared.tree.serialize(spellings, focus, buf.writer(alloc)) catch |err| { | ||
| 1134 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | ||
| 1135 | return; | ||
| 1136 | }; | ||
| 1137 | wall.saveLayout(path, buf.items) catch |err| { | ||
| 1138 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | ||
| 1139 | }; | ||
| 1140 | } | ||
| 1141 | |||
| 1142 | /// Resolves the sidecar path from env and delegates to `saveLayoutTo`. | ||
| 1143 | fn saveSidecar( | ||
| 1144 | alloc: std.mem.Allocator, | ||
| 1145 | tiles: []Tile, | ||
| 1146 | present: []const bool, | ||
| 1147 | shared: *Shared, | ||
| 1148 | ) void { | ||
| 1149 | // A pipe has no stripes, so it has no layout worth remembering — and | ||
| 1150 | // a tree it saved would be a tree the next TERMINAL restores over the | ||
| 1151 | // aspect rule. See `restoreSidecar` for the cost of the other half. | ||
| 1152 | if (!shared.is_tty) return; | ||
| 1153 | const path = wall.layoutPath(alloc) catch |err| { | ||
| 1154 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | ||
| 1155 | return; | ||
| 1156 | }; | ||
| 1157 | defer alloc.free(path); | ||
| 1158 | saveLayoutTo(alloc, path, tiles, present, shared); | ||
| 1159 | } | ||
| 1160 | |||
| 1161 | /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an | 831 | /// Two tiles on the same daemon, as SPELLED. Identity dedup would need an |
| 1162 | /// endpoint handshake the spec deliberately refuses, so the same session | 832 | /// endpoint handshake the spec deliberately refuses, so the same session |
| 1163 | /// reached as `HOST#S` and as `quic://…#S` is two tiles — and the ring | 833 | /// reached as `HOST#S` and as `quic://…#S` is two tiles — and the ring |
| @@ -1331,7 +1001,7 @@ pub fn birthTile( | |||
| 1331 | alloc, | 1001 | alloc, |
| 1332 | shared.size.rows, | 1002 | shared.size.rows, |
| 1333 | shared.size.cols, | 1003 | shared.size.cols, |
| 1334 | wallFloors(new_live), | 1004 | wall_layout.wallFloors(new_live), |
| 1335 | null, | 1005 | null, |
| 1336 | ) catch { | 1006 | ) catch { |
| 1337 | shared.tree.remove(@intCast(at)); | 1007 | shared.tree.remove(@intCast(at)); |
| @@ -1693,88 +1363,6 @@ fn awaitDetach(t: *Tile, shared: *Shared) void { | |||
| 1693 | } | 1363 | } |
| 1694 | } | 1364 | } |
| 1695 | 1365 | ||
| 1696 | /// The saved layout over the tiles the hosts turned out to have; null | ||
| 1697 | /// when nothing matched. | ||
| 1698 | fn restoreSidecar( | ||
| 1699 | alloc: std.mem.Allocator, | ||
| 1700 | tiles: []Tile, | ||
| 1701 | present: []bool, | ||
| 1702 | live: usize, | ||
| 1703 | shared: *Shared, | ||
| 1704 | focus_out: *?usize, | ||
| 1705 | ) ?void { | ||
| 1706 | // Not on a pipe, and the cost is why. A restored tree is applied by | ||
| 1707 | // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending` | ||
| 1708 | // on EVERY present tile whether or not that tile's rect moved; each | ||
| 1709 | // pump then sends `.resize`, and `Server.onResize` answers every one of | ||
| 1710 | // them with `resyncSnapshot` — which has no "nothing changed" guard. | ||
| 1711 | // Measured on a scripted `mux quic://...` whose state home already held | ||
| 1712 | // a sidecar: a second full snapshot on the wire for a wall of one tile | ||
| 1713 | // that was already the right shape. The CLAIM is innocent; it stopped | ||
| 1714 | // sending a resize (see the pump's focus-claim block). | ||
| 1715 | if (!shared.is_tty) return null; | ||
| 1716 | const path = wall.layoutPath(alloc) catch return null; | ||
| 1717 | defer alloc.free(path); | ||
| 1718 | const bytes = wall.loadLayout(alloc, path) orelse return null; | ||
| 1719 | defer alloc.free(bytes); | ||
| 1720 | return restoreLayoutFrom(alloc, tiles, present, live, shared, bytes, focus_out); | ||
| 1721 | } | ||
| 1722 | |||
| 1723 | /// Pure of file I/O like `restoreLayout`: the chained id translations — | ||
| 1724 | /// saved→dense, then dense→real — are what a test must reach. | ||
| 1725 | fn restoreLayoutFrom( | ||
| 1726 | alloc: std.mem.Allocator, | ||
| 1727 | tiles: []Tile, | ||
| 1728 | present: []bool, | ||
| 1729 | live: usize, | ||
| 1730 | shared: *Shared, | ||
| 1731 | bytes: []const u8, | ||
| 1732 | focus_out: *?usize, | ||
| 1733 | ) ?void { | ||
| 1734 | const n = presentCount(present[0..live]); | ||
| 1735 | if (n == 0) return null; | ||
| 1736 | const dense = alloc.alloc(Resolved, n) catch return null; | ||
| 1737 | defer alloc.free(dense); | ||
| 1738 | const remap = alloc.alloc(?u8, n) catch return null; | ||
| 1739 | defer alloc.free(remap); | ||
| 1740 | var di: usize = 0; | ||
| 1741 | for (tiles[0..live], present[0..live], 0..) |*t, p, ti| { | ||
| 1742 | if (p) { | ||
| 1743 | dense[di] = t.r; | ||
| 1744 | remap[di] = @intCast(ti); | ||
| 1745 | di += 1; | ||
| 1746 | } | ||
| 1747 | } | ||
| 1748 | var dense_focus: ?u8 = null; | ||
| 1749 | if (restoreLayout(alloc, dense, shared, bytes, &dense_focus)) { | ||
| 1750 | // The tree's leaf ids are dense indices into `dense`; remap them | ||
| 1751 | // to the real tile indices relayout reads, and the saved focus | ||
| 1752 | // with them. | ||
| 1753 | shared.tree.remapLeaves(remap); | ||
| 1754 | if (dense_focus) |d| { | ||
| 1755 | if (d < remap.len) { | ||
| 1756 | if (remap[d]) |real| focus_out.* = real; | ||
| 1757 | } | ||
| 1758 | } | ||
| 1759 | return {}; | ||
| 1760 | } | ||
| 1761 | return null; | ||
| 1762 | } | ||
| 1763 | |||
| 1764 | /// The leaf a birth sits beside: `birthTile` inserts against a LEAF, and | ||
| 1765 | /// the focus can be a hole. | ||
| 1766 | pub fn anchorTile(present: []const bool, sel: usize) usize { | ||
| 1767 | if (sel < present.len and present[sel]) return sel; | ||
| 1768 | return firstPresent(present) orelse 0; | ||
| 1769 | } | ||
| 1770 | |||
| 1771 | pub fn firstPresent(present: []const bool) ?usize { | ||
| 1772 | for (present, 0..) |p, i| { | ||
| 1773 | if (p) return i; | ||
| 1774 | } | ||
| 1775 | return null; | ||
| 1776 | } | ||
| 1777 | |||
| 1778 | /// The session on this host that mux itself is running inside, if any. | 1366 | /// The session on this host that mux itself is running inside, if any. |
| 1779 | pub fn selfSession(target: client.Target, env_sock: ?[]const u8, env_session: ?[]const u8) ?[]const u8 { | 1367 | pub fn selfSession(target: client.Target, env_sock: ?[]const u8, env_session: ?[]const u8) ?[]const u8 { |
| 1780 | const es = env_session orelse return null; | 1368 | const es = env_session orelse return null; |
| @@ -1935,7 +1523,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1935 | alloc, | 1523 | alloc, |
| 1936 | size.rows, | 1524 | size.rows, |
| 1937 | size.cols, | 1525 | size.cols, |
| 1938 | wallFloors(@intFromBool(has_entry)), | 1526 | wall_layout.wallFloors(@intFromBool(has_entry)), |
| 1939 | null, | 1527 | null, |
| 1940 | ) catch { | 1528 | ) catch { |
| 1941 | std.debug.print("mux: terminal too small\n", .{}); | 1529 | std.debug.print("mux: terminal too small\n", .{}); |
| @@ -2055,7 +1643,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2055 | // A wall with no tile yet paints its one line rather than nothing: a | 1643 | // A wall with no tile yet paints its one line rather than nothing: a |
| 2056 | // blank terminal with no cursor reads as hung, and the hosts are up to | 1644 | // blank terminal with no cursor reads as hung, and the hosts are up to |
| 2057 | // a poll away from having anything to show. | 1645 | // a poll away from having anything to show. |
| 2058 | if (live == 0) relayout(alloc, tiles[0..live], present[0..live], &shared, 0); | 1646 | if (live == 0) wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, 0); |
| 2059 | 1647 | ||
| 2060 | // One poller per host, all of them at once and none of them on this | 1648 | // One poller per host, all of them at once and none of them on this |
| 2061 | // thread: the user asked for one session and must not be held on | 1649 | // thread: the user asked for one session and must not be held on |
| @@ -2166,7 +1754,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2166 | shared.paint_mu.lock(); | 1754 | shared.paint_mu.lock(); |
| 2167 | shared.size = measured2; | 1755 | shared.size = measured2; |
| 2168 | shared.paint_mu.unlock(); | 1756 | shared.paint_mu.unlock(); |
| 2169 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1757 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2170 | } | 1758 | } |
| 2171 | } | 1759 | } |
| 2172 | 1760 | ||
| @@ -2238,10 +1826,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2238 | setNotice(&shared, "[focus on a dead tile - no live neighbour]"); | 1826 | setNotice(&shared, "[focus on a dead tile - no live neighbour]"); |
| 2239 | } | 1827 | } |
| 2240 | vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back); | 1828 | vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back); |
| 2241 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1829 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2242 | }, | 1830 | }, |
| 2243 | .finish => |how| { | 1831 | .finish => |how| { |
| 2244 | saveSidecar(alloc, tiles[0..live], present[0..live], &shared); | 1832 | wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared); |
| 2245 | exit_code = how.code; | 1833 | exit_code = how.code; |
| 2246 | exit_msg = how.msg; | 1834 | exit_msg = how.msg; |
| 2247 | break :keys; | 1835 | break :keys; |
| @@ -2263,7 +1851,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2263 | if (all_reported or std.time.milliTimestamp() >= restore_due) { | 1851 | if (all_reported or std.time.milliTimestamp() >= restore_due) { |
| 2264 | restore_tried = true; | 1852 | restore_tried = true; |
| 2265 | var saved_focus: ?usize = null; | 1853 | var saved_focus: ?usize = null; |
| 2266 | if (live > 0 and restoreSidecar(alloc, tiles, present, live, &shared, &saved_focus) != null) { | 1854 | if (live > 0 and wall_layout.restoreSidecar(alloc, tiles, present, live, &shared, &saved_focus) != null) { |
| 2267 | restore_ok = true; | 1855 | restore_ok = true; |
| 2268 | // The entry tile is the one the user is already typing | 1856 | // The entry tile is the one the user is already typing |
| 2269 | // into; a saved focus record must not move them off it. | 1857 | // into; a saved focus record must not move them off it. |
| @@ -2275,9 +1863,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2275 | if (restore_tried and !restore_ok and !oriented and presentCount(present[0..live]) > 1) { | 1863 | if (restore_tried and !restore_ok and !oriented and presentCount(present[0..live]) > 1) { |
| 2276 | oriented = true; | 1864 | oriented = true; |
| 2277 | shared.paint_mu.lock(); | 1865 | shared.paint_mu.lock(); |
| 2278 | shared.tree.setRootOrient(rootOrient(shared.size)); | 1866 | shared.tree.setRootOrient(wall_layout.rootOrient(shared.size)); |
| 2279 | shared.paint_mu.unlock(); | 1867 | shared.paint_mu.unlock(); |
| 2280 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1868 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2281 | } | 1869 | } |
| 2282 | // An empty wall IS the picker: there is nothing else on the screen | 1870 | // An empty wall IS the picker: there is nothing else on the screen |
| 2283 | // to act from, and the last `x` is exactly when the user needs the | 1871 | // to act from, and the last `x` is exactly when the user needs the |
| @@ -2305,7 +1893,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2305 | picker_shown = false; | 1893 | picker_shown = false; |
| 2306 | shared.picker_open.store(false, .release); | 1894 | shared.picker_open.store(false, .release); |
| 2307 | shared.picker_stamp = 0; | 1895 | shared.picker_stamp = 0; |
| 2308 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1896 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2309 | }, | 1897 | }, |
| 2310 | .leave => {}, | 1898 | .leave => {}, |
| 2311 | } | 1899 | } |
| @@ -2420,7 +2008,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2420 | if (birth_at) |at| | 2008 | if (birth_at) |at| |
| 2421 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) | 2009 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) |
| 2422 | else | 2010 | else |
| 2423 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2011 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2424 | // The focused pump may be holding a claim the popup refused; | 2012 | // The focused pump may be holding a claim the popup refused; |
| 2425 | // it re-arms and retries on its next pass, and this is what | 2013 | // it re-arms and retries on its next pass, and this is what |
| 2426 | // makes that pass happen now rather than within a poll. | 2014 | // makes that pass happen now rather than within a poll. |
| @@ -2448,7 +2036,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2448 | }, | 2036 | }, |
| 2449 | .new_session, .split_right, .split_below => { | 2037 | .new_session, .split_right, .split_below => { |
| 2450 | setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]"); | 2038 | setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]"); |
| 2451 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2039 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2452 | }, | 2040 | }, |
| 2453 | else => {}, | 2041 | else => {}, |
| 2454 | } | 2042 | } |
| @@ -2525,7 +2113,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2525 | tiles[z].detach_req.store(true, .release); | 2113 | tiles[z].detach_req.store(true, .release); |
| 2526 | ring(&tiles[z]); | 2114 | ring(&tiles[z]); |
| 2527 | awaitDetach(&tiles[z], &shared); | 2115 | awaitDetach(&tiles[z], &shared); |
| 2528 | saveSidecar(alloc, tiles[0..live], present[0..live], &shared); | 2116 | wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared); |
| 2529 | exit_code = 0; | 2117 | exit_code = 0; |
| 2530 | exit_msg = "mux: detached (session still running; run mux to reattach)"; | 2118 | exit_msg = "mux: detached (session still running; run mux to reattach)"; |
| 2531 | break :keys; | 2119 | break :keys; |
| @@ -2534,7 +2122,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2534 | // Nothing left to fold in: every tile the wall will ever | 2122 | // Nothing left to fold in: every tile the wall will ever |
| 2535 | // have is already here or on its way from a host's list. | 2123 | // have is already here or on its way from a host's list. |
| 2536 | // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom. | 2124 | // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom. |
| 2537 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2125 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2538 | }, | 2126 | }, |
| 2539 | .new_session, .split_right, .split_below => { | 2127 | .new_session, .split_right, .split_below => { |
| 2540 | // Ask the focused tile's daemon for a new session. The | 2128 | // Ask the focused tile's daemon for a new session. The |
| @@ -2565,7 +2153,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2565 | .focus_dir => |d| { | 2153 | .focus_dir => |d| { |
| 2566 | const flat = shared.base_flat orelse shared.last_flat; | 2154 | const flat = shared.base_flat orelse shared.last_flat; |
| 2567 | if (flat) |f| { | 2155 | if (flat) |f| { |
| 2568 | if (layout.neighbor(f, @intCast(z), dirOf(d))) |nb| { | 2156 | if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| { |
| 2569 | if (nb < live and present[nb] and nb != z) { | 2157 | if (nb < live and present[nb] and nb != z) { |
| 2570 | if (shared.fullscreen) | 2158 | if (shared.fullscreen) |
| 2571 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb) | 2159 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb) |
| @@ -2577,10 +2165,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2577 | }, | 2165 | }, |
| 2578 | .fullscreen => { | 2166 | .fullscreen => { |
| 2579 | shared.fullscreen = !shared.fullscreen; | 2167 | shared.fullscreen = !shared.fullscreen; |
| 2580 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2168 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2581 | }, | 2169 | }, |
| 2582 | .resize => |d| { | 2170 | .resize => |d| { |
| 2583 | _ = doResize(alloc, tiles[0..live], present[0..live], &shared, z, d); | 2171 | _ = wall_layout.doResize(alloc, tiles[0..live], present[0..live], &shared, z, d); |
| 2584 | }, | 2172 | }, |
| 2585 | .focus => |idx| { | 2173 | .focus => |idx| { |
| 2586 | if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { | 2174 | if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { |
| @@ -2614,7 +2202,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2614 | break :keys; | 2202 | break :keys; |
| 2615 | } | 2203 | } |
| 2616 | setNotice(&shared, "[nothing attached there yet - tile closed]"); | 2204 | setNotice(&shared, "[nothing attached there yet - tile closed]"); |
| 2617 | relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2205 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); |
| 2618 | }, | 2206 | }, |
| 2619 | .none => {}, | 2207 | .none => {}, |
| 2620 | } | 2208 | } |
| @@ -2663,7 +2251,7 @@ test "the tree's stacked cut splits rows with the remainder at the top" { | |||
| 2663 | try tree.addFirst(0); | 2251 | try tree.addFirst(0); |
| 2664 | try tree.insert(0, 1); | 2252 | try tree.insert(0, 1); |
| 2665 | try tree.insert(1, 2); | 2253 | try tree.insert(1, 2); |
| 2666 | var f = try tree.flatten(alloc, 25, 80, wallFloors(3), null); | 2254 | var f = try tree.flatten(alloc, 25, 80, wall_layout.wallFloors(3), null); |
| 2667 | defer f.deinit(alloc); | 2255 | defer f.deinit(alloc); |
| 2668 | try std.testing.expectEqual(layout.Rect{ .top = 0, .left = 0, .rows = 9, .cols = 80 }, f.rectOf(0).?); | 2256 | try std.testing.expectEqual(layout.Rect{ .top = 0, .left = 0, .rows = 9, .cols = 80 }, f.rectOf(0).?); |
| 2669 | try std.testing.expectEqual(layout.Rect{ .top = 9, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(1).?); | 2257 | try std.testing.expectEqual(layout.Rect{ .top = 9, .left = 0, .rows = 8, .cols = 80 }, f.rectOf(1).?); |
| @@ -2677,7 +2265,7 @@ test "the tree refuses a wall that cannot show a content row" { | |||
| 2677 | // 12 tiles over 23 rows: 1 row each — a label with no content. | 2265 | // 12 tiles over 23 rows: 1 row each — a label with no content. |
| 2678 | try tree.addFirst(0); | 2266 | try tree.addFirst(0); |
| 2679 | for (1..12) |i| try tree.insert(@intCast(i - 1), @intCast(i)); | 2267 | for (1..12) |i| try tree.insert(@intCast(i - 1), @intCast(i)); |
| 2680 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 23, 80, wallFloors(12), null)); | 2268 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 23, 80, wall_layout.wallFloors(12), null)); |
| 2681 | } | 2269 | } |
| 2682 | 2270 | ||
| 2683 | test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { | 2271 | test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { |
| @@ -2689,11 +2277,11 @@ test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { | |||
| 2689 | // Two tiles on five rows: each rect is two rows, and under a label | 2277 | // Two tiles on five rows: each rect is two rows, and under a label |
| 2690 | // bar that is one content row — below `min_session_rows`, so the | 2278 | // bar that is one content row — below `min_session_rows`, so the |
| 2691 | // daemon refuses the resize and the tile freezes on a stale grid. | 2279 | // daemon refuses the resize and the tile freezes on a stale grid. |
| 2692 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 5, 80, wallFloors(2), null)); | 2280 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 5, 80, wall_layout.wallFloors(2), null)); |
| 2693 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 4, 80, wallFloors(2), null)); | 2281 | try std.testing.expectError(error.TooSmall, tree.flatten(alloc, 4, 80, wall_layout.wallFloors(2), null)); |
| 2694 | // Six rows is the first cut that fits: three a rect, two of them | 2282 | // Six rows is the first cut that fits: three a rect, two of them |
| 2695 | // content under the bar — exactly the daemon's row floor. | 2283 | // content under the bar — exactly the daemon's row floor. |
| 2696 | var f = try tree.flatten(alloc, 6, 80, wallFloors(2), null); | 2284 | var f = try tree.flatten(alloc, 6, 80, wall_layout.wallFloors(2), null); |
| 2697 | defer f.deinit(alloc); | 2285 | defer f.deinit(alloc); |
| 2698 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows); | 2286 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows); |
| 2699 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows); | 2287 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows); |
| @@ -2721,7 +2309,7 @@ test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { | |||
| 2721 | var one = layout.Tree.init(alloc); | 2309 | var one = layout.Tree.init(alloc); |
| 2722 | defer one.deinit(); | 2310 | defer one.deinit(); |
| 2723 | try one.addFirst(0); | 2311 | try one.addFirst(0); |
| 2724 | var of = try one.flatten(alloc, 2, 80, wallFloors(1), null); | 2312 | var of = try one.flatten(alloc, 2, 80, wall_layout.wallFloors(1), null); |
| 2725 | defer of.deinit(alloc); | 2313 | defer of.deinit(alloc); |
| 2726 | try std.testing.expectEqual(@as(u16, 2), of.rectOf(0).?.rows); | 2314 | try std.testing.expectEqual(@as(u16, 2), of.rectOf(0).?.rows); |
| 2727 | } | 2315 | } |
| @@ -4272,7 +3860,7 @@ test "birthTile: a beside wall admits more panes than rows/3" { | |||
| 4272 | try std.testing.expectEqual(@as(usize, n), shared.tree.count()); | 3860 | try std.testing.expectEqual(@as(usize, n), shared.tree.count()); |
| 4273 | // Admitted AND habitable: every pane keeps the full height and clears | 3861 | // Admitted AND habitable: every pane keeps the full height and clears |
| 4274 | // the column floor, which is what makes the refusal wrong. | 3862 | // the column floor, which is what makes the refusal wrong. |
| 4275 | const flat = try shared.tree.flatten(alloc, 24, 200, wallFloors(n), null); | 3863 | const flat = try shared.tree.flatten(alloc, 24, 200, wall_layout.wallFloors(n), null); |
| 4276 | defer flat.deinit(alloc); | 3864 | defer flat.deinit(alloc); |
| 4277 | try std.testing.expectEqual(@as(usize, n), flat.placed.len); | 3865 | try std.testing.expectEqual(@as(usize, n), flat.placed.len); |
| 4278 | for (flat.placed) |p| { | 3866 | for (flat.placed) |p| { |
| @@ -4928,7 +4516,7 @@ test "relayout sets resize_pending on every live tile" { | |||
| 4928 | .wake_w = -1, | 4516 | .wake_w = -1, |
| 4929 | }; | 4517 | }; |
| 4930 | } | 4518 | } |
| 4931 | relayout(alloc, tiles, present, &shared, 0); | 4519 | wall_layout.relayout(alloc, tiles, present, &shared, 0); |
| 4932 | // Two tiles: a label bar appears. | 4520 | // Two tiles: a label bar appears. |
| 4933 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 4521 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); |
| 4934 | // Every tile is doorbelled: its pump sends the new rect. | 4522 | // Every tile is doorbelled: its pump sends the new rect. |
| @@ -4997,7 +4585,7 @@ test "a relayout landing mid-pass is still sent" { | |||
| 4997 | shared.paint_mu.lock(); | 4585 | shared.paint_mu.lock(); |
| 4998 | shared.size = .{ .cols = 80, .rows = heights[i % heights.len] }; | 4586 | shared.size = .{ .cols = 80, .rows = heights[i % heights.len] }; |
| 4999 | shared.paint_mu.unlock(); | 4587 | shared.paint_mu.unlock(); |
| 5000 | relayout(alloc, tiles, &present, &shared, 0); | 4588 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5001 | 4589 | ||
| 5002 | // Quiesce: the pump owes nothing, and the pass that took the last | 4590 | // Quiesce: the pump owes nothing, and the pass that took the last |
| 5003 | // doorbell has finished. Only then is "what the daemon holds" a | 4591 | // doorbell has finished. Only then is "what the daemon holds" a |
| @@ -5106,7 +4694,7 @@ test "a terminal too small for the cut falls back to the focused pane, and grows | |||
| 5106 | shared.paint_mu.lock(); | 4694 | shared.paint_mu.lock(); |
| 5107 | shared.size = .{ .cols = 80, .rows = rows }; | 4695 | shared.size = .{ .cols = 80, .rows = rows }; |
| 5108 | shared.paint_mu.unlock(); | 4696 | shared.paint_mu.unlock(); |
| 5109 | relayout(alloc, tiles, present, &shared, 0); | 4697 | wall_layout.relayout(alloc, tiles, present, &shared, 0); |
| 5110 | // What the pumps do with the generation bump: repaint their bars. | 4698 | // What the pumps do with the generation bump: repaint their bars. |
| 5111 | for (tiles, present) |*t, p| if (p) paintLabel(t, .up); | 4699 | for (tiles, present) |*t, p| if (p) paintLabel(t, .up); |
| 5112 | screen.drain(); | 4700 | screen.drain(); |
| @@ -5170,7 +4758,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" { | |||
| 5170 | }; | 4758 | }; |
| 5171 | } | 4759 | } |
| 5172 | shared.fullscreen = true; | 4760 | shared.fullscreen = true; |
| 5173 | relayout(alloc, tiles, &present, &shared, 0); | 4761 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5174 | // No label bar: the fullscreened pane is a one-tile wall. | 4762 | // No label bar: the fullscreened pane is a one-tile wall. |
| 5175 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | 4763 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); |
| 5176 | // Tile 0 gets the whole terminal; tile 1 gets nothing. | 4764 | // Tile 0 gets the whole terminal; tile 1 gets nothing. |
| @@ -5206,9 +4794,9 @@ test "toggle off fullscreen restores the real rects" { | |||
| 5206 | }; | 4794 | }; |
| 5207 | } | 4795 | } |
| 5208 | shared.fullscreen = true; | 4796 | shared.fullscreen = true; |
| 5209 | relayout(alloc, tiles, &present, &shared, 0); | 4797 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5210 | shared.fullscreen = false; | 4798 | shared.fullscreen = false; |
| 5211 | relayout(alloc, tiles, &present, &shared, 0); | 4799 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5212 | // Two tiles again: label bar back, both have real rects (stacked: | 4800 | // Two tiles again: label bar back, both have real rects (stacked: |
| 5213 | // full width, half height each). | 4801 | // full width, half height each). |
| 5214 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 4802 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); |
| @@ -5242,7 +4830,7 @@ test "focus_dir while fullscreened follows the focus" { | |||
| 5242 | }; | 4830 | }; |
| 5243 | } | 4831 | } |
| 5244 | shared.fullscreen = true; | 4832 | shared.fullscreen = true; |
| 5245 | relayout(alloc, tiles, &present, &shared, 0); | 4833 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5246 | // Move focus to tile 1 while fullscreened. | 4834 | // Move focus to tile 1 while fullscreened. |
| 5247 | focusAnswer(alloc, tiles, &present, &shared, false, 1); | 4835 | focusAnswer(alloc, tiles, &present, &shared, false, 1); |
| 5248 | // The full rect followed: tile 1 now owns the terminal. | 4836 | // The full rect followed: tile 1 now owns the terminal. |
| @@ -5279,13 +4867,13 @@ test "resize: l grows the focused pane, h shrinks it" { | |||
| 5279 | .wake_w = -1, | 4867 | .wake_w = -1, |
| 5280 | }; | 4868 | }; |
| 5281 | } | 4869 | } |
| 5282 | relayout(alloc, tiles, &present, &shared, 0); | 4870 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5283 | const before = tiles[0].rect.cols; | 4871 | const before = tiles[0].rect.cols; |
| 5284 | // l (grow width): focus 0 gains from its right sibling. | 4872 | // l (grow width): focus 0 gains from its right sibling. |
| 5285 | try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .right)); | 4873 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right)); |
| 5286 | try std.testing.expect(tiles[0].rect.cols > before); | 4874 | try std.testing.expect(tiles[0].rect.cols > before); |
| 5287 | // h (shrink width): the right neighbor gains a cell back from focus. | 4875 | // h (shrink width): the right neighbor gains a cell back from focus. |
| 5288 | try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .left)); | 4876 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .left)); |
| 5289 | try std.testing.expectEqual(before, tiles[0].rect.cols); | 4877 | try std.testing.expectEqual(before, tiles[0].rect.cols); |
| 5290 | } | 4878 | } |
| 5291 | 4879 | ||
| @@ -5312,11 +4900,11 @@ test "resize: j grows height, k shrinks height" { | |||
| 5312 | .wake_w = -1, | 4900 | .wake_w = -1, |
| 5313 | }; | 4901 | }; |
| 5314 | } | 4902 | } |
| 5315 | relayout(alloc, tiles, &present, &shared, 0); | 4903 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5316 | const before = tiles[0].rect.rows; | 4904 | const before = tiles[0].rect.rows; |
| 5317 | try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .down)); | 4905 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .down)); |
| 5318 | try std.testing.expect(tiles[0].rect.rows > before); | 4906 | try std.testing.expect(tiles[0].rect.rows > before); |
| 5319 | try std.testing.expect(doResize(alloc, tiles, &present, &shared, 0, .up)); | 4907 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .up)); |
| 5320 | try std.testing.expectEqual(before, tiles[0].rect.rows); | 4908 | try std.testing.expectEqual(before, tiles[0].rect.rows); |
| 5321 | } | 4909 | } |
| 5322 | 4910 | ||
| @@ -5344,8 +4932,8 @@ test "resize refuses while fullscreened" { | |||
| 5344 | }; | 4932 | }; |
| 5345 | } | 4933 | } |
| 5346 | shared.fullscreen = true; | 4934 | shared.fullscreen = true; |
| 5347 | relayout(alloc, tiles, &present, &shared, 0); | 4935 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 5348 | try std.testing.expect(!doResize(alloc, tiles, &present, &shared, 0, .right)); | 4936 | try std.testing.expect(!wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right)); |
| 5349 | } | 4937 | } |
| 5350 | 4938 | ||
| 5351 | fn noticeBench(shared: *Shared, tiles: []Tile) void { | 4939 | fn noticeBench(shared: *Shared, tiles: []Tile) void { |
| @@ -5455,7 +5043,7 @@ test "a one-tile wall draws no label bar and paints row 1" { | |||
| 5455 | .wake_r = -1, | 5043 | .wake_r = -1, |
| 5456 | .wake_w = -1, | 5044 | .wake_w = -1, |
| 5457 | }; | 5045 | }; |
| 5458 | relayout(alloc, tiles, present, &shared, 0); | 5046 | wall_layout.relayout(alloc, tiles, present, &shared, 0); |
| 5459 | // One tile: no label bar, every row is content. | 5047 | // One tile: no label bar, every row is content. |
| 5460 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | 5048 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); |
| 5461 | try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); | 5049 | try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); |
| @@ -5674,7 +5262,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it | |||
| 5674 | }; | 5262 | }; |
| 5675 | } | 5263 | } |
| 5676 | const before = shared.repaint_gen.load(.acquire); | 5264 | const before = shared.repaint_gen.load(.acquire); |
| 5677 | relayout(alloc, tiles, present, &shared, 0); | 5265 | wall_layout.relayout(alloc, tiles, present, &shared, 0); |
| 5678 | // One bump, every tile's pump clears its drag on it: the stripes moved, | 5266 | // One bump, every tile's pump clears its drag on it: the stripes moved, |
| 5679 | // so a held drag's anchor names a rect that is somewhere else now. | 5267 | // so a held drag's anchor names a rect that is somewhere else now. |
| 5680 | try std.testing.expect(shared.repaint_gen.load(.acquire) > before); | 5268 | try std.testing.expect(shared.repaint_gen.load(.acquire) > before); |
| @@ -6249,7 +5837,7 @@ test "saveLayoutTo writes the sidecar for the present tiles, spellings verbatim" | |||
| 6249 | }; | 5837 | }; |
| 6250 | } | 5838 | } |
| 6251 | const present = [_]bool{ true, true }; | 5839 | const present = [_]bool{ true, true }; |
| 6252 | saveLayoutTo(alloc, path, tiles, &present, &shared); | 5840 | wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); |
| 6253 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; | 5841 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; |
| 6254 | defer alloc.free(got); | 5842 | defer alloc.free(got); |
| 6255 | try std.testing.expect(std.mem.startsWith(u8, got, "mux-layout 1\nbeside 0\n")); | 5843 | try std.testing.expect(std.mem.startsWith(u8, got, "mux-layout 1\nbeside 0\n")); |
| @@ -6299,7 +5887,7 @@ test "saveLayoutTo handles a vanished middle tile without panicking" { | |||
| 6299 | }; | 5887 | }; |
| 6300 | } | 5888 | } |
| 6301 | const present = [_]bool{ true, false, true }; | 5889 | const present = [_]bool{ true, false, true }; |
| 6302 | saveLayoutTo(alloc, path, tiles, &present, &shared); | 5890 | wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); |
| 6303 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; | 5891 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; |
| 6304 | defer alloc.free(got); | 5892 | defer alloc.free(got); |
| 6305 | // Tile 2's label is verbatim; the hole (tile 1) is never serialized. | 5893 | // Tile 2's label is verbatim; the hole (tile 1) is never serialized. |
| @@ -6322,7 +5910,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" { | |||
| 6322 | }; | 5910 | }; |
| 6323 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\n"; | 5911 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\n"; |
| 6324 | var focus_out: ?u8 = null; | 5912 | var focus_out: ?u8 = null; |
| 6325 | try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); | 5913 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); |
| 6326 | const tiles = try alloc.alloc(Tile, 2); | 5914 | const tiles = try alloc.alloc(Tile, 2); |
| 6327 | defer alloc.free(tiles); | 5915 | defer alloc.free(tiles); |
| 6328 | const present = [_]bool{ true, true }; | 5916 | const present = [_]bool{ true, true }; |
| @@ -6336,7 +5924,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" { | |||
| 6336 | .wake_w = -1, | 5924 | .wake_w = -1, |
| 6337 | }; | 5925 | }; |
| 6338 | } | 5926 | } |
| 6339 | relayout(alloc, tiles, &present, &shared, 0); | 5927 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); |
| 6340 | try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top); | 5928 | try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top); |
| 6341 | try std.testing.expect(tiles[1].rect.left > 0); | 5929 | try std.testing.expect(tiles[1].rect.left > 0); |
| 6342 | } | 5930 | } |
| @@ -6361,9 +5949,9 @@ test "restore: heals — unknown wall line inserted, lost leaf dropped" { | |||
| 6361 | const bytes = "mux-layout 1\nbeside 0\n leaf 26 --sock /tmp/x#a\n" ++ | 5949 | const bytes = "mux-layout 1\nbeside 0\n leaf 26 --sock /tmp/x#a\n" ++ |
| 6362 | " leaf 26 --sock /tmp/x#gone\n leaf 26 --sock /tmp/x#c\n"; | 5950 | " leaf 26 --sock /tmp/x#gone\n leaf 26 --sock /tmp/x#c\n"; |
| 6363 | var focus_out: ?u8 = null; | 5951 | var focus_out: ?u8 = null; |
| 6364 | try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); | 5952 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); |
| 6365 | try std.testing.expectEqual(@as(usize, 3), shared.tree.count()); | 5953 | try std.testing.expectEqual(@as(usize, 3), shared.tree.count()); |
| 6366 | const flat = try shared.tree.flatten(alloc, 24, 80, wallFloors(3), null); | 5954 | const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null); |
| 6367 | defer flat.deinit(alloc); | 5955 | defer flat.deinit(alloc); |
| 6368 | const a = flat.rectOf(0) orelse return error.TestUnexpectedResult; | 5956 | const a = flat.rectOf(0) orelse return error.TestUnexpectedResult; |
| 6369 | const c = flat.rectOf(1) orelse return error.TestUnexpectedResult; | 5957 | const c = flat.rectOf(1) orelse return error.TestUnexpectedResult; |
| @@ -6389,9 +5977,9 @@ test "restore: zero matches or garbage degrade to false" { | |||
| 6389 | .{ .target = .{ .sock = "/tmp/x" }, .label = "--sock /tmp/x#a", .session = "a" }, | 5977 | .{ .target = .{ .sock = "/tmp/x" }, .label = "--sock /tmp/x#a", .session = "a" }, |
| 6390 | }; | 5978 | }; |
| 6391 | var focus_out: ?u8 = null; | 5979 | var focus_out: ?u8 = null; |
| 6392 | try std.testing.expect(!restoreLayout(alloc, &resolved, &shared, "not a sidecar", &focus_out)); | 5980 | try std.testing.expect(!wall_layout.restoreLayout(alloc, &resolved, &shared, "not a sidecar", &focus_out)); |
| 6393 | const nomatch = "mux-layout 1\nleaf 0 --sock /nowhere#z\n"; | 5981 | const nomatch = "mux-layout 1\nleaf 0 --sock /nowhere#z\n"; |
| 6394 | try std.testing.expect(!restoreLayout(alloc, &resolved, &shared, nomatch, &focus_out)); | 5982 | try std.testing.expect(!wall_layout.restoreLayout(alloc, &resolved, &shared, nomatch, &focus_out)); |
| 6395 | } | 5983 | } |
| 6396 | 5984 | ||
| 6397 | test "restore: duplicate spellings pair positionally" { | 5985 | test "restore: duplicate spellings pair positionally" { |
| @@ -6412,12 +6000,12 @@ test "restore: duplicate spellings pair positionally" { | |||
| 6412 | // near-equal rects and hides in the leaf count. | 6000 | // near-equal rects and hides in the leaf count. |
| 6413 | const bytes = "mux-layout 1\nbeside 0\n leaf 59 --sock /tmp/x#dup\n leaf 20 --sock /tmp/x#dup\n"; | 6001 | const bytes = "mux-layout 1\nbeside 0\n leaf 59 --sock /tmp/x#dup\n leaf 20 --sock /tmp/x#dup\n"; |
| 6414 | var focus_out: ?u8 = null; | 6002 | var focus_out: ?u8 = null; |
| 6415 | try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); | 6003 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); |
| 6416 | try std.testing.expectEqual(@as(usize, 2), shared.tree.count()); | 6004 | try std.testing.expectEqual(@as(usize, 2), shared.tree.count()); |
| 6417 | // Pairing is the whole claim and it IS observable: saved leaf 0 owns | 6005 | // Pairing is the whole claim and it IS observable: saved leaf 0 owns |
| 6418 | // the left, wide slot, so it must be wall tile 0. Paired the other | 6006 | // the left, wide slot, so it must be wall tile 0. Paired the other |
| 6419 | // way round the count is still 2 and the tiles have swapped screens. | 6007 | // way round the count is still 2 and the tiles have swapped screens. |
| 6420 | const flat = try shared.tree.flatten(alloc, 24, 80, wallFloors(2), null); | 6008 | const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(2), null); |
| 6421 | defer flat.deinit(alloc); | 6009 | defer flat.deinit(alloc); |
| 6422 | const t0 = flat.rectOf(0) orelse return error.TestUnexpectedResult; | 6010 | const t0 = flat.rectOf(0) orelse return error.TestUnexpectedResult; |
| 6423 | const t1 = flat.rectOf(1) orelse return error.TestUnexpectedResult; | 6011 | const t1 = flat.rectOf(1) orelse return error.TestUnexpectedResult; |
| @@ -6460,9 +6048,9 @@ test "restoreLayoutFrom: dense sidecar indices land on the real tile indices" { | |||
| 6460 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++ | 6048 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++ |
| 6461 | " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n"; | 6049 | " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n"; |
| 6462 | var restored_focus: ?usize = null; | 6050 | var restored_focus: ?usize = null; |
| 6463 | restoreLayoutFrom(alloc, &tiles, &present, 4, &shared, bytes, &restored_focus) orelse | 6051 | wall_layout.restoreLayoutFrom(alloc, &tiles, &present, 4, &shared, bytes, &restored_focus) orelse |
| 6464 | return error.TestUnexpectedResult; | 6052 | return error.TestUnexpectedResult; |
| 6465 | const flat = try shared.tree.flatten(alloc, 24, 80, wallFloors(3), null); | 6053 | const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null); |
| 6466 | defer flat.deinit(alloc); | 6054 | defer flat.deinit(alloc); |
| 6467 | try std.testing.expectEqual(@as(?layout.Rect, null), flat.rectOf(1)); | 6055 | try std.testing.expectEqual(@as(?layout.Rect, null), flat.rectOf(1)); |
| 6468 | const a = flat.rectOf(0) orelse return error.TestUnexpectedResult; | 6056 | const a = flat.rectOf(0) orelse return error.TestUnexpectedResult; |
| @@ -6494,7 +6082,7 @@ test "restore: focus_out maps the saved focus through the spelling match" { | |||
| 6494 | }; | 6082 | }; |
| 6495 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\nfocus 1\n"; | 6083 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#b\nfocus 1\n"; |
| 6496 | var focus_out: ?u8 = null; | 6084 | var focus_out: ?u8 = null; |
| 6497 | try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); | 6085 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); |
| 6498 | try std.testing.expectEqual(@as(?u8, 1), focus_out); | 6086 | try std.testing.expectEqual(@as(?u8, 1), focus_out); |
| 6499 | } | 6087 | } |
| 6500 | 6088 | ||
| @@ -6512,7 +6100,7 @@ test "restore: focus_out maps the saved focus through the spelling match" { | |||
| 6512 | }; | 6100 | }; |
| 6513 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#gone\nfocus 1\n"; | 6101 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n leaf 39 --sock /tmp/x#gone\nfocus 1\n"; |
| 6514 | var focus_out: ?u8 = null; | 6102 | var focus_out: ?u8 = null; |
| 6515 | try std.testing.expect(restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); | 6103 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); |
| 6516 | try std.testing.expectEqual(@as(?u8, null), focus_out); | 6104 | try std.testing.expectEqual(@as(?u8, null), focus_out); |
| 6517 | } | 6105 | } |
| 6518 | } | 6106 | } |