Skip to content
Merged
79 changes: 79 additions & 0 deletions src/terminal/formatter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ pub const TerminalFormatter = struct {
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}

}

// Emit terminal modes that differ from defaults. We probably have
Expand Down Expand Up @@ -416,6 +417,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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use relative CUP only when emitting its origin context

When a caller enables screen.cursor without both modes and scrolling_region, a source terminal with origin mode and nonzero margins produces margin-relative coordinates here even though the output does not recreate that coordinate system. Replaying such a partial snapshot on a fresh terminal therefore places the cursor too far up and left, and this final CUP overrides the earlier absolute CUP emitted by ScreenFormatter; only convert to relative coordinates when the corresponding origin mode and margins are also emitted.

Useful? React with 👍 / 👎.

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;
}
}
}
}
};
Expand Down Expand Up @@ -5202,6 +5232,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;
Expand Down