131681f2
refactor: the wall is one value, not six parameters
a73x 2026-08-29 10:01
Commit message
docscheck.budget
| Old | New | ||
|---|---|---|---|
| @@ -51,7 +51,7 @@ 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 | 54 | wall_layout.zig 1432 |
| 55 | wall_test_harness.zig 0 | 55 | wall_test_harness.zig 0 |
| 56 | wall_test_host.zig 0 | 56 | wall_test_host.zig 0 |
| 57 | wall_test_picker.zig 0 | 57 | wall_test_picker.zig 0 |
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -15,6 +15,7 @@ const wall_layout = @import("wall_layout.zig"); | |||
| 15 | const wv = @import("wallview.zig"); | 15 | const wv = @import("wallview.zig"); |
| 16 | const Shared = wv.Shared; | 16 | const Shared = wv.Shared; |
| 17 | const Tile = wv.Tile; | 17 | const Tile = wv.Tile; |
| 18 | const Wall = wv.Wall; | ||
| 18 | 19 | ||
| 19 | pub const Resolved = struct { | 20 | pub const Resolved = struct { |
| 20 | target: client.Target, | 21 | target: client.Target, |
| @@ -418,23 +419,16 @@ pub fn pokeHost(host_table: []Host, t: *const Tile) void { | |||
| 418 | } | 419 | } |
| 419 | 420 | ||
| 420 | /// Every host with news, applied to the wall. True when any reported. | 421 | /// Every host with news, applied to the wall. True when any reported. |
| 421 | pub fn applyReadyLists( | 422 | pub fn applyReadyLists(w: Wall) bool { |
| 422 | alloc: std.mem.Allocator, | ||
| 423 | tiles: []Tile, | ||
| 424 | present: []bool, | ||
| 425 | live: *usize, | ||
| 426 | shared: *Shared, | ||
| 427 | host_table: []Host, | ||
| 428 | ) bool { | ||
| 429 | var news = false; | 423 | var news = false; |
| 430 | for (host_table, 0..) |*h, hi| { | 424 | for (w.hosts, 0..) |*h, hi| { |
| 431 | if (!h.list_ready.swap(false, .acq_rel)) continue; | 425 | if (!h.list_ready.swap(false, .acq_rel)) continue; |
| 432 | news = true; | 426 | news = true; |
| 433 | // A poll already in flight when the host was forgotten still lands. | 427 | // A poll already in flight when the host was forgotten still lands. |
| 434 | // Applying it would re-birth the tiles the forget just took off the | 428 | // Applying it would re-birth the tiles the forget just took off the |
| 435 | // wall, one poll later. | 429 | // wall, one poll later. |
| 436 | if (h.forgotten.load(.acquire)) continue; | 430 | if (h.forgotten.load(.acquire)) continue; |
| 437 | applyHostList(alloc, tiles, present, live, shared, host_table, hi); | 431 | applyHostList(w, hi); |
| 438 | h.applied = true; | 432 | h.applied = true; |
| 439 | } | 433 | } |
| 440 | return news; | 434 | return news; |
| @@ -442,16 +436,8 @@ pub fn applyReadyLists( | |||
| 442 | 436 | ||
| 443 | /// One host's list, applied to the wall. The keyboard thread only: it is | 437 | /// One host's list, applied to the wall. The keyboard thread only: it is |
| 444 | /// the single writer of the tile array and the layout tree. | 438 | /// the single writer of the tile array and the layout tree. |
| 445 | pub fn applyHostList( | 439 | pub fn applyHostList(w: Wall, hi: usize) void { |
| 446 | alloc: std.mem.Allocator, | 440 | const h = &w.hosts[hi]; |
| 447 | tiles: []Tile, | ||
| 448 | present: []bool, | ||
| 449 | live: *usize, | ||
| 450 | shared: *Shared, | ||
| 451 | host_table: []Host, | ||
| 452 | hi: usize, | ||
| 453 | ) void { | ||
| 454 | const h = &host_table[hi]; | ||
| 455 | const reachable = h.reachable.load(.acquire); | 441 | const reachable = h.reachable.load(.acquire); |
| 456 | var list_buf: [proto.sessions_text_max]u8 = undefined; | 442 | var list_buf: [proto.sessions_text_max]u8 = undefined; |
| 457 | var list: []const u8 = ""; | 443 | var list: []const u8 = ""; |
| @@ -460,7 +446,7 @@ pub fn applyHostList( | |||
| 460 | // wall has none, and a vanish can take the one there was — either way | 446 | // wall has none, and a vanish can take the one there was — either way |
| 461 | // the wall owes the tile it ends up with a `setFocus`, which is the only | 447 | // the wall owes the tile it ends up with a `setFocus`, which is the only |
| 462 | // thing that arms a claim. | 448 | // thing that arms a claim. |
| 463 | const had_focus = shared.sel < live.* and present[shared.sel]; | 449 | const had_focus = w.shared.sel < w.live.* and w.present[w.shared.sel]; |
| 464 | var changed = false; | 450 | var changed = false; |
| 465 | // Only a list DRIVES the diff. A host that has gone quiet keeps its | 451 | // Only a list DRIVES the diff. A host that has gone quiet keeps its |
| 466 | // tiles, which reconnect on their own; vanishing them on a failed poll | 452 | // tiles, which reconnect on their own; vanishing them on a failed poll |
| @@ -468,9 +454,9 @@ pub fn applyHostList( | |||
| 468 | if (reachable) { | 454 | if (reachable) { |
| 469 | var births = BirthNames{}; | 455 | var births = BirthNames{}; |
| 470 | var vanish = TileIdxs{}; | 456 | var vanish = TileIdxs{}; |
| 471 | planHostDiff(tiles[0..live.*], present[0..live.*], live.*, hi, list, h.self_name, &births, &vanish); | 457 | planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &vanish); |
| 472 | for (vanish.items[0..vanish.len]) |v| { | 458 | for (vanish.items[0..vanish.len]) |v| { |
| 473 | wv.vanishTile(tiles[0..live.*], present[0..live.*], shared, v, null); | 459 | wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null); |
| 474 | changed = true; | 460 | changed = true; |
| 475 | } | 461 | } |
| 476 | var placed: usize = 0; | 462 | var placed: usize = 0; |
| @@ -479,14 +465,14 @@ pub fn applyHostList( | |||
| 479 | // anchor, so anchoring every birth at the focus would lay a list of | 465 | // anchor, so anchoring every birth at the focus would lay a list of |
| 480 | // {b, c} out as c, b — a wall reading back-to-front against the | 466 | // {b, c} out as c, b — a wall reading back-to-front against the |
| 481 | // order its daemon reported, and against the digits the chords use. | 467 | // order its daemon reported, and against the digits the chords use. |
| 482 | var anchor = wall_layout.anchorTile(present[0..live.*], shared.sel); | 468 | var anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel); |
| 483 | // Stops at the FIRST refusal rather than retrying each name: the | 469 | // Stops at the FIRST refusal rather than retrying each name: the |
| 484 | // wall refuses for a reason that holds for the whole list (no slot, | 470 | // wall refuses for a reason that holds for the whole list (no slot, |
| 485 | // no room to cut), and this list comes back every second — a | 471 | // no room to cut), and this list comes back every second — a |
| 486 | // per-name retry is an insert, a flatten and an undo per name per | 472 | // per-name retry is an insert, a flatten and an undo per name per |
| 487 | // poll, forever. | 473 | // poll, forever. |
| 488 | while (placed < births.len) : (placed += 1) { | 474 | while (placed < births.len) : (placed += 1) { |
| 489 | const at = wv.birthTile(alloc, tiles, present, live, shared, .{ | 475 | const at = wv.birthTile(w, .{ |
| 490 | // Joins, never creates: the daemon already has this session, | 476 | // Joins, never creates: the daemon already has this session, |
| 491 | // and a sized attach on a live one would resize somebody. | 477 | // and a sized attach on a live one would resize somebody. |
| 492 | // The name is this poll's reply buffer until the wall takes | 478 | // The name is this poll's reply buffer until the wall takes |
| @@ -500,7 +486,7 @@ pub fn applyHostList( | |||
| 500 | .borrowed = true, | 486 | .borrowed = true, |
| 501 | }) orelse break; | 487 | }) orelse break; |
| 502 | anchor = at; | 488 | anchor = at; |
| 503 | wv.spawnPump(&tiles[at]); | 489 | wv.spawnPump(&w.tiles[at]); |
| 504 | changed = true; | 490 | changed = true; |
| 505 | } | 491 | } |
| 506 | const unplaced = births.dropped + (births.len - placed); | 492 | const unplaced = births.dropped + (births.len - placed); |
| @@ -508,13 +494,13 @@ pub fn applyHostList( | |||
| 508 | // daemon's sessions is a wall lying about what it is. | 494 | // daemon's sessions is a wall lying about what it is. |
| 509 | if (unplaced > 0) { | 495 | if (unplaced > 0) { |
| 510 | var buf: [48]u8 = undefined; | 496 | var buf: [48]u8 = undefined; |
| 511 | wv.setNoticeIdle(shared, std.fmt.bufPrint(&buf, "[+{d} not shown]", .{unplaced}) catch "[not shown]"); | 497 | wv.setNoticeIdle(w.shared, std.fmt.bufPrint(&buf, "[+{d} not shown]", .{unplaced}) catch "[not shown]"); |
| 512 | } | 498 | } |
| 513 | } | 499 | } |
| 514 | if ((!had_focus or shared.sel >= live.* or !present[shared.sel]) and | 500 | if ((!had_focus or w.shared.sel >= w.live.* or !w.present[w.shared.sel]) and |
| 515 | wv.presentCount(present[0..live.*]) > 0) | 501 | wv.presentCount(w.livePresent()) > 0) |
| 516 | wv.setFocus(tiles[0..live.*], shared, wall_layout.firstPresent(present[0..live.*]) orelse 0); | 502 | wv.setFocus(w.liveTiles(), w.shared, wall_layout.firstPresent(w.livePresent()) orelse 0); |
| 517 | if (changed) wall_layout.relayout(alloc, tiles[0..live.*], present[0..live.*], shared, shared.sel); | 503 | if (changed) wall_layout.relayout(w, w.shared.sel); |
| 518 | } | 504 | } |
| 519 | 505 | ||
| 520 | /// The host grammar's own spelling of a target, for a wall entered by | 506 | /// The host grammar's own spelling of a target, for a wall entered by |
src/tui/wall_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -12,6 +12,7 @@ const wv = @import("wallview.zig"); | |||
| 12 | const Resolved = wall_host.Resolved; | 12 | const Resolved = wall_host.Resolved; |
| 13 | const Shared = wv.Shared; | 13 | const Shared = wv.Shared; |
| 14 | const Tile = wv.Tile; | 14 | const Tile = wv.Tile; |
| 15 | const Wall = wv.Wall; | ||
| 15 | 16 | ||
| 16 | /// The daemon's row floor plus the label-bar arithmetic: a one-tile wall | 17 | /// 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 | /// draws no bar so its floor is `min_session_rows`; two or more tiles each |
| @@ -46,28 +47,21 @@ pub fn dirOf(d: interact.PrefixFilter.Dir) layout.Dir { | |||
| 46 | /// layout.resize always GAINS focus cells; shrink = grow a neighbor at | 47 | /// layout.resize always GAINS focus cells; shrink = grow a neighbor at |
| 47 | /// focus's expense. Fullscreen refuses — a hidden layout resizing | 48 | /// focus's expense. Fullscreen refuses — a hidden layout resizing |
| 48 | /// invisibly is surprise, not power. | 49 | /// invisibly is surprise, not power. |
| 49 | pub fn doResize( | 50 | pub fn doResize(w: Wall, sel: usize, d: interact.PrefixFilter.Dir) bool { |
| 50 | alloc: std.mem.Allocator, | 51 | if (w.shared.fullscreen) return false; |
| 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); | 52 | const ld = dirOf(d); |
| 59 | const grow = switch (ld) { | 53 | const grow = switch (ld) { |
| 60 | .right, .down => true, | 54 | .right, .down => true, |
| 61 | .left, .up => false, | 55 | .left, .up => false, |
| 62 | }; | 56 | }; |
| 63 | const flat = shared.base_flat orelse shared.last_flat orelse return false; | 57 | const flat = w.shared.base_flat orelse w.shared.last_flat orelse return false; |
| 64 | const focus_tile: u8 = @intCast(sel); | 58 | const focus_tile: u8 = @intCast(sel); |
| 65 | var moved = false; | 59 | var moved = false; |
| 66 | if (grow) { | 60 | if (grow) { |
| 67 | // Focus gains from the sibling toward `ld`; if none there (edge | 61 | // Focus gains from the sibling toward `ld`; if none there (edge |
| 68 | // pane), try the opposite side — gaining from either sibling | 62 | // pane), try the opposite side — gaining from either sibling |
| 69 | // widens or tallens the focus. | 63 | // widens or tallens the focus. |
| 70 | if (shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, ld, 1)) { | 64 | if (w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), focus_tile, ld, 1)) { |
| 71 | moved = true; | 65 | moved = true; |
| 72 | } else { | 66 | } else { |
| 73 | const opp = switch (ld) { | 67 | const opp = switch (ld) { |
| @@ -76,7 +70,7 @@ pub fn doResize( | |||
| 76 | .left => layout.Dir.right, | 70 | .left => layout.Dir.right, |
| 77 | .up => layout.Dir.down, | 71 | .up => layout.Dir.down, |
| 78 | }; | 72 | }; |
| 79 | moved = shared.tree.resize(alloc, shared.size.rows, shared.size.cols, wallFloors(tiles.len), focus_tile, opp, 1); | 73 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), focus_tile, opp, 1); |
| 80 | } | 74 | } |
| 81 | } else { | 75 | } else { |
| 82 | // Shrink: a neighbor on the same axis gains a cell from focus. | 76 | // Shrink: a neighbor on the same axis gains a cell from focus. |
| @@ -89,15 +83,15 @@ pub fn doResize( | |||
| 89 | .down => layout.Dir.up, | 83 | .down => layout.Dir.up, |
| 90 | }; | 84 | }; |
| 91 | if (layout.neighbor(flat, focus_tile, ld)) |nb| { | 85 | 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); | 86 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), nb, opp, 1); |
| 93 | } | 87 | } |
| 94 | if (!moved) { | 88 | if (!moved) { |
| 95 | if (layout.neighbor(flat, focus_tile, opp)) |nb| { | 89 | 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); | 90 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.liveTiles().len), nb, ld, 1); |
| 97 | } | 91 | } |
| 98 | } | 92 | } |
| 99 | } | 93 | } |
| 100 | if (moved) relayout(alloc, tiles, present, shared, sel); | 94 | if (moved) relayout(w, sel); |
| 101 | return moved; | 95 | return moved; |
| 102 | } | 96 | } |
| 103 | 97 | ||
| @@ -120,41 +114,35 @@ fn paintRailsLocked(shared: *Shared, flat: layout.Flat) void { | |||
| 120 | 114 | ||
| 121 | /// One `paint_mu` hold: no window where a pump paints rows that just | 115 | /// One `paint_mu` hold: no window where a pump paints rows that just |
| 122 | /// changed owner. | 116 | /// changed owner. |
| 123 | pub fn relayout( | 117 | pub fn relayout(w: Wall, sel: usize) void { |
| 124 | alloc: std.mem.Allocator, | 118 | w.shared.paint_mu.lock(); |
| 125 | tiles: []Tile, | 119 | defer w.shared.paint_mu.unlock(); |
| 126 | present: []const bool, | 120 | w.shared.sel = sel; |
| 127 | shared: *Shared, | ||
| 128 | sel: usize, | ||
| 129 | ) void { | ||
| 130 | shared.paint_mu.lock(); | ||
| 131 | defer shared.paint_mu.unlock(); | ||
| 132 | shared.sel = sel; | ||
| 133 | 121 | ||
| 134 | var live: usize = 0; | 122 | var live: usize = 0; |
| 135 | for (present) |p| { | 123 | for (w.livePresent()) |p| { |
| 136 | if (p) live += 1; | 124 | if (p) live += 1; |
| 137 | } | 125 | } |
| 138 | // A one-tile wall owns every row and draws no label bar; two or more | 126 | // 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 | 127 | // tiles each lose their top row to one. Fullscreen is the same: one |
| 140 | // visible pane, no bar — the plain client's byte stream. | 128 | // visible pane, no bar — the plain client's byte stream. |
| 141 | shared.label_rows = if (shared.fullscreen or live <= 1) 0 else 1; | 129 | w.shared.label_rows = if (w.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 {}; | 130 | if (w.shared.is_tty) proto.writeAllFd(w.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 | 131 | // The screen the popup was on has just been cleared, so the next paint |
| 144 | // owes it however unchanged its rows are. | 132 | // owes it however unchanged its rows are. |
| 145 | if (shared.picker_open.load(.acquire)) shared.picker_stamp = 0; | 133 | if (w.shared.picker_open.load(.acquire)) w.shared.picker_stamp = 0; |
| 146 | if (live == 0) { | 134 | if (live == 0) { |
| 147 | wv.paintEmptyWallLocked(shared); | 135 | wv.paintEmptyWallLocked(w.shared); |
| 148 | return; | 136 | return; |
| 149 | } | 137 | } |
| 150 | // The base flat (null) is always computed so `focus_dir` can read | 138 | // The base flat (null) is always computed so `focus_dir` can read |
| 151 | // adjacency from the real layout while fullscreened. | 139 | // adjacency from the real layout while fullscreened. |
| 152 | if (shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), null)) |base| { | 140 | if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(live), null)) |base| { |
| 153 | if (shared.base_flat) |*old| old.deinit(shared.flat_alloc); | 141 | if (w.shared.base_flat) |*old| old.deinit(w.shared.flat_alloc); |
| 154 | shared.base_flat = base; | 142 | w.shared.base_flat = base; |
| 155 | } else |_| {} | 143 | } else |_| {} |
| 156 | const fs_arg: ?u8 = if (shared.fullscreen) @intCast(sel) else null; | 144 | const fs_arg: ?u8 = if (w.shared.fullscreen) @intCast(sel) else null; |
| 157 | var cut = shared.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(live), fs_arg); | 145 | var cut = w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(live), fs_arg); |
| 158 | if (cut) |_| {} else |e| { | 146 | if (cut) |_| {} else |e| { |
| 159 | // A split, an insert, a resize key: those are OPERATIONS, the user | 147 | // A split, an insert, a resize key: those are OPERATIONS, the user |
| 160 | // asked, and refusing leaves the screen exactly as it was. A | 148 | // asked, and refusing leaves the screen exactly as it was. A |
| @@ -165,32 +153,32 @@ pub fn relayout( | |||
| 165 | // whole, the rest at 0x0, which paints nothing and claims no size. | 153 | // whole, the rest at 0x0, which paints nothing and claims no size. |
| 166 | // The tree is untouched, so growing back re-cuts every pane. | 154 | // The tree is untouched, so growing back re-cuts every pane. |
| 167 | if (e == error.TooSmall and fs_arg == null) { | 155 | 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| { | 156 | if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(live), @intCast(sel))) |only| { |
| 169 | cut = only; | 157 | cut = only; |
| 170 | // One visible pane draws no bar. Left at 1, `viewRows` | 158 | // One visible pane draws no bar. Left at 1, `viewRows` |
| 171 | // would owe the daemon a row the tile does not have. | 159 | // would owe the daemon a row the tile does not have. |
| 172 | shared.label_rows = 0; | 160 | w.shared.label_rows = 0; |
| 173 | } else |_| {} | 161 | } else |_| {} |
| 174 | } | 162 | } |
| 175 | } | 163 | } |
| 176 | if (cut) |flat| { | 164 | if (cut) |flat| { |
| 177 | if (shared.last_flat) |*old| old.deinit(shared.flat_alloc); | 165 | if (w.shared.last_flat) |*old| old.deinit(w.shared.flat_alloc); |
| 178 | shared.last_flat = flat; | 166 | w.shared.last_flat = flat; |
| 179 | for (tiles, present) |*t, p| { | 167 | for (w.liveTiles(), w.livePresent()) |*t, p| { |
| 180 | if (!p) continue; | 168 | if (!p) continue; |
| 181 | if (flat.rectOf(@intCast(t.idx))) |r| { | 169 | if (flat.rectOf(@intCast(t.idx))) |r| { |
| 182 | t.rect = r; | 170 | t.rect = r; |
| 183 | } | 171 | } |
| 184 | t.resize_pending = true; | 172 | t.resize_pending = true; |
| 185 | } | 173 | } |
| 186 | paintRailsLocked(shared, flat); | 174 | paintRailsLocked(w.shared, flat); |
| 187 | } else |_| {} | 175 | } else |_| {} |
| 188 | 176 | ||
| 189 | // The generation bump is what puts the rects back: every surviving | 177 | // 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 | 178 | // pump repaints from its hot replica at its NEW rows, and the doorbell |
| 191 | // makes that immediate rather than one poll timeout away. | 179 | // makes that immediate rather than one poll timeout away. |
| 192 | _ = shared.repaint_gen.fetchAdd(1, .release); | 180 | _ = w.shared.repaint_gen.fetchAdd(1, .release); |
| 193 | for (tiles, present) |*t, p| { | 181 | for (w.liveTiles(), w.livePresent()) |*t, p| { |
| 194 | if (p) wv.ring(t); | 182 | if (p) wv.ring(t); |
| 195 | } | 183 | } |
| 196 | // ...except the tiles with no pump left to hear it: their bars are the | 184 | // ...except the tiles with no pump left to hear it: their bars are the |
| @@ -198,7 +186,7 @@ pub fn relayout( | |||
| 198 | // `label_rows` guard here, unlike there: the screen was just cleared, | 186 | // `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 | 187 | // so on a one-tile wall of a dead tile that bar is the only thing left |
| 200 | // to say the target refused. | 188 | // to say the target refused. |
| 201 | wv.paintDeadBarsLocked(tiles); | 189 | wv.paintDeadBarsLocked(w.liveTiles()); |
| 202 | } | 190 | } |
| 203 | 191 | ||
| 204 | /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each | 192 | /// Rebuild the wall's tree from a saved sidecar, healing per leaf: each |
| @@ -326,34 +314,22 @@ pub fn saveLayoutTo( | |||
| 326 | } | 314 | } |
| 327 | 315 | ||
| 328 | /// Resolves the sidecar path from env and delegates to `saveLayoutTo`. | 316 | /// Resolves the sidecar path from env and delegates to `saveLayoutTo`. |
| 329 | pub fn saveSidecar( | 317 | pub fn saveSidecar(w: Wall) void { |
| 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 | 318 | // 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 | 319 | // 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. | 320 | // aspect rule. See `restoreSidecar` for the cost of the other half. |
| 338 | if (!shared.is_tty) return; | 321 | if (!w.shared.is_tty) return; |
| 339 | const path = wall.layoutPath(alloc) catch |err| { | 322 | const path = wall.layoutPath(w.alloc) catch |err| { |
| 340 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); | 323 | std.debug.print("mux: wall layout not saved: {s}\n", .{@errorName(err)}); |
| 341 | return; | 324 | return; |
| 342 | }; | 325 | }; |
| 343 | defer alloc.free(path); | 326 | defer w.alloc.free(path); |
| 344 | saveLayoutTo(alloc, path, tiles, present, shared); | 327 | saveLayoutTo(w.alloc, path, w.liveTiles(), w.livePresent(), w.shared); |
| 345 | } | 328 | } |
| 346 | 329 | ||
| 347 | /// The saved layout over the tiles the hosts turned out to have; null | 330 | /// The saved layout over the tiles the hosts turned out to have; null |
| 348 | /// when nothing matched. | 331 | /// when nothing matched. |
| 349 | pub fn restoreSidecar( | 332 | pub fn restoreSidecar(w: Wall, focus_out: *?usize) ?void { |
| 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 | 333 | // Not on a pipe, and the cost is why. A restored tree is applied by |
| 358 | // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending` | 334 | // `focusAnswer(recut = true)`, whose `relayout` flags `resize_pending` |
| 359 | // on EVERY present tile whether or not that tile's rect moved; each | 335 | // on EVERY present tile whether or not that tile's rect moved; each |
| @@ -363,33 +339,25 @@ pub fn restoreSidecar( | |||
| 363 | // a sidecar: a second full snapshot on the wire for a wall of one tile | 339 | // 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 | 340 | // that was already the right shape. The CLAIM is innocent; it stopped |
| 365 | // sending a resize (see the pump's focus-claim block). | 341 | // sending a resize (see the pump's focus-claim block). |
| 366 | if (!shared.is_tty) return null; | 342 | if (!w.shared.is_tty) return null; |
| 367 | const path = wall.layoutPath(alloc) catch return null; | 343 | const path = wall.layoutPath(w.alloc) catch return null; |
| 368 | defer alloc.free(path); | 344 | defer w.alloc.free(path); |
| 369 | const bytes = wall.loadLayout(alloc, path) orelse return null; | 345 | const bytes = wall.loadLayout(w.alloc, path) orelse return null; |
| 370 | defer alloc.free(bytes); | 346 | defer w.alloc.free(bytes); |
| 371 | return restoreLayoutFrom(alloc, tiles, present, live, shared, bytes, focus_out); | 347 | return restoreLayoutFrom(w, bytes, focus_out); |
| 372 | } | 348 | } |
| 373 | 349 | ||
| 374 | /// Pure of file I/O like `restoreLayout`: the chained id translations — | 350 | /// Pure of file I/O like `restoreLayout`: the chained id translations — |
| 375 | /// saved→dense, then dense→real — are what a test must reach. | 351 | /// saved→dense, then dense→real — are what a test must reach. |
| 376 | pub fn restoreLayoutFrom( | 352 | pub fn restoreLayoutFrom(w: Wall, bytes: []const u8, focus_out: *?usize) ?void { |
| 377 | alloc: std.mem.Allocator, | 353 | const n = wv.presentCount(w.livePresent()); |
| 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; | 354 | if (n == 0) return null; |
| 387 | const dense = alloc.alloc(Resolved, n) catch return null; | 355 | const dense = w.alloc.alloc(Resolved, n) catch return null; |
| 388 | defer alloc.free(dense); | 356 | defer w.alloc.free(dense); |
| 389 | const remap = alloc.alloc(?u8, n) catch return null; | 357 | const remap = w.alloc.alloc(?u8, n) catch return null; |
| 390 | defer alloc.free(remap); | 358 | defer w.alloc.free(remap); |
| 391 | var di: usize = 0; | 359 | var di: usize = 0; |
| 392 | for (tiles[0..live], present[0..live], 0..) |*t, p, ti| { | 360 | for (w.liveTiles(), w.livePresent(), 0..) |*t, p, ti| { |
| 393 | if (p) { | 361 | if (p) { |
| 394 | dense[di] = t.r; | 362 | dense[di] = t.r; |
| 395 | remap[di] = @intCast(ti); | 363 | remap[di] = @intCast(ti); |
| @@ -397,11 +365,11 @@ pub fn restoreLayoutFrom( | |||
| 397 | } | 365 | } |
| 398 | } | 366 | } |
| 399 | var dense_focus: ?u8 = null; | 367 | var dense_focus: ?u8 = null; |
| 400 | if (restoreLayout(alloc, dense, shared, bytes, &dense_focus)) { | 368 | if (restoreLayout(w.alloc, dense, w.shared, bytes, &dense_focus)) { |
| 401 | // The tree's leaf ids are dense indices into `dense`; remap them | 369 | // The tree's leaf ids are dense indices into `dense`; remap them |
| 402 | // to the real tile indices relayout reads, and the saved focus | 370 | // to the real tile indices relayout reads, and the saved focus |
| 403 | // with them. | 371 | // with them. |
| 404 | shared.tree.remapLeaves(remap); | 372 | w.shared.tree.remapLeaves(remap); |
| 405 | if (dense_focus) |d| { | 373 | if (dense_focus) |d| { |
| 406 | if (d < remap.len) { | 374 | if (d < remap.len) { |
| 407 | if (remap[d]) |real| focus_out.* = real; | 375 | if (remap[d]) |real| focus_out.* = real; |
src/tui/wall_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -15,6 +15,7 @@ const wv = @import("wallview.zig"); | |||
| 15 | const Host = wall_host.Host; | 15 | const Host = wall_host.Host; |
| 16 | const Shared = wv.Shared; | 16 | const Shared = wv.Shared; |
| 17 | const Tile = wv.Tile; | 17 | const Tile = wv.Tile; |
| 18 | const Wall = wv.Wall; | ||
| 18 | 19 | ||
| 19 | /// The widest row the picker draws. A spelling past it is cut, never | 20 | /// The widest row the picker draws. A spelling past it is cut, never |
| 20 | /// wrapped: a host list that reflows is one a digit cannot address. | 21 | /// wrapped: a host list that reflows is one a digit cannot address. |
| @@ -168,15 +169,7 @@ pub fn isPickAction(a: interact.PrefixFilter.Action) bool { | |||
| 168 | 169 | ||
| 169 | /// Enter or `c`: a new session on the SELECTED host, tile or no tile. | 170 | /// Enter or `c`: a new session on the SELECTED host, tile or no tile. |
| 170 | /// Null when nothing was made. | 171 | /// Null when nothing was made. |
| 171 | pub fn pickBirth( | 172 | pub fn pickBirth(w: Wall, sel: usize) ?usize { |
| 172 | alloc: std.mem.Allocator, | ||
| 173 | tiles: []Tile, | ||
| 174 | present: []bool, | ||
| 175 | live: *usize, | ||
| 176 | shared: *Shared, | ||
| 177 | host_table: []Host, | ||
| 178 | sel: usize, | ||
| 179 | ) ?usize { | ||
| 180 | // The tile creates on attach exactly as a chord-born one does: no side | 173 | // The tile creates on attach exactly as a chord-born one does: no side |
| 181 | // connection, and no second road onto the wall to keep in step. | 174 | // connection, and no second road onto the wall to keep in step. |
| 182 | // A forgotten host is off the rows and out of the file, and its poller | 175 | // A forgotten host is off the rows and out of the file, and its poller |
| @@ -184,14 +177,14 @@ pub fn pickBirth( | |||
| 184 | // would sit on the wall naming a machine the user has just removed. | 177 | // would sit on the wall naming a machine the user has just removed. |
| 185 | // `pickerNearest` cannot save this — with every row gone it returns the | 178 | // `pickerNearest` cannot save this — with every row gone it returns the |
| 186 | // selection unchanged. | 179 | // selection unchanged. |
| 187 | if (sel >= host_table.len or host_table[sel].forgotten.load(.acquire)) { | 180 | if (sel >= w.hosts.len or w.hosts[sel].forgotten.load(.acquire)) { |
| 188 | // The one key the footer advertises, on a wall with nothing to | 181 | // The one key the footer advertises, on a wall with nothing to |
| 189 | // birth on: an Enter that closes the popup and does nothing reads | 182 | // birth on: an Enter that closes the popup and does nothing reads |
| 190 | // as a broken key rather than as an empty hosts file. | 183 | // as a broken key rather than as an empty hosts file. |
| 191 | wv.setNotice(shared, "[no hosts to start a session on - a adds one]"); | 184 | wv.setNotice(w.shared, "[no hosts to start a session on - a adds one]"); |
| 192 | return null; | 185 | return null; |
| 193 | } | 186 | } |
| 194 | const h = &host_table[sel]; | 187 | const h = &w.hosts[sel]; |
| 195 | // Enter IS the ask, and this copy is where that is written down: the | 188 | // Enter IS the ask, and this copy is where that is written down: the |
| 196 | // row it lands on is often the one the poller calls unreachable, and | 189 | // row it lands on is often the one the poller calls unreachable, and |
| 197 | // starting that machine's daemon is what choosing it means. The SPEC | 190 | // starting that machine's daemon is what choosing it means. The SPEC |
| @@ -212,15 +205,15 @@ pub fn pickBirth( | |||
| 212 | // `c` chord would have landed on, reached without a pump to ask. | 205 | // `c` chord would have landed on, reached without a pump to ask. |
| 213 | var name_buf: [proto.session_name_max]u8 = undefined; | 206 | var name_buf: [proto.session_name_max]u8 = undefined; |
| 214 | const name = client.nextFreeName(&name_buf, list); | 207 | const name = client.nextFreeName(&name_buf, list); |
| 215 | const anchor = wall_layout.anchorTile(present[0..live.*], shared.sel); | 208 | const anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel); |
| 216 | const has_anchor = wv.presentCount(present[0..live.*]) > 0; | 209 | const has_anchor = wv.presentCount(w.livePresent()) > 0; |
| 217 | // `-A` is inherited only within one host. A chord inherits it because | 210 | // `-A` is inherited only within one host. A chord inherits it because |
| 218 | // the new session is on the machine the offer was already made to; the | 211 | // the new session is on the machine the offer was already made to; the |
| 219 | // picker can cross to a host the user never offered an agent, and a | 212 | // picker can cross to a host the user never offered an agent, and a |
| 220 | // popup must not be the thing that hands a stranger the keys. | 213 | // popup must not be the thing that hands a stranger the keys. |
| 221 | const agent = has_anchor and tiles[anchor].host != null and | 214 | const agent = has_anchor and w.tiles[anchor].host != null and |
| 222 | tiles[anchor].host.? == sel and tiles[anchor].r.agent; | 215 | w.tiles[anchor].host.? == sel and w.tiles[anchor].r.agent; |
| 223 | const at = wv.birthTile(alloc, tiles, present, live, shared, .{ | 216 | const at = wv.birthTile(w, .{ |
| 224 | // `name` is this stack's buffer until the wall takes the tile — | 217 | // `name` is this stack's buffer until the wall takes the tile — |
| 225 | // see `Birth.borrowed`. | 218 | // see `Birth.borrowed`. |
| 226 | .r = .{ .target = target, .label = "", .session = name, .agent = agent }, | 219 | .r = .{ .target = target, .label = "", .session = name, .agent = agent }, |
| @@ -234,10 +227,10 @@ pub fn pickBirth( | |||
| 234 | .host = sel, | 227 | .host = sel, |
| 235 | .borrowed = true, | 228 | .borrowed = true, |
| 236 | }) orelse { | 229 | }) orelse { |
| 237 | wv.setNotice(shared, "[no room on the wall for another session]"); | 230 | wv.setNotice(w.shared, "[no room on the wall for another session]"); |
| 238 | return null; | 231 | return null; |
| 239 | }; | 232 | }; |
| 240 | wv.spawnPump(&tiles[at]); | 233 | wv.spawnPump(&w.tiles[at]); |
| 241 | // The new session must not wait out a poll to be confirmed by the list | 234 | // The new session must not wait out a poll to be confirmed by the list |
| 242 | // that will also stop the diff from vanishing it. | 235 | // that will also stop the diff from vanishing it. |
| 243 | h.poke.store(true, .release); | 236 | h.poke.store(true, .release); |
| @@ -245,20 +238,11 @@ pub fn pickBirth( | |||
| 245 | } | 238 | } |
| 246 | 239 | ||
| 247 | /// `x`: the host leaves the file, its poller stops and its tiles go. | 240 | /// `x`: the host leaves the file, its poller stops and its tiles go. |
| 248 | pub fn pickForget( | 241 | pub fn pickForget(w: Wall, sel: usize, path: ?[]const u8) void { |
| 249 | alloc: std.mem.Allocator, | ||
| 250 | tiles: []Tile, | ||
| 251 | present: []bool, | ||
| 252 | live: usize, | ||
| 253 | shared: *Shared, | ||
| 254 | host_table: []Host, | ||
| 255 | sel: usize, | ||
| 256 | path: ?[]const u8, | ||
| 257 | ) void { | ||
| 258 | // The SESSIONS keep running: forgetting a daemon is `mux hosts rm` | 242 | // The SESSIONS keep running: forgetting a daemon is `mux hosts rm` |
| 259 | // typed from inside, and that ends nothing. | 243 | // typed from inside, and that ends nothing. |
| 260 | if (sel >= host_table.len) return; | 244 | if (sel >= w.hosts.len) return; |
| 261 | const h = &host_table[sel]; | 245 | const h = &w.hosts[sel]; |
| 262 | if (h.forgotten.load(.acquire)) return; | 246 | if (h.forgotten.load(.acquire)) return; |
| 263 | // The file first, for `addHost`'s reason: the wall the user is looking | 247 | // The file first, for `addHost`'s reason: the wall the user is looking |
| 264 | // at and the wall they get back next time are the same wall. | 248 | // at and the wall they get back next time are the same wall. |
| @@ -270,15 +254,15 @@ pub fn pickForget( | |||
| 270 | var gone_from_file = true; | 254 | var gone_from_file = true; |
| 271 | var why: ?anyerror = null; | 255 | var why: ?anyerror = null; |
| 272 | if (path) |p| { | 256 | if (path) |p| { |
| 273 | gone_from_file = hosts.forget(alloc, p, h.spec.spelling) catch |e| blk: { | 257 | gone_from_file = hosts.forget(w.alloc, p, h.spec.spelling) catch |e| blk: { |
| 274 | why = e; | 258 | why = e; |
| 275 | break :blk false; | 259 | break :blk false; |
| 276 | }; | 260 | }; |
| 277 | } | 261 | } |
| 278 | h.forgotten.store(true, .release); | 262 | h.forgotten.store(true, .release); |
| 279 | for (0..live) |i| { | 263 | for (0..w.live.*) |i| { |
| 280 | if (present[i] and wall_host.ownedBy(&tiles[i], sel)) | 264 | if (w.present[i] and wall_host.ownedBy(&w.tiles[i], sel)) |
| 281 | wv.vanishTile(tiles[0..live], present[0..live], shared, i, null); | 265 | wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, i, null); |
| 282 | } | 266 | } |
| 283 | var buf: [96]u8 = undefined; | 267 | var buf: [96]u8 = undefined; |
| 284 | const said = if (why) |e| | 268 | const said = if (why) |e| |
| @@ -287,7 +271,7 @@ pub fn pickForget( | |||
| 287 | std.fmt.bufPrint(&buf, "[{s} was not on the wall]", .{h.spec.spelling}) catch "[that host was not on the wall]" | 271 | std.fmt.bufPrint(&buf, "[{s} was not on the wall]", .{h.spec.spelling}) catch "[that host was not on the wall]" |
| 288 | else | 272 | else |
| 289 | std.fmt.bufPrint(&buf, "[forgot {s}]", .{h.spec.spelling}) catch "[forgot the host]"; | 273 | std.fmt.bufPrint(&buf, "[forgot {s}]", .{h.spec.spelling}) catch "[forgot the host]"; |
| 290 | wv.setNotice(shared, said); | 274 | wv.setNotice(w.shared, said); |
| 291 | } | 275 | } |
| 292 | 276 | ||
| 293 | /// The popup. Painted by the KEYBOARD thread, which is the only one that | 277 | /// The popup. Painted by the KEYBOARD thread, which is the only one that |
src/tui/wall_test_harness.zig
| Old | New | ||
|---|---|---|---|
| @@ -14,6 +14,37 @@ const Host = wall_host.Host; | |||
| 14 | const Shared = wv.Shared; | 14 | const Shared = wv.Shared; |
| 15 | const Tile = wv.Tile; | 15 | const Tile = wv.Tile; |
| 16 | 16 | ||
| 17 | /// The `Wall` the product passes around, from a test's own arrays. | ||
| 18 | pub fn wallOf( | ||
| 19 | alloc: std.mem.Allocator, | ||
| 20 | tiles: []Tile, | ||
| 21 | present: []bool, | ||
| 22 | live: *usize, | ||
| 23 | shared: *Shared, | ||
| 24 | hosts: []Host, | ||
| 25 | ) wv.Wall { | ||
| 26 | return .{ .alloc = alloc, .tiles = tiles, .present = present, .live = live, .shared = shared, .hosts = hosts }; | ||
| 27 | } | ||
| 28 | |||
| 29 | /// The cell `wallLive` points at. File-scope because a layout test builds | ||
| 30 | /// its wall per call and Zig runs the tests in one process, one at a time; | ||
| 31 | /// nothing here births, so nothing writes it but the call being made. The | ||
| 32 | /// nearer trap is intra-test: two `wallLive` calls with different `live` | ||
| 33 | /// share this cell, and the first wall then reads the second's count. | ||
| 34 | var standing_live: usize = 0; | ||
| 35 | |||
| 36 | /// A wall of `live` slots over arrays a test declared, with no hosts: what | ||
| 37 | /// the layout and focus calls take, none of which grows the wall. | ||
| 38 | pub fn wallLive(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: usize, shared: *Shared) wv.Wall { | ||
| 39 | standing_live = live; | ||
| 40 | return wallOf(alloc, tiles, present, &standing_live, shared, &.{}); | ||
| 41 | } | ||
| 42 | |||
| 43 | /// `wallLive` where every slot the test declared is on the wall. | ||
| 44 | pub fn wallAll(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, shared: *Shared) wv.Wall { | ||
| 45 | return wallLive(alloc, tiles, present, tiles.len, shared); | ||
| 46 | } | ||
| 47 | |||
| 17 | /// Three tiles: two sessions on host 0 and a same-named one on host 1, so a | 48 | /// Three tiles: two sessions on host 0 and a same-named one on host 1, so a |
| 18 | /// diff that aliased by NAME alone would be caught. | 49 | /// diff that aliased by NAME alone would be caught. |
| 19 | pub fn diffFixture(shared: *Shared) [3]Tile { | 50 | pub fn diffFixture(shared: *Shared) [3]Tile { |
src/tui/wall_test_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -151,7 +151,7 @@ test "applyReadyLists: a host added after the wall opened gets its sessions, and | |||
| 151 | fixture.setList(&table[2], "late\n"); | 151 | fixture.setList(&table[2], "late\n"); |
| 152 | table[2].list_ready.store(true, .release); | 152 | table[2].list_ready.store(true, .release); |
| 153 | 153 | ||
| 154 | _ = wall_host.applyReadyLists(alloc, &tiles, &present, &live, &shared, table[0..3]); | 154 | _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3])); |
| 155 | 155 | ||
| 156 | // The added host's session is a tile, and only the host that reported is | 156 | // The added host's session is a tile, and only the host that reported is |
| 157 | // marked: the two that opened the wall are still owed a first list. | 157 | // marked: the two that opened the wall are still owed a first list. |
| @@ -205,8 +205,8 @@ test "applyHostList: a host with no live session gets no tile — the wall shows | |||
| 205 | 205 | ||
| 206 | // Twice, because a placeholder that is born once is still a placeholder. | 206 | // Twice, because a placeholder that is born once is still a placeholder. |
| 207 | for (0..2) |_| { | 207 | for (0..2) |_| { |
| 208 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 208 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 209 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 1); | 209 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); |
| 210 | } | 210 | } |
| 211 | 211 | ||
| 212 | try std.testing.expectEqual(@as(usize, 0), live); | 212 | try std.testing.expectEqual(@as(usize, 0), live); |
| @@ -214,7 +214,7 @@ test "applyHostList: a host with no live session gets no tile — the wall shows | |||
| 214 | 214 | ||
| 215 | // A session on the empty host, and the wall has exactly the one tile. | 215 | // A session on the empty host, and the wall has exactly the one tile. |
| 216 | fixture.setList(&table[1], "a\n"); | 216 | fixture.setList(&table[1], "a\n"); |
| 217 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 1); | 217 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); |
| 218 | 218 | ||
| 219 | try std.testing.expectEqual(@as(usize, 1), wv.presentCount(present[0..live])); | 219 | try std.testing.expectEqual(@as(usize, 1), wv.presentCount(present[0..live])); |
| 220 | try std.testing.expectEqualStrings("a", tiles[0].r.session); | 220 | try std.testing.expectEqualStrings("a", tiles[0].r.session); |
| @@ -235,7 +235,7 @@ test "applyHostList: a live tile survives one list that lost its session, and go | |||
| 235 | // grace is per tile and not a wall-wide pause. | 235 | // grace is per tile and not a wall-wide pause. |
| 236 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | 236 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; |
| 237 | fixture.setList(&table[0], "a\nb\n"); | 237 | fixture.setList(&table[0], "a\nb\n"); |
| 238 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 238 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 239 | try std.testing.expectEqual(@as(usize, 2), live); | 239 | try std.testing.expectEqual(@as(usize, 2), live); |
| 240 | 240 | ||
| 241 | // The wall's pumps are stopped in this fixture, so the liveness the rule | 241 | // The wall's pumps are stopped in this fixture, so the liveness the rule |
| @@ -243,13 +243,13 @@ test "applyHostList: a live tile survives one list that lost its session, and go | |||
| 243 | tiles[0].alive.store(true, .release); | 243 | tiles[0].alive.store(true, .release); |
| 244 | tiles[1].alive.store(true, .release); | 244 | tiles[1].alive.store(true, .release); |
| 245 | fixture.setList(&table[0], "a\n"); | 245 | fixture.setList(&table[0], "a\n"); |
| 246 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 246 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 247 | // Still there: this list may have overtaken an `exit_status` the pump | 247 | // Still there: this list may have overtaken an `exit_status` the pump |
| 248 | // has not read yet, and that code is the run's own on a wall of one. | 248 | // has not read yet, and that code is the run's own on a wall of one. |
| 249 | try std.testing.expect(present[1]); | 249 | try std.testing.expect(present[1]); |
| 250 | try std.testing.expect(present[0]); | 250 | try std.testing.expect(present[0]); |
| 251 | 251 | ||
| 252 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 252 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 253 | try std.testing.expect(!present[1]); | 253 | try std.testing.expect(!present[1]); |
| 254 | try std.testing.expect(present[0]); | 254 | try std.testing.expect(present[0]); |
| 255 | } | 255 | } |
| @@ -267,7 +267,7 @@ test "applyHostList: two sessions on an empty wall are two tiles, the first focu | |||
| 267 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | 267 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; |
| 268 | fixture.setList(&table[0], "a\nb\n"); | 268 | fixture.setList(&table[0], "a\nb\n"); |
| 269 | 269 | ||
| 270 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 270 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 271 | 271 | ||
| 272 | // The first birth has no leaf to sit beside — an empty tree takes it as | 272 | // The first birth has no leaf to sit beside — an empty tree takes it as |
| 273 | // its root, and the second inserts against it. | 273 | // its root, and the second inserts against it. |
| @@ -297,7 +297,7 @@ test "applyHostList: one list's tiles are laid out in the order the daemon repor | |||
| 297 | // its two siblings. | 297 | // its two siblings. |
| 298 | fixture.setList(&table[0], "a\nb\nc\n"); | 298 | fixture.setList(&table[0], "a\nb\nc\n"); |
| 299 | 299 | ||
| 300 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 300 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 301 | 301 | ||
| 302 | try std.testing.expectEqual(@as(usize, 3), live); | 302 | try std.testing.expectEqual(@as(usize, 3), live); |
| 303 | // Down the screen in the daemon's own order. The tile ARRAY is in that | 303 | // Down the screen in the daemon's own order. The tile ARRAY is in that |
| @@ -345,7 +345,7 @@ test "applyHostList: sessions past the wall's capacity are counted in the notice | |||
| 345 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | 345 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; |
| 346 | fixture.setList(&table[0], text.items); | 346 | fixture.setList(&table[0], text.items); |
| 347 | 347 | ||
| 348 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 348 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 349 | 349 | ||
| 350 | try std.testing.expectEqual(@as(usize, wv.max_tiles), live); | 350 | try std.testing.expectEqual(@as(usize, wv.max_tiles), live); |
| 351 | var buf: [96]u8 = undefined; | 351 | var buf: [96]u8 = undefined; |
| @@ -377,7 +377,7 @@ test "applyHostList: a wall too thin for a second pane takes what fits and retai | |||
| 377 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | 377 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; |
| 378 | fixture.setList(&table[0], "a\nb\n"); | 378 | fixture.setList(&table[0], "a\nb\n"); |
| 379 | 379 | ||
| 380 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 380 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 381 | 381 | ||
| 382 | try std.testing.expectEqual(@as(usize, 1), live); | 382 | try std.testing.expectEqual(@as(usize, 1), live); |
| 383 | try std.testing.expectEqualStrings("a", tiles[0].r.session); | 383 | try std.testing.expectEqualStrings("a", tiles[0].r.session); |
| @@ -411,7 +411,7 @@ test "applyHostList: when a host's last session goes the wall empties, and nothi | |||
| 411 | // The daemon has nothing left: a restart, or another client ended it. | 411 | // The daemon has nothing left: a restart, or another client ended it. |
| 412 | fixture.setList(&table[0], ""); | 412 | fixture.setList(&table[0], ""); |
| 413 | 413 | ||
| 414 | wall_host.applyHostList(alloc, &tiles, &present, &live, &shared, &table, 0); | 414 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 415 | 415 | ||
| 416 | // The host is up with nothing on it, which the wall says by showing | 416 | // The host is up with nothing on it, which the wall says by showing |
| 417 | // nothing: no placeholder tile takes the gone session's place. | 417 | // nothing: no placeholder tile takes the gone session's place. |
src/tui/wall_test_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -146,7 +146,7 @@ test "relayout sets resize_pending on every live tile" { | |||
| 146 | .wake_w = -1, | 146 | .wake_w = -1, |
| 147 | }; | 147 | }; |
| 148 | } | 148 | } |
| 149 | wall_layout.relayout(alloc, tiles, present, &shared, 0); | 149 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); |
| 150 | // Two tiles: a label bar appears. | 150 | // Two tiles: a label bar appears. |
| 151 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 151 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); |
| 152 | // Every tile is doorbelled: its pump sends the new rect. | 152 | // Every tile is doorbelled: its pump sends the new rect. |
| @@ -166,7 +166,7 @@ test "a relayout landing mid-pass is still sent" { | |||
| 166 | try shared.tree.insert(0, 1); | 166 | try shared.tree.insert(0, 1); |
| 167 | const tiles = try alloc.alloc(Tile, 2); | 167 | const tiles = try alloc.alloc(Tile, 2); |
| 168 | defer alloc.free(tiles); | 168 | defer alloc.free(tiles); |
| 169 | const present = [_]bool{ true, true }; | 169 | var present = [_]bool{ true, true }; |
| 170 | for (tiles, 0..) |*t, i| { | 170 | for (tiles, 0..) |*t, i| { |
| 171 | t.* = Tile{ | 171 | t.* = Tile{ |
| 172 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 172 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -194,7 +194,7 @@ test "a relayout landing mid-pass is still sent" { | |||
| 194 | shared.paint_mu.lock(); | 194 | shared.paint_mu.lock(); |
| 195 | shared.size = .{ .cols = 80, .rows = heights[i % heights.len] }; | 195 | shared.size = .{ .cols = 80, .rows = heights[i % heights.len] }; |
| 196 | shared.paint_mu.unlock(); | 196 | shared.paint_mu.unlock(); |
| 197 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 197 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 198 | 198 | ||
| 199 | // Quiesce: the pump owes nothing, and the pass that took the last | 199 | // Quiesce: the pump owes nothing, and the pass that took the last |
| 200 | // doorbell has finished. Only then is "what the daemon holds" a | 200 | // doorbell has finished. Only then is "what the daemon holds" a |
| @@ -263,7 +263,7 @@ test "a terminal too small for the cut falls back to the focused pane, and grows | |||
| 263 | shared.paint_mu.lock(); | 263 | shared.paint_mu.lock(); |
| 264 | shared.size = .{ .cols = 80, .rows = rows }; | 264 | shared.size = .{ .cols = 80, .rows = rows }; |
| 265 | shared.paint_mu.unlock(); | 265 | shared.paint_mu.unlock(); |
| 266 | wall_layout.relayout(alloc, tiles, present, &shared, 0); | 266 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); |
| 267 | // What the pumps do with the generation bump: repaint their bars. | 267 | // What the pumps do with the generation bump: repaint their bars. |
| 268 | for (tiles, present) |*t, p| if (p) wv.paintLabel(t, .up); | 268 | for (tiles, present) |*t, p| if (p) wv.paintLabel(t, .up); |
| 269 | screen.drain(); | 269 | screen.drain(); |
| @@ -315,7 +315,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" { | |||
| 315 | try shared.tree.insert(0, 1); | 315 | try shared.tree.insert(0, 1); |
| 316 | const tiles = try alloc.alloc(Tile, 2); | 316 | const tiles = try alloc.alloc(Tile, 2); |
| 317 | defer alloc.free(tiles); | 317 | defer alloc.free(tiles); |
| 318 | const present = [_]bool{ true, true }; | 318 | var present = [_]bool{ true, true }; |
| 319 | for (tiles, 0..) |*t, i| { | 319 | for (tiles, 0..) |*t, i| { |
| 320 | t.* = Tile{ | 320 | t.* = Tile{ |
| 321 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 321 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -327,7 +327,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" { | |||
| 327 | }; | 327 | }; |
| 328 | } | 328 | } |
| 329 | shared.fullscreen = true; | 329 | shared.fullscreen = true; |
| 330 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 330 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 331 | // No label bar: the fullscreened pane is a one-tile wall. | 331 | // No label bar: the fullscreened pane is a one-tile wall. |
| 332 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | 332 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); |
| 333 | // Tile 0 gets the whole terminal; tile 1 gets nothing. | 333 | // Tile 0 gets the whole terminal; tile 1 gets nothing. |
| @@ -351,7 +351,7 @@ test "toggle off fullscreen restores the real rects" { | |||
| 351 | try shared.tree.insert(0, 1); | 351 | try shared.tree.insert(0, 1); |
| 352 | const tiles = try alloc.alloc(Tile, 2); | 352 | const tiles = try alloc.alloc(Tile, 2); |
| 353 | defer alloc.free(tiles); | 353 | defer alloc.free(tiles); |
| 354 | const present = [_]bool{ true, true }; | 354 | var present = [_]bool{ true, true }; |
| 355 | for (tiles, 0..) |*t, i| { | 355 | for (tiles, 0..) |*t, i| { |
| 356 | t.* = Tile{ | 356 | t.* = Tile{ |
| 357 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 357 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -363,9 +363,9 @@ test "toggle off fullscreen restores the real rects" { | |||
| 363 | }; | 363 | }; |
| 364 | } | 364 | } |
| 365 | shared.fullscreen = true; | 365 | shared.fullscreen = true; |
| 366 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 366 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 367 | shared.fullscreen = false; | 367 | shared.fullscreen = false; |
| 368 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 368 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 369 | // Two tiles again: label bar back, both have real rects (stacked: | 369 | // Two tiles again: label bar back, both have real rects (stacked: |
| 370 | // full width, half height each). | 370 | // full width, half height each). |
| 371 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 371 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); |
| @@ -387,7 +387,7 @@ test "focus_dir while fullscreened follows the focus" { | |||
| 387 | try shared.tree.insert(0, 1); | 387 | try shared.tree.insert(0, 1); |
| 388 | const tiles = try alloc.alloc(Tile, 2); | 388 | const tiles = try alloc.alloc(Tile, 2); |
| 389 | defer alloc.free(tiles); | 389 | defer alloc.free(tiles); |
| 390 | const present = [_]bool{ true, true }; | 390 | var present = [_]bool{ true, true }; |
| 391 | for (tiles, 0..) |*t, i| { | 391 | for (tiles, 0..) |*t, i| { |
| 392 | t.* = Tile{ | 392 | t.* = Tile{ |
| 393 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 393 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -399,9 +399,9 @@ test "focus_dir while fullscreened follows the focus" { | |||
| 399 | }; | 399 | }; |
| 400 | } | 400 | } |
| 401 | shared.fullscreen = true; | 401 | shared.fullscreen = true; |
| 402 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 402 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 403 | // Move focus to tile 1 while fullscreened. | 403 | // Move focus to tile 1 while fullscreened. |
| 404 | wv.focusAnswer(alloc, tiles, &present, &shared, false, 1); | 404 | wv.focusAnswer(fixture.wallAll(alloc, tiles, &present, &shared), false, 1); |
| 405 | // The full rect followed: tile 1 now owns the terminal. | 405 | // The full rect followed: tile 1 now owns the terminal. |
| 406 | try std.testing.expectEqual(@as(u16, 24), tiles[1].rect.rows); | 406 | try std.testing.expectEqual(@as(u16, 24), tiles[1].rect.rows); |
| 407 | try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); | 407 | try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); |
| @@ -425,7 +425,7 @@ test "resize: l grows the focused pane, h shrinks it" { | |||
| 425 | try shared.tree.splitRight(0, 1); | 425 | try shared.tree.splitRight(0, 1); |
| 426 | const tiles = try alloc.alloc(Tile, 2); | 426 | const tiles = try alloc.alloc(Tile, 2); |
| 427 | defer alloc.free(tiles); | 427 | defer alloc.free(tiles); |
| 428 | const present = [_]bool{ true, true }; | 428 | var present = [_]bool{ true, true }; |
| 429 | for (tiles, 0..) |*t, i| { | 429 | for (tiles, 0..) |*t, i| { |
| 430 | t.* = Tile{ | 430 | t.* = Tile{ |
| 431 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 431 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -436,13 +436,13 @@ test "resize: l grows the focused pane, h shrinks it" { | |||
| 436 | .wake_w = -1, | 436 | .wake_w = -1, |
| 437 | }; | 437 | }; |
| 438 | } | 438 | } |
| 439 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 439 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 440 | const before = tiles[0].rect.cols; | 440 | const before = tiles[0].rect.cols; |
| 441 | // l (grow width): focus 0 gains from its right sibling. | 441 | // l (grow width): focus 0 gains from its right sibling. |
| 442 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right)); | 442 | try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .right)); |
| 443 | try std.testing.expect(tiles[0].rect.cols > before); | 443 | try std.testing.expect(tiles[0].rect.cols > before); |
| 444 | // h (shrink width): the right neighbor gains a cell back from focus. | 444 | // h (shrink width): the right neighbor gains a cell back from focus. |
| 445 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .left)); | 445 | try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .left)); |
| 446 | try std.testing.expectEqual(before, tiles[0].rect.cols); | 446 | try std.testing.expectEqual(before, tiles[0].rect.cols); |
| 447 | } | 447 | } |
| 448 | 448 | ||
| @@ -458,7 +458,7 @@ test "resize: j grows height, k shrinks height" { | |||
| 458 | try shared.tree.splitBelow(0, 1); | 458 | try shared.tree.splitBelow(0, 1); |
| 459 | const tiles = try alloc.alloc(Tile, 2); | 459 | const tiles = try alloc.alloc(Tile, 2); |
| 460 | defer alloc.free(tiles); | 460 | defer alloc.free(tiles); |
| 461 | const present = [_]bool{ true, true }; | 461 | var present = [_]bool{ true, true }; |
| 462 | for (tiles, 0..) |*t, i| { | 462 | for (tiles, 0..) |*t, i| { |
| 463 | t.* = Tile{ | 463 | t.* = Tile{ |
| 464 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 464 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -469,11 +469,11 @@ test "resize: j grows height, k shrinks height" { | |||
| 469 | .wake_w = -1, | 469 | .wake_w = -1, |
| 470 | }; | 470 | }; |
| 471 | } | 471 | } |
| 472 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 472 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 473 | const before = tiles[0].rect.rows; | 473 | const before = tiles[0].rect.rows; |
| 474 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .down)); | 474 | try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .down)); |
| 475 | try std.testing.expect(tiles[0].rect.rows > before); | 475 | try std.testing.expect(tiles[0].rect.rows > before); |
| 476 | try std.testing.expect(wall_layout.doResize(alloc, tiles, &present, &shared, 0, .up)); | 476 | try std.testing.expect(wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .up)); |
| 477 | try std.testing.expectEqual(before, tiles[0].rect.rows); | 477 | try std.testing.expectEqual(before, tiles[0].rect.rows); |
| 478 | } | 478 | } |
| 479 | 479 | ||
| @@ -489,7 +489,7 @@ test "resize refuses while fullscreened" { | |||
| 489 | try shared.tree.splitRight(0, 1); | 489 | try shared.tree.splitRight(0, 1); |
| 490 | const tiles = try alloc.alloc(Tile, 2); | 490 | const tiles = try alloc.alloc(Tile, 2); |
| 491 | defer alloc.free(tiles); | 491 | defer alloc.free(tiles); |
| 492 | const present = [_]bool{ true, true }; | 492 | var present = [_]bool{ true, true }; |
| 493 | for (tiles, 0..) |*t, i| { | 493 | for (tiles, 0..) |*t, i| { |
| 494 | t.* = Tile{ | 494 | t.* = Tile{ |
| 495 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 495 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -501,8 +501,8 @@ test "resize refuses while fullscreened" { | |||
| 501 | }; | 501 | }; |
| 502 | } | 502 | } |
| 503 | shared.fullscreen = true; | 503 | shared.fullscreen = true; |
| 504 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 504 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 505 | try std.testing.expect(!wall_layout.doResize(alloc, tiles, &present, &shared, 0, .right)); | 505 | try std.testing.expect(!wall_layout.doResize(fixture.wallAll(alloc, tiles, &present, &shared), 0, .right)); |
| 506 | } | 506 | } |
| 507 | 507 | ||
| 508 | test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" { | 508 | test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" { |
| @@ -529,7 +529,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" | |||
| 529 | // Growth: without the re-cut the new tile sits on placeholder geometry | 529 | // Growth: without the re-cut the new tile sits on placeholder geometry |
| 530 | // and the old tile still owns the whole terminal — the screen keeps | 530 | // and the old tile still owns the whole terminal — the screen keeps |
| 531 | // showing tile 0 while the keyboard types into tile 1's rows. | 531 | // showing tile 0 while the keyboard types into tile 1's rows. |
| 532 | wv.focusAnswer(alloc, tiles, present, &shared, true, 1); | 532 | wv.focusAnswer(fixture.wallAll(alloc, tiles, present, &shared), true, 1); |
| 533 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 533 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); |
| 534 | try std.testing.expect(tiles[0].resize_pending); | 534 | try std.testing.expect(tiles[0].resize_pending); |
| 535 | try std.testing.expect(tiles[1].resize_pending); | 535 | try std.testing.expect(tiles[1].resize_pending); |
| @@ -538,7 +538,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" | |||
| 538 | tiles[0].resize_pending = false; | 538 | tiles[0].resize_pending = false; |
| 539 | tiles[1].resize_pending = false; | 539 | tiles[1].resize_pending = false; |
| 540 | // No growth: a full clear here would flash the wall for a focus move. | 540 | // No growth: a full clear here would flash the wall for a focus move. |
| 541 | wv.focusAnswer(alloc, tiles, present, &shared, false, 0); | 541 | wv.focusAnswer(fixture.wallAll(alloc, tiles, present, &shared), false, 0); |
| 542 | try std.testing.expect(!tiles[0].resize_pending); | 542 | try std.testing.expect(!tiles[0].resize_pending); |
| 543 | try std.testing.expect(!tiles[1].resize_pending); | 543 | try std.testing.expect(!tiles[1].resize_pending); |
| 544 | try std.testing.expectEqual(@as(usize, 0), shared.sel); | 544 | try std.testing.expectEqual(@as(usize, 0), shared.sel); |
| @@ -567,7 +567,7 @@ test "a one-tile wall draws no label bar and paints row 1" { | |||
| 567 | .wake_r = -1, | 567 | .wake_r = -1, |
| 568 | .wake_w = -1, | 568 | .wake_w = -1, |
| 569 | }; | 569 | }; |
| 570 | wall_layout.relayout(alloc, tiles, present, &shared, 0); | 570 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); |
| 571 | // One tile: no label bar, every row is content. | 571 | // One tile: no label bar, every row is content. |
| 572 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | 572 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); |
| 573 | try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); | 573 | try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); |
| @@ -605,7 +605,7 @@ test "a relayout drops every tile's highlight, because the stripes move under it | |||
| 605 | }; | 605 | }; |
| 606 | } | 606 | } |
| 607 | const before = shared.repaint_gen.load(.acquire); | 607 | const before = shared.repaint_gen.load(.acquire); |
| 608 | wall_layout.relayout(alloc, tiles, present, &shared, 0); | 608 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); |
| 609 | // One bump, every tile's pump clears its drag on it: the stripes moved, | 609 | // One bump, every tile's pump clears its drag on it: the stripes moved, |
| 610 | // so a held drag's anchor names a rect that is somewhere else now. | 610 | // so a held drag's anchor names a rect that is somewhere else now. |
| 611 | try std.testing.expect(shared.repaint_gen.load(.acquire) > before); | 611 | try std.testing.expect(shared.repaint_gen.load(.acquire) > before); |
| @@ -646,7 +646,7 @@ test "saveLayoutTo writes the sidecar for the present tiles, spellings verbatim" | |||
| 646 | .wake_w = -1, | 646 | .wake_w = -1, |
| 647 | }; | 647 | }; |
| 648 | } | 648 | } |
| 649 | const present = [_]bool{ true, true }; | 649 | var present = [_]bool{ true, true }; |
| 650 | wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); | 650 | wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); |
| 651 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; | 651 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; |
| 652 | defer alloc.free(got); | 652 | defer alloc.free(got); |
| @@ -696,7 +696,7 @@ test "saveLayoutTo handles a vanished middle tile without panicking" { | |||
| 696 | .wake_w = -1, | 696 | .wake_w = -1, |
| 697 | }; | 697 | }; |
| 698 | } | 698 | } |
| 699 | const present = [_]bool{ true, false, true }; | 699 | var present = [_]bool{ true, false, true }; |
| 700 | wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); | 700 | wall_layout.saveLayoutTo(alloc, path, tiles, &present, &shared); |
| 701 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; | 701 | const got = wall.loadLayout(alloc, path) orelse return error.TestUnexpectedResult; |
| 702 | defer alloc.free(got); | 702 | defer alloc.free(got); |
| @@ -723,7 +723,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" { | |||
| 723 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); | 723 | try std.testing.expect(wall_layout.restoreLayout(alloc, &resolved, &shared, bytes, &focus_out)); |
| 724 | const tiles = try alloc.alloc(Tile, 2); | 724 | const tiles = try alloc.alloc(Tile, 2); |
| 725 | defer alloc.free(tiles); | 725 | defer alloc.free(tiles); |
| 726 | const present = [_]bool{ true, true }; | 726 | var present = [_]bool{ true, true }; |
| 727 | for (tiles, 0..) |*t, i| { | 727 | for (tiles, 0..) |*t, i| { |
| 728 | t.* = Tile{ | 728 | t.* = Tile{ |
| 729 | .r = resolved[i], | 729 | .r = resolved[i], |
| @@ -734,7 +734,7 @@ test "restore: a saved beside pair comes back verbatim on a tall terminal" { | |||
| 734 | .wake_w = -1, | 734 | .wake_w = -1, |
| 735 | }; | 735 | }; |
| 736 | } | 736 | } |
| 737 | wall_layout.relayout(alloc, tiles, &present, &shared, 0); | 737 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 738 | try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top); | 738 | try std.testing.expectEqual(@as(u16, 0), tiles[1].rect.top); |
| 739 | try std.testing.expect(tiles[1].rect.left > 0); | 739 | try std.testing.expect(tiles[1].rect.left > 0); |
| 740 | } | 740 | } |
| @@ -858,7 +858,7 @@ test "restoreLayoutFrom: dense sidecar indices land on the real tile indices" { | |||
| 858 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++ | 858 | const bytes = "mux-layout 1\nbeside 0\n leaf 40 --sock /tmp/x#a\n" ++ |
| 859 | " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n"; | 859 | " leaf 20 --sock /tmp/x#c\n leaf 18 --sock /tmp/x#d\n"; |
| 860 | var restored_focus: ?usize = null; | 860 | var restored_focus: ?usize = null; |
| 861 | wall_layout.restoreLayoutFrom(alloc, &tiles, &present, 4, &shared, bytes, &restored_focus) orelse | 861 | wall_layout.restoreLayoutFrom(fixture.wallLive(alloc, &tiles, &present, 4, &shared), bytes, &restored_focus) orelse |
| 862 | return error.TestUnexpectedResult; | 862 | return error.TestUnexpectedResult; |
| 863 | const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null); | 863 | const flat = try shared.tree.flatten(alloc, 24, 80, wall_layout.wallFloors(3), null); |
| 864 | defer flat.deinit(alloc); | 864 | defer flat.deinit(alloc); |
src/tui/wall_test_picker.zig
| Old | New | ||
|---|---|---|---|
| @@ -400,7 +400,7 @@ test "pickBirth: Enter on an emptied popup births nothing, not a session on a ho | |||
| 400 | h.forgotten.store(true, .release); | 400 | h.forgotten.store(true, .release); |
| 401 | } | 401 | } |
| 402 | 402 | ||
| 403 | const at = wall_picker.pickBirth(alloc, &tiles, &present, &live, &shared, &table, 1); | 403 | const at = wall_picker.pickBirth(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); |
| 404 | 404 | ||
| 405 | if (at != null) return error.BornOnAForgottenHost; | 405 | if (at != null) return error.BornOnAForgottenHost; |
| 406 | if (live != 0) return error.AForgottenHostTookASlot; | 406 | if (live != 0) return error.AForgottenHostTookASlot; |
| @@ -438,7 +438,7 @@ test "pickBirth: Enter is an ask — the tile it births may start a daemon, the | |||
| 438 | h.applied = true; | 438 | h.applied = true; |
| 439 | } | 439 | } |
| 440 | 440 | ||
| 441 | const at = wall_picker.pickBirth(alloc, &tiles, &present, &live, &shared, &table, 1) orelse | 441 | const at = wall_picker.pickBirth(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1) orelse |
| 442 | return error.EnterBornNothing; | 442 | return error.EnterBornNothing; |
| 443 | 443 | ||
| 444 | // The row Enter lands on is often exactly the one the poller calls | 444 | // The row Enter lands on is often exactly the one the poller calls |
src/tui/wall_test_wall.zig
| Old | New | ||
|---|---|---|---|
| @@ -33,7 +33,7 @@ test "birthTile: a chord-born tile creates and offers no agent" { | |||
| 33 | var present = [_]bool{ true, false }; | 33 | var present = [_]bool{ true, false }; |
| 34 | var live: usize = 1; | 34 | var live: usize = 1; |
| 35 | const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; | 35 | const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; |
| 36 | const at = wv.birthTile(alloc, &tiles, &present, &live, &shared, .{ | 36 | const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{ |
| 37 | .r = r, | 37 | .r = r, |
| 38 | .from = 0, | 38 | .from = 0, |
| 39 | .place = .beside_focus, | 39 | .place = .beside_focus, |
| @@ -64,6 +64,24 @@ test "the empty wall advertises the key the picker really answers to" { | |||
| 64 | try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action); | 64 | try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action); |
| 65 | } | 65 | } |
| 66 | 66 | ||
| 67 | test "the empty-wall notice names the picker key" { | ||
| 68 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | ||
| 69 | // The refusal a birth chord earns when the focused tile has no pump | ||
| 70 | // left. A SENTENCE, not code: a mechanical rename over `live` rewrote | ||
| 71 | // this line and three like it, and every gate stayed green because | ||
| 72 | // nothing read a notice back. | ||
| 73 | wv.setNotice(&shared, wv.no_live_here); | ||
| 74 | var buf: [96]u8 = undefined; | ||
| 75 | try std.testing.expectEqualStrings( | ||
| 76 | "[no live session here - Ctrl-\\ s picks a host]", | ||
| 77 | wv.takeNotice(&shared, &buf), | ||
| 78 | ); | ||
| 79 | // And the way out it names is a chord the filter really answers to. | ||
| 80 | var f: interact.PrefixFilter = .{}; | ||
| 81 | var keys = "\x1cs".*; | ||
| 82 | try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action); | ||
| 83 | } | ||
| 84 | |||
| 67 | test "emptyWallHint: a chord an empty wall refuses is said there, and said once" { | 85 | test "emptyWallHint: a chord an empty wall refuses is said there, and said once" { |
| 68 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | 86 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 69 | const way_out = "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave"; | 87 | const way_out = "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave"; |
| @@ -224,7 +242,7 @@ test "birthTile: no room is null and the tree is left as it was" { | |||
| 224 | var present = [_]bool{ true, false }; | 242 | var present = [_]bool{ true, false }; |
| 225 | var live: usize = 1; | 243 | var live: usize = 1; |
| 226 | const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; | 244 | const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; |
| 227 | try std.testing.expectEqual(@as(?usize, null), wv.birthTile(alloc, &tiles, &present, &live, &shared, .{ | 245 | try std.testing.expectEqual(@as(?usize, null), wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{ |
| 228 | .r = r, | 246 | .r = r, |
| 229 | .from = 0, | 247 | .from = 0, |
| 230 | .place = .beside_focus, | 248 | .place = .beside_focus, |
| @@ -269,7 +287,7 @@ test "birthTile: a beside wall admits more panes than rows/3" { | |||
| 269 | live += 1; | 287 | live += 1; |
| 270 | } | 288 | } |
| 271 | const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; | 289 | const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" }; |
| 272 | const at = wv.birthTile(alloc, &tiles, &present, &live, &shared, .{ | 290 | const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{ |
| 273 | .r = r, | 291 | .r = r, |
| 274 | .from = n - 2, | 292 | .from = n - 2, |
| 275 | .place = .beside_focus, | 293 | .place = .beside_focus, |
| @@ -326,7 +344,7 @@ test "birthTile: a vanished digit is taken back, and not before its pump returne | |||
| 326 | }; | 344 | }; |
| 327 | const born = struct { | 345 | const born = struct { |
| 328 | fn at(a: std.mem.Allocator, ts: []Tile, ps: []bool, lv: *usize, sh: *Shared, tg: client.Target, name: []const u8, host: ?usize) ?usize { | 346 | fn at(a: std.mem.Allocator, ts: []Tile, ps: []bool, lv: *usize, sh: *Shared, tg: client.Target, name: []const u8, host: ?usize) ?usize { |
| 329 | return wv.birthTile(a, ts, ps, lv, sh, .{ | 347 | return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{ |
| 330 | .r = .{ .target = tg, .label = "", .session = name }, | 348 | .r = .{ .target = tg, .label = "", .session = name }, |
| 331 | .from = 0, | 349 | .from = 0, |
| 332 | .place = .beside_focus, | 350 | .place = .beside_focus, |
| @@ -410,7 +428,7 @@ test "birthTile: the digit's next tile frees the copies an unborrowed birth hand | |||
| 410 | a.free(session); | 428 | a.free(session); |
| 411 | return null; | 429 | return null; |
| 412 | }; | 430 | }; |
| 413 | return wv.birthTile(a, ts, ps, lv, sh, .{ | 431 | return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{ |
| 414 | .r = .{ .target = tg, .label = label, .session = session }, | 432 | .r = .{ .target = tg, .label = label, .session = session }, |
| 415 | .from = 0, | 433 | .from = 0, |
| 416 | .place = .beside_focus, | 434 | .place = .beside_focus, |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -391,6 +391,32 @@ pub const Tile = struct { | |||
| 391 | } | 391 | } |
| 392 | }; | 392 | }; |
| 393 | 393 | ||
| 394 | /// The keyboard thread's whole wall, as one value. `liveTiles`/ | ||
| 395 | /// `livePresent` name the prefix walk; a walk of the whole array is a walk | ||
| 396 | /// into uninitialised slots. | ||
| 397 | /// | ||
| 398 | /// Keyboard-thread only, so `paint_mu`'s ownership of the tree is | ||
| 399 | /// unchanged: a pump is handed a `*Tile` and a `*Shared` and never this. | ||
| 400 | pub const Wall = struct { | ||
| 401 | alloc: std.mem.Allocator, | ||
| 402 | tiles: []Tile, | ||
| 403 | present: []bool, | ||
| 404 | /// The high-water mark of slots ever used, not the tile count: a reused | ||
| 405 | /// digit is already inside it, and it never falls. | ||
| 406 | live: *usize, | ||
| 407 | shared: *Shared, | ||
| 408 | hosts: []Host = &.{}, | ||
| 409 | |||
| 410 | /// The slots ever used; the array past it is uninitialised. | ||
| 411 | pub fn liveTiles(self: Wall) []Tile { | ||
| 412 | return self.tiles[0..self.live.*]; | ||
| 413 | } | ||
| 414 | |||
| 415 | pub fn livePresent(self: Wall) []bool { | ||
| 416 | return self.present[0..self.live.*]; | ||
| 417 | } | ||
| 418 | }; | ||
| 419 | |||
| 394 | /// Which tile's rectangle a zero-based terminal (row, col) falls in, or | 420 | /// Which tile's rectangle a zero-based terminal (row, col) falls in, or |
| 395 | /// null when none does. A beside layout gives every tile the same rows, so | 421 | /// null when none does. A beside layout gives every tile the same rows, so |
| 396 | /// the column is what separates them; a click on a rail column hits no | 422 | /// the column is what separates them; a click on a rail column hits no |
| @@ -743,20 +769,15 @@ pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void { | |||
| 743 | 769 | ||
| 744 | /// Re-cut before the move: `setFocus` releases the outgoing session | 770 | /// Re-cut before the move: `setFocus` releases the outgoing session |
| 745 | /// and needs the true previous focus. | 771 | /// and needs the true previous focus. |
| 746 | pub fn focusAnswer( | 772 | pub fn focusAnswer(w: Wall, recut: bool, to: usize) void { |
| 747 | alloc: std.mem.Allocator, | 773 | if (recut) wall_layout.relayout(w, w.shared.sel); |
| 748 | tiles: []Tile, | 774 | setFocus(w.liveTiles(), w.shared, to); |
| 749 | present: []const bool, | ||
| 750 | shared: *Shared, | ||
| 751 | recut: bool, | ||
| 752 | to: usize, | ||
| 753 | ) void { | ||
| 754 | if (recut) wall_layout.relayout(alloc, tiles, present, shared, shared.sel); | ||
| 755 | setFocus(tiles, shared, to); | ||
| 756 | // Fullscreen re-flattens with the new focus so the full rect follows. | 775 | // Fullscreen re-flattens with the new focus so the full rect follows. |
| 757 | if (shared.fullscreen) wall_layout.relayout(alloc, tiles, present, shared, shared.sel); | 776 | if (w.shared.fullscreen) wall_layout.relayout(w, w.shared.sel); |
| 758 | } | 777 | } |
| 759 | 778 | ||
| 779 | pub const no_live_here = "[no live session here - Ctrl-\\ s picks a host]"; | ||
| 780 | |||
| 760 | /// TAKEN, not read: an empty wall has no pump to paint a notice as a | 781 | /// TAKEN, not read: an empty wall has no pump to paint a notice as a |
| 761 | /// banner, and every relayout would repaint a sentence left on this line. | 782 | /// banner, and every relayout would repaint a sentence left on this line. |
| 762 | pub fn emptyWallHint(shared: *Shared) []const u8 { | 783 | pub fn emptyWallHint(shared: *Shared) []const u8 { |
| @@ -974,58 +995,43 @@ const Birth = struct { | |||
| 974 | }; | 995 | }; |
| 975 | 996 | ||
| 976 | /// Every road onto a running wall — chord, fold, prompt — one body. | 997 | /// Every road onto a running wall — chord, fold, prompt — one body. |
| 977 | /// Null: nothing was added. | 998 | pub fn birthTile(w: Wall, b: Birth) ?usize { |
| 978 | pub fn birthTile( | 999 | return birthTileOrRefuse(w, b) catch null; |
| 979 | alloc: std.mem.Allocator, | ||
| 980 | tiles: []Tile, | ||
| 981 | present: []bool, | ||
| 982 | live: *usize, | ||
| 983 | shared: *Shared, | ||
| 984 | b: Birth, | ||
| 985 | ) ?usize { | ||
| 986 | return birthTileOrRefuse(alloc, tiles, present, live, shared, b) catch null; | ||
| 987 | } | 1000 | } |
| 988 | 1001 | ||
| 989 | fn birthTileOrRefuse( | 1002 | fn birthTileOrRefuse(w: Wall, b: Birth) !usize { |
| 990 | alloc: std.mem.Allocator, | ||
| 991 | tiles: []Tile, | ||
| 992 | present: []bool, | ||
| 993 | live: *usize, | ||
| 994 | shared: *Shared, | ||
| 995 | b: Birth, | ||
| 996 | ) !usize { | ||
| 997 | // The lowest digit a departed tile left behind, before a new one: | 1003 | // The lowest digit a departed tile left behind, before a new one: |
| 998 | // create 1 2 3, end 2, create — and the wall says 2, not 4. Reuse, not | 1004 | // create 1 2 3, end 2, create — and the wall says 2, not 4. Reuse, not |
| 999 | // renumbering: the tiles that stayed keep the digit their user learned. | 1005 | // renumbering: the tiles that stayed keep the digit their user learned. |
| 1000 | const reuse = freeSlot(tiles[0..live.*], present[0..live.*]); | 1006 | const reuse = freeSlot(w.liveTiles(), w.livePresent()); |
| 1001 | if (reuse == null and live.* >= max_tiles) return error.WallFull; | 1007 | if (reuse == null and w.live.* >= max_tiles) return error.WallFull; |
| 1002 | const new_live = presentCount(present[0..live.*]) + 1; | 1008 | const new_live = presentCount(w.livePresent()) + 1; |
| 1003 | // "Does it fit" has ONE owner, and it is the tree: insert, flatten, | 1009 | // "Does it fit" has ONE owner, and it is the tree: insert, flatten, |
| 1004 | // and undo the insert when flatten refuses. Row arithmetic here | 1010 | // and undo the insert when flatten refuses. Row arithmetic here |
| 1005 | // capped every terminal at rows/3 panes however wide, because it | 1011 | // capped every terminal at rows/3 panes however wide, because it |
| 1006 | // cannot see that a `.beside` cut spends columns. | 1012 | // cannot see that a `.beside` cut spends columns. |
| 1007 | const at = reuse orelse live.*; | 1013 | const at = reuse orelse w.live.*; |
| 1008 | switch (b.place) { | 1014 | switch (b.place) { |
| 1009 | .beside_focus => if (shared.tree.root == null) | 1015 | .beside_focus => if (w.shared.tree.root == null) |
| 1010 | // A wall whose tiles all arrive from a host's list starts with | 1016 | // A wall whose tiles all arrive from a host's list starts with |
| 1011 | // no tree at all; `insert` has no leaf to sit beside. | 1017 | // no tree at all; `insert` has no leaf to sit beside. |
| 1012 | try shared.tree.addFirst(@intCast(at)) | 1018 | try w.shared.tree.addFirst(@intCast(at)) |
| 1013 | else | 1019 | else |
| 1014 | try shared.tree.insert(@intCast(b.from), @intCast(at)), | 1020 | try w.shared.tree.insert(@intCast(b.from), @intCast(at)), |
| 1015 | .right_of => try shared.tree.splitRight(@intCast(b.from), @intCast(at)), | 1021 | .right_of => try w.shared.tree.splitRight(@intCast(b.from), @intCast(at)), |
| 1016 | .below => try shared.tree.splitBelow(@intCast(b.from), @intCast(at)), | 1022 | .below => try w.shared.tree.splitBelow(@intCast(b.from), @intCast(at)), |
| 1017 | } | 1023 | } |
| 1018 | // Past the insert, so every refusal below puts the tree back exactly | 1024 | // Past the insert, so every refusal below puts the tree back exactly |
| 1019 | // as the caller found it. | 1025 | // as the caller found it. |
| 1020 | errdefer shared.tree.remove(@intCast(at)); | 1026 | errdefer w.shared.tree.remove(@intCast(at)); |
| 1021 | const flat = try shared.tree.flatten( | 1027 | const flat = try w.shared.tree.flatten( |
| 1022 | alloc, | 1028 | w.alloc, |
| 1023 | shared.size.rows, | 1029 | w.shared.size.rows, |
| 1024 | shared.size.cols, | 1030 | w.shared.size.cols, |
| 1025 | wall_layout.wallFloors(new_live), | 1031 | wall_layout.wallFloors(new_live), |
| 1026 | null, | 1032 | null, |
| 1027 | ); | 1033 | ); |
| 1028 | defer flat.deinit(alloc); | 1034 | defer flat.deinit(w.alloc); |
| 1029 | const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; | 1035 | const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; |
| 1030 | // The real rect, not a placeholder: a creating tile puts its rect on | 1036 | // The real rect, not a placeholder: a creating tile puts its rect on |
| 1031 | // the first attach frame and the daemon refuses creates under | 1037 | // the first attach frame and the daemon refuses creates under |
| @@ -1033,75 +1039,66 @@ fn birthTileOrRefuse( | |||
| 1033 | // pass, and a 2-row rect on a live session is destructive under | 1039 | // pass, and a 2-row rect on a live session is destructive under |
| 1034 | // latest-wins. `label_rows` is set here so `viewRows` is right from | 1040 | // latest-wins. `label_rows` is set here so `viewRows` is right from |
| 1035 | // the first attach; relayout re-flattens every rect and sets it again. | 1041 | // the first attach; relayout re-flattens every rect and sets it again. |
| 1036 | shared.label_rows = if (new_live > 1) 1 else 0; | 1042 | w.shared.label_rows = if (new_live > 1) 1 else 0; |
| 1037 | // Past every refusal: the tile is the wall's now, so the copies a pump | 1043 | // Past every refusal: the tile is the wall's now, so the copies a pump |
| 1038 | // will hold for its whole life are worth making. Made BEFORE the slot | 1044 | // will hold for its whole life are worth making. Made BEFORE the slot |
| 1039 | // is overwritten, so the last thing that can fail here still fails | 1045 | // is overwritten, so the last thing that can fail here still fails |
| 1040 | // against a slot that is exactly as the caller found it. | 1046 | // against a slot that is exactly as the caller found it. |
| 1041 | var r = b.r; | 1047 | var r = b.r; |
| 1042 | if (b.borrowed) { | 1048 | if (b.borrowed) { |
| 1043 | const session = try alloc.dupe(u8, b.r.session); | 1049 | const session = try w.alloc.dupe(u8, b.r.session); |
| 1044 | errdefer alloc.free(session); | 1050 | errdefer w.alloc.free(session); |
| 1045 | r.session = session; | 1051 | r.session = session; |
| 1046 | r.label = try tileLabel(alloc, b.r.target, session); | 1052 | r.label = try tileLabel(w.alloc, b.r.target, session); |
| 1047 | } | 1053 | } |
| 1048 | // Only the copies THIS call made: a caller that owns them frees its own. | 1054 | // Only the copies THIS call made: a caller that owns them frees its own. |
| 1049 | errdefer if (b.borrowed) { | 1055 | errdefer if (b.borrowed) { |
| 1050 | alloc.free(r.session); | 1056 | w.alloc.free(r.session); |
| 1051 | alloc.free(r.label); | 1057 | w.alloc.free(r.label); |
| 1052 | }; | 1058 | }; |
| 1053 | // The departed tile's copies go with its digit. Every tile owns these | 1059 | // The departed tile's copies go with its digit. Every tile owns these |
| 1054 | // two — `run` dupes even the entry tile's session for this — so a | 1060 | // two — `run` dupes even the entry tile's session for this — so a |
| 1055 | // reused slot that kept them would leak one label and one name per | 1061 | // reused slot that kept them would leak one label and one name per |
| 1056 | // birth for the wall's whole life. | 1062 | // birth for the wall's whole life. |
| 1057 | if (reuse != null) { | 1063 | if (reuse != null) { |
| 1058 | alloc.free(tiles[at].r.session); | 1064 | w.alloc.free(w.tiles[at].r.session); |
| 1059 | alloc.free(tiles[at].r.label); | 1065 | w.alloc.free(w.tiles[at].r.label); |
| 1060 | } | 1066 | } |
| 1061 | // Only a `.fresh` doorbell can fail, and it fails before the slot is | 1067 | // Only a `.fresh` doorbell can fail, and it fails before the slot is |
| 1062 | // written, so the errdefers above are the whole unwind. | 1068 | // written, so the errdefers above are the whole unwind. |
| 1063 | try initTile(&tiles[at], r, new_rect, shared, at, if (reuse == null) .fresh else .kept); | 1069 | try initTile(&w.tiles[at], r, new_rect, w.shared, at, if (reuse == null) .fresh else .kept); |
| 1064 | // The caller's row of the birth table, and the whole of what separates | 1070 | // The caller's row of the birth table, and the whole of what separates |
| 1065 | // the roads: a chord row inherits its target and agent and CREATES, a | 1071 | // the roads: a chord row inherits its target and agent and CREATES, a |
| 1066 | // poll row joins a session the daemon already has. The pump spawn stays | 1072 | // poll row joins a session the daemon already has. The pump spawn stays |
| 1067 | // the caller's. | 1073 | // the caller's. |
| 1068 | tiles[at].creates = b.creates; | 1074 | w.tiles[at].creates = b.creates; |
| 1069 | tiles[at].born_from = b.born_from; | 1075 | w.tiles[at].born_from = b.born_from; |
| 1070 | tiles[at].keeps_wall = b.keeps_wall; | 1076 | w.tiles[at].keeps_wall = b.keeps_wall; |
| 1071 | tiles[at].host = b.host; | 1077 | w.tiles[at].host = b.host; |
| 1072 | present[at] = true; | 1078 | w.present[at] = true; |
| 1073 | // `live` is the high-water mark of slots ever used, not the tile count: | 1079 | // `live` is the high-water mark of slots ever used, not the tile count: |
| 1074 | // a reused digit is already inside it, and growing here would walk the | 1080 | // a reused digit is already inside it, and growing here would walk the |
| 1075 | // keyboard's `tiles[0..live]` off the end of the array. | 1081 | // keyboard's `liveTiles` walk off the end of the array. |
| 1076 | if (reuse == null) live.* += 1; | 1082 | if (reuse == null) w.live.* += 1; |
| 1077 | return at; | 1083 | return at; |
| 1078 | } | 1084 | } |
| 1079 | 1085 | ||
| 1080 | /// A sibling is focused if it has a tile and GETS one if not — otherwise a | 1086 | /// A sibling is focused if it has a tile and GETS one if not — otherwise a |
| 1081 | /// tile labelled S would paint T. | 1087 | /// tile labelled S would paint T. |
| 1082 | fn addSessionTile( | 1088 | fn addSessionTile(w: Wall, from: usize, name: []const u8, place: Place) FocusTo { |
| 1083 | alloc: std.mem.Allocator, | ||
| 1084 | tiles: []Tile, | ||
| 1085 | present: []bool, | ||
| 1086 | live: *usize, | ||
| 1087 | shared: *Shared, | ||
| 1088 | from: usize, | ||
| 1089 | name: []const u8, | ||
| 1090 | place: Place, | ||
| 1091 | ) FocusTo { | ||
| 1092 | // The parent's ask does not descend. A chord is a session on a daemon | 1089 | // The parent's ask does not descend. A chord is a session on a daemon |
| 1093 | // this wall is already attached to, so there is nothing here to start; | 1090 | // this wall is already attached to, so there is nothing here to start; |
| 1094 | // inheriting the bit would hand every descendant of a picker-born tile | 1091 | // inheriting the bit would hand every descendant of a picker-born tile |
| 1095 | // a permission nobody asked for, for the rest of its life. | 1092 | // a permission nobody asked for, for the rest of its life. |
| 1096 | var target = tiles[from].r.target; | 1093 | var target = w.tiles[from].r.target; |
| 1097 | if (target == .hand) target.hand.asked = false; | 1094 | if (target == .hand) target.hand.asked = false; |
| 1098 | const want = proto.resolveName(name); | 1095 | const want = proto.resolveName(name); |
| 1099 | for (tiles[0..live.*], present[0..live.*], 0..) |*t, p, i| { | 1096 | for (w.liveTiles(), w.livePresent(), 0..) |*t, p, i| { |
| 1100 | if (!p) continue; | 1097 | if (!p) continue; |
| 1101 | if (!sameTarget(t.r.target, target)) continue; | 1098 | if (!sameTarget(t.r.target, target)) continue; |
| 1102 | if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i }; | 1099 | if (std.mem.eql(u8, proto.resolveName(t.r.session), want)) return .{ .moved = i }; |
| 1103 | } | 1100 | } |
| 1104 | const at = birthTile(alloc, tiles, present, live, shared, .{ | 1101 | const at = birthTile(w, .{ |
| 1105 | // The offer is inherited from the tile this one grew out of. Same | 1102 | // The offer is inherited from the tile this one grew out of. Same |
| 1106 | // target, so `-A` exposes nothing the user has not already exposed | 1103 | // target, so `-A` exposes nothing the user has not already exposed |
| 1107 | // to that host — and a chord-made tile has no command line to spell | 1104 | // to that host — and a chord-made tile has no command line to spell |
| @@ -1109,15 +1106,15 @@ fn addSessionTile( | |||
| 1109 | // at the first session switch. | 1106 | // at the first session switch. |
| 1110 | // `want` borrows the caller's name: the wall makes its own copies | 1107 | // `want` borrows the caller's name: the wall makes its own copies |
| 1111 | // only if it keeps the tile — see `Birth.borrowed`. | 1108 | // only if it keeps the tile — see `Birth.borrowed`. |
| 1112 | .r = .{ .target = target, .label = "", .session = want, .agent = tiles[from].r.agent }, | 1109 | .r = .{ .target = target, .label = "", .session = want, .agent = w.tiles[from].r.agent }, |
| 1113 | .from = from, | 1110 | .from = from, |
| 1114 | .place = place, | 1111 | .place = place, |
| 1115 | .creates = true, | 1112 | .creates = true, |
| 1116 | .born_from = from, | 1113 | .born_from = from, |
| 1117 | .host = tiles[from].host, | 1114 | .host = w.tiles[from].host, |
| 1118 | .borrowed = true, | 1115 | .borrowed = true, |
| 1119 | }) orelse return .full; | 1116 | }) orelse return .full; |
| 1120 | spawnPump(&tiles[at]); | 1117 | spawnPump(&w.tiles[at]); |
| 1121 | return .{ .moved = at }; | 1118 | return .{ .moved = at }; |
| 1122 | } | 1119 | } |
| 1123 | 1120 | ||
| @@ -1592,6 +1589,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1592 | var live: usize = 0; | 1589 | var live: usize = 0; |
| 1593 | const present = try alloc.alloc(bool, max_tiles); | 1590 | const present = try alloc.alloc(bool, max_tiles); |
| 1594 | @memset(present, false); | 1591 | @memset(present, false); |
| 1592 | // One value for the six the wall used to pass around. `hosts` is | ||
| 1593 | // re-sliced wherever `hosts_live` grows — the picker's `a` is the only | ||
| 1594 | // thing that does. | ||
| 1595 | var w: Wall = .{ .alloc = alloc, .tiles = tiles, .present = present, .live = &live, .shared = &shared }; | ||
| 1595 | const env_sock = std.posix.getenv(proto.sock_env); | 1596 | const env_sock = std.posix.getenv(proto.sock_env); |
| 1596 | const env_session = std.posix.getenv(proto.session_env); | 1597 | const env_session = std.posix.getenv(proto.session_env); |
| 1597 | if (has_entry) { | 1598 | if (has_entry) { |
| @@ -1624,7 +1625,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1624 | @memcpy(tiles[0].in[0..n], entry.carry[0..n]); | 1625 | @memcpy(tiles[0].in[0..n], entry.carry[0..n]); |
| 1625 | tiles[0].in_len = n; | 1626 | tiles[0].in_len = n; |
| 1626 | present[0] = true; | 1627 | present[0] = true; |
| 1627 | live = 1; | 1628 | w.live.* = 1; |
| 1628 | // Its pump claims the terminal on its first pass. Set before | 1629 | // Its pump claims the terminal on its first pass. Set before |
| 1629 | // `spawnPump` so the claim is the first thing the pump does after | 1630 | // `spawnPump` so the claim is the first thing the pump does after |
| 1630 | // the dial, and the attach already carries the terminal's size. | 1631 | // the dial, and the attach already carries the terminal's size. |
| @@ -1633,12 +1634,12 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1633 | // A one-tile wall owns every row and draws no label bar; two or more | 1634 | // A one-tile wall owns every row and draws no label bar; two or more |
| 1634 | // tiles each lose their top row to one. Set before the pumps start so | 1635 | // tiles each lose their top row to one. Set before the pumps start so |
| 1635 | // `viewRows` is right on the first attach. | 1636 | // `viewRows` is right on the first attach. |
| 1636 | shared.label_rows = if (live > 1) 1 else 0; | 1637 | shared.label_rows = if (w.live.* > 1) 1 else 0; |
| 1637 | for (tiles[0..live]) |*t| spawnPump(t); | 1638 | for (tiles[0..live]) |*t| spawnPump(t); |
| 1638 | // A wall with no tile yet paints its one line rather than nothing: a | 1639 | // A wall with no tile yet paints its one line rather than nothing: a |
| 1639 | // blank terminal with no cursor reads as hung, and the hosts are up to | 1640 | // blank terminal with no cursor reads as hung, and the hosts are up to |
| 1640 | // a poll away from having anything to show. | 1641 | // a poll away from having anything to show. |
| 1641 | if (live == 0) wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, 0); | 1642 | if (w.live.* == 0) wall_layout.relayout(w, 0); |
| 1642 | 1643 | ||
| 1643 | // One poller per host, all of them at once and none of them on this | 1644 | // One poller per host, all of them at once and none of them on this |
| 1644 | // thread: the user asked for one session and must not be held on | 1645 | // thread: the user asked for one session and must not be held on |
| @@ -1649,6 +1650,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1649 | // when the picker's `a` adds a host to it. | 1650 | // when the picker's `a` adds a host to it. |
| 1650 | const host_table = try alloc.alloc(Host, max_tiles); | 1651 | const host_table = try alloc.alloc(Host, max_tiles); |
| 1651 | var hosts_live: usize = @min(host_specs.len, max_tiles); | 1652 | var hosts_live: usize = @min(host_specs.len, max_tiles); |
| 1653 | w.hosts = host_table[0..hosts_live]; | ||
| 1652 | var over_buf: [64]u8 = undefined; | 1654 | var over_buf: [64]u8 = undefined; |
| 1653 | if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said); | 1655 | if (wall_host.hostsOverCapacity(&over_buf, host_specs.len)) |said| setNotice(&shared, said); |
| 1654 | for (host_table[0..hosts_live], host_specs[0..hosts_live]) |*h, spec| { | 1656 | for (host_table[0..hosts_live], host_specs[0..hosts_live]) |*h, spec| { |
| @@ -1749,7 +1751,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1749 | shared.paint_mu.lock(); | 1751 | shared.paint_mu.lock(); |
| 1750 | shared.size = measured2; | 1752 | shared.size = measured2; |
| 1751 | shared.paint_mu.unlock(); | 1753 | shared.paint_mu.unlock(); |
| 1752 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1754 | wall_layout.relayout(w, shared.sel); |
| 1753 | } | 1755 | } |
| 1754 | } | 1756 | } |
| 1755 | 1757 | ||
| @@ -1760,7 +1762,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1760 | // about to read, and acting on the end of a tile nobody is | 1762 | // about to read, and acting on the end of a tile nobody is |
| 1761 | // looking at any more is the wrong order to notice things in. | 1763 | // looking at any more is the wrong order to notice things in. |
| 1762 | const z = shared.sel; | 1764 | const z = shared.sel; |
| 1763 | if (z < live and tiles[z].ans_ready.swap(false, .acq_rel)) { | 1765 | if (z < w.live.* and tiles[z].ans_ready.swap(false, .acq_rel)) { |
| 1764 | var name: client.SessionName = undefined; | 1766 | var name: client.SessionName = undefined; |
| 1765 | { | 1767 | { |
| 1766 | tiles[z].ans_mu.lock(); | 1768 | tiles[z].ans_mu.lock(); |
| @@ -1772,30 +1774,21 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1772 | // it was, and a wall that read growth off it would put the | 1774 | // it was, and a wall that read growth off it would put the |
| 1773 | // new tile on a screen nothing re-cut. | 1775 | // new tile on a screen nothing re-cut. |
| 1774 | const before = presentCount(present[0..live]); | 1776 | const before = presentCount(present[0..live]); |
| 1775 | switch (addSessionTile( | 1777 | switch (addSessionTile(w, z, name.slice(), tiles[z].pending_place)) { |
| 1776 | alloc, | ||
| 1777 | tiles, | ||
| 1778 | present, | ||
| 1779 | &live, | ||
| 1780 | &shared, | ||
| 1781 | z, | ||
| 1782 | name.slice(), | ||
| 1783 | tiles[z].pending_place, | ||
| 1784 | )) { | ||
| 1785 | .moved => |to| { | 1778 | .moved => |to| { |
| 1786 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, presentCount(present[0..live]) > before, to); | 1779 | focusAnswer(w, presentCount(present[0..live]) > before, to); |
| 1787 | }, | 1780 | }, |
| 1788 | .full => setNotice(&shared, "[no room on the wall for another tile]"), | 1781 | .full => setNotice(&shared, "[no room on the wall for another tile]"), |
| 1789 | .stay => {}, | 1782 | .stay => {}, |
| 1790 | } | 1783 | } |
| 1791 | wall_host.pokeHost(host_table[0..hosts_live], &tiles[z]); | 1784 | wall_host.pokeHost(w.hosts, &tiles[z]); |
| 1792 | } | 1785 | } |
| 1793 | } | 1786 | } |
| 1794 | // Ends are read every pass, not only on the bell: two pumps dying | 1787 | // Ends are read every pass, not only on the bell: two pumps dying |
| 1795 | // together share one ring, and `drainBell` clears it, so a residual | 1788 | // together share one ring, and `drainBell` clears it, so a residual |
| 1796 | // end would otherwise wait for a ring that may not come. | 1789 | // end would otherwise wait for a ring that may not come. |
| 1797 | if (endedTile(tiles[0..live], present[0..live], &shared)) |ended| { | 1790 | if (endedTile(tiles[0..live], present[0..live], &shared)) |ended| { |
| 1798 | switch (endAction(tiles, present, live, ended, stdin_open, shared.is_tty)) { | 1791 | switch (endAction(tiles, present, w.live.*, ended, stdin_open, shared.is_tty)) { |
| 1799 | .refocus => |to| { | 1792 | .refocus => |to| { |
| 1800 | if (!tiles[to].alive.load(.acquire)) | 1793 | if (!tiles[to].alive.load(.acquire)) |
| 1801 | setNotice(&shared, "[focus on a dead tile - no live neighbour]"); | 1794 | setNotice(&shared, "[focus on a dead tile - no live neighbour]"); |
| @@ -1821,10 +1814,10 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1821 | setNotice(&shared, "[focus on a dead tile - no live neighbour]"); | 1814 | setNotice(&shared, "[focus on a dead tile - no live neighbour]"); |
| 1822 | } | 1815 | } |
| 1823 | vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back); | 1816 | vanishTile(tiles[0..live], present[0..live], &shared, ended, v.back); |
| 1824 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1817 | wall_layout.relayout(w, shared.sel); |
| 1825 | }, | 1818 | }, |
| 1826 | .finish => |how| { | 1819 | .finish => |how| { |
| 1827 | wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared); | 1820 | wall_layout.saveSidecar(w); |
| 1828 | exit_code = how.code; | 1821 | exit_code = how.code; |
| 1829 | exit_msg = how.msg; | 1822 | exit_msg = how.msg; |
| 1830 | break :keys; | 1823 | break :keys; |
| @@ -1834,7 +1827,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1834 | // Ends first, lists second: a session that exited is its pump's | 1827 | // Ends first, lists second: a session that exited is its pump's |
| 1835 | // news and arrives at once, while a list is up to a poll behind. | 1828 | // news and arrives at once, while a list is up to a poll behind. |
| 1836 | // Reading the list first would vanish the tile the exit code is on. | 1829 | // Reading the list first would vanish the tile the exit code is on. |
| 1837 | const host_news = wall_host.applyReadyLists(alloc, tiles, present, &live, &shared, host_table[0..hosts_live]); | 1830 | const host_news = wall_host.applyReadyLists(w); |
| 1838 | if (!restore_tried) { | 1831 | if (!restore_tried) { |
| 1839 | var all_reported = true; | 1832 | var all_reported = true; |
| 1840 | for (host_table[0..opening_hosts]) |*h| { | 1833 | for (host_table[0..opening_hosts]) |*h| { |
| @@ -1846,12 +1839,12 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1846 | if (all_reported or std.time.milliTimestamp() >= restore_due) { | 1839 | if (all_reported or std.time.milliTimestamp() >= restore_due) { |
| 1847 | restore_tried = true; | 1840 | restore_tried = true; |
| 1848 | var saved_focus: ?usize = null; | 1841 | var saved_focus: ?usize = null; |
| 1849 | if (live > 0 and wall_layout.restoreSidecar(alloc, tiles, present, live, &shared, &saved_focus) != null) { | 1842 | if (w.live.* > 0 and wall_layout.restoreSidecar(w, &saved_focus) != null) { |
| 1850 | restore_ok = true; | 1843 | restore_ok = true; |
| 1851 | // The entry tile is the one the user is already typing | 1844 | // The entry tile is the one the user is already typing |
| 1852 | // into; a saved focus record must not move them off it. | 1845 | // into; a saved focus record must not move them off it. |
| 1853 | const to = if (has_entry) shared.sel else (saved_focus orelse shared.sel); | 1846 | const to = if (has_entry) shared.sel else (saved_focus orelse shared.sel); |
| 1854 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, to); | 1847 | focusAnswer(w, true, to); |
| 1855 | } | 1848 | } |
| 1856 | } | 1849 | } |
| 1857 | } | 1850 | } |
| @@ -1860,7 +1853,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1860 | shared.paint_mu.lock(); | 1853 | shared.paint_mu.lock(); |
| 1861 | shared.tree.setRootOrient(wall_layout.rootOrient(shared.size)); | 1854 | shared.tree.setRootOrient(wall_layout.rootOrient(shared.size)); |
| 1862 | shared.paint_mu.unlock(); | 1855 | shared.paint_mu.unlock(); |
| 1863 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1856 | wall_layout.relayout(w, shared.sel); |
| 1864 | } | 1857 | } |
| 1865 | // An empty wall IS the picker: there is nothing else on the screen | 1858 | // An empty wall IS the picker: there is nothing else on the screen |
| 1866 | // to act from, and the last `x` is exactly when the user needs the | 1859 | // to act from, and the last `x` is exactly when the user needs the |
| @@ -1881,14 +1874,14 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1881 | picker_opened = true; | 1874 | picker_opened = true; |
| 1882 | picker_shown = true; | 1875 | picker_shown = true; |
| 1883 | input.prefix.picking = true; | 1876 | input.prefix.picking = true; |
| 1884 | picker_sel = wall_picker.pickerNearest(host_table[0..hosts_live], picker_sel); | 1877 | picker_sel = wall_picker.pickerNearest(w.hosts, picker_sel); |
| 1885 | }, | 1878 | }, |
| 1886 | .close => { | 1879 | .close => { |
| 1887 | input.prefix.picking = false; | 1880 | input.prefix.picking = false; |
| 1888 | picker_shown = false; | 1881 | picker_shown = false; |
| 1889 | shared.picker_open.store(false, .release); | 1882 | shared.picker_open.store(false, .release); |
| 1890 | shared.picker_stamp = 0; | 1883 | shared.picker_stamp = 0; |
| 1891 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1884 | wall_layout.relayout(w, shared.sel); |
| 1892 | }, | 1885 | }, |
| 1893 | .leave => {}, | 1886 | .leave => {}, |
| 1894 | } | 1887 | } |
| @@ -1905,7 +1898,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1905 | var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; | 1898 | var foot_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; |
| 1906 | const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or | 1899 | const repair = wall_picker.pickerRepaint(&foot_buf, &input.prefix, picker_opened or host_news or |
| 1907 | winch or fds[1].revents != 0 or shared.picker_stamp == 0); | 1900 | winch or fds[1].revents != 0 or shared.picker_stamp == 0); |
| 1908 | if (repair.due) wall_picker.paintPicker(&shared, host_table[0..hosts_live], picker_sel, repair.line); | 1901 | if (repair.due) wall_picker.paintPicker(&shared, w.hosts, picker_sel, repair.line); |
| 1909 | if (fds[0].revents == 0) continue; | 1902 | if (fds[0].revents == 0) continue; |
| 1910 | 1903 | ||
| 1911 | const n = std.posix.read(stdin_fd, &b) catch break; | 1904 | const n = std.posix.read(stdin_fd, &b) catch break; |
| @@ -1924,42 +1917,25 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1924 | // no tile at all, which is why it is answered ahead of both. | 1917 | // no tile at all, which is why it is answered ahead of both. |
| 1925 | if (input.prefix.picking or wall_picker.isPickAction(cmd.action)) { | 1918 | if (input.prefix.picking or wall_picker.isPickAction(cmd.action)) { |
| 1926 | // Typed AT the session, ahead of the chord, in the same read. | 1919 | // Typed AT the session, ahead of the chord, in the same read. |
| 1927 | if (cmd.forward.len > 0 and z < live and present[z]) sendKeys(&tiles[z], cmd.forward); | 1920 | if (cmd.forward.len > 0 and z < w.live.* and present[z]) sendKeys(&tiles[z], cmd.forward); |
| 1928 | var birth_at: ?usize = null; | 1921 | var birth_at: ?usize = null; |
| 1929 | // Opening on a focused tile pre-selects that tile's host: the | 1922 | // Opening on a focused tile pre-selects that tile's host: the |
| 1930 | // machine the user is already on is the one they mean. Judged | 1923 | // machine the user is already on is the one they mean. Judged |
| 1931 | // on the popup's own state, not on `.pick_open`, because one | 1924 | // on the popup's own state, not on `.pick_open`, because one |
| 1932 | // read can open the picker and its editor together. | 1925 | // read can open the picker and its editor together. |
| 1933 | if (!picker_shown) { | 1926 | if (!picker_shown) { |
| 1934 | if (z < live and present[z]) { | 1927 | if (z < w.live.* and present[z]) { |
| 1935 | if (tiles[z].host) |hi| picker_sel = hi; | 1928 | if (tiles[z].host) |hi| picker_sel = hi; |
| 1936 | } | 1929 | } |
| 1937 | picker_sel = wall_picker.pickerNearest(host_table[0..hosts_live], picker_sel); | 1930 | picker_sel = wall_picker.pickerNearest(w.hosts, picker_sel); |
| 1938 | } | 1931 | } |
| 1939 | switch (cmd.action) { | 1932 | switch (cmd.action) { |
| 1940 | .pick_move => |d| picker_sel = wall_picker.pickerStep(host_table[0..hosts_live], picker_sel, d), | 1933 | .pick_move => |d| picker_sel = wall_picker.pickerStep(w.hosts, picker_sel, d), |
| 1941 | .pick_select => |row| { | 1934 | .pick_select => |row| { |
| 1942 | if (wall_picker.pickerAt(host_table[0..hosts_live], row - 1)) |hi| picker_sel = hi; | 1935 | if (wall_picker.pickerAt(w.hosts, row - 1)) |hi| picker_sel = hi; |
| 1943 | }, | 1936 | }, |
| 1944 | .pick_birth => birth_at = wall_picker.pickBirth( | 1937 | .pick_birth => birth_at = wall_picker.pickBirth(w, picker_sel), |
| 1945 | alloc, | 1938 | .pick_forget => wall_picker.pickForget(w, picker_sel, hosts_path), |
| 1946 | tiles, | ||
| 1947 | present, | ||
| 1948 | &live, | ||
| 1949 | &shared, | ||
| 1950 | host_table[0..hosts_live], | ||
| 1951 | picker_sel, | ||
| 1952 | ), | ||
| 1953 | .pick_forget => wall_picker.pickForget( | ||
| 1954 | alloc, | ||
| 1955 | tiles, | ||
| 1956 | present, | ||
| 1957 | live, | ||
| 1958 | &shared, | ||
| 1959 | host_table[0..hosts_live], | ||
| 1960 | picker_sel, | ||
| 1961 | hosts_path, | ||
| 1962 | ), | ||
| 1963 | .add_tile => |spelling| { | 1939 | .add_tile => |spelling| { |
| 1964 | switch (wall_host.addHost( | 1940 | switch (wall_host.addHost( |
| 1965 | alloc, | 1941 | alloc, |
| @@ -1972,6 +1948,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1972 | hosts_path, | 1948 | hosts_path, |
| 1973 | )) { | 1949 | )) { |
| 1974 | .added => |hi| { | 1950 | .added => |hi| { |
| 1951 | // The only thing that grows `hosts_live`, so the | ||
| 1952 | // only place the wall's own slice has to follow. | ||
| 1953 | w.hosts = host_table[0..hosts_live]; | ||
| 1975 | const th = std.Thread.spawn(.{}, wall_host.pollHost, .{&host_table[hi]}) catch null; | 1954 | const th = std.Thread.spawn(.{}, wall_host.pollHost, .{&host_table[hi]}) catch null; |
| 1976 | if (th) |handle| handle.detach(); | 1955 | if (th) |handle| handle.detach(); |
| 1977 | // The row the user just made is the row they meant. | 1956 | // The row the user just made is the row they meant. |
| @@ -1989,7 +1968,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1989 | if (input.prefix.picking) { | 1968 | if (input.prefix.picking) { |
| 1990 | var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; | 1969 | var line_buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined; |
| 1991 | const keyed = wall_picker.pickerRepaint(&line_buf, &input.prefix, true); | 1970 | const keyed = wall_picker.pickerRepaint(&line_buf, &input.prefix, true); |
| 1992 | wall_picker.paintPicker(&shared, host_table[0..hosts_live], picker_sel, keyed.line); | 1971 | wall_picker.paintPicker(&shared, w.hosts, picker_sel, keyed.line); |
| 1993 | } else { | 1972 | } else { |
| 1994 | // The close gives the terminal back: `relayout` clears it | 1973 | // The close gives the terminal back: `relayout` clears it |
| 1995 | // and bumps `repaint_gen`, which is the only thing that | 1974 | // and bumps `repaint_gen`, which is the only thing that |
| @@ -2001,9 +1980,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2001 | picker_shown = false; | 1980 | picker_shown = false; |
| 2002 | picker_auto.taken(); | 1981 | picker_auto.taken(); |
| 2003 | if (birth_at) |at| | 1982 | if (birth_at) |at| |
| 2004 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, true, at) | 1983 | focusAnswer(w, true, at) |
| 2005 | else | 1984 | else |
| 2006 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 1985 | wall_layout.relayout(w, shared.sel); |
| 2007 | // The focused pump may be holding a claim the popup refused; | 1986 | // The focused pump may be holding a claim the popup refused; |
| 2008 | // it re-arms and retries on its next pass, and this is what | 1987 | // it re-arms and retries on its next pass, and this is what |
| 2009 | // makes that pass happen now rather than within a poll. | 1988 | // makes that pass happen now rather than within a poll. |
| @@ -2031,7 +2010,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2031 | }, | 2010 | }, |
| 2032 | .new_session, .split_right, .split_below => { | 2011 | .new_session, .split_right, .split_below => { |
| 2033 | setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]"); | 2012 | setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]"); |
| 2034 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2013 | wall_layout.relayout(w, shared.sel); |
| 2035 | }, | 2014 | }, |
| 2036 | else => {}, | 2015 | else => {}, |
| 2037 | } | 2016 | } |
| @@ -2108,7 +2087,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2108 | tiles[z].detach_req.store(true, .release); | 2087 | tiles[z].detach_req.store(true, .release); |
| 2109 | ring(&tiles[z]); | 2088 | ring(&tiles[z]); |
| 2110 | awaitDetach(&tiles[z], &shared); | 2089 | awaitDetach(&tiles[z], &shared); |
| 2111 | wall_layout.saveSidecar(alloc, tiles[0..live], present[0..live], &shared); | 2090 | wall_layout.saveSidecar(w); |
| 2112 | exit_code = 0; | 2091 | exit_code = 0; |
| 2113 | exit_msg = "mux: detached (session still running; run mux to reattach)"; | 2092 | exit_msg = "mux: detached (session still running; run mux to reattach)"; |
| 2114 | break :keys; | 2093 | break :keys; |
| @@ -2117,7 +2096,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2117 | // Nothing left to fold in: every tile the wall will ever | 2096 | // Nothing left to fold in: every tile the wall will ever |
| 2118 | // have is already here or on its way from a host's list. | 2097 | // have is already here or on its way from a host's list. |
| 2119 | // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom. | 2098 | // `Ctrl-\ w` is a re-cut of the stripes, and an unzoom. |
| 2120 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2099 | wall_layout.relayout(w, shared.sel); |
| 2121 | }, | 2100 | }, |
| 2122 | .new_session, .split_right, .split_below => { | 2101 | .new_session, .split_right, .split_below => { |
| 2123 | // Ask the focused tile's daemon for a new session. The | 2102 | // Ask the focused tile's daemon for a new session. The |
| @@ -2131,9 +2110,9 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2131 | }; | 2110 | }; |
| 2132 | tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release); | 2111 | tiles[z].ask.store(@intFromEnum(client.SwitchIntent.new), .release); |
| 2133 | if (ringLive(&tiles[z])) { | 2112 | if (ringLive(&tiles[z])) { |
| 2134 | wall_host.pokeHost(host_table[0..hosts_live], &tiles[z]); | 2113 | wall_host.pokeHost(w.hosts, &tiles[z]); |
| 2135 | } else { | 2114 | } else { |
| 2136 | setNotice(&shared, "[no live session here - Ctrl-\\ s picks a host]"); | 2115 | setNotice(&shared, no_live_here); |
| 2137 | showRefusal(tiles[0..live], &shared, z); | 2116 | showRefusal(tiles[0..live], &shared, z); |
| 2138 | } | 2117 | } |
| 2139 | }, | 2118 | }, |
| @@ -2142,16 +2121,16 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2142 | // every session every host has, so asking one daemon for a | 2121 | // every session every host has, so asking one daemon for a |
| 2143 | // ring would walk its slice and skip the rest of the wall. | 2122 | // ring would walk its slice and skip the rest of the wall. |
| 2144 | if (walkTiles(present[0..live], z, cmd.action == .next_session)) |to| { | 2123 | if (walkTiles(present[0..live], z, cmd.action == .next_session)) |to| { |
| 2145 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, to); | 2124 | focusAnswer(w, false, to); |
| 2146 | } | 2125 | } |
| 2147 | }, | 2126 | }, |
| 2148 | .focus_dir => |d| { | 2127 | .focus_dir => |d| { |
| 2149 | const flat = shared.base_flat orelse shared.last_flat; | 2128 | const flat = shared.base_flat orelse shared.last_flat; |
| 2150 | if (flat) |f| { | 2129 | if (flat) |f| { |
| 2151 | if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| { | 2130 | if (layout.neighbor(f, @intCast(z), wall_layout.dirOf(d))) |nb| { |
| 2152 | if (nb < live and present[nb] and nb != z) { | 2131 | if (nb < w.live.* and present[nb] and nb != z) { |
| 2153 | if (shared.fullscreen) | 2132 | if (shared.fullscreen) |
| 2154 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, nb) | 2133 | focusAnswer(w, false, nb) |
| 2155 | else | 2134 | else |
| 2156 | setFocus(tiles[0..live], &shared, nb); | 2135 | setFocus(tiles[0..live], &shared, nb); |
| 2157 | } | 2136 | } |
| @@ -2160,20 +2139,20 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2160 | }, | 2139 | }, |
| 2161 | .fullscreen => { | 2140 | .fullscreen => { |
| 2162 | shared.fullscreen = !shared.fullscreen; | 2141 | shared.fullscreen = !shared.fullscreen; |
| 2163 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2142 | wall_layout.relayout(w, shared.sel); |
| 2164 | }, | 2143 | }, |
| 2165 | .resize => |d| { | 2144 | .resize => |d| { |
| 2166 | _ = wall_layout.doResize(alloc, tiles[0..live], present[0..live], &shared, z, d); | 2145 | _ = wall_layout.doResize(w, z, d); |
| 2167 | }, | 2146 | }, |
| 2168 | .focus => |idx| { | 2147 | .focus => |idx| { |
| 2169 | if (idx > 0 and idx <= live and present[idx - 1] and idx - 1 != z) { | 2148 | if (idx > 0 and idx <= w.live.* and present[idx - 1] and idx - 1 != z) { |
| 2170 | if (shared.fullscreen) | 2149 | if (shared.fullscreen) |
| 2171 | focusAnswer(alloc, tiles[0..live], present[0..live], &shared, false, idx - 1) | 2150 | focusAnswer(w, false, idx - 1) |
| 2172 | else | 2151 | else |
| 2173 | setFocus(tiles[0..live], &shared, idx - 1); | 2152 | setFocus(tiles[0..live], &shared, idx - 1); |
| 2174 | } | 2153 | } |
| 2175 | }, | 2154 | }, |
| 2176 | .end_session => if (z < live and present[z]) { | 2155 | .end_session => if (z < w.live.* and present[z]) { |
| 2177 | // `Ctrl-\ x` ends the SESSION on its daemon. The tile leaves | 2156 | // `Ctrl-\ x` ends the SESSION on its daemon. The tile leaves |
| 2178 | // when the daemon's list no longer has it, not when the key | 2157 | // when the daemon's list no longer has it, not when the key |
| 2179 | // is pressed. | 2158 | // is pressed. |
| @@ -2197,7 +2176,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 2197 | break :keys; | 2176 | break :keys; |
| 2198 | } | 2177 | } |
| 2199 | setNotice(&shared, "[nothing attached there yet - tile closed]"); | 2178 | setNotice(&shared, "[nothing attached there yet - tile closed]"); |
| 2200 | wall_layout.relayout(alloc, tiles[0..live], present[0..live], &shared, shared.sel); | 2179 | wall_layout.relayout(w, shared.sel); |
| 2201 | }, | 2180 | }, |
| 2202 | .none => {}, | 2181 | .none => {}, |
| 2203 | } | 2182 | } |