diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index ecc501c6a..d5709ea53 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -629,7 +629,16 @@ pub fn print(self: *Terminal, c: u21) !void { // We only create a spacer head if we're at the real edge // of the screen. Otherwise, we clear the space with a narrow. // This allows soft wrapping to work correctly. - self.printCell(0, if (right_limit == self.cols) .spacer_head else .narrow); + if (right_limit == self.cols) { + // Special-case: we need to set wrap to true even + // though we call printWrap below because if there is + // a page resize during printCell then it'll fail + // integrity checks. + self.screens.active.cursor.page_row.wrap = true; + self.printCell(0, .spacer_head); + } else { + self.printCell(0, .narrow); + } try self.printWrap(); } @@ -5112,6 +5121,50 @@ test "Terminal: overwrite hyperlink" { try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } +// Printing a wide char at the right edge with an active hyperlink causes +// printCell to write a spacer_head before printWrap sets the row wrap +// flag. The integrity check inside setHyperlink (or increaseCapacity) +// sees the unwrapped spacer head and panics. Found via fuzzing. +test "Terminal: print wide char at right edge with hyperlink" { + var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); + defer t.deinit(testing.allocator); + + try t.screens.active.startHyperlink("http://example.com", null); + + // Move cursor to the last column (1-indexed) + t.setCursorPos(1, 10); + + // Print a wide character; this will call printCell(0, .spacer_head) + // at the right edge before calling printWrap, triggering the + // integrity violation. + try t.print(0x4E2D); // U+4E2D 'δΈ­' + + // Cursor wraps to row 2, after the wide char + spacer tail + try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + + // Row 0, col 9: spacer head with hyperlink + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; + try testing.expectEqual(Cell.Wide.spacer_head, list_cell.cell.wide); + try testing.expect(list_cell.cell.hyperlink); + try testing.expect(list_cell.row.wrap); + } + // Row 1, col 0: the wide char with hyperlink + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + try testing.expectEqual(@as(u21, 0x4E2D), list_cell.cell.content.codepoint); + try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide); + try testing.expect(list_cell.cell.hyperlink); + } + // Row 1, col 1: spacer tail with hyperlink + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; + try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide); + try testing.expect(list_cell.cell.hyperlink); + } +} + test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);