From d14847183844e84fb8282ebe5a6c9061f40530e3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 2 Aug 2026 20:46:38 -0700 Subject: [PATCH] terminal/snapshot: preserve mixed-width pending wrap SCREEN decode clamped the cursor x coordinate to the physical page width, but validated pending wrap against the terminal-wide column count. A lazily reflowed page narrower than the current terminal could therefore lose a valid pending-wrap state at its last physical column. The next write would continue on the same row instead of wrapping. Validate pending wrap against the cursor page width, matching the clamp and the page-local cursor pin. Add a mixed-width decode regression that places the cursor at the narrow page boundary. --- src/terminal/snapshot/screen.zig | 54 +++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/terminal/snapshot/screen.zig b/src/terminal/snapshot/screen.zig index d05ff00ba..defb9ce24 100644 --- a/src/terminal/snapshot/screen.zig +++ b/src/terminal/snapshot/screen.zig @@ -411,7 +411,7 @@ pub fn decode( .y = y, .cursor_style = header.cursor_style, .pending_wrap = header.cursor_flags.pending_wrap and - x == options.cols - 1, + x == row_pin.node.cols() - 1, .protected = header.cursor_flags.protected, .style = header.cursor_pen, .hyperlink_implicit_id = header.hyperlink_implicit_id, @@ -2048,6 +2048,58 @@ test "SCREEN restoration normalizes invalid cursor positions" { } } +test "SCREEN validates pending wrap against a mixed-width cursor page" { + var screen = try TerminalScreen.init( + std.testing.io, + std.testing.allocator, + .{ .cols = 8, .rows = 1, .max_scrollback_bytes = 0 }, + ); + defer screen.deinit(); + + // Model a lazily reflowed active page that is narrower than the terminal. + // Column three is its physical final column even though it is not column + // seven of the current terminal dimensions. + var narrow_page = try terminal_page.Page.init(.{ .cols = 4, .rows = 1 }); + defer narrow_page.deinit(); + + var destination: std.Io.Writer.Allocating = .init( + std.testing.allocator, + ); + defer destination.deinit(); + var stream: record.Writer = .init( + std.testing.allocator, + &destination.writer, + ); + defer stream.deinit(); + + var header = Header.init(&screen, .primary, 1); + header.cursor_x = 3; + header.cursor_y = 0; + header.cursor_flags.pending_wrap = true; + header.saved_cursor_present = false; + + const screen_payload = stream.begin(.screen); + errdefer stream.cancel(); + try header.encode(screen_payload); + try screen_payload.writeByte(0); + try stream.finish(); + try page.encode(&narrow_page, &stream); + + var source: std.Io.Reader = .fixed(destination.written()); + var decoded = try decode( + &source, + std.testing.io, + std.testing.allocator, + .{ .cols = 8, .rows = 1, .max_scrollback_bytes = 0 }, + ); + defer decoded.deinit(); + + try std.testing.expectEqual(@as(u16, 4), decoded.screen.cursor.page_pin.node.cols()); + try std.testing.expectEqual(@as(u16, 3), decoded.screen.cursor.x); + try std.testing.expect(decoded.screen.cursor.pending_wrap); + decoded.screen.assertIntegrity(); +} + test "SCREEN restoration rejects invalid and incomplete sequences" { var screen = try TerminalScreen.init( std.testing.io,