From 5543a00ff256e42008a70f9772ed073f4bbb6d94 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:58:45 -0700 Subject: [PATCH 01/10] test: cover formatter cursor after margins --- src/terminal/formatter.zig | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 7afc8fb1413..3d5393db292 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -416,6 +416,7 @@ pub const TerminalFormatter = struct { std.math.cast(usize, discarding.count) orelse return error.WriteFailed, ) catch return error.WriteFailed; } + } } }; @@ -5202,6 +5203,55 @@ test "Terminal vt with scrolling region" { try testing.expectEqual(t.scrolling_region.right, t2.scrolling_region.right); } +test "Terminal vt restores cursor after scrolling margins" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + // Use both margin axes and origin mode, then place the cursor at the + // second row and column inside those margins. + s.nextSlice("\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b[?6h\x1b[2;2H"); + + var formatter: TerminalFormatter = .init(&t, .vt); + formatter.extra = .none; + formatter.extra.modes = true; + formatter.extra.scrolling_region = true; + formatter.extra.screen.cursor = true; + + try formatter.format(&builder.writer); + const output = builder.writer.buffered(); + + var t2 = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t2.deinit(alloc); + + var s2 = t2.vtStream(); + defer s2.deinit(); + s2.nextSlice(output); + + try testing.expect(t2.modes.get(.origin)); + try testing.expectEqual(t.scrolling_region.top, t2.scrolling_region.top); + try testing.expectEqual(t.scrolling_region.bottom, t2.scrolling_region.bottom); + try testing.expectEqual(t.scrolling_region.left, t2.scrolling_region.left); + try testing.expectEqual(t.scrolling_region.right, t2.scrolling_region.right); + try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); + try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); +} + test "Terminal vt with modes" { const testing = std.testing; const alloc = testing.allocator; From 9e49174bebd562b113768a376bdb1c541d13cbdb Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:58:57 -0700 Subject: [PATCH 02/10] fix: restore formatted cursor after margins --- src/terminal/formatter.zig | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 3d5393db292..3300cf3f88c 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -294,6 +294,35 @@ pub const TerminalFormatter = struct { std.math.cast(usize, discarding.count) orelse return error.WriteFailed, ) catch return error.WriteFailed; } + + // Scrolling regions and tabstop restoration move the cursor after + // ScreenFormatter emits it. Restore the cursor after all terminal + // state, with CUP coordinates relative to the origin margins. + if (self.extra.screen.cursor) { + const cursor = &self.terminal.screens.active.cursor; + const region = &self.terminal.scrolling_region; + const origin = self.terminal.modes.get(.origin); + const row = if (origin) cursor.y - region.top else cursor.y; + const col = if (origin) cursor.x - region.left else cursor.x; + try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + + if (self.pin_map) |*m| { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + try discarding.writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + m.map.appendNTimes( + m.alloc, + if (m.map.items.len > 0) pin: { + const last = m.map.items[m.map.items.len - 1]; + break :pin .{ + .node = last.node, + .x = last.x, + .y = last.y, + }; + } else self.terminal.screens.active.pages.getTopLeft(.screen), + std.math.cast(usize, discarding.count) orelse return error.WriteFailed, + ) catch return error.WriteFailed; + } + } } // Emit terminal modes that differ from defaults. We probably have From d6fdb42e1bb4c7ef9f9a4f8edc8f11005a2ac11f Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:17:07 -0700 Subject: [PATCH 03/10] fix: place cursor restoration after terminal state --- src/terminal/formatter.zig | 56 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 3300cf3f88c..2dac75f8bff 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -295,34 +295,6 @@ pub const TerminalFormatter = struct { ) catch return error.WriteFailed; } - // Scrolling regions and tabstop restoration move the cursor after - // ScreenFormatter emits it. Restore the cursor after all terminal - // state, with CUP coordinates relative to the origin margins. - if (self.extra.screen.cursor) { - const cursor = &self.terminal.screens.active.cursor; - const region = &self.terminal.scrolling_region; - const origin = self.terminal.modes.get(.origin); - const row = if (origin) cursor.y - region.top else cursor.y; - const col = if (origin) cursor.x - region.left else cursor.x; - try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); - - if (self.pin_map) |*m| { - var discarding: std.Io.Writer.Discarding = .init(&.{}); - try discarding.writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); - m.map.appendNTimes( - m.alloc, - if (m.map.items.len > 0) pin: { - const last = m.map.items[m.map.items.len - 1]; - break :pin .{ - .node = last.node, - .x = last.x, - .y = last.y, - }; - } else self.terminal.screens.active.pages.getTopLeft(.screen), - std.math.cast(usize, discarding.count) orelse return error.WriteFailed, - ) catch return error.WriteFailed; - } - } } // Emit terminal modes that differ from defaults. We probably have @@ -446,6 +418,34 @@ pub const TerminalFormatter = struct { ) catch return error.WriteFailed; } + // Scrolling regions and tabstop restoration move the cursor after + // ScreenFormatter emits it. Restore the cursor after all terminal + // state, with CUP coordinates relative to the origin margins. + if (self.extra.screen.cursor) { + const cursor = &self.terminal.screens.active.cursor; + const region = &self.terminal.scrolling_region; + const origin = self.terminal.modes.get(.origin); + const row = if (origin) cursor.y - region.top else cursor.y; + const col = if (origin) cursor.x - region.left else cursor.x; + try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + + if (self.pin_map) |*m| { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + try discarding.writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + m.map.appendNTimes( + m.alloc, + if (m.map.items.len > 0) pin: { + const last = m.map.items[m.map.items.len - 1]; + break :pin .{ + .node = last.node, + .x = last.x, + .y = last.y, + }; + } else self.terminal.screens.active.pages.getTopLeft(.screen), + std.math.cast(usize, discarding.count) orelse return error.WriteFailed, + ) catch return error.WriteFailed; + } + } } } }; From 6fd6762a93c77fcf02b416abf471580e1fafa718 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:14:58 -0700 Subject: [PATCH 04/10] test: cover tabstop-only cursor replay --- src/terminal/formatter.zig | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 2dac75f8bff..1ecd9b1b2e4 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -5380,6 +5380,46 @@ test "Terminal vt with tabstops" { try testing.expect(!t2.tabstops.get(8)); // Not a tab } +test "Terminal vt restores cursor after tabstops without scrolling region" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(io, alloc, .{ + .cols = 8, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + s.nextSlice("\x1b[3g\x1b[2G\x1bH\x1b[6G\x1bH\x1b[3;3H"); + + var formatter: TerminalFormatter = .init(&t, .vt); + formatter.extra = .none; + formatter.extra.tabstops = true; + formatter.extra.screen.cursor = true; + try testing.expect(!formatter.extra.scrolling_region); + + try formatter.format(&builder.writer); + + var t2 = try Terminal.init(io, alloc, .{ + .cols = 8, + .rows = 5, + }); + defer t2.deinit(alloc); + + var s2 = t2.vtStream(); + defer s2.deinit(); + s2.nextSlice(builder.writer.buffered()); + + try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); + try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); +} + test "Terminal vt with keyboard modes" { const testing = std.testing; const alloc = testing.allocator; From b1d0adddd58b6401777d6677c814431b0b10b990 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:19:36 -0700 Subject: [PATCH 05/10] test: cover omitted cursor coordinate state --- src/terminal/formatter.zig | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 1ecd9b1b2e4..2c7da7e28e5 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -5281,6 +5281,90 @@ test "Terminal vt restores cursor after scrolling margins" { try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); } +test "Terminal vt cursor is absolute when origin mode is omitted" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + s.nextSlice("\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b[?6h\x1b[2;2H"); + + var formatter: TerminalFormatter = .init(&t, .vt); + formatter.extra = .none; + formatter.extra.scrolling_region = true; + formatter.extra.screen.cursor = true; + + try formatter.format(&builder.writer); + + var t2 = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t2.deinit(alloc); + + var s2 = t2.vtStream(); + defer s2.deinit(); + s2.nextSlice(builder.writer.buffered()); + + try testing.expect(!t2.modes.get(.origin)); + try testing.expectEqual(t.scrolling_region.top, t2.scrolling_region.top); + try testing.expectEqual(t.scrolling_region.left, t2.scrolling_region.left); + try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); + try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); +} + +test "Terminal vt cursor uses default margins when scrolling region is omitted" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + s.nextSlice("\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b[?6h\x1b[2;2H"); + + var formatter: TerminalFormatter = .init(&t, .vt); + formatter.extra = .none; + formatter.extra.modes = true; + formatter.extra.screen.cursor = true; + + try formatter.format(&builder.writer); + + var t2 = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t2.deinit(alloc); + + var s2 = t2.vtStream(); + defer s2.deinit(); + s2.nextSlice(builder.writer.buffered()); + + try testing.expect(t2.modes.get(.origin)); + try testing.expectEqual(@as(usize, 0), t2.scrolling_region.top); + try testing.expectEqual(@as(usize, 0), t2.scrolling_region.left); + try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); + try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); +} + test "Terminal vt with modes" { const testing = std.testing; const alloc = testing.allocator; From 28b6fc6f415f9ac3e6f7dfb4f9cf1e55aa7aa13b Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:20:08 -0700 Subject: [PATCH 06/10] test: isolate omitted origin coordinate state --- src/terminal/formatter.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 2c7da7e28e5..d081572fbb0 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -5251,7 +5251,7 @@ test "Terminal vt restores cursor after scrolling margins" { // Use both margin axes and origin mode, then place the cursor at the // second row and column inside those margins. - s.nextSlice("\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b[?6h\x1b[2;2H"); + s.nextSlice("\x1b[2;4r\x1b[?6h\x1b[2;3H"); var formatter: TerminalFormatter = .init(&t, .vt); formatter.extra = .none; @@ -5318,7 +5318,6 @@ test "Terminal vt cursor is absolute when origin mode is omitted" { try testing.expect(!t2.modes.get(.origin)); try testing.expectEqual(t.scrolling_region.top, t2.scrolling_region.top); - try testing.expectEqual(t.scrolling_region.left, t2.scrolling_region.left); try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); } @@ -5339,7 +5338,7 @@ test "Terminal vt cursor uses default margins when scrolling region is omitted" var s = t.vtStream(); defer s.deinit(); - s.nextSlice("\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b[?6h\x1b[2;2H"); + s.nextSlice("\x1b[2;4r\x1b[?6h\x1b[2;3H"); var formatter: TerminalFormatter = .init(&t, .vt); formatter.extra = .none; @@ -5360,7 +5359,6 @@ test "Terminal vt cursor uses default margins when scrolling region is omitted" try testing.expect(t2.modes.get(.origin)); try testing.expectEqual(@as(usize, 0), t2.scrolling_region.top); - try testing.expectEqual(@as(usize, 0), t2.scrolling_region.left); try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); } From 2bba4149b962a9850345970dc0d75fad0bab182c Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:20:23 -0700 Subject: [PATCH 07/10] fix: derive cursor from emitted terminal state --- src/terminal/formatter.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index d081572fbb0..d4513fc9b0e 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -424,9 +424,11 @@ pub const TerminalFormatter = struct { if (self.extra.screen.cursor) { const cursor = &self.terminal.screens.active.cursor; const region = &self.terminal.scrolling_region; - const origin = self.terminal.modes.get(.origin); - const row = if (origin) cursor.y - region.top else cursor.y; - const col = if (origin) cursor.x - region.left else cursor.x; + const origin = self.extra.modes and self.terminal.modes.get(.origin); + const emitted_top = if (self.extra.scrolling_region) region.top else 0; + const emitted_left = if (self.extra.scrolling_region) region.left else 0; + const row = if (origin) cursor.y - emitted_top else cursor.y; + const col = if (origin) cursor.x - emitted_left else cursor.x; try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); if (self.pin_map) |*m| { From e57ffa985d98c49d80208722c055b2303035f556 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:25:29 -0700 Subject: [PATCH 08/10] test: cover origin cursor outside margins --- src/terminal/formatter.zig | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index d4513fc9b0e..48e614207dc 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -5283,6 +5283,52 @@ test "Terminal vt restores cursor after scrolling margins" { try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); } +test "Terminal vt restores origin cursor outside new margins" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + // Save an origin-mode cursor before narrowing the margins, then restore + // it above the new top margin. + s.nextSlice("\x1b[?6h\x1b[1;3H\x1b7\x1b[2;4r\x1b8"); + try testing.expect(t.screens.active.cursor.y < t.scrolling_region.top); + + var formatter: TerminalFormatter = .init(&t, .vt); + formatter.extra = .none; + formatter.extra.modes = true; + formatter.extra.scrolling_region = true; + formatter.extra.screen.cursor = true; + + try formatter.format(&builder.writer); + + var t2 = try Terminal.init(io, alloc, .{ + .cols = 5, + .rows = 5, + }); + defer t2.deinit(alloc); + + var s2 = t2.vtStream(); + defer s2.deinit(); + s2.nextSlice(builder.writer.buffered()); + + try testing.expect(t2.modes.get(.origin)); + try testing.expectEqual(t.scrolling_region.top, t2.scrolling_region.top); + try testing.expectEqual(t.scrolling_region.bottom, t2.scrolling_region.bottom); + try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); + try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); +} + test "Terminal vt cursor is absolute when origin mode is omitted" { const testing = std.testing; const alloc = testing.allocator; From 9ddb1ab57159df8a662cc675b95d1ddf9f1303ff Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:26:35 -0700 Subject: [PATCH 09/10] fix: restore cursors outside origin margins --- src/terminal/formatter.zig | 58 ++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 48e614207dc..93d0de090c4 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -422,18 +422,11 @@ pub const TerminalFormatter = struct { // ScreenFormatter emits it. Restore the cursor after all terminal // state, with CUP coordinates relative to the origin margins. if (self.extra.screen.cursor) { - const cursor = &self.terminal.screens.active.cursor; - const region = &self.terminal.scrolling_region; - const origin = self.extra.modes and self.terminal.modes.get(.origin); - const emitted_top = if (self.extra.scrolling_region) region.top else 0; - const emitted_left = if (self.extra.scrolling_region) region.left else 0; - const row = if (origin) cursor.y - emitted_top else cursor.y; - const col = if (origin) cursor.x - emitted_left else cursor.x; - try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + try self.formatCursorRestore(writer); if (self.pin_map) |*m| { var discarding: std.Io.Writer.Discarding = .init(&.{}); - try discarding.writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + try self.formatCursorRestore(&discarding.writer); m.map.appendNTimes( m.alloc, if (m.map.items.len > 0) pin: { @@ -450,6 +443,53 @@ pub const TerminalFormatter = struct { } } } + + fn formatCursorRestore( + self: TerminalFormatter, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + const cursor = &self.terminal.screens.active.cursor; + const region = &self.terminal.scrolling_region; + const origin = self.extra.modes and self.terminal.modes.get(.origin); + const horizontal_margins = self.extra.scrolling_region and + self.extra.modes and + self.terminal.modes.get(.enable_left_and_right_margin); + const emitted_top = if (self.extra.scrolling_region) region.top else 0; + const emitted_bottom = if (self.extra.scrolling_region) + region.bottom + else + self.terminal.rows - 1; + const emitted_left = if (horizontal_margins) region.left else 0; + const emitted_right = if (horizontal_margins) region.right else self.terminal.cols - 1; + const outside_origin_region = origin and + (cursor.y < emitted_top or + cursor.y > emitted_bottom or + cursor.x < emitted_left or + cursor.x > emitted_right); + + if (!outside_origin_region) { + const row = if (origin) cursor.y - emitted_top else cursor.y; + const col = if (origin) cursor.x - emitted_left else cursor.x; + try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); + return; + } + + // CUP cannot address a cell outside active origin margins. Widen the + // margins, save the absolute cursor with origin mode still enabled, + // restore the emitted margins, then restore that saved cursor. + try writer.writeAll("\x1b[r"); + if (horizontal_margins) try writer.writeAll("\x1b[s"); + try writer.print("\x1b[{d};{d}H\x1b7", .{ cursor.y + 1, cursor.x + 1 }); + if (region.top != 0 or region.bottom != self.terminal.rows - 1) { + try writer.print("\x1b[{d};{d}r", .{ region.top + 1, region.bottom + 1 }); + } + if (horizontal_margins and + (region.left != 0 or region.right != self.terminal.cols - 1)) + { + try writer.print("\x1b[{d};{d}s", .{ region.left + 1, region.right + 1 }); + } + try writer.writeAll("\x1b8"); + } }; /// Screen formatter formats a single terminal screen (e.g. primary vs alt). From 533c27ae1c87c02671e36d44baf5973f1d6b7113 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:56:51 -0700 Subject: [PATCH 10/10] Preserve saved cursors during formatter replay --- src/terminal/formatter.zig | 62 ++++++++++++++------------------------ 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 93d0de090c4..1b562eb565d 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -455,40 +455,13 @@ pub const TerminalFormatter = struct { self.extra.modes and self.terminal.modes.get(.enable_left_and_right_margin); const emitted_top = if (self.extra.scrolling_region) region.top else 0; - const emitted_bottom = if (self.extra.scrolling_region) - region.bottom - else - self.terminal.rows - 1; const emitted_left = if (horizontal_margins) region.left else 0; - const emitted_right = if (horizontal_margins) region.right else self.terminal.cols - 1; - const outside_origin_region = origin and - (cursor.y < emitted_top or - cursor.y > emitted_bottom or - cursor.x < emitted_left or - cursor.x > emitted_right); - - if (!outside_origin_region) { - const row = if (origin) cursor.y - emitted_top else cursor.y; - const col = if (origin) cursor.x - emitted_left else cursor.x; - try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); - return; - } - - // CUP cannot address a cell outside active origin margins. Widen the - // margins, save the absolute cursor with origin mode still enabled, - // restore the emitted margins, then restore that saved cursor. - try writer.writeAll("\x1b[r"); - if (horizontal_margins) try writer.writeAll("\x1b[s"); - try writer.print("\x1b[{d};{d}H\x1b7", .{ cursor.y + 1, cursor.x + 1 }); - if (region.top != 0 or region.bottom != self.terminal.rows - 1) { - try writer.print("\x1b[{d};{d}r", .{ region.top + 1, region.bottom + 1 }); - } - if (horizontal_margins and - (region.left != 0 or region.right != self.terminal.cols - 1)) - { - try writer.print("\x1b[{d};{d}s", .{ region.left + 1, region.right + 1 }); - } - try writer.writeAll("\x1b8"); + // CUP cannot address a cell outside active origin margins. Saturate + // its relative coordinates instead of using DECSC/DECRC as scratch + // storage, because that would overwrite the consumer's saved cursor. + const row = if (origin) cursor.y -| emitted_top else cursor.y; + const col = if (origin) cursor.x -| emitted_left else cursor.x; + try writer.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); } }; @@ -5293,7 +5266,7 @@ test "Terminal vt restores cursor after scrolling margins" { // Use both margin axes and origin mode, then place the cursor at the // second row and column inside those margins. - s.nextSlice("\x1b[2;4r\x1b[?6h\x1b[2;3H"); + s.nextSlice("\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b[?6h\x1b[2;2H"); var formatter: TerminalFormatter = .init(&t, .vt); formatter.extra = .none; @@ -5323,7 +5296,7 @@ test "Terminal vt restores cursor after scrolling margins" { try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); } -test "Terminal vt restores origin cursor outside new margins" { +test "Terminal vt clamps an outside origin cursor without overwriting saved cursor" { const testing = std.testing; const alloc = testing.allocator; const io = testing.io; @@ -5339,10 +5312,11 @@ test "Terminal vt restores origin cursor outside new margins" { var s = t.vtStream(); defer s.deinit(); - // Save an origin-mode cursor before narrowing the margins, then restore - // it above the new top margin. - s.nextSlice("\x1b[?6h\x1b[1;3H\x1b7\x1b[2;4r\x1b8"); + // Save an origin-mode cursor before narrowing both margin axes, then + // restore it above and to the left of the new margins. + s.nextSlice("\x1b[?6h\x1b[1;1H\x1b7\x1b[?69h\x1b[2;4s\x1b[2;4r\x1b8"); try testing.expect(t.screens.active.cursor.y < t.scrolling_region.top); + try testing.expect(t.screens.active.cursor.x < t.scrolling_region.left); var formatter: TerminalFormatter = .init(&t, .vt); formatter.extra = .none; @@ -5360,13 +5334,21 @@ test "Terminal vt restores origin cursor outside new margins" { var s2 = t2.vtStream(); defer s2.deinit(); + s2.nextSlice("\x1b[5;5H\x1b7"); s2.nextSlice(builder.writer.buffered()); try testing.expect(t2.modes.get(.origin)); try testing.expectEqual(t.scrolling_region.top, t2.scrolling_region.top); try testing.expectEqual(t.scrolling_region.bottom, t2.scrolling_region.bottom); - try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x); - try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y); + try testing.expectEqual(t.scrolling_region.left, t2.scrolling_region.left); + try testing.expectEqual(t.scrolling_region.right, t2.scrolling_region.right); + try testing.expectEqual(t2.scrolling_region.left, t2.screens.active.cursor.x); + try testing.expectEqual(t2.scrolling_region.top, t2.screens.active.cursor.y); + + s2.nextSlice("\x1b8"); + try testing.expect(!t2.modes.get(.origin)); + try testing.expectEqual(@as(size.CellCountInt, 4), t2.screens.active.cursor.x); + try testing.expectEqual(@as(size.CellCountInt, 4), t2.screens.active.cursor.y); } test "Terminal vt cursor is absolute when origin mode is omitted" {