5b4cbbe7
refactor: derive the label bar from the tty at one point
a73x 2026-09-01 08:44
Commit message
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -306,7 +306,7 @@ fn dressSilent(w: Wall, hi: usize) void { | |||
| 306 | } | 306 | } |
| 307 | // A pending pane has no pump, so no doorbell reaches its bar: the | 307 | // A pending pane has no pump, so no doorbell reaches its bar: the |
| 308 | // keyboard is the only thread that can repaint one. | 308 | // keyboard is the only thread that can repaint one. |
| 309 | if (dressed and w.shared.label_rows != 0) wv.paintDeadBarsLocked(w.liveTiles()); | 309 | if (dressed and w.shared.labelRows() != 0) wv.paintDeadBarsLocked(w.liveTiles()); |
| 310 | } | 310 | } |
| 311 | 311 | ||
| 312 | /// One host's list, applied to the wall. The keyboard thread only: it is | 312 | /// One host's list, applied to the wall. The keyboard thread only: it is |
src/tui/wall_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -26,6 +26,13 @@ pub fn wallFloors(bar: bool) layout.Floors { | |||
| 26 | }; | 26 | }; |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | /// The floors for THIS wall: `wallFloors` keyed by the one bar rule, | ||
| 30 | /// `Shared.labelRows`, so a production call site cannot key the floor off | ||
| 31 | /// anything else and cut stripes the painted bars then overflow. | ||
| 32 | pub fn floorsOf(shared: *const Shared) layout.Floors { | ||
| 33 | return wallFloors(shared.labelRows() != 0); | ||
| 34 | } | ||
| 35 | |||
| 29 | /// The root container's orientation for N tiles at a given terminal size: | 36 | /// The root container's orientation for N tiles at a given terminal size: |
| 30 | /// `.beside` when the terminal is wide enough that columns are the natural | 37 | /// `.beside` when the terminal is wide enough that columns are the natural |
| 31 | /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal | 38 | /// cut, `.stacked` otherwise. The 2x corrects for cell shape — a terminal |
| @@ -62,7 +69,7 @@ pub fn doResize(w: Wall, sel: usize, d: interact.PrefixFilter.Dir) bool { | |||
| 62 | // Focus gains from the sibling toward `ld`; if none there (edge | 69 | // Focus gains from the sibling toward `ld`; if none there (edge |
| 63 | // pane), try the opposite side — gaining from either sibling | 70 | // pane), try the opposite side — gaining from either sibling |
| 64 | // widens or tallens the focus. | 71 | // widens or tallens the focus. |
| 65 | if (w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), focus_tile, ld, 1)) { | 72 | if (w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), focus_tile, ld, 1)) { |
| 66 | moved = true; | 73 | moved = true; |
| 67 | } else { | 74 | } else { |
| 68 | const opp = switch (ld) { | 75 | const opp = switch (ld) { |
| @@ -71,7 +78,7 @@ pub fn doResize(w: Wall, sel: usize, d: interact.PrefixFilter.Dir) bool { | |||
| 71 | .left => layout.Dir.right, | 78 | .left => layout.Dir.right, |
| 72 | .up => layout.Dir.down, | 79 | .up => layout.Dir.down, |
| 73 | }; | 80 | }; |
| 74 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), focus_tile, opp, 1); | 81 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), focus_tile, opp, 1); |
| 75 | } | 82 | } |
| 76 | } else { | 83 | } else { |
| 77 | // Shrink: a neighbor on the same axis gains a cell from focus. | 84 | // Shrink: a neighbor on the same axis gains a cell from focus. |
| @@ -84,11 +91,11 @@ pub fn doResize(w: Wall, sel: usize, d: interact.PrefixFilter.Dir) bool { | |||
| 84 | .down => layout.Dir.up, | 91 | .down => layout.Dir.up, |
| 85 | }; | 92 | }; |
| 86 | if (layout.neighbor(flat, focus_tile, ld)) |nb| { | 93 | if (layout.neighbor(flat, focus_tile, ld)) |nb| { |
| 87 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), nb, opp, 1); | 94 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), nb, opp, 1); |
| 88 | } | 95 | } |
| 89 | if (!moved) { | 96 | if (!moved) { |
| 90 | if (layout.neighbor(flat, focus_tile, opp)) |nb| { | 97 | if (layout.neighbor(flat, focus_tile, opp)) |nb| { |
| 91 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), nb, ld, 1); | 98 | moved = w.shared.tree.resize(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), nb, ld, 1); |
| 92 | } | 99 | } |
| 93 | } | 100 | } |
| 94 | } | 101 | } |
| @@ -124,12 +131,6 @@ pub fn relayout(w: Wall, sel: usize) void { | |||
| 124 | for (w.livePresent()) |p| { | 131 | for (w.livePresent()) |p| { |
| 125 | if (p) live += 1; | 132 | if (p) live += 1; |
| 126 | } | 133 | } |
| 127 | // The bar follows the tty, not the tile count: it is what names the | ||
| 128 | // session on screen, and hiding it when one tile was visible left a | ||
| 129 | // solo or fullscreened session anonymous. A piped `mux` never draws | ||
| 130 | // one — its byte stream is a script's input, and a bar in it would be | ||
| 131 | // bytes the session never wrote. | ||
| 132 | w.shared.label_rows = @intFromBool(w.shared.is_tty); | ||
| 133 | if (w.shared.is_tty) proto.writeAllFd(w.shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; | 134 | if (w.shared.is_tty) proto.writeAllFd(w.shared.out_fd, "\x1b[?25l\x1b[H\x1b[2J") catch {}; |
| 134 | // The screen the popup was on has just been cleared, so the next paint | 135 | // The screen the popup was on has just been cleared, so the next paint |
| 135 | // owes it however unchanged its rows are. | 136 | // owes it however unchanged its rows are. |
| @@ -140,12 +141,12 @@ pub fn relayout(w: Wall, sel: usize) void { | |||
| 140 | } | 141 | } |
| 141 | // The base flat (null) is always computed so `focus_dir` can read | 142 | // The base flat (null) is always computed so `focus_dir` can read |
| 142 | // adjacency from the real layout while fullscreened. | 143 | // adjacency from the real layout while fullscreened. |
| 143 | if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), null)) |base| { | 144 | if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), null)) |base| { |
| 144 | if (w.shared.base_flat) |*old| old.deinit(w.shared.flat_alloc); | 145 | if (w.shared.base_flat) |*old| old.deinit(w.shared.flat_alloc); |
| 145 | w.shared.base_flat = base; | 146 | w.shared.base_flat = base; |
| 146 | } else |_| {} | 147 | } else |_| {} |
| 147 | const fs_arg: ?u8 = if (w.shared.fullscreen) @intCast(sel) else null; | 148 | const fs_arg: ?u8 = if (w.shared.fullscreen) @intCast(sel) else null; |
| 148 | var cut = w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), fs_arg); | 149 | var cut = w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), fs_arg); |
| 149 | if (cut) |_| {} else |e| { | 150 | if (cut) |_| {} else |e| { |
| 150 | // A split or a resize key is an OPERATION the user asked for, and | 151 | // A split or a resize key is an OPERATION the user asked for, and |
| 151 | // refusing leaves the screen as it was. A SIGWINCH is not: the terminal | 152 | // refusing leaves the screen as it was. A SIGWINCH is not: the terminal |
| @@ -153,7 +154,7 @@ pub fn relayout(w: Wall, sel: usize) void { | |||
| 153 | // bottom of a cleared screen. Degrade to the focused tile whole and the | 154 | // bottom of a cleared screen. Degrade to the focused tile whole and the |
| 154 | // rest at 0x0. The TREE is untouched, so growing back re-cuts. | 155 | // rest at 0x0. The TREE is untouched, so growing back re-cuts. |
| 155 | if (e == error.TooSmall and fs_arg == null) { | 156 | if (e == error.TooSmall and fs_arg == null) { |
| 156 | if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, wallFloors(w.shared.is_tty), @intCast(sel))) |only| { | 157 | if (w.shared.tree.flatten(w.alloc, w.shared.size.rows, w.shared.size.cols, floorsOf(w.shared), @intCast(sel))) |only| { |
| 157 | cut = only; | 158 | cut = only; |
| 158 | } else |_| {} | 159 | } else |_| {} |
| 159 | } | 160 | } |
| @@ -179,7 +180,7 @@ pub fn relayout(w: Wall, sel: usize) void { | |||
| 179 | if (p) wv.ring(t); | 180 | if (p) wv.ring(t); |
| 180 | } | 181 | } |
| 181 | // ...except the tiles with no pump left to hear it: their bars are the | 182 | // ...except the tiles with no pump left to hear it: their bars are the |
| 182 | // keyboard's. No `label_rows` guard, unlike `setFocus` — the screen was | 183 | // keyboard's. No `labelRows` guard, unlike `setFocus` — the screen was |
| 183 | // just cleared, and that bar is all that says the target refused. | 184 | // just cleared, and that bar is all that says the target refused. |
| 184 | wv.paintDeadBarsLocked(w.liveTiles()); | 185 | wv.paintDeadBarsLocked(w.liveTiles()); |
| 185 | } | 186 | } |
| @@ -418,7 +419,7 @@ fn seedAttempt( | |||
| 418 | return null; | 419 | return null; |
| 419 | }; | 420 | }; |
| 420 | } | 421 | } |
| 421 | if (parsed.tree.flatten(alloc, shared.size.rows, shared.size.cols, wallFloors(shared.is_tty), null)) |flat| { | 422 | if (parsed.tree.flatten(alloc, shared.size.rows, shared.size.cols, floorsOf(shared), null)) |flat| { |
| 422 | var f = flat; | 423 | var f = flat; |
| 423 | f.deinit(alloc); | 424 | f.deinit(alloc); |
| 424 | } else |_| { | 425 | } else |_| { |
src/tui/wall_pump.zig
| Old | New | ||
|---|---|---|---|
| @@ -406,7 +406,7 @@ pub fn takePass(t: *Tile) Pass { | |||
| 406 | .left = t.rect.left, | 406 | .left = t.rect.left, |
| 407 | .rows = t.rect.rows, | 407 | .rows = t.rect.rows, |
| 408 | .cols = t.rect.cols, | 408 | .cols = t.rect.cols, |
| 409 | .label_rows = t.shared.label_rows, | 409 | .label_rows = t.shared.labelRows(), |
| 410 | .term_cols = t.shared.size.cols, | 410 | .term_cols = t.shared.size.cols, |
| 411 | .term_rows = t.shared.size.rows, | 411 | .term_rows = t.shared.size.rows, |
| 412 | .resize = owed, | 412 | .resize = owed, |
src/tui/wall_test_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -55,8 +55,7 @@ test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { | |||
| 55 | defer f.deinit(alloc); | 55 | defer f.deinit(alloc); |
| 56 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows); | 56 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(0).?.rows); |
| 57 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows); | 57 | try std.testing.expectEqual(@as(u16, 3), f.rectOf(1).?.rows); |
| 58 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 6 }, .is_tty = false }; | 58 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 6 }, .is_tty = true }; |
| 59 | shared.label_rows = 1; // two tiles draw a bar | ||
| 60 | var t0 = Tile{ | 59 | var t0 = Tile{ |
| 61 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 60 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 62 | .rect = f.rectOf(0).?, | 61 | .rect = f.rectOf(0).?, |
| @@ -85,9 +84,8 @@ test "the tree refuses a multi-tile cut too thin for the daemon's row floor" { | |||
| 85 | } | 84 | } |
| 86 | 85 | ||
| 87 | test "viewRows: a bar-less wall keeps every row, a barred tile loses one" { | 86 | test "viewRows: a bar-less wall keeps every row, a barred tile loses one" { |
| 88 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | ||
| 89 | // No bar (a pipe): the tile claims every row. | 87 | // No bar (a pipe): the tile claims every row. |
| 90 | shared.label_rows = 0; | 88 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 91 | var t0 = Tile{ | 89 | var t0 = Tile{ |
| 92 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 90 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 93 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | 91 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| @@ -99,7 +97,7 @@ test "viewRows: a bar-less wall keeps every row, a barred tile loses one" { | |||
| 99 | try std.testing.expectEqual(@as(u16, 24), t0.viewRows()); | 97 | try std.testing.expectEqual(@as(u16, 24), t0.viewRows()); |
| 100 | 98 | ||
| 101 | // Under a bar (any tty wall): each tile loses one row to it. | 99 | // Under a bar (any tty wall): each tile loses one row to it. |
| 102 | shared.label_rows = 1; | 100 | shared.is_tty = true; |
| 103 | var t1 = Tile{ | 101 | var t1 = Tile{ |
| 104 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 102 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 105 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, | 103 | .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 }, |
| @@ -148,7 +146,7 @@ test "relayout sets resize_pending on every live tile" { | |||
| 148 | } | 146 | } |
| 149 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); | 147 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); |
| 150 | // A tty wall draws its label bar. | 148 | // A tty wall draws its label bar. |
| 151 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 149 | try std.testing.expectEqual(@as(u8, 1), shared.labelRows()); |
| 152 | // Every tile is doorbelled: its pump sends the new rect. | 150 | // Every tile is doorbelled: its pump sends the new rect. |
| 153 | try std.testing.expect(tiles[0].resize_pending); | 151 | try std.testing.expect(tiles[0].resize_pending); |
| 154 | try std.testing.expect(tiles[1].resize_pending); | 152 | try std.testing.expect(tiles[1].resize_pending); |
| @@ -209,7 +207,7 @@ test "a relayout landing mid-pass is still sent" { | |||
| 209 | while (w.passes.load(.acquire) < n + 1) {} | 207 | while (w.passes.load(.acquire) < n + 1) {} |
| 210 | 208 | ||
| 211 | shared.paint_mu.lock(); | 209 | shared.paint_mu.lock(); |
| 212 | const want = ResizeWitness.pack(tiles[0].rect.cols, tiles[0].rect.rows -| shared.label_rows); | 210 | const want = ResizeWitness.pack(tiles[0].rect.cols, tiles[0].rect.rows -| shared.labelRows()); |
| 213 | shared.paint_mu.unlock(); | 211 | shared.paint_mu.unlock(); |
| 214 | // The claim: a quiet wall and a quiet pump agree on the grid. A | 212 | // The claim: a quiet wall and a quiet pump agree on the grid. A |
| 215 | // doorbell consumed apart from the rect it describes breaks this — | 213 | // doorbell consumed apart from the rect it describes breaks this — |
| @@ -283,7 +281,7 @@ test "a terminal too small for the cut falls back to the focused pane, and grows | |||
| 283 | // — a rect that paints nothing and claims no size — and the | 281 | // — a rect that paints nothing and claims no size — and the |
| 284 | // tree untouched underneath. The bar stays: it follows the | 282 | // tree untouched underneath. The bar stays: it follows the |
| 285 | // tty, and the one visible pane still deserves its name. | 283 | // tty, and the one visible pane still deserves its name. |
| 286 | try std.testing.expectEqual(@as(u16, 1), shared.label_rows); | 284 | try std.testing.expectEqual(@as(u16, 1), shared.labelRows()); |
| 287 | try std.testing.expectEqual(@as(u16, 20), tiles[0].rect.rows); | 285 | try std.testing.expectEqual(@as(u16, 20), tiles[0].rect.rows); |
| 288 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); | 286 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); |
| 289 | try std.testing.expectEqual(@as(u16, 0), tiles[0].rect.top); | 287 | try std.testing.expectEqual(@as(u16, 0), tiles[0].rect.top); |
| @@ -300,7 +298,7 @@ test "a terminal too small for the cut falls back to the focused pane, and grows | |||
| 300 | try std.testing.expectEqual(@as(usize, 0), std.mem.trim(u8, l, " ").len); | 298 | try std.testing.expectEqual(@as(usize, 0), std.mem.trim(u8, l, " ").len); |
| 301 | } | 299 | } |
| 302 | } else { | 300 | } else { |
| 303 | try std.testing.expectEqual(@as(u16, 1), shared.label_rows); | 301 | try std.testing.expectEqual(@as(u16, 1), shared.labelRows()); |
| 304 | // Seven bars, each on the first row of the pane it names. | 302 | // Seven bars, each on the first row of the pane it names. |
| 305 | for (tiles) |*t| { | 303 | for (tiles) |*t| { |
| 306 | const l = WallScreen.line(dump, t.rect.top) orelse return error.NoSuchRow; | 304 | const l = WallScreen.line(dump, t.rect.top) orelse return error.NoSuchRow; |
| @@ -336,7 +334,7 @@ test "fullscreen gives the focused tile the whole terminal and hides the rest" { | |||
| 336 | shared.fullscreen = true; | 334 | shared.fullscreen = true; |
| 337 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); | 335 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 338 | // No label bar on a pipe, fullscreened or not — the bar is the tty's. | 336 | // No label bar on a pipe, fullscreened or not — the bar is the tty's. |
| 339 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | 337 | try std.testing.expectEqual(@as(u8, 0), shared.labelRows()); |
| 340 | // Tile 0 gets the whole terminal; tile 1 gets nothing. | 338 | // Tile 0 gets the whole terminal; tile 1 gets nothing. |
| 341 | try std.testing.expectEqual(@as(u16, 24), tiles[0].rect.rows); | 339 | try std.testing.expectEqual(@as(u16, 24), tiles[0].rect.rows); |
| 342 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); | 340 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); |
| @@ -375,7 +373,7 @@ test "toggle off fullscreen restores the real rects" { | |||
| 375 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); | 373 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 376 | // Two tiles again: the bar (never gone on a tty) and both tiles' | 374 | // Two tiles again: the bar (never gone on a tty) and both tiles' |
| 377 | // real rects are back (stacked: full width, half height each). | 375 | // real rects are back (stacked: full width, half height each). |
| 378 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 376 | try std.testing.expectEqual(@as(u8, 1), shared.labelRows()); |
| 379 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); | 377 | try std.testing.expectEqual(@as(u16, 80), tiles[0].rect.cols); |
| 380 | try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); | 378 | try std.testing.expectEqual(@as(u16, 80), tiles[1].rect.cols); |
| 381 | try std.testing.expect(tiles[0].rect.rows < 24); | 379 | try std.testing.expect(tiles[0].rect.rows < 24); |
| @@ -409,17 +407,17 @@ test "the label bar follows the tty, not the tile count" { | |||
| 409 | }; | 407 | }; |
| 410 | // One tile on a tty: the bar stays, and the session runs a row short. | 408 | // One tile on a tty: the bar stays, and the session runs a row short. |
| 411 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); | 409 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 412 | try std.testing.expectEqual(@as(u16, 1), shared.label_rows); | 410 | try std.testing.expectEqual(@as(u16, 1), shared.labelRows()); |
| 413 | try std.testing.expectEqual(@as(u16, 23), tiles[0].viewRows()); | 411 | try std.testing.expectEqual(@as(u16, 23), tiles[0].viewRows()); |
| 414 | // Fullscreen is still a tty view, so the zoomed tile keeps its name. | 412 | // Fullscreen is still a tty view, so the zoomed tile keeps its name. |
| 415 | shared.fullscreen = true; | 413 | shared.fullscreen = true; |
| 416 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); | 414 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 417 | try std.testing.expectEqual(@as(u16, 1), shared.label_rows); | 415 | try std.testing.expectEqual(@as(u16, 1), shared.labelRows()); |
| 418 | // A pipe draws no bar whatever the count. | 416 | // A pipe draws no bar whatever the count. |
| 419 | shared.fullscreen = false; | 417 | shared.fullscreen = false; |
| 420 | shared.is_tty = false; | 418 | shared.is_tty = false; |
| 421 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); | 419 | wall_layout.relayout(fixture.wallAll(alloc, tiles, &present, &shared), 0); |
| 422 | try std.testing.expectEqual(@as(u16, 0), shared.label_rows); | 420 | try std.testing.expectEqual(@as(u16, 0), shared.labelRows()); |
| 423 | } | 421 | } |
| 424 | 422 | ||
| 425 | test "focus_dir while fullscreened follows the focus" { | 423 | test "focus_dir while fullscreened follows the focus" { |
| @@ -577,7 +575,7 @@ test "a pump's answer that grew the wall re-cuts it; a mere focus move does not" | |||
| 577 | // and the old tile still owns the whole terminal — the screen keeps | 575 | // and the old tile still owns the whole terminal — the screen keeps |
| 578 | // showing tile 0 while the keyboard types into tile 1's rows. | 576 | // showing tile 0 while the keyboard types into tile 1's rows. |
| 579 | wv.focusAnswer(fixture.wallAll(alloc, tiles, present, &shared), true, 1); | 577 | wv.focusAnswer(fixture.wallAll(alloc, tiles, present, &shared), true, 1); |
| 580 | try std.testing.expectEqual(@as(u8, 1), shared.label_rows); | 578 | try std.testing.expectEqual(@as(u8, 1), shared.labelRows()); |
| 581 | try std.testing.expect(tiles[0].resize_pending); | 579 | try std.testing.expect(tiles[0].resize_pending); |
| 582 | try std.testing.expect(tiles[1].resize_pending); | 580 | try std.testing.expect(tiles[1].resize_pending); |
| 583 | try std.testing.expectEqual(@as(usize, 1), shared.sel); | 581 | try std.testing.expectEqual(@as(usize, 1), shared.sel); |
| @@ -616,7 +614,7 @@ test "a piped wall draws no label bar and paints row 1" { | |||
| 616 | }; | 614 | }; |
| 617 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); | 615 | wall_layout.relayout(fixture.wallAll(alloc, tiles, present, &shared), 0); |
| 618 | // A pipe: no label bar, every row is content the script can read. | 616 | // A pipe: no label bar, every row is content the script can read. |
| 619 | try std.testing.expectEqual(@as(u8, 0), shared.label_rows); | 617 | try std.testing.expectEqual(@as(u8, 0), shared.labelRows()); |
| 620 | try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); | 618 | try std.testing.expectEqual(@as(u16, 24), tiles[0].viewRows()); |
| 621 | } | 619 | } |
| 622 | 620 | ||
src/tui/wall_test_pump.zig
| Old | New | ||
|---|---|---|---|
| @@ -445,8 +445,7 @@ test "a view tile's attach makes no size claim; a tile the user asked for does" | |||
| 445 | // a tile the user asked for (creates = true) claims its rect in the | 445 | // a tile the user asked for (creates = true) claims its rect in the |
| 446 | // attach itself. | 446 | // attach itself. |
| 447 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 447 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 448 | // A one-tile wall: no label bar, so viewRows is the whole stripe. | 448 | // A piped wall draws no bar, so viewRows is the whole stripe. |
| 449 | shared.label_rows = 0; | ||
| 450 | var t = Tile{ | 449 | var t = Tile{ |
| 451 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, | 450 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, |
| 452 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | 451 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, |
| @@ -495,8 +494,7 @@ test "a newborn tile's first claim is its real stripe, not the placeholder" { | |||
| 495 | // placeholder stripe silently freezes the session on a stale grid. | 494 | // placeholder stripe silently freezes the session on a stale grid. |
| 496 | // The tile's stripe must be what relayout would give it BEFORE the | 495 | // The tile's stripe must be what relayout would give it BEFORE the |
| 497 | // pump's first sendAttach. | 496 | // pump's first sendAttach. |
| 498 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 497 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 499 | shared.label_rows = 1; // two tiles draw a bar | ||
| 500 | // Real stripe for the second tile of a 2-tile wall on 24 rows. | 498 | // Real stripe for the second tile of a 2-tile wall on 24 rows. |
| 501 | var t = Tile{ | 499 | var t = Tile{ |
| 502 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, | 500 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false }, |
src/tui/wall_test_wall.zig
| Old | New | ||
|---|---|---|---|
| @@ -48,9 +48,9 @@ test "birthTile: a chord-born tile creates and offers no agent" { | |||
| 48 | try std.testing.expectEqual(@as(?usize, 0), tiles[at].born_from); | 48 | try std.testing.expectEqual(@as(?usize, 0), tiles[at].born_from); |
| 49 | try std.testing.expect(present[1]); | 49 | try std.testing.expect(present[1]); |
| 50 | try std.testing.expectEqual(@as(usize, 2), live); | 50 | try std.testing.expectEqual(@as(usize, 2), live); |
| 51 | // Two tiles draw a bar, and the bar is set at birth so viewRows is | 51 | // A tty wall draws a bar — derived from `is_tty`, so a birth cannot |
| 52 | // right from the first attach. | 52 | // forget it and viewRows is right from the first attach. |
| 53 | try std.testing.expectEqual(@as(u16, 1), shared.label_rows); | 53 | try std.testing.expectEqual(@as(u16, 1), shared.labelRows()); |
| 54 | } | 54 | } |
| 55 | 55 | ||
| 56 | test "the empty wall advertises the key the picker really answers to" { | 56 | test "the empty wall advertises the key the picker really answers to" { |
| @@ -887,8 +887,7 @@ test "bare keys reach the focused tile; the prefix does not" { | |||
| 887 | } | 887 | } |
| 888 | 888 | ||
| 889 | test "focus follows a click: rectHit picks the tile under a row and column" { | 889 | test "focus follows a click: rectHit picks the tile under a row and column" { |
| 890 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 890 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 891 | shared.label_rows = 1; | ||
| 892 | var tiles: [2]Tile = undefined; | 891 | var tiles: [2]Tile = undefined; |
| 893 | tiles[0] = Tile{ | 892 | tiles[0] = Tile{ |
| 894 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 893 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| @@ -1023,7 +1022,6 @@ test "setFocus: the marker moves between two tiles with no pump to move it" { | |||
| 1023 | defer std.posix.close(p[0]); | 1022 | defer std.posix.close(p[0]); |
| 1024 | defer std.posix.close(p[1]); | 1023 | defer std.posix.close(p[1]); |
| 1025 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | 1024 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 1026 | shared.label_rows = 1; // two tiles draw a bar | ||
| 1027 | var tiles: [3]Tile = undefined; | 1025 | var tiles: [3]Tile = undefined; |
| 1028 | tiles[0] = Tile{ | 1026 | tiles[0] = Tile{ |
| 1029 | .r = .{ .target = .{ .sock = "/s" }, .label = "one", .session = "a" }, | 1027 | .r = .{ .target = .{ .sock = "/s" }, .label = "one", .session = "a" }, |
| @@ -1085,7 +1083,6 @@ test "the bar's digit is the tile's chord: idx + 1, and a two-digit tile still s | |||
| 1085 | defer std.posix.close(p[0]); | 1083 | defer std.posix.close(p[0]); |
| 1086 | defer std.posix.close(p[1]); | 1084 | defer std.posix.close(p[1]); |
| 1087 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | 1085 | var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 1088 | shared.label_rows = 1; | ||
| 1089 | shared.sel = 0; | 1086 | shared.sel = 0; |
| 1090 | var tiles: [3]Tile = undefined; | 1087 | var tiles: [3]Tile = undefined; |
| 1091 | const labels = [_][]const u8{ "one", "two", "twelve" }; | 1088 | const labels = [_][]const u8{ "one", "two", "twelve" }; |
| @@ -1158,7 +1155,6 @@ test "tileBanner: a prompt wider than its pane shows the tail inside the pane" { | |||
| 1158 | const pipe = try std.posix.pipe(); | 1155 | const pipe = try std.posix.pipe(); |
| 1159 | defer std.posix.close(pipe[0]); | 1156 | defer std.posix.close(pipe[0]); |
| 1160 | var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; | 1157 | var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true }; |
| 1161 | shared.label_rows = 1; | ||
| 1162 | var t = Tile{ | 1158 | var t = Tile{ |
| 1163 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, | 1159 | .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" }, |
| 1164 | .rect = .{ .top = 2, .left = 5, .rows = 5, .cols = 10 }, | 1160 | .rect = .{ .top = 2, .left = 5, .rows = 5, .cols = 10 }, |
| @@ -1175,7 +1171,7 @@ test "tileBanner: a prompt wider than its pane shows the tail inside the pane" { | |||
| 1175 | var out: [256]u8 = undefined; | 1171 | var out: [256]u8 = undefined; |
| 1176 | const n = try std.posix.read(pipe[0], &out); | 1172 | const n = try std.posix.read(pipe[0], &out); |
| 1177 | const got = out[0..n]; | 1173 | const got = out[0..n]; |
| 1178 | // The tile's own corner: rect.top + label_rows + 1, rect.left + 1. | 1174 | // The tile's own corner: rect.top + labelRows + 1, rect.left + 1. |
| 1179 | try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[4;6H") != null); | 1175 | try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[4;6H") != null); |
| 1180 | const from = std.mem.indexOf(u8, got, "\x1b[7m").? + 4; | 1176 | const from = std.mem.indexOf(u8, got, "\x1b[7m").? + 4; |
| 1181 | const to = std.mem.indexOf(u8, got, "\x1b[0m").?; | 1177 | const to = std.mem.indexOf(u8, got, "\x1b[0m").?; |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -121,13 +121,9 @@ pub const Shared = struct { | |||
| 121 | /// Where the focused tile's last paint left the cursor. The cursor | 121 | /// Where the focused tile's last paint left the cursor. The cursor |
| 122 | /// belongs to the focus: an unfocused paint's last act puts it back here. | 122 | /// belongs to the focus: an unfocused paint's last act puts it back here. |
| 123 | cursor: Engine.CursorPos = .{ .x = 0, .y = 0 }, | 123 | cursor: Engine.CursorPos = .{ .x = 0, .y = 0 }, |
| 124 | /// One label bar per tile on a tty, however many tiles there are; a | ||
| 125 | /// piped wall owns every row and draws no bar. Set by `relayout`, so | ||
| 126 | /// `viewRows` and `core.row_off` agree with what is on the screen. | ||
| 127 | label_rows: u16 = 1, | ||
| 128 | /// The container tree that owns every tile's rect. The keyboard thread | 124 | /// The container tree that owns every tile's rect. The keyboard thread |
| 129 | /// mutates it under `paint_mu` — the same single-writer rule as `sel` | 125 | /// mutates it under `paint_mu` — the same single-writer rule as `sel` |
| 130 | /// and `label_rows` — and relayout flattens it to read rects. | 126 | /// — and relayout flattens it to read rects. |
| 131 | tree: layout.Tree = layout.Tree.init(std.heap.page_allocator), | 127 | tree: layout.Tree = layout.Tree.init(std.heap.page_allocator), |
| 132 | /// The last flatten result, kept so `focus_dir` can read adjacency | 128 | /// The last flatten result, kept so `focus_dir` can read adjacency |
| 133 | /// without re-flattening under the keyboard's `paint_mu` hold. | 129 | /// without re-flattening under the keyboard's `paint_mu` hold. |
| @@ -163,6 +159,18 @@ pub const Shared = struct { | |||
| 163 | /// the socket would land on a shared /tmp, answerable by any local user. | 159 | /// the socket would land on a shared /tmp, answerable by any local user. |
| 164 | prompts: ?*askpass.Listener = null, | 160 | prompts: ?*askpass.Listener = null, |
| 165 | prompt_exe: []const u8 = "", | 161 | prompt_exe: []const u8 = "", |
| 162 | |||
| 163 | /// One label bar per tile on a tty, however many tiles there are: the | ||
| 164 | /// bar is what names a session on screen, and hiding it when one tile | ||
| 165 | /// was visible left a solo or fullscreened session anonymous. A piped | ||
| 166 | /// wall owns every row and draws none — its byte stream is a script's | ||
| 167 | /// input, and a bar in it would be bytes the session never wrote. | ||
| 168 | /// Derived, never assigned: `viewRows`, `core.row_off` and the layout | ||
| 169 | /// floors (`wall_layout.floorsOf`) all read this one function, so no | ||
| 170 | /// site can drift from the rule the way three assignment copies could. | ||
| 171 | pub fn labelRows(s: *const Shared) u16 { | ||
| 172 | return @intFromBool(s.is_tty); | ||
| 173 | } | ||
| 166 | }; | 174 | }; |
| 167 | 175 | ||
| 168 | /// Opens the prompt box on whatever ssh is asking, if anything is. False | 176 | /// Opens the prompt box on whatever ssh is asking, if anything is. False |
| @@ -342,7 +350,7 @@ pub const Tile = struct { | |||
| 342 | release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), | 350 | release_pending: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 343 | 351 | ||
| 344 | pub fn viewRows(t: *const Tile) u16 { | 352 | pub fn viewRows(t: *const Tile) u16 { |
| 345 | return t.rect.rows -| t.shared.label_rows; | 353 | return t.rect.rows -| t.shared.labelRows(); |
| 346 | } | 354 | } |
| 347 | 355 | ||
| 348 | pub fn viewCols(t: *const Tile) u16 { | 356 | pub fn viewCols(t: *const Tile) u16 { |
| @@ -481,7 +489,7 @@ pub fn paintLabel(t: *Tile, state: State) void { | |||
| 481 | t.shared.paint_mu.lock(); | 489 | t.shared.paint_mu.lock(); |
| 482 | defer t.shared.paint_mu.unlock(); | 490 | defer t.shared.paint_mu.unlock(); |
| 483 | t.state = state; | 491 | t.state = state; |
| 484 | if (t.shared.label_rows != 0) paintLabelLocked(t); | 492 | if (t.shared.labelRows() != 0) paintLabelLocked(t); |
| 485 | } | 493 | } |
| 486 | 494 | ||
| 487 | /// Bounded by `cols` AND by `buf`: `cols` alone overran past 250 columns. | 495 | /// Bounded by `cols` AND by `buf`: `cols` alone overran past 250 columns. |
| @@ -506,7 +514,7 @@ pub fn labelText( | |||
| 506 | } | 514 | } |
| 507 | 515 | ||
| 508 | /// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the | 516 | /// The label bar: inverse, full width, `N> LABEL [state]`, truncated at the |
| 509 | /// terminal edge. Caller holds `paint_mu`. Only called when `label_rows` is | 517 | /// terminal edge. Caller holds `paint_mu`. Only called when `labelRows` is |
| 510 | /// nonzero — a piped wall owns every row and has no bar. | 518 | /// nonzero — a piped wall owns every row and has no bar. |
| 511 | pub fn paintLabelLocked(t: *Tile) void { | 519 | pub fn paintLabelLocked(t: *Tile) void { |
| 512 | if (popupOpen(t.shared)) return; | 520 | if (popupOpen(t.shared)) return; |
| @@ -571,7 +579,7 @@ pub fn tileBanner(t: *Tile, text: []const u8) void { | |||
| 571 | // banner that lost the race for `paint_mu` lands on a box row, and | 579 | // banner that lost the race for `paint_mu` lands on a box row, and |
| 572 | // `picker_stamp` suppresses the repaint that would repair it. | 580 | // `picker_stamp` suppresses the repaint that would repair it. |
| 573 | if (popupOpen(t.shared)) return; | 581 | if (popupOpen(t.shared)) return; |
| 574 | paint.paintBanner(t.shared.out_fd, t.rect.cols, shown, t.rect.top + t.shared.label_rows, t.rect.left); | 582 | paint.paintBanner(t.shared.out_fd, t.rect.cols, shown, t.rect.top + t.shared.labelRows(), t.rect.left); |
| 575 | } | 583 | } |
| 576 | 584 | ||
| 577 | /// Never blocks and never reports: a full pipe is a bell already ringing. | 585 | /// Never blocks and never reports: a full pipe is a bell already ringing. |
| @@ -691,10 +699,10 @@ pub fn setFocus(tiles: []Tile, shared: *Shared, next: usize) void { | |||
| 691 | // tiles immediate, and the rest follow on their next poll timeout. | 699 | // tiles immediate, and the rest follow on their next poll timeout. |
| 692 | _ = shared.repaint_gen.fetchAdd(1, .release); | 700 | _ = shared.repaint_gen.fetchAdd(1, .release); |
| 693 | for (tiles) |*t| ring(t); | 701 | for (tiles) |*t| ring(t); |
| 694 | // Only where there IS a bar: a piped wall sets `label_rows` to zero, | 702 | // Only where there IS a bar: a piped wall's `labelRows` is zero, and |
| 695 | // and a bar painted there lands on a content row the tile that owns | 703 | // a bar painted there lands on a content row the tile that owns the |
| 696 | // the screen has already written. | 704 | // screen has already written. |
| 697 | if (shared.label_rows != 0) paintDeadBarsLocked(tiles); | 705 | if (shared.labelRows() != 0) paintDeadBarsLocked(tiles); |
| 698 | } | 706 | } |
| 699 | 707 | ||
| 700 | /// Re-cut before the move: `setFocus` releases the outgoing session | 708 | /// Re-cut before the move: `setFocus` releases the outgoing session |
| @@ -970,16 +978,14 @@ fn birthTileOrRefuse(w: Wall, b: Birth) !usize { | |||
| 970 | w.alloc, | 978 | w.alloc, |
| 971 | w.shared.size.rows, | 979 | w.shared.size.rows, |
| 972 | w.shared.size.cols, | 980 | w.shared.size.cols, |
| 973 | wall_layout.wallFloors(w.shared.is_tty), | 981 | wall_layout.floorsOf(w.shared), |
| 974 | null, | 982 | null, |
| 975 | ); | 983 | ); |
| 976 | defer flat.deinit(w.alloc); | 984 | defer flat.deinit(w.alloc); |
| 977 | const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; | 985 | const new_rect = flat.rectOf(@intCast(at)) orelse return error.NoRect; |
| 978 | // The real rect, not a placeholder: the daemon refuses creates under | 986 | // The real rect, not a placeholder: the daemon refuses creates under |
| 979 | // `min_session_rows`, and a 2-row rect on a live session is destructive | 987 | // `min_session_rows`, and a 2-row rect on a live session is destructive |
| 980 | // under latest-wins. `label_rows` too, so `viewRows` is right at attach — | 988 | // under latest-wins. |
| 981 | // the bar follows the tty, not the tile count (`relayout`). | ||
| 982 | w.shared.label_rows = @intFromBool(w.shared.is_tty); | ||
| 983 | // Past every refusal: the tile is the wall's now, so the copies a pump | 989 | // Past every refusal: the tile is the wall's now, so the copies a pump |
| 984 | // will hold for its whole life are worth making. Made BEFORE the slot | 990 | // will hold for its whole life are worth making. Made BEFORE the slot |
| 985 | // is overwritten, so the last thing that can fail here still fails | 991 | // is overwritten, so the last thing that can fail here still fails |
| @@ -1460,7 +1466,7 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1460 | alloc, | 1466 | alloc, |
| 1461 | size.rows, | 1467 | size.rows, |
| 1462 | size.cols, | 1468 | size.cols, |
| 1463 | wall_layout.wallFloors(shared.is_tty), | 1469 | wall_layout.floorsOf(&shared), |
| 1464 | null, | 1470 | null, |
| 1465 | ) catch { | 1471 | ) catch { |
| 1466 | std.debug.print("mux: terminal too small\n", .{}); | 1472 | std.debug.print("mux: terminal too small\n", .{}); |
| @@ -1601,9 +1607,6 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1601 | alloc.free(pl.panes); | 1607 | alloc.free(pl.panes); |
| 1602 | seed_plan = null; | 1608 | seed_plan = null; |
| 1603 | } | 1609 | } |
| 1604 | // The bar follows the tty, not the tile count (`relayout` states why). | ||
| 1605 | // Set before the pumps start so `viewRows` is right on the first attach. | ||
| 1606 | shared.label_rows = @intFromBool(shared.is_tty); | ||
| 1607 | for (tiles[0..live]) |*t| spawnPump(t); | 1610 | for (tiles[0..live]) |*t| spawnPump(t); |
| 1608 | // A wall with no tile yet paints its one line rather than nothing (a | 1611 | // A wall with no tile yet paints its one line rather than nothing (a |
| 1609 | // blank terminal with no cursor reads as hung), and a seeded wall | 1612 | // blank terminal with no cursor reads as hung), and a seeded wall |