11682d56
feat: painters address a column offset and erase only their span
a73x 2026-08-24 18:06
Commit message
src/interact.zig
| Old | New | ||
|---|---|---|---|
| @@ -804,6 +804,7 @@ fn paintOverlay( | |||
| 804 | base: Engine.CursorPos, | 804 | base: Engine.CursorPos, |
| 805 | tty: proto.Size, | 805 | tty: proto.Size, |
| 806 | row_off: u16, | 806 | row_off: u16, |
| 807 | col_off: u16, | ||
| 807 | out_fd: std.posix.fd_t, | 808 | out_fd: std.posix.fd_t, |
| 808 | ) void { | 809 | ) void { |
| 809 | if (!overlay.visible() or overlay.pendingCount() == 0) return; | 810 | if (!overlay.visible() or overlay.pendingCount() == 0) return; |
| @@ -820,7 +821,7 @@ fn paintOverlay( | |||
| 820 | // daemon's own row content replaces it. | 821 | // daemon's own row content replaces it. |
| 821 | const s = std.fmt.bufPrint(&b, "\x1b[{d};{d}H\x1b[4m{c}\x1b[0m", .{ | 822 | const s = std.fmt.bufPrint(&b, "\x1b[{d};{d}H\x1b[4m{c}\x1b[0m", .{ |
| 822 | cell.row + row_off + 1, | 823 | cell.row + row_off + 1, |
| 823 | cell.col + 1, | 824 | cell.col + col_off + 1, |
| 824 | cell.ch, | 825 | cell.ch, |
| 825 | }) catch continue; | 826 | }) catch continue; |
| 826 | paint.appendSlice(alloc, s) catch return; | 827 | paint.appendSlice(alloc, s) catch return; |
| @@ -836,7 +837,7 @@ fn paintOverlay( | |||
| 836 | var cbuf: [32]u8 = undefined; | 837 | var cbuf: [32]u8 = undefined; |
| 837 | const tail = std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H" ++ paint_mod.sync_end, .{ | 838 | const tail = std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H" ++ paint_mod.sync_end, .{ |
| 838 | cur.y + row_off + 1, | 839 | cur.y + row_off + 1, |
| 839 | cur.x + 1, | 840 | cur.x + col_off + 1, |
| 840 | }) catch return; | 841 | }) catch return; |
| 841 | paint.appendSlice(alloc, tail) catch return; | 842 | paint.appendSlice(alloc, tail) catch return; |
| 842 | proto.writeAllFd(out_fd, paint.items) catch {}; | 843 | proto.writeAllFd(out_fd, paint.items) catch {}; |
| @@ -851,6 +852,7 @@ fn offerKeystroke( | |||
| 851 | chunk: []const u8, | 852 | chunk: []const u8, |
| 852 | tty: proto.Size, | 853 | tty: proto.Size, |
| 853 | row_off: u16, | 854 | row_off: u16, |
| 855 | col_off: u16, | ||
| 854 | out_fd: std.posix.fd_t, | 856 | out_fd: std.posix.fd_t, |
| 855 | ) void { | 857 | ) void { |
| 856 | if (chunk.len != 1) { | 858 | if (chunk.len != 1) { |
| @@ -873,7 +875,7 @@ fn offerKeystroke( | |||
| 873 | .now_ms = std.time.milliTimestamp(), | 875 | .now_ms = std.time.milliTimestamp(), |
| 874 | }); | 876 | }); |
| 875 | switch (out) { | 877 | switch (out) { |
| 876 | .display => paintOverlay(alloc, overlay, base, tty, row_off, out_fd), | 878 | .display => paintOverlay(alloc, overlay, base, tty, row_off, col_off, out_fd), |
| 877 | // Queued but unearned, or refused outright: either way nothing is | 879 | // Queued but unearned, or refused outright: either way nothing is |
| 878 | // drawn, which is the entire safety property. | 880 | // drawn, which is the entire safety property. |
| 879 | .hidden, .suppressed => {}, | 881 | .hidden, .suppressed => {}, |
| @@ -1143,6 +1145,10 @@ pub const Core = struct { | |||
| 1143 | /// inside its own rect, not the screen's origin; the plain client and | 1145 | /// inside its own rect, not the screen's origin; the plain client and |
| 1144 | /// muxa leave it 0 and paint from row 1 as before. | 1146 | /// muxa leave it 0 and paint from row 1 as before. |
| 1145 | row_off: u16 = 0, | 1147 | row_off: u16 = 0, |
| 1148 | /// The terminal column this tile's rect starts at. A beside-neighbour | ||
| 1149 | /// would be blanked by a whole-line clear, so every painter addresses | ||
| 1150 | /// its own column origin. The plain client and muxa leave it 0. | ||
| 1151 | col_off: u16 = 0, | ||
| 1146 | /// Whether this Core may clear the whole screen. A plain client or a | 1152 | /// Whether this Core may clear the whole screen. A plain client or a |
| 1147 | /// one-tile wall owns every row; a tile among neighbours does not. Set | 1153 | /// one-tile wall owns every row; a tile among neighbours does not. Set |
| 1148 | /// by the driver that knows the layout, not inferred from `row_off`. | 1154 | /// by the driver that knows the layout, not inferred from `row_off`. |
| @@ -1371,8 +1377,8 @@ pub const Core = struct { | |||
| 1371 | if (!self.beginPaint()) return; | 1377 | if (!self.beginPaint()) return; |
| 1372 | defer self.endPaint(); | 1378 | defer self.endPaint(); |
| 1373 | const hl = self.highlight(); | 1379 | const hl = self.highlight(); |
| 1374 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.owns_screen, self.out_fd); | 1380 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.col_off, self.size.cols, self.owns_screen, self.out_fd); |
| 1375 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.size, self.row_off, self.out_fd); | 1381 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.size, self.row_off, self.col_off, self.out_fd); |
| 1376 | } | 1382 | } |
| 1377 | 1383 | ||
| 1378 | /// The terminal cursor the painters end on — the same | 1384 | /// The terminal cursor the painters end on — the same |
| @@ -1380,7 +1386,7 @@ pub const Core = struct { | |||
| 1380 | /// replica cursor is the approximation. | 1386 | /// replica cursor is the approximation. |
| 1381 | pub fn screenCursor(self: *Core) Engine.CursorPos { | 1387 | pub fn screenCursor(self: *Core) Engine.CursorPos { |
| 1382 | const c = paint_mod.clampCursor(self.rep.eng.cursorPos(), self.size); | 1388 | const c = paint_mod.clampCursor(self.rep.eng.cursorPos(), self.size); |
| 1383 | return .{ .x = c.x, .y = c.y + self.row_off }; | 1389 | return .{ .x = c.x + self.col_off, .y = c.y + self.row_off }; |
| 1384 | } | 1390 | } |
| 1385 | 1391 | ||
| 1386 | /// Repaint the rows a drag report changed, and only those. | 1392 | /// Repaint the rows a drag report changed, and only those. |
| @@ -1433,9 +1439,11 @@ pub const Core = struct { | |||
| 1433 | hl.sink(), | 1439 | hl.sink(), |
| 1434 | rows.items, | 1440 | rows.items, |
| 1435 | self.row_off, | 1441 | self.row_off, |
| 1442 | self.col_off, | ||
| 1443 | self.size.cols, | ||
| 1436 | self.out_fd, | 1444 | self.out_fd, |
| 1437 | ); | 1445 | ); |
| 1438 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.size, self.row_off, self.out_fd); | 1446 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.size, self.row_off, self.col_off, self.out_fd); |
| 1439 | } | 1447 | } |
| 1440 | 1448 | ||
| 1441 | /// The whole screen from the replica, with nothing put back on top — | 1449 | /// The whole screen from the replica, with nothing put back on top — |
| @@ -1444,7 +1452,7 @@ pub const Core = struct { | |||
| 1444 | if (!self.beginPaint()) return; | 1452 | if (!self.beginPaint()) return; |
| 1445 | defer self.endPaint(); | 1453 | defer self.endPaint(); |
| 1446 | const hl = self.highlight(); | 1454 | const hl = self.highlight(); |
| 1447 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.owns_screen, self.out_fd); | 1455 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.col_off, self.size.cols, self.owns_screen, self.out_fd); |
| 1448 | } | 1456 | } |
| 1449 | 1457 | ||
| 1450 | /// A one-line marker in the corner, painted over by the next full | 1458 | /// A one-line marker in the corner, painted over by the next full |
| @@ -1452,7 +1460,7 @@ pub const Core = struct { | |||
| 1452 | pub fn banner(self: *Core, text: []const u8) void { | 1460 | pub fn banner(self: *Core, text: []const u8) void { |
| 1453 | if (!self.is_tty or !self.beginPaint()) return; | 1461 | if (!self.is_tty or !self.beginPaint()) return; |
| 1454 | defer self.endPaint(); | 1462 | defer self.endPaint(); |
| 1455 | paint_mod.paintBanner(self.out_fd, self.size, text, self.row_off); | 1463 | paint_mod.paintBanner(self.out_fd, self.size.cols, text, self.row_off, self.col_off); |
| 1456 | } | 1464 | } |
| 1457 | 1465 | ||
| 1458 | /// The only thing that can retire a prediction the application | 1466 | /// The only thing that can retire a prediction the application |
| @@ -1516,16 +1524,16 @@ pub const Core = struct { | |||
| 1516 | // between them would put the two halves of this frame | 1524 | // between them would put the two halves of this frame |
| 1517 | // on two different screens), and the sink is not reentrant. | 1525 | // on two different screens), and the sink is not reentrant. |
| 1518 | const hl = self.highlight(); | 1526 | const hl = self.highlight(); |
| 1519 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.owns_screen, self.out_fd); | 1527 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.size, hl.sink(), self.row_off, self.col_off, self.size.cols, self.owns_screen, self.out_fd); |
| 1520 | self.repaint_after_resync = false; | 1528 | self.repaint_after_resync = false; |
| 1521 | } else { | 1529 | } else { |
| 1522 | const hl = self.highlight(); | 1530 | const hl = self.highlight(); |
| 1523 | try paint_mod.paintDeltaClipped(self.alloc, payload, self.rep.eng, self.size, hl.sink(), self.row_off, self.out_fd); | 1531 | try paint_mod.paintDeltaClipped(self.alloc, payload, self.rep.eng, self.size, hl.sink(), self.row_off, self.col_off, self.size.cols, self.out_fd); |
| 1524 | } | 1532 | } |
| 1525 | // Last, and after either paint: the rows the daemon just sent | 1533 | // Last, and after either paint: the rows the daemon just sent |
| 1526 | // have overwritten anything drawn on them, including predictions | 1534 | // have overwritten anything drawn on them, including predictions |
| 1527 | // that are still outstanding. | 1535 | // that are still outstanding. |
| 1528 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.size, self.row_off, self.out_fd); | 1536 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.size, self.row_off, self.col_off, self.out_fd); |
| 1529 | } | 1537 | } |
| 1530 | } | 1538 | } |
| 1531 | 1539 | ||
| @@ -1553,7 +1561,7 @@ pub const Core = struct { | |||
| 1553 | if (self.scroll_rows == 0 or payload.len < 6) return .skip; | 1561 | if (self.scroll_rows == 0 or payload.len < 6) return .skip; |
| 1554 | if (!self.beginPaint()) return .carry_on; | 1562 | if (!self.beginPaint()) return .carry_on; |
| 1555 | defer self.endPaint(); | 1563 | defer self.endPaint(); |
| 1556 | try paint_mod.renderScrollback(self.alloc, payload[6..], self.size, self.row_off, self.owns_screen, self.out_fd); | 1564 | try paint_mod.renderScrollback(self.alloc, payload[6..], self.row_off, self.col_off, self.size.cols, self.owns_screen, self.out_fd); |
| 1557 | return .carry_on; | 1565 | return .carry_on; |
| 1558 | } | 1566 | } |
| 1559 | 1567 | ||
| @@ -1874,7 +1882,7 @@ pub const Core = struct { | |||
| 1874 | // whichever session holds the terminal now. | 1882 | // whichever session holds the terminal now. |
| 1875 | if (self.beginPaint()) { | 1883 | if (self.beginPaint()) { |
| 1876 | defer self.endPaint(); | 1884 | defer self.endPaint(); |
| 1877 | offerKeystroke(self.alloc, &self.overlay, self.rep.eng, keys, self.size, self.row_off, self.out_fd); | 1885 | offerKeystroke(self.alloc, &self.overlay, self.rep.eng, keys, self.size, self.row_off, self.col_off, self.out_fd); |
| 1878 | } | 1886 | } |
| 1879 | transport.writeFrame(.input, keys) catch return .lost; | 1887 | transport.writeFrame(.input, keys) catch return .lost; |
| 1880 | // These keystrokes are lost with the transport, by the same | 1888 | // These keystrokes are lost with the transport, by the same |
| @@ -2496,8 +2504,8 @@ test "prediction: prev_ch is read at the predicted cursor, not the replica's" { | |||
| 2496 | ov.noteSeq(1); | 2504 | ov.noteSeq(1); |
| 2497 | 2505 | ||
| 2498 | const tty = proto.Size{ .cols = 80, .rows = 24 }; | 2506 | const tty = proto.Size{ .cols = 80, .rows = 24 }; |
| 2499 | offerKeystroke(alloc, &ov, replica, "d", tty, 0, null_fd); | 2507 | offerKeystroke(alloc, &ov, replica, "d", tty, 0, 0, null_fd); |
| 2500 | offerKeystroke(alloc, &ov, replica, "e", tty, 0, null_fd); | 2508 | offerKeystroke(alloc, &ov, replica, "e", tty, 0, 0, null_fd); |
| 2501 | 2509 | ||
| 2502 | try std.testing.expectEqual(@as(usize, 2), ov.pendingCount()); | 2510 | try std.testing.expectEqual(@as(usize, 2), ov.pendingCount()); |
| 2503 | try std.testing.expectEqual(@as(u8, 'X'), ov.pendingAt(0).prev_ch); | 2511 | try std.testing.expectEqual(@as(u8, 'X'), ov.pendingAt(0).prev_ch); |
| @@ -2542,7 +2550,7 @@ test "prediction: a burst advances the predicted cursor one cell per keystroke" | |||
| 2542 | ov.setMode(.{ .icanon = true, .echo = true }); | 2550 | ov.setMode(.{ .icanon = true, .echo = true }); |
| 2543 | 2551 | ||
| 2544 | const tty = proto.Size{ .cols = 80, .rows = 24 }; | 2552 | const tty = proto.Size{ .cols = 80, .rows = 24 }; |
| 2545 | for ("hello") |ch| offerKeystroke(alloc, &ov, replica, &.{ch}, tty, 0, null_fd); | 2553 | for ("hello") |ch| offerKeystroke(alloc, &ov, replica, &.{ch}, tty, 0, 0, null_fd); |
| 2546 | 2554 | ||
| 2547 | // The replica's own cursor has not moved — the daemon has answered | 2555 | // The replica's own cursor has not moved — the daemon has answered |
| 2548 | // nothing — so every one of these came from the overlay. | 2556 | // nothing — so every one of these came from the overlay. |
| @@ -2573,7 +2581,7 @@ test "prediction paints underlined, and parks the cursor past what it drew" { | |||
| 2573 | defer ov.deinit(); | 2581 | defer ov.deinit(); |
| 2574 | ov.setMode(.{ .icanon = true, .echo = true }); | 2582 | ov.setMode(.{ .icanon = true, .echo = true }); |
| 2575 | 2583 | ||
| 2576 | offerKeystroke(alloc, &ov, replica, "z", .{ .cols = 80, .rows = 24 }, 0, p[1]); | 2584 | offerKeystroke(alloc, &ov, replica, "z", .{ .cols = 80, .rows = 24 }, 0, 0, p[1]); |
| 2577 | std.posix.close(p[1]); | 2585 | std.posix.close(p[1]); |
| 2578 | 2586 | ||
| 2579 | var out: std.ArrayList(u8) = .empty; | 2587 | var out: std.ArrayList(u8) = .empty; |
| @@ -2609,7 +2617,7 @@ test "a keystroke's prediction paints at the tile's row offset" { | |||
| 2609 | defer ov.deinit(); | 2617 | defer ov.deinit(); |
| 2610 | ov.setMode(.{ .icanon = true, .echo = true }); | 2618 | ov.setMode(.{ .icanon = true, .echo = true }); |
| 2611 | 2619 | ||
| 2612 | offerKeystroke(alloc, &ov, replica, "z", .{ .cols = 80, .rows = 24 }, 5, p[1]); | 2620 | offerKeystroke(alloc, &ov, replica, "z", .{ .cols = 80, .rows = 24 }, 5, 0, p[1]); |
| 2613 | std.posix.close(p[1]); | 2621 | std.posix.close(p[1]); |
| 2614 | 2622 | ||
| 2615 | var rbuf: [4096]u8 = undefined; | 2623 | var rbuf: [4096]u8 = undefined; |
| @@ -2630,7 +2638,7 @@ test "prediction: nothing is drawn for a context that has not earned it" { | |||
| 2630 | defer ov.deinit(); | 2638 | defer ov.deinit(); |
| 2631 | ov.setMode(.{ .icanon = false, .echo = false }); // raw: display is earned | 2639 | ov.setMode(.{ .icanon = false, .echo = false }); // raw: display is earned |
| 2632 | 2640 | ||
| 2633 | offerKeystroke(alloc, &ov, replica, "z", .{ .cols = 80, .rows = 24 }, 0, p[1]); | 2641 | offerKeystroke(alloc, &ov, replica, "z", .{ .cols = 80, .rows = 24 }, 0, 0, p[1]); |
| 2634 | std.posix.close(p[1]); | 2642 | std.posix.close(p[1]); |
| 2635 | 2643 | ||
| 2636 | // Queued, so it can be judged and earn the next one its visibility... | 2644 | // Queued, so it can be judged and earn the next one its visibility... |
| @@ -2658,12 +2666,12 @@ test "prediction: a chunk that is not one printable byte is never speculated abo | |||
| 2658 | 2666 | ||
| 2659 | // An arrow key: three bytes, and predicting its lead byte would paint an | 2667 | // An arrow key: three bytes, and predicting its lead byte would paint an |
| 2660 | // escape character on the screen. | 2668 | // escape character on the screen. |
| 2661 | offerKeystroke(alloc, &ov, replica, "\x1b[A", tty, 0, null_fd); | 2669 | offerKeystroke(alloc, &ov, replica, "\x1b[A", tty, 0, 0, null_fd); |
| 2662 | // A multi-byte character, whose display width we do not know. | 2670 | // A multi-byte character, whose display width we do not know. |
| 2663 | offerKeystroke(alloc, &ov, replica, "é", tty, 0, null_fd); | 2671 | offerKeystroke(alloc, &ov, replica, "é", tty, 0, 0, null_fd); |
| 2664 | // And a lone control byte, which goes down the single-byte path and is | 2672 | // And a lone control byte, which goes down the single-byte path and is |
| 2665 | // refused there. | 2673 | // refused there. |
| 2666 | offerKeystroke(alloc, &ov, replica, "\r", tty, 0, null_fd); | 2674 | offerKeystroke(alloc, &ov, replica, "\r", tty, 0, 0, null_fd); |
| 2667 | 2675 | ||
| 2668 | try std.testing.expectEqual(@as(usize, 0), ov.pendingCount()); | 2676 | try std.testing.expectEqual(@as(usize, 0), ov.pendingCount()); |
| 2669 | try std.testing.expectEqual(@as(u64, 0), ov.counters.made); | 2677 | try std.testing.expectEqual(@as(u64, 0), ov.counters.made); |
| @@ -2674,7 +2682,7 @@ test "prediction: a chunk that is not one printable byte is never speculated abo | |||
| 2674 | // guard is the only thing refusing it — and it is refused, because a | 2682 | // guard is the only thing refusing it — and it is refused, because a |
| 2675 | // paste can carry newlines and bracketed-paste markers that are not one | 2683 | // paste can carry newlines and bracketed-paste markers that are not one |
| 2676 | // cell's worth of change each. | 2684 | // cell's worth of change each. |
| 2677 | offerKeystroke(alloc, &ov, replica, "abc", tty, 0, null_fd); | 2685 | offerKeystroke(alloc, &ov, replica, "abc", tty, 0, 0, null_fd); |
| 2678 | try std.testing.expectEqual(@as(usize, 0), ov.pendingCount()); | 2686 | try std.testing.expectEqual(@as(usize, 0), ov.pendingCount()); |
| 2679 | try std.testing.expectEqual(@as(u64, 0), ov.counters.made); | 2687 | try std.testing.expectEqual(@as(u64, 0), ov.counters.made); |
| 2680 | // Counted like every other refusal. The decision is the client's — the | 2688 | // Counted like every other refusal. The decision is the client's — the |
| @@ -2697,8 +2705,8 @@ test "prediction: a repaint never reveals what was never shown" { | |||
| 2697 | defer ov.deinit(); | 2705 | defer ov.deinit(); |
| 2698 | ov.setMode(.{ .icanon = false, .echo = false }); // raw: unconfident | 2706 | ov.setMode(.{ .icanon = false, .echo = false }); // raw: unconfident |
| 2699 | const tty = proto.Size{ .cols = 80, .rows = 24 }; | 2707 | const tty = proto.Size{ .cols = 80, .rows = 24 }; |
| 2700 | offerKeystroke(alloc, &ov, replica, "a", tty, 0, null_fd); | 2708 | offerKeystroke(alloc, &ov, replica, "a", tty, 0, 0, null_fd); |
| 2701 | offerKeystroke(alloc, &ov, replica, "b", tty, 0, null_fd); | 2709 | offerKeystroke(alloc, &ov, replica, "b", tty, 0, 0, null_fd); |
| 2702 | try std.testing.expectEqual(@as(usize, 2), ov.pendingCount()); | 2710 | try std.testing.expectEqual(@as(usize, 2), ov.pendingCount()); |
| 2703 | try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed); | 2711 | try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed); |
| 2704 | 2712 | ||
| @@ -2707,7 +2715,7 @@ test "prediction: a repaint never reveals what was never shown" { | |||
| 2707 | // repaint is a second, quieter chance to show a prediction that was | 2715 | // repaint is a second, quieter chance to show a prediction that was |
| 2708 | // never displayed in the first place — so it asks the same question the | 2716 | // never displayed in the first place — so it asks the same question the |
| 2709 | // keystroke path did, and gets the same answer. | 2717 | // keystroke path did, and gets the same answer. |
| 2710 | paintOverlay(alloc, &ov, replica.cursorPos(), tty, 0, p[1]); | 2718 | paintOverlay(alloc, &ov, replica.cursorPos(), tty, 0, 0, p[1]); |
| 2711 | std.posix.close(p[1]); | 2719 | std.posix.close(p[1]); |
| 2712 | 2720 | ||
| 2713 | var rbuf: [64]u8 = undefined; | 2721 | var rbuf: [64]u8 = undefined; |
| @@ -2735,7 +2743,7 @@ test "prediction: a promotion mid-burst counts the cell it makes visible" { | |||
| 2735 | const later = std.time.milliTimestamp() + 100; | 2743 | const later = std.time.milliTimestamp() + 100; |
| 2736 | 2744 | ||
| 2737 | // One confirm banked, one short of promotion. | 2745 | // One confirm banked, one short of promotion. |
| 2738 | offerKeystroke(alloc, &ov, replica, "a", tty, 0, null_fd); | 2746 | offerKeystroke(alloc, &ov, replica, "a", tty, 0, 0, null_fd); |
| 2739 | replica.feed("a"); | 2747 | replica.feed("a"); |
| 2740 | try std.testing.expectEqual( | 2748 | try std.testing.expectEqual( |
| 2741 | predict.Verdict.confirmed, | 2749 | predict.Verdict.confirmed, |
| @@ -2744,8 +2752,8 @@ test "prediction: a promotion mid-burst counts the cell it makes visible" { | |||
| 2744 | 2752 | ||
| 2745 | // Two more typed while still invisible, and the promoting confirmation | 2753 | // Two more typed while still invisible, and the promoting confirmation |
| 2746 | // lands while the second of them is outstanding. | 2754 | // lands while the second of them is outstanding. |
| 2747 | offerKeystroke(alloc, &ov, replica, "b", tty, 0, null_fd); | 2755 | offerKeystroke(alloc, &ov, replica, "b", tty, 0, 0, null_fd); |
| 2748 | offerKeystroke(alloc, &ov, replica, "c", tty, 0, null_fd); | 2756 | offerKeystroke(alloc, &ov, replica, "c", tty, 0, 0, null_fd); |
| 2749 | try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed); | 2757 | try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed); |
| 2750 | replica.feed("b"); | 2758 | replica.feed("b"); |
| 2751 | try std.testing.expectEqual( | 2759 | try std.testing.expectEqual( |
| @@ -2761,7 +2769,7 @@ test "prediction: a promotion mid-burst counts the cell it makes visible" { | |||
| 2761 | // The post-frame re-lay is where it reaches the screen — nobody typed | 2769 | // The post-frame re-lay is where it reaches the screen — nobody typed |
| 2762 | // anything to make that happen, so counting only at prediction time | 2770 | // anything to make that happen, so counting only at prediction time |
| 2763 | // would lose it. | 2771 | // would lose it. |
| 2764 | paintOverlay(alloc, &ov, replica.cursorPos(), tty, 0, p[1]); | 2772 | paintOverlay(alloc, &ov, replica.cursorPos(), tty, 0, 0, p[1]); |
| 2765 | std.posix.close(p[1]); | 2773 | std.posix.close(p[1]); |
| 2766 | try std.testing.expectEqual(@as(u64, 1), ov.counters.displayed); | 2774 | try std.testing.expectEqual(@as(u64, 1), ov.counters.displayed); |
| 2767 | 2775 | ||
| @@ -3220,7 +3228,7 @@ test "interact: a drag at the focus tile inverts what it crossed, and a click do | |||
| 3220 | // why the inversion count above is the whole story. | 3228 | // why the inversion count above is the whole story. |
| 3221 | try std.testing.expect(std.mem.indexOf(u8, painted, "row-zero") == null); | 3229 | try std.testing.expect(std.mem.indexOf(u8, painted, "row-zero") == null); |
| 3222 | try std.testing.expect(std.mem.indexOf(u8, painted, "row-two") == null); | 3230 | try std.testing.expect(std.mem.indexOf(u8, painted, "row-two") == null); |
| 3223 | try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, painted, "\x1b[2K")); | 3231 | try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, painted, "\x1b[20X")); |
| 3224 | 3232 | ||
| 3225 | // The release does not move the highlight, so it does not repaint one. | 3233 | // The release does not move the highlight, so it does not repaint one. |
| 3226 | _ = try core.forward(&tr, "\x1b[<0;7;2m"); | 3234 | _ = try core.forward(&tr, "\x1b[<0;7;2m"); |
| @@ -3430,7 +3438,7 @@ test "interact: the anchor is an absolute row, and the paint converts it back" { | |||
| 3430 | // row literally would invert a row 500 below the one pointed at, or | 3438 | // row literally would invert a row 500 below the one pointed at, or |
| 3431 | // none at all. | 3439 | // none at all. |
| 3432 | const painted = drainPipe(p[0], &buf); | 3440 | const painted = drainPipe(p[0], &buf); |
| 3433 | try std.testing.expect(std.mem.indexOf(u8, painted, "\x1b[2;1H\x1b[2K\x1b[0mro\x1b[3G\x1b[0m\x1b[7m") != null); | 3441 | try std.testing.expect(std.mem.indexOf(u8, painted, "\x1b[2;1H\x1b[20X\x1b[0mro\x1b[3G\x1b[0m\x1b[7m") != null); |
| 3434 | } | 3442 | } |
| 3435 | 3443 | ||
| 3436 | test "interact: a drag repaint puts the predictions back on top" { | 3444 | test "interact: a drag repaint puts the predictions back on top" { |
| @@ -3534,8 +3542,8 @@ test "interact: a drag repaints the rows it changed, not the screen" { | |||
| 3534 | // No screen clear: that is what opens a full repaint. | 3542 | // No screen clear: that is what opens a full repaint. |
| 3535 | try std.testing.expect(std.mem.indexOf(u8, painted, "\x1b[2J") == null); | 3543 | try std.testing.expect(std.mem.indexOf(u8, painted, "\x1b[2J") == null); |
| 3536 | // One row addressed and cleared, and it is the row under the pointer. | 3544 | // One row addressed and cleared, and it is the row under the pointer. |
| 3537 | try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, painted, "\x1b[2K")); | 3545 | try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, painted, "\x1b[20X")); |
| 3538 | try std.testing.expect(std.mem.indexOf(u8, painted, "\x1b[2;1H\x1b[2K") != null); | 3546 | try std.testing.expect(std.mem.indexOf(u8, painted, "\x1b[2;1H\x1b[20X") != null); |
| 3539 | } | 3547 | } |
| 3540 | 3548 | ||
| 3541 | test "interact: a drag edge inside a wide cell repaints the row unshifted" { | 3549 | test "interact: a drag edge inside a wide cell repaints the row unshifted" { |
src/paint.zig
| Old | New | ||
|---|---|---|---|
| @@ -52,6 +52,8 @@ pub fn renderClipped( | |||
| 52 | tty: proto.Size, | 52 | tty: proto.Size, |
| 53 | hl: Highlight, | 53 | hl: Highlight, |
| 54 | row_off: u16, | 54 | row_off: u16, |
| 55 | col_off: u16, | ||
| 56 | view_cols: u16, | ||
| 55 | owns_screen: bool, | 57 | owns_screen: bool, |
| 56 | out_fd: std.posix.fd_t, | 58 | out_fd: std.posix.fd_t, |
| 57 | ) !void { | 59 | ) !void { |
| @@ -64,11 +66,13 @@ pub fn renderClipped( | |||
| 64 | 66 | ||
| 65 | const grid_rows: u16 = @intCast(replica.term.rows); | 67 | const grid_rows: u16 = @intCast(replica.term.rows); |
| 66 | const limit = @min(grid_rows, tty.rows); | 68 | const limit = @min(grid_rows, tty.rows); |
| 69 | var ech_buf: [16]u8 = undefined; | ||
| 70 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{view_cols}) catch ""; | ||
| 67 | var y: u16 = 0; | 71 | var y: u16 = 0; |
| 68 | while (y < limit) : (y += 1) { | 72 | while (y < limit) : (y += 1) { |
| 69 | var cup: [20]u8 = undefined; | 73 | var cup: [24]u8 = undefined; |
| 70 | const clear: []const u8 = if (owns_screen) "" else "\x1b[2K"; | 74 | const clear: []const u8 = if (owns_screen) "" else ech; |
| 71 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};1H{s}", .{ y + row_off + 1, clear })); | 75 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ y + row_off + 1, col_off + 1, clear })); |
| 72 | const row = try dumpRow(alloc, replica, y, hl); | 76 | const row = try dumpRow(alloc, replica, y, hl); |
| 73 | defer alloc.free(row); | 77 | defer alloc.free(row); |
| 74 | try paint.appendSlice(alloc, row); | 78 | try paint.appendSlice(alloc, row); |
| @@ -76,7 +80,7 @@ pub fn renderClipped( | |||
| 76 | 80 | ||
| 77 | const cur = clampCursor(replica.cursorPos(), tty); | 81 | const cur = clampCursor(replica.cursorPos(), tty); |
| 78 | var cbuf: [16]u8 = undefined; | 82 | var cbuf: [16]u8 = undefined; |
| 79 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + row_off + 1, cur.x + 1 })); | 83 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + row_off + 1, cur.x + col_off + 1 })); |
| 80 | try paint.appendSlice(alloc, sync_end); | 84 | try paint.appendSlice(alloc, sync_end); |
| 81 | try proto.writeAllFd(out_fd, paint.items); | 85 | try proto.writeAllFd(out_fd, paint.items); |
| 82 | } | 86 | } |
| @@ -90,6 +94,8 @@ pub fn renderRowsClipped( | |||
| 90 | hl: Highlight, | 94 | hl: Highlight, |
| 91 | rows: []const u16, | 95 | rows: []const u16, |
| 92 | row_off: u16, | 96 | row_off: u16, |
| 97 | col_off: u16, | ||
| 98 | view_cols: u16, | ||
| 93 | out_fd: std.posix.fd_t, | 99 | out_fd: std.posix.fd_t, |
| 94 | ) !void { | 100 | ) !void { |
| 95 | if (rows.len == 0) return; | 101 | if (rows.len == 0) return; |
| @@ -99,12 +105,14 @@ pub fn renderRowsClipped( | |||
| 99 | 105 | ||
| 100 | const grid_rows: u16 = @intCast(replica.term.rows); | 106 | const grid_rows: u16 = @intCast(replica.term.rows); |
| 101 | const limit = @min(grid_rows, tty.rows); | 107 | const limit = @min(grid_rows, tty.rows); |
| 108 | var ech_buf: [16]u8 = undefined; | ||
| 109 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{view_cols}) catch ""; | ||
| 102 | for (rows) |y| { | 110 | for (rows) |y| { |
| 103 | if (y >= limit) continue; | 111 | if (y >= limit) continue; |
| 104 | // \x1b[2K per row: unlike a full render this never clears the | 112 | // ECH per row: unlike a full render this never clears the |
| 105 | // screen, so a shorter row would leave the old one showing. | 113 | // screen, so a shorter row would leave the old one showing. |
| 106 | var cup: [16]u8 = undefined; | 114 | var cup: [24]u8 = undefined; |
| 107 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};1H\x1b[2K", .{@as(u32, y) + row_off + 1})); | 115 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, y) + row_off + 1, col_off + 1, ech })); |
| 108 | const row = try dumpRow(alloc, replica, y, hl); | 116 | const row = try dumpRow(alloc, replica, y, hl); |
| 109 | defer alloc.free(row); | 117 | defer alloc.free(row); |
| 110 | try paint.appendSlice(alloc, row); | 118 | try paint.appendSlice(alloc, row); |
| @@ -112,7 +120,7 @@ pub fn renderRowsClipped( | |||
| 112 | 120 | ||
| 113 | const cur = clampCursor(replica.cursorPos(), tty); | 121 | const cur = clampCursor(replica.cursorPos(), tty); |
| 114 | var cbuf: [16]u8 = undefined; | 122 | var cbuf: [16]u8 = undefined; |
| 115 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + row_off + 1, cur.x + 1 })); | 123 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + row_off + 1, cur.x + col_off + 1 })); |
| 116 | try paint.appendSlice(alloc, sync_end); | 124 | try paint.appendSlice(alloc, sync_end); |
| 117 | try proto.writeAllFd(out_fd, paint.items); | 125 | try proto.writeAllFd(out_fd, paint.items); |
| 118 | } | 126 | } |
| @@ -135,6 +143,8 @@ pub fn paintDeltaClipped( | |||
| 135 | tty: proto.Size, | 143 | tty: proto.Size, |
| 136 | hl: Highlight, | 144 | hl: Highlight, |
| 137 | row_off: u16, | 145 | row_off: u16, |
| 146 | col_off: u16, | ||
| 147 | view_cols: u16, | ||
| 138 | out_fd: std.posix.fd_t, | 148 | out_fd: std.posix.fd_t, |
| 139 | ) !void { | 149 | ) !void { |
| 140 | const hdr = try proto.readDeltaHeader(payload); | 150 | const hdr = try proto.readDeltaHeader(payload); |
| @@ -144,11 +154,13 @@ pub fn paintDeltaClipped( | |||
| 144 | 154 | ||
| 145 | const grid_cols: u16 = @intCast(replica.term.cols); | 155 | const grid_cols: u16 = @intCast(replica.term.cols); |
| 146 | const grid_rows: u16 = @intCast(replica.term.rows); | 156 | const grid_rows: u16 = @intCast(replica.term.rows); |
| 157 | var ech_buf: [16]u8 = undefined; | ||
| 158 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{view_cols}) catch ""; | ||
| 147 | var it = proto.deltaRowIterator(payload); | 159 | var it = proto.deltaRowIterator(payload); |
| 148 | while (try it.next()) |row| { | 160 | while (try it.next()) |row| { |
| 149 | if (row.row >= tty.rows) continue; | 161 | if (row.row >= tty.rows) continue; |
| 150 | var cup: [16]u8 = undefined; | 162 | var cup: [24]u8 = undefined; |
| 151 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};1H\x1b[2K", .{@as(u32, row.row) + row_off + 1})); | 163 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, row.row) + row_off + 1, col_off + 1, ech })); |
| 152 | // A row under the selection is redrawn from the replica, which has | 164 | // A row under the selection is redrawn from the replica, which has |
| 153 | // already been fed this very frame — same content, inversion on | 165 | // already been fed this very frame — same content, inversion on |
| 154 | // top. Every other row keeps the daemon's bytes verbatim, and that | 166 | // top. Every other row keeps the daemon's bytes verbatim, and that |
| @@ -165,7 +177,7 @@ pub fn paintDeltaClipped( | |||
| 165 | 177 | ||
| 166 | const cur = clampCursor(.{ .x = hdr.cursor_x, .y = hdr.cursor_y }, tty); | 178 | const cur = clampCursor(.{ .x = hdr.cursor_x, .y = hdr.cursor_y }, tty); |
| 167 | var cbuf: [16]u8 = undefined; | 179 | var cbuf: [16]u8 = undefined; |
| 168 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + row_off + 1, cur.x + 1 })); | 180 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + row_off + 1, cur.x + col_off + 1 })); |
| 169 | try paint.appendSlice(alloc, sync_end); | 181 | try paint.appendSlice(alloc, sync_end); |
| 170 | try proto.writeAllFd(out_fd, paint.items); | 182 | try proto.writeAllFd(out_fd, paint.items); |
| 171 | } | 183 | } |
| @@ -174,19 +186,20 @@ pub fn paintDeltaClipped( | |||
| 174 | /// viewing history, `[reconnecting]` when the transport is being rebuilt. | 186 | /// viewing history, `[reconnecting]` when the transport is being rebuilt. |
| 175 | /// Text only, so a caller mid-repaint can append it into its own paint | 187 | /// Text only, so a caller mid-repaint can append it into its own paint |
| 176 | /// buffer and keep the whole screen one synchronized update. | 188 | /// buffer and keep the whole screen one synchronized update. |
| 177 | fn bannerText(buf: []u8, size: proto.Size, label: []const u8, row_off: u16) ![]const u8 { | 189 | fn bannerText(buf: []u8, view_cols: u16, label: []const u8, row_off: u16, col_off: u16) ![]const u8 { |
| 178 | // Columns are 1-based; a label wider than the whole tty would otherwise | 190 | // Right-aligned inside the pane: the label's left column is the pane's |
| 179 | // address column 0, which terminals only silently forgive. | 191 | // left edge plus the pane's width less the label, so at col_off 0 and |
| 180 | const col = @max(1, size.cols -| @as(u16, @intCast(label.len))); | 192 | // view_cols 80 the column is 66 — matching the plain client's edge. |
| 193 | const col = @max(col_off + 1, col_off +| view_cols -| @as(u16, @intCast(label.len))); | ||
| 181 | return std.fmt.bufPrint(buf, "\x1b[{d};{d}H\x1b[7m{s}\x1b[0m", .{ row_off + 1, col, label }); | 194 | return std.fmt.bufPrint(buf, "\x1b[{d};{d}H\x1b[7m{s}\x1b[0m", .{ row_off + 1, col, label }); |
| 182 | } | 195 | } |
| 183 | 196 | ||
| 184 | /// Drop a banner onto a screen that is otherwise staying put — the cursor is | 197 | /// Drop a banner onto a screen that is otherwise staying put — the cursor is |
| 185 | /// saved and restored around it, so the shell's cursor does not visibly jump | 198 | /// saved and restored around it, so the shell's cursor does not visibly jump |
| 186 | /// to the corner. Best-effort: a status marker is never worth failing over. | 199 | /// to the corner. Best-effort: a status marker is never worth failing over. |
| 187 | pub fn paintBanner(out_fd: std.posix.fd_t, size: proto.Size, label: []const u8, row_off: u16) void { | 200 | pub fn paintBanner(out_fd: std.posix.fd_t, view_cols: u16, label: []const u8, row_off: u16, col_off: u16) void { |
| 188 | var buf: [96]u8 = undefined; | 201 | var buf: [96]u8 = undefined; |
| 189 | const mark = bannerText(&buf, size, label, row_off) catch return; | 202 | const mark = bannerText(&buf, view_cols, label, row_off, col_off) catch return; |
| 190 | var paint: [128]u8 = undefined; | 203 | var paint: [128]u8 = undefined; |
| 191 | const text = std.fmt.bufPrint(&paint, "\x1b[s{s}\x1b[u", .{mark}) catch return; | 204 | const text = std.fmt.bufPrint(&paint, "\x1b[s{s}\x1b[u", .{mark}) catch return; |
| 192 | proto.writeAllFd(out_fd, text) catch {}; | 205 | proto.writeAllFd(out_fd, text) catch {}; |
| @@ -196,8 +209,9 @@ pub fn paintBanner(out_fd: std.posix.fd_t, size: proto.Size, label: []const u8, | |||
| 196 | pub fn renderScrollback( | 209 | pub fn renderScrollback( |
| 197 | alloc: std.mem.Allocator, | 210 | alloc: std.mem.Allocator, |
| 198 | rows_vt: []const u8, | 211 | rows_vt: []const u8, |
| 199 | size: proto.Size, | ||
| 200 | row_off: u16, | 212 | row_off: u16, |
| 213 | col_off: u16, | ||
| 214 | view_cols: u16, | ||
| 201 | owns_screen: bool, | 215 | owns_screen: bool, |
| 202 | out_fd: std.posix.fd_t, | 216 | out_fd: std.posix.fd_t, |
| 203 | ) !void { | 217 | ) !void { |
| @@ -212,6 +226,8 @@ pub fn renderScrollback( | |||
| 212 | try paint.appendSlice(alloc, rows_vt); | 226 | try paint.appendSlice(alloc, rows_vt); |
| 213 | } else { | 227 | } else { |
| 214 | try paint.appendSlice(alloc, sync_begin); | 228 | try paint.appendSlice(alloc, sync_begin); |
| 229 | var ech_buf: [16]u8 = undefined; | ||
| 230 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{view_cols}) catch ""; | ||
| 215 | var rest = rows_vt; | 231 | var rest = rows_vt; |
| 216 | var n: u16 = 0; | 232 | var n: u16 = 0; |
| 217 | while (rest.len > 0) : (n += 1) { | 233 | while (rest.len > 0) : (n += 1) { |
| @@ -221,14 +237,14 @@ pub fn renderScrollback( | |||
| 221 | var seg = rest[0..nl]; | 237 | var seg = rest[0..nl]; |
| 222 | if (seg.len > 0 and seg[seg.len - 1] == '\r') seg = seg[0 .. seg.len - 1]; | 238 | if (seg.len > 0 and seg[seg.len - 1] == '\r') seg = seg[0 .. seg.len - 1]; |
| 223 | var cup: [24]u8 = undefined; | 239 | var cup: [24]u8 = undefined; |
| 224 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};1H\x1b[2K", .{@as(u32, n) + row_off + 1})); | 240 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, n) + row_off + 1, col_off + 1, ech })); |
| 225 | try paint.appendSlice(alloc, seg); | 241 | try paint.appendSlice(alloc, seg); |
| 226 | if (nl == rest.len) break; | 242 | if (nl == rest.len) break; |
| 227 | rest = rest[nl + 1 ..]; | 243 | rest = rest[nl + 1 ..]; |
| 228 | } | 244 | } |
| 229 | } | 245 | } |
| 230 | var mark_buf: [96]u8 = undefined; | 246 | var mark_buf: [96]u8 = undefined; |
| 231 | try paint.appendSlice(alloc, try bannerText(&mark_buf, size, "[scroll]", row_off)); | 247 | try paint.appendSlice(alloc, try bannerText(&mark_buf, view_cols, "[scroll]", row_off, col_off)); |
| 232 | // The open above is the shared half; only this commit is deliberately | 248 | // The open above is the shared half; only this commit is deliberately |
| 233 | // unpaired, and it must not be "simplified" into sync_end: it closes the | 249 | // unpaired, and it must not be "simplified" into sync_end: it closes the |
| 234 | // update WITHOUT the cursor-show, because a cursor parked in a history | 250 | // update WITHOUT the cursor-show, because a cursor parked in a history |
| @@ -264,7 +280,7 @@ test "renderClipped inverts the highlighted rows and leaves the rest alone" { | |||
| 264 | const pipe = try std.posix.pipe(); | 280 | const pipe = try std.posix.pipe(); |
| 265 | defer std.posix.close(pipe[0]); | 281 | defer std.posix.close(pipe[0]); |
| 266 | var h: TestHighlight = .{}; | 282 | var h: TestHighlight = .{}; |
| 267 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, h.hl(), 0, true, pipe[1]); | 283 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, h.hl(), 0, 0, 80, true, pipe[1]); |
| 268 | std.posix.close(pipe[1]); | 284 | std.posix.close(pipe[1]); |
| 269 | 285 | ||
| 270 | var out: [8192]u8 = undefined; | 286 | var out: [8192]u8 = undefined; |
| @@ -289,7 +305,7 @@ test "renderClipped inverts the highlighted rows and leaves the rest alone" { | |||
| 289 | test "paintBanner parks an inverse label top-right without moving the cursor" { | 305 | test "paintBanner parks an inverse label top-right without moving the cursor" { |
| 290 | const pipe = try std.posix.pipe(); | 306 | const pipe = try std.posix.pipe(); |
| 291 | defer std.posix.close(pipe[0]); | 307 | defer std.posix.close(pipe[0]); |
| 292 | paintBanner(pipe[1], .{ .cols = 80, .rows = 24 }, "[reconnecting]", 0); | 308 | paintBanner(pipe[1], 80, "[reconnecting]", 0, 0); |
| 293 | std.posix.close(pipe[1]); | 309 | std.posix.close(pipe[1]); |
| 294 | 310 | ||
| 295 | var out: [256]u8 = undefined; | 311 | var out: [256]u8 = undefined; |
| @@ -309,7 +325,7 @@ test "paintBanner on a narrow tty clamps to column 1 instead of underflowing" { | |||
| 309 | defer std.posix.close(pipe[0]); | 325 | defer std.posix.close(pipe[0]); |
| 310 | // Label longer than the whole terminal: the saturating subtraction must | 326 | // Label longer than the whole terminal: the saturating subtraction must |
| 311 | // land on column 1, never wrap around to a huge column. | 327 | // land on column 1, never wrap around to a huge column. |
| 312 | paintBanner(pipe[1], .{ .cols = 4, .rows = 24 }, "[reconnecting]", 0); | 328 | paintBanner(pipe[1], 4, "[reconnecting]", 0, 0); |
| 313 | std.posix.close(pipe[1]); | 329 | std.posix.close(pipe[1]); |
| 314 | 330 | ||
| 315 | var out: [256]u8 = undefined; | 331 | var out: [256]u8 = undefined; |
| @@ -322,7 +338,7 @@ test "paintBanner parks at row_off+1" { | |||
| 322 | // status marker in its own top-right corner, not the screen's. | 338 | // status marker in its own top-right corner, not the screen's. |
| 323 | const pipe = try std.posix.pipe(); | 339 | const pipe = try std.posix.pipe(); |
| 324 | defer std.posix.close(pipe[0]); | 340 | defer std.posix.close(pipe[0]); |
| 325 | paintBanner(pipe[1], .{ .cols = 80, .rows = 24 }, "[reconnecting]", 2); | 341 | paintBanner(pipe[1], 80, "[reconnecting]", 2, 0); |
| 326 | std.posix.close(pipe[1]); | 342 | std.posix.close(pipe[1]); |
| 327 | 343 | ||
| 328 | var out: [256]u8 = undefined; | 344 | var out: [256]u8 = undefined; |
| @@ -341,7 +357,7 @@ test "renderScrollback paints rows with an inverse scroll marker" { | |||
| 341 | const alloc = std.testing.allocator; | 357 | const alloc = std.testing.allocator; |
| 342 | const pipe = try std.posix.pipe(); | 358 | const pipe = try std.posix.pipe(); |
| 343 | defer std.posix.close(pipe[0]); | 359 | defer std.posix.close(pipe[0]); |
| 344 | try renderScrollback(alloc, "old-row-1\r\nold-row-2", .{ .cols = 80, .rows = 24 }, 0, true, pipe[1]); | 360 | try renderScrollback(alloc, "old-row-1\r\nold-row-2", 0, 0, 80, true, pipe[1]); |
| 345 | std.posix.close(pipe[1]); | 361 | std.posix.close(pipe[1]); |
| 346 | 362 | ||
| 347 | var out: [4096]u8 = undefined; | 363 | var out: [4096]u8 = undefined; |
| @@ -357,7 +373,7 @@ test "renderScrollback at row 0 is byte-identical to the plain client" { | |||
| 357 | const alloc = std.testing.allocator; | 373 | const alloc = std.testing.allocator; |
| 358 | const pipe = try std.posix.pipe(); | 374 | const pipe = try std.posix.pipe(); |
| 359 | defer std.posix.close(pipe[0]); | 375 | defer std.posix.close(pipe[0]); |
| 360 | try renderScrollback(alloc, "old-row-1\r\nold-row-2", .{ .cols = 80, .rows = 24 }, 0, true, pipe[1]); | 376 | try renderScrollback(alloc, "old-row-1\r\nold-row-2", 0, 0, 80, true, pipe[1]); |
| 361 | std.posix.close(pipe[1]); | 377 | std.posix.close(pipe[1]); |
| 362 | 378 | ||
| 363 | var out: [4096]u8 = undefined; | 379 | var out: [4096]u8 = undefined; |
| @@ -382,7 +398,7 @@ test "renderScrollback at a row_off owns only its sub-rect" { | |||
| 382 | // CRLF-separated rows, the shape `dumpScrollback` composes: the split | 398 | // CRLF-separated rows, the shape `dumpScrollback` composes: the split |
| 383 | // this paint makes on the row breaks. The leading reset is the | 399 | // this paint makes on the row breaks. The leading reset is the |
| 384 | // formatter's own prefix on the first row. | 400 | // formatter's own prefix on the first row. |
| 385 | try renderScrollback(alloc, "\x1b[0mold-row-1\r\nold-row-2", size, row_off, false, pipe[1]); | 401 | try renderScrollback(alloc, "\x1b[0mold-row-1\r\nold-row-2", row_off, 0, 80, false, pipe[1]); |
| 386 | std.posix.close(pipe[1]); | 402 | std.posix.close(pipe[1]); |
| 387 | 403 | ||
| 388 | var out: [4096]u8 = undefined; | 404 | var out: [4096]u8 = undefined; |
| @@ -394,7 +410,7 @@ test "renderScrollback at a row_off owns only its sub-rect" { | |||
| 394 | // pattern `renderClipped` uses for a live tile at an offset. | 410 | // pattern `renderClipped` uses for a live tile at an offset. |
| 395 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[2J") == null); | 411 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[2J") == null); |
| 396 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[H") == null); | 412 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[H") == null); |
| 397 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[2K") != null); | 413 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[80X") != null); |
| 398 | 414 | ||
| 399 | // Every CUP this paint emits lands inside [row_off+1, row_off+rows]: | 415 | // Every CUP this paint emits lands inside [row_off+1, row_off+rows]: |
| 400 | // a row outside that band belongs to another tile. | 416 | // a row outside that band belongs to another tile. |
| @@ -420,8 +436,8 @@ test "renderScrollback at a row_off owns only its sub-rect" { | |||
| 420 | // and each history row is addressed at row_off + n + 1. | 436 | // and each history row is addressed at row_off + n + 1. |
| 421 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[6;72H\x1b[7m[scroll]\x1b[0m") != null); | 437 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[6;72H\x1b[7m[scroll]\x1b[0m") != null); |
| 422 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;72H") == null); | 438 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;72H") == null); |
| 423 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[6;1H\x1b[2K\x1b[0mold-row-1") != null); | 439 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[6;1H\x1b[80X\x1b[0mold-row-1") != null); |
| 424 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[7;1H\x1b[2Kold-row-2") != null); | 440 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[7;1H\x1b[80Xold-row-2") != null); |
| 425 | // The unpaired close is unchanged: scroll mode hides the cursor. | 441 | // The unpaired close is unchanged: scroll mode hides the cursor. |
| 426 | try std.testing.expect(std.mem.endsWith(u8, text, "\x1b[?2026l")); | 442 | try std.testing.expect(std.mem.endsWith(u8, text, "\x1b[?2026l")); |
| 427 | } | 443 | } |
| @@ -438,7 +454,7 @@ test "renderClipped paints only rows that fit and clamps the cursor" { | |||
| 438 | const pipe = try std.posix.pipe(); | 454 | const pipe = try std.posix.pipe(); |
| 439 | defer std.posix.close(pipe[0]); | 455 | defer std.posix.close(pipe[0]); |
| 440 | // Local tty is smaller than the 100x30 grid. | 456 | // Local tty is smaller than the 100x30 grid. |
| 441 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, true, pipe[1]); | 457 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, 0, 80, true, pipe[1]); |
| 442 | std.posix.close(pipe[1]); | 458 | std.posix.close(pipe[1]); |
| 443 | 459 | ||
| 444 | var out: std.ArrayList(u8) = .empty; | 460 | var out: std.ArrayList(u8) = .empty; |
| @@ -477,7 +493,7 @@ test "renderClipped stops at the grid when the tty is the larger one" { | |||
| 477 | 493 | ||
| 478 | const pipe = try std.posix.pipe(); | 494 | const pipe = try std.posix.pipe(); |
| 479 | defer std.posix.close(pipe[0]); | 495 | defer std.posix.close(pipe[0]); |
| 480 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, true, pipe[1]); | 496 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, 0, 80, true, pipe[1]); |
| 481 | std.posix.close(pipe[1]); | 497 | std.posix.close(pipe[1]); |
| 482 | 498 | ||
| 483 | var out: std.ArrayList(u8) = .empty; | 499 | var out: std.ArrayList(u8) = .empty; |
| @@ -506,7 +522,7 @@ test "renderClipped paints at a row offset" { | |||
| 506 | 522 | ||
| 507 | const pipe = try std.posix.pipe(); | 523 | const pipe = try std.posix.pipe(); |
| 508 | defer std.posix.close(pipe[0]); | 524 | defer std.posix.close(pipe[0]); |
| 509 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 2, false, pipe[1]); | 525 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 2, 0, 80, false, pipe[1]); |
| 510 | std.posix.close(pipe[1]); | 526 | std.posix.close(pipe[1]); |
| 511 | 527 | ||
| 512 | var out: [8192]u8 = undefined; | 528 | var out: [8192]u8 = undefined; |
| @@ -538,12 +554,12 @@ test "paintDeltaClipped skips rows beyond the tty and clamps the cursor" { | |||
| 538 | 554 | ||
| 539 | const pipe = try std.posix.pipe(); | 555 | const pipe = try std.posix.pipe(); |
| 540 | defer std.posix.close(pipe[0]); | 556 | defer std.posix.close(pipe[0]); |
| 541 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, pipe[1]); | 557 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, 0, 80, pipe[1]); |
| 542 | std.posix.close(pipe[1]); | 558 | std.posix.close(pipe[1]); |
| 543 | var out: [4096]u8 = undefined; | 559 | var out: [4096]u8 = undefined; |
| 544 | const n = try std.posix.read(pipe[0], &out); | 560 | const n = try std.posix.read(pipe[0], &out); |
| 545 | 561 | ||
| 546 | try std.testing.expect(std.mem.indexOf(u8, out[0..n], "\x1b[4;1H\x1b[2K\x1b[0mfits") != null); | 562 | try std.testing.expect(std.mem.indexOf(u8, out[0..n], "\x1b[4;1H\x1b[80X\x1b[0mfits") != null); |
| 547 | try std.testing.expect(std.mem.indexOf(u8, out[0..n], "does-not-fit") == null); | 563 | try std.testing.expect(std.mem.indexOf(u8, out[0..n], "does-not-fit") == null); |
| 548 | try std.testing.expect(std.mem.indexOf(u8, out[0..n], "\x1b[24;80H") != null); // clamped | 564 | try std.testing.expect(std.mem.indexOf(u8, out[0..n], "\x1b[24;80H") != null); // clamped |
| 549 | } | 565 | } |
| @@ -571,7 +587,7 @@ test "paintDeltaClipped re-inverts a delta row the selection covers" { | |||
| 571 | const pipe = try std.posix.pipe(); | 587 | const pipe = try std.posix.pipe(); |
| 572 | defer std.posix.close(pipe[0]); | 588 | defer std.posix.close(pipe[0]); |
| 573 | var h: TestHighlight = .{}; | 589 | var h: TestHighlight = .{}; |
| 574 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, h.hl(), 0, pipe[1]); | 590 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, h.hl(), 0, 0, 80, pipe[1]); |
| 575 | std.posix.close(pipe[1]); | 591 | std.posix.close(pipe[1]); |
| 576 | var out: [8192]u8 = undefined; | 592 | var out: [8192]u8 = undefined; |
| 577 | const n = try std.posix.read(pipe[0], &out); | 593 | const n = try std.posix.read(pipe[0], &out); |
| @@ -587,7 +603,7 @@ test "paintDeltaClipped re-inverts a delta row the selection covers" { | |||
| 587 | // The uncovered row is still the daemon's own bytes, verbatim: this | 603 | // The uncovered row is still the daemon's own bytes, verbatim: this |
| 588 | // path exists to keep a held selection off the full-repaint arm, so it | 604 | // path exists to keep a held selection off the full-repaint arm, so it |
| 589 | // must not turn every other row into a replica dump either. | 605 | // must not turn every other row into a replica dump either. |
| 590 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;1H\x1b[2K\x1b[0mrow-zero") != null); | 606 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;1H\x1b[80X\x1b[0mrow-zero") != null); |
| 591 | } | 607 | } |
| 592 | 608 | ||
| 593 | test "paintDeltaClipped brackets the whole paint in one synchronized update" { | 609 | test "paintDeltaClipped brackets the whole paint in one synchronized update" { |
| @@ -614,7 +630,7 @@ test "paintDeltaClipped brackets the whole paint in one synchronized update" { | |||
| 614 | 630 | ||
| 615 | const pipe = try std.posix.pipe(); | 631 | const pipe = try std.posix.pipe(); |
| 616 | defer std.posix.close(pipe[0]); | 632 | defer std.posix.close(pipe[0]); |
| 617 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, pipe[1]); | 633 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, 0, 80, pipe[1]); |
| 618 | std.posix.close(pipe[1]); | 634 | std.posix.close(pipe[1]); |
| 619 | var out: [4096]u8 = undefined; | 635 | var out: [4096]u8 = undefined; |
| 620 | const n = try std.posix.read(pipe[0], &out); | 636 | const n = try std.posix.read(pipe[0], &out); |
| @@ -649,15 +665,15 @@ test "paintDeltaClipped offsets every delta row" { | |||
| 649 | 665 | ||
| 650 | const pipe = try std.posix.pipe(); | 666 | const pipe = try std.posix.pipe(); |
| 651 | defer std.posix.close(pipe[0]); | 667 | defer std.posix.close(pipe[0]); |
| 652 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, .{}, 2, pipe[1]); | 668 | try paintDeltaClipped(alloc, payload.items, replica, .{ .cols = 80, .rows = 24 }, .{}, 2, 0, 80, pipe[1]); |
| 653 | std.posix.close(pipe[1]); | 669 | std.posix.close(pipe[1]); |
| 654 | var out: [8192]u8 = undefined; | 670 | var out: [8192]u8 = undefined; |
| 655 | const n = try std.posix.read(pipe[0], &out); | 671 | const n = try std.posix.read(pipe[0], &out); |
| 656 | const text = out[0..n]; | 672 | const text = out[0..n]; |
| 657 | // Both delta rows shift by row_off: grid row 0 -> terminal row 3, grid | 673 | // Both delta rows shift by row_off: grid row 0 -> terminal row 3, grid |
| 658 | // row 1 -> terminal row 4. | 674 | // row 1 -> terminal row 4. |
| 659 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[3;1H\x1b[2K") != null); | 675 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[3;1H\x1b[80X") != null); |
| 660 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[4;1H\x1b[2K") != null); | 676 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[4;1H\x1b[80X") != null); |
| 661 | // The un-offset addresses never appear. | 677 | // The un-offset addresses never appear. |
| 662 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;1H") == null); | 678 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;1H") == null); |
| 663 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[2;1H") == null); | 679 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[2;1H") == null); |
| @@ -665,6 +681,26 @@ test "paintDeltaClipped offsets every delta row" { | |||
| 665 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[3;6H") != null); | 681 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[3;6H") != null); |
| 666 | } | 682 | } |
| 667 | 683 | ||
| 684 | test "a pane off the left edge paints inside its own span" { | ||
| 685 | // col_off 40, 39 cols: CUP lands at column 41 and the erase covers 39 | ||
| 686 | // cells — the bytes a beside-neighbour's survival depends on. | ||
| 687 | const alloc = std.testing.allocator; | ||
| 688 | var replica = try Engine.init(alloc, .{ .cols = 40, .rows = 4 }); | ||
| 689 | defer replica.deinit(); | ||
| 690 | replica.feed("row-zero\r\nrow-one\r\nrow-two\r\nrow-three"); | ||
| 691 | |||
| 692 | const pipe = try std.posix.pipe(); | ||
| 693 | defer std.posix.close(pipe[0]); | ||
| 694 | try renderClipped(alloc, replica, .{ .cols = 80, .rows = 24 }, .{}, 0, 40, 39, false, pipe[1]); | ||
| 695 | std.posix.close(pipe[1]); | ||
| 696 | |||
| 697 | var out: [8192]u8 = undefined; | ||
| 698 | const n = try std.posix.read(pipe[0], &out); | ||
| 699 | const text = out[0..n]; | ||
| 700 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[1;41H\x1b[39X") != null); | ||
| 701 | try std.testing.expect(std.mem.indexOf(u8, text, "\x1b[2K") == null); | ||
| 702 | } | ||
| 703 | |||
| 668 | // Forces semantic analysis of every pub decl under `zig build test`, so an | 704 | // Forces semantic analysis of every pub decl under `zig build test`, so an |
| 669 | // unreferenced decl must at least compile (the silent-module-loss hazard, | 705 | // unreferenced decl must at least compile (the silent-module-loss hazard, |
| 670 | // decisions.md). Pub decls only: std.meta.declarations sees nothing private. | 706 | // decisions.md). Pub decls only: std.meta.declarations sees nothing private. |
src/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -526,7 +526,7 @@ fn wallBanner(shared: *Shared, text: []const u8, row_off: u16) void { | |||
| 526 | if (!shared.is_tty) return; | 526 | if (!shared.is_tty) return; |
| 527 | shared.paint_mu.lock(); | 527 | shared.paint_mu.lock(); |
| 528 | defer shared.paint_mu.unlock(); | 528 | defer shared.paint_mu.unlock(); |
| 529 | paint.paintBanner(shared.out_fd, shared.size, text, row_off); | 529 | paint.paintBanner(shared.out_fd, shared.size.cols, text, row_off, 0); |
| 530 | } | 530 | } |
| 531 | 531 | ||
| 532 | /// Under `paint_mu`: a clear spliced into a 64 KiB OSC 52 write eats the | 532 | /// Under `paint_mu`: a clear spliced into a 64 KiB OSC 52 write eats the |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -2579,9 +2579,9 @@ grep -q "$(printf '\033\[4mz')" "$OUT.p1.early" || { | |||
| 2579 | echo "--- early snapshot ---"; cat -v "$OUT.p1.early"; exit 1; | 2579 | echo "--- early snapshot ---"; cat -v "$OUT.p1.early"; exit 1; |
| 2580 | } | 2580 | } |
| 2581 | # ...and the daemon's own answer was NOT there yet, which is what makes the | 2581 | # ...and the daemon's own answer was NOT there yet, which is what makes the |
| 2582 | # line above mean anything. A delta paints a row with EL(2) before its | 2582 | # line above mean anything. A delta paints a row with ECH before its |
| 2583 | # content; the prediction never does. | 2583 | # content; the prediction never does. |
| 2584 | if grep -q "$(printf '\033\[2K\033\[0mz')" "$OUT.p1.early"; then | 2584 | if grep -q "$(printf '\033\[80X\033\[0mz')" "$OUT.p1.early"; then |
| 2585 | echo "e2e FAIL: the echo arrived within 450ms, so the path is not $((PDELAY * 2))ms;" | 2585 | echo "e2e FAIL: the echo arrived within 450ms, so the path is not $((PDELAY * 2))ms;" |
| 2586 | echo " either DELAY_MS was ignored (default is 150) or delaypipe is not delaying" | 2586 | echo " either DELAY_MS was ignored (default is 150) or delaypipe is not delaying" |
| 2587 | exit 1 | 2587 | exit 1 |
| @@ -3177,7 +3177,7 @@ expect 60 10000 | |||
| 3177 | expect reconnecting 20000 | 3177 | expect reconnecting 20000 |
| 3178 | expect \x1b[0m100 20000 | 3178 | expect \x1b[0m100 20000 |
| 3179 | send t | 3179 | send t |
| 3180 | expect \x1b[2K\x1b[0mt 20000 | 3180 | expect \x1b[80X\x1b[0mt 20000 |
| 3181 | settle 500 15000 | 3181 | settle 500 15000 |
| 3182 | send \x1c\x1c | 3182 | send \x1c\x1c |
| 3183 | waitexit 15000 | 3183 | waitexit 15000 |
| @@ -3208,7 +3208,7 @@ TP1PID=$! | |||
| 3208 | # the last needle is STRUCTURAL, and it has to be. In canonical mode the | 3208 | # the last needle is STRUCTURAL, and it has to be. In canonical mode the |
| 3209 | # only thing a single keystroke produces is the line discipline's echo of | 3209 | # only thing a single keystroke produces is the line discipline's echo of |
| 3210 | # the very glyph the prediction just painted, so no content needle can | 3210 | # the very glyph the prediction just painted, so no content needle can |
| 3211 | # tell the daemon's answer from the client's own guess. `\x1b[2K` can: | 3211 | # tell the daemon's answer from the client's own guess. `\x1b[80X` can: |
| 3212 | # it is emitted by exactly one paint path in the client, the one that | 3212 | # it is emitted by exactly one paint path in the client, the one that |
| 3213 | # paints a DELTA (client.zig, paintDeltaClipped), so its arrival means a | 3213 | # paints a DELTA (client.zig, paintDeltaClipped), so its arrival means a |
| 3214 | # frame came back. The prediction paints the same glyph underlined and | 3214 | # frame came back. The prediction paints the same glyph underlined and |
| @@ -5620,7 +5620,7 @@ ok "a client with no terminal of its own forwards SGR-shaped bytes untouched" | |||
| 5620 | # type mark a with zs-one (a is focused from the first byte) | 5620 | # type mark a with zs-one (a is focused from the first byte) |
| 5621 | # Ctrl-\ n move the focus to tile 2 (session b) | 5621 | # Ctrl-\ n move the focus to tile 2 (session b) |
| 5622 | # type mark b with zs-two | 5622 | # type mark b with zs-two |
| 5623 | # Ctrl-\ l skip back to the last tile focused, which is a | 5623 | # Ctrl-\ 1 focus tile 1 (session a) |
| 5624 | # type mark a again with zs-three | 5624 | # type mark a again with zs-three |
| 5625 | # Ctrl-\ d leave | 5625 | # Ctrl-\ d leave |
| 5626 | # | 5626 | # |
| @@ -5666,7 +5666,7 @@ settle 700 20000 | |||
| 5666 | send printf 'zs-%s\\n' two\n | 5666 | send printf 'zs-%s\\n' two\n |
| 5667 | expect zs-two 15000 | 5667 | expect zs-two 15000 |
| 5668 | settle 400 15000 | 5668 | settle 400 15000 |
| 5669 | send \x1cl | 5669 | send \x1c1 |
| 5670 | settle 700 20000 | 5670 | settle 700 20000 |
| 5671 | send printf 'zs-%s\\n' three\n | 5671 | send printf 'zs-%s\\n' three\n |
| 5672 | expect zs-three 15000 | 5672 | expect zs-three 15000 |
| @@ -5679,7 +5679,7 @@ set -e | |||
| 5679 | unwatch_clients "$OUT.zswatch" | 5679 | unwatch_clients "$OUT.zswatch" |
| 5680 | ZSATT_AFTER=$(attaches_now "$SOCK37") | 5680 | ZSATT_AFTER=$(attaches_now "$SOCK37") |
| 5681 | [ "$RC" -eq 0 ] || { | 5681 | [ "$RC" -eq 0 ] || { |
| 5682 | echo "e2e FAIL: focus skip: ptyclient leg exited $RC (did \\x1cn and \\x1cl move the focus?):" | 5682 | echo "e2e FAIL: focus skip: ptyclient leg exited $RC (did \\x1cn and \\x1c1 move the focus?):" |
| 5683 | cat "$OUT.zspc"; exit 1; } | 5683 | cat "$OUT.zspc"; exit 1; } |
| 5684 | # Where each marker landed. `muxa capture` reads the session's own grid, so | 5684 | # Where each marker landed. `muxa capture` reads the session's own grid, so |
| 5685 | # none of this can be an echo of what this script typed at a terminal. | 5685 | # none of this can be an echo of what this script typed at a terminal. |