a73x

1ad73c5e

refactor: one clipped render, a row subset or all of them

a73x   2026-08-29 10:01

Commit message
refactor: one clipped render, a row subset or all of them

renderClipped and renderRowsClipped were the same twenty-two lines —
bracket, viewport, ECH, dumpRow, cursor, unbracket — differing only in
which rows they walked and whether the screen was cleared first.
renderClipped now takes `rows: ?[]const u16` and renderRowsClipped is the
moving-selection wrapper, so the ECH stays span-bounded on exactly one
path and a tile still cannot reach a rail.

Pinned by paint.zig's oracle tests, which replay the emitted bytes into an
engine and judge the grid (the beside-offset cases included), and by e2e
07_wallcli 5, 12_panes 10, 09_hosts 10.

src/tui/interact.zig
Old New
@@ -1530,7 +1530,7 @@ pub const Core = struct {
1530 if (!self.beginPaint()) return; 1530 if (!self.beginPaint()) return;
1531 defer self.endPaint(); 1531 defer self.endPaint();
1532 const hl = self.highlight(); 1532 const hl = self.highlight();
1533 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), self.owns_screen, self.out_fd); 1533 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd);
1534 paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.viewport(), self.out_fd); 1534 paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.viewport(), self.out_fd);
1535 } 1535 }
1536 1536
@@ -1603,7 +1603,7 @@ pub const Core = struct {
1603 if (!self.beginPaint()) return; 1603 if (!self.beginPaint()) return;
1604 defer self.endPaint(); 1604 defer self.endPaint();
1605 const hl = self.highlight(); 1605 const hl = self.highlight();
1606 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), self.owns_screen, self.out_fd); 1606 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd);
1607 } 1607 }
1608 1608
1609 /// A one-line marker in the corner, painted over by the next full 1609 /// A one-line marker in the corner, painted over by the next full
@@ -1675,7 +1675,7 @@ pub const Core = struct {
1675 // between them would put the two halves of this frame 1675 // between them would put the two halves of this frame
1676 // on two different screens), and the sink is not reentrant. 1676 // on two different screens), and the sink is not reentrant.
1677 const hl = self.highlight(); 1677 const hl = self.highlight();
1678 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), self.owns_screen, self.out_fd); 1678 try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd);
1679 self.repaint_after_resync = false; 1679 self.repaint_after_resync = false;
1680 } else { 1680 } else {
1681 const hl = self.highlight(); 1681 const hl = self.highlight();
src/tui/paint.zig
Old New
@@ -61,12 +61,13 @@ pub fn clampCursor(cur: Engine.CursorPos, vp: Viewport) Engine.CursorPos {
61 } 61 }
62 62
63 /// The replica may exceed the tty under latest-wins; rows clip at the 63 /// The replica may exceed the tty under latest-wins; rows clip at the
64 /// right edge because DECAWM is off from attach. 64 /// right edge (DECAWM off). `rows` null is every row of the viewport.
65 pub fn renderClipped( 65 pub fn renderClipped(
66 alloc: std.mem.Allocator, 66 alloc: std.mem.Allocator,
67 replica: *Engine, 67 replica: *Engine,
68 vp: Viewport, 68 vp: Viewport,
69 hl: Highlight, 69 hl: Highlight,
70 rows: ?[]const u16,
70 owns_screen: bool, 71 owns_screen: bool,
71 out_fd: std.posix.fd_t, 72 out_fd: std.posix.fd_t,
72 ) !void { 73 ) !void {
@@ -82,11 +83,16 @@ pub fn renderClipped(
82 const view: Engine.RowView = .{ .col_off = vp.left, .cols = vp.cols }; 83 const view: Engine.RowView = .{ .col_off = vp.left, .cols = vp.cols };
83 var ech_buf: [16]u8 = undefined; 84 var ech_buf: [16]u8 = undefined;
84 const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{vp.cols}) catch ""; 85 const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{vp.cols}) catch "";
85 var y: u16 = 0; 86 // Span-bounded ECH per row where the screen was not cleared, so a
86 while (y < limit) : (y += 1) { 87 // shorter row cannot leave the old one showing and a tile cannot reach
88 // a neighbour's cells or a rail.
89 const clear: []const u8 = if (owns_screen) "" else ech;
90 const n = if (rows) |r| r.len else limit;
91 for (0..n) |k| {
92 const y: u16 = if (rows) |r| r[k] else @intCast(k);
93 if (y >= limit) continue;
87 var cup: [24]u8 = undefined; 94 var cup: [24]u8 = undefined;
88 const clear: []const u8 = if (owns_screen) "" else ech; 95 try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, y) + vp.top + 1, vp.left + 1, clear }));
89 try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ y + vp.top + 1, vp.left + 1, clear }));
90 const row = try dumpRow(alloc, replica, y, hl, view); 96 const row = try dumpRow(alloc, replica, y, hl, view);
91 defer alloc.free(row); 97 defer alloc.free(row);
92 try paint.appendSlice(alloc, row); 98 try paint.appendSlice(alloc, row);
@@ -110,31 +116,7 @@ pub fn renderRowsClipped(
110 out_fd: std.posix.fd_t, 116 out_fd: std.posix.fd_t,
111 ) !void { 117 ) !void {
112 if (rows.len == 0) return; 118 if (rows.len == 0) return;
113 var paint: std.ArrayList(u8) = .empty; 119 return renderClipped(alloc, replica, vp, hl, rows, false, out_fd);
114 defer paint.deinit(alloc);
115 try paint.appendSlice(alloc, sync_begin);
116
117 const grid_rows: u16 = @intCast(replica.term.rows);
118 const limit = @min(grid_rows, vp.rows);
119 const view: Engine.RowView = .{ .col_off = vp.left, .cols = vp.cols };
120 var ech_buf: [16]u8 = undefined;
121 const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{vp.cols}) catch "";
122 for (rows) |y| {
123 if (y >= limit) continue;
124 // ECH per row: unlike a full render this never clears the
125 // screen, so a shorter row would leave the old one showing.
126 var cup: [24]u8 = undefined;
127 try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, y) + vp.top + 1, vp.left + 1, ech }));
128 const row = try dumpRow(alloc, replica, y, hl, view);
129 defer alloc.free(row);
130 try paint.appendSlice(alloc, row);
131 }
132
133 const cur = clampCursor(replica.cursorPos(), vp);
134 var cbuf: [16]u8 = undefined;
135 try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + vp.top + 1, cur.x + vp.left + 1 }));
136 try paint.appendSlice(alloc, sync_end);
137 try proto.writeAllFd(out_fd, paint.items);
138 } 120 }
139 121
140 /// Bounded by the REPLICA's grid, not the tty: the answer feeds 122 /// Bounded by the REPLICA's grid, not the tty: the answer feeds
@@ -345,7 +327,7 @@ test "renderClipped inverts the highlighted rows and leaves the rest alone" {
345 const pipe = try std.posix.pipe(); 327 const pipe = try std.posix.pipe();
346 defer std.posix.close(pipe[0]); 328 defer std.posix.close(pipe[0]);
347 var h: TestHighlight = .{}; 329 var h: TestHighlight = .{};
348 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, h.hl(), true, pipe[1]); 330 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, h.hl(), null, true, pipe[1]);
349 std.posix.close(pipe[1]); 331 std.posix.close(pipe[1]);
350 332
351 var out: [8192]u8 = undefined; 333 var out: [8192]u8 = undefined;
@@ -594,7 +576,7 @@ test "renderClipped paints only rows that fit and clamps the cursor" {
594 const pipe = try std.posix.pipe(); 576 const pipe = try std.posix.pipe();
595 defer std.posix.close(pipe[0]); 577 defer std.posix.close(pipe[0]);
596 // Local tty is smaller than the 100x30 grid. 578 // Local tty is smaller than the 100x30 grid.
597 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, true, pipe[1]); 579 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, null, true, pipe[1]);
598 std.posix.close(pipe[1]); 580 std.posix.close(pipe[1]);
599 581
600 var out: std.ArrayList(u8) = .empty; 582 var out: std.ArrayList(u8) = .empty;
@@ -633,7 +615,7 @@ test "renderClipped stops at the grid when the tty is the larger one" {
633 615
634 const pipe = try std.posix.pipe(); 616 const pipe = try std.posix.pipe();
635 defer std.posix.close(pipe[0]); 617 defer std.posix.close(pipe[0]);
636 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, true, pipe[1]); 618 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, null, true, pipe[1]);
637 std.posix.close(pipe[1]); 619 std.posix.close(pipe[1]);
638 620
639 var out: std.ArrayList(u8) = .empty; 621 var out: std.ArrayList(u8) = .empty;
@@ -671,6 +653,7 @@ test "paint: a tile at a row and column offset paints only inside its rect" {
671 replica, 653 replica,
672 .{ .top = row_off, .left = beside_off, .rows = tile_rows, .cols = beside_width }, 654 .{ .top = row_off, .left = beside_off, .rows = tile_rows, .cols = beside_width },
673 .{}, 655 .{},
656 null,
674 false, 657 false,
675 pipe[1], 658 pipe[1],
676 ); 659 );
@@ -875,7 +858,7 @@ test "a pane off the left edge paints inside its own span" {
875 858
876 const pipe = try std.posix.pipe(); 859 const pipe = try std.posix.pipe();
877 defer std.posix.close(pipe[0]); 860 defer std.posix.close(pipe[0]);
878 try renderClipped(alloc, replica, .{ .top = 0, .left = 40, .rows = 24, .cols = 39 }, .{}, false, pipe[1]); 861 try renderClipped(alloc, replica, .{ .top = 0, .left = 40, .rows = 24, .cols = 39 }, .{}, null, false, pipe[1]);
879 std.posix.close(pipe[1]); 862 std.posix.close(pipe[1]);
880 863
881 var out: [8192]u8 = undefined; 864 var out: [8192]u8 = undefined;
@@ -961,7 +944,7 @@ test "paint: a pane narrower than the grid paints no cell past its own edge" {
961 944
962 const pipe = try std.posix.pipe(); 945 const pipe = try std.posix.pipe();
963 defer std.posix.close(pipe[0]); 946 defer std.posix.close(pipe[0]);
964 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, false, pipe[1]); 947 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, null, false, pipe[1]);
965 std.posix.close(pipe[1]); 948 std.posix.close(pipe[1]);
966 var out: [8192]u8 = undefined; 949 var out: [8192]u8 = undefined;
967 const n = try std.posix.read(pipe[0], &out); 950 const n = try std.posix.read(pipe[0], &out);
@@ -982,7 +965,7 @@ test "paint: a wide cell astride the pane's edge is dropped, not halved" {
982 965
983 const pipe = try std.posix.pipe(); 966 const pipe = try std.posix.pipe();
984 defer std.posix.close(pipe[0]); 967 defer std.posix.close(pipe[0]);
985 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, false, pipe[1]); 968 try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, null, false, pipe[1]);
986 std.posix.close(pipe[1]); 969 std.posix.close(pipe[1]);
987 var out: [8192]u8 = undefined; 970 var out: [8192]u8 = undefined;
988 const n = try std.posix.read(pipe[0], &out); 971 const n = try std.posix.read(pipe[0], &out);
@@ -1009,6 +992,7 @@ test "paint: a highlight in an offset pane inverts inside the pane" {
1009 replica, 992 replica,
1010 .{ .top = 0, .left = beside_off, .rows = 4, .cols = beside_width }, 993 .{ .top = 0, .left = beside_off, .rows = 4, .cols = beside_width },
1011 h.hl(), 994 h.hl(),
995 null,
1012 false, 996 false,
1013 pipe[1], 997 pipe[1],
1014 ); 998 );