Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xterm audit: CUF, CUP, HPA, VPA, HPR, VPR #631

Merged
merged 6 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 184 additions & 62 deletions src/terminal/Terminal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -975,9 +975,9 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {
x_max: usize,
y_max: usize,
} = if (self.modes.get(.origin)) .{
.x_offset = 0, // TODO: left/right margins
.x_offset = self.scrolling_region.left,
.y_offset = self.scrolling_region.top,
.x_max = self.cols, // TODO: left/right margins
.x_max = self.scrolling_region.right + 1, // We need this 1-indexed
.y_max = self.scrolling_region.bottom + 1, // We need this 1-indexed
} else .{
.x_max = self.cols,
Expand All @@ -986,42 +986,14 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {

const row = if (row_req == 0) 1 else row_req;
const col = if (col_req == 0) 1 else col_req;
self.screen.cursor.x = @min(params.x_max, col) -| 1;
self.screen.cursor.x = @min(params.x_max, col + params.x_offset) -| 1;
self.screen.cursor.y = @min(params.y_max, row + params.y_offset) -| 1;
// log.info("set cursor position: col={} row={}", .{ self.screen.cursor.x, self.screen.cursor.y });

// Unset pending wrap state
self.screen.cursor.pending_wrap = false;
}

/// Move the cursor to column `col_req` (1-indexed) without modifying the row.
/// If `col_req` is 0, it is changed to 1. If `col_req` is greater than the
/// total number of columns, it is set to the right-most column.
///
/// If cursor origin mode is set, the cursor row will be set inside the
/// current scroll region.
pub fn setCursorColAbsolute(self: *Terminal, col_req: usize) void {
const tracy = trace(@src());
defer tracy.end();

// TODO: test

// TODO
if (self.modes.get(.origin)) {
log.err("setCursorColAbsolute: cursor origin mode handling not implemented yet", .{});
return;
}

if (self.status_display != .main) {
log.err("setCursorColAbsolute: not implemented on status display", .{});
return; // TODO
}

const col = if (col_req == 0) 1 else col_req;
self.screen.cursor.x = @min(self.cols, col) - 1;
self.screen.cursor.pending_wrap = false;
}

/// Erase the display.
pub fn eraseDisplay(
self: *Terminal,
Expand Down Expand Up @@ -1439,16 +1411,21 @@ pub fn cursorLeft(self: *Terminal, count_req: usize) void {
/// maximum move distance then it is internally adjusted to the maximum.
/// This sequence will not scroll the screen or scroll region. If amount is
/// 0, adjust it to 1.
/// TODO: test
pub fn cursorRight(self: *Terminal, count: usize) void {
pub fn cursorRight(self: *Terminal, count_req: usize) void {
const tracy = trace(@src());
defer tracy.end();

self.screen.cursor.x += if (count == 0) 1 else count;
// Always resets pending wrap
self.screen.cursor.pending_wrap = false;
if (self.screen.cursor.x >= self.cols) {
self.screen.cursor.x = self.cols - 1;
}

// The max the cursor can move to depends where the cursor currently is
const max = if (self.screen.cursor.x <= self.scrolling_region.right)
self.scrolling_region.right
else
self.cols - 1;

const count = @max(count_req, 1);
self.screen.cursor.x = @min(max, self.screen.cursor.x +| count);
}

/// Move the cursor down amount lines. If amount is greater than the maximum
Expand Down Expand Up @@ -2417,7 +2394,98 @@ test "Terminal: horizontal tabs with left margin in origin mode" {
}
}

test "Terminal: setCursorPosition" {
test "Terminal: cursorPos resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.setCursorPos(1, 1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("XBCDE", str);
}
}

test "Terminal: cursorPos off the screen" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.setCursorPos(500, 500);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\n\n X", str);
}
}

test "Terminal: cursorPos relative to origin" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.modes.set(.origin, true);
t.setCursorPos(1, 1);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\nX", str);
}
}

test "Terminal: cursorPos relative to origin with left/right" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.modes.set(.origin, true);
t.setCursorPos(1, 1);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n X", str);
}
}

test "Terminal: cursorPos limits with full scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.modes.set(.origin, true);
t.setCursorPos(500, 500);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\n X", str);
}
}

test "Terminal: setCursorPos (original test)" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);

Expand Down Expand Up @@ -3509,18 +3577,6 @@ test "Terminal: eraseChars resets wrap" {
}
}

test "Terminal: setCursorColAbsolute resets pending wrap" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.setCursorColAbsolute(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
}

// https://github.com/mitchellh/ghostty/issues/272
// This is also tested in depth in screen resize tests but I want to keep
// this test around to ensure we don't regress at multiple layers.
Expand Down Expand Up @@ -3624,10 +3680,10 @@ test "Terminal: eraseLine protected right" {
defer t.deinit(alloc);

for ("12345678") |c| try t.print(c);
t.setCursorColAbsolute(6);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorColAbsolute(4);
t.setCursorPos(t.screen.cursor.y + 1, 4);
t.eraseLine(.right, true);

{
Expand All @@ -3643,10 +3699,10 @@ test "Terminal: eraseLine protected left" {
defer t.deinit(alloc);

for ("123456789") |c| try t.print(c);
t.setCursorColAbsolute(6);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorColAbsolute(8);
t.setCursorPos(t.screen.cursor.y + 1, 8);
t.eraseLine(.left, true);

{
Expand All @@ -3662,10 +3718,10 @@ test "Terminal: eraseLine protected complete" {
defer t.deinit(alloc);

for ("123456789") |c| try t.print(c);
t.setCursorColAbsolute(6);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorColAbsolute(8);
t.setCursorPos(t.screen.cursor.y + 1, 8);
t.eraseLine(.complete, true);

{
Expand All @@ -3684,10 +3740,10 @@ test "Terminal: eraseDisplay protected complete" {
t.carriageReturn();
try t.linefeed();
for ("123456789") |c| try t.print(c);
t.setCursorColAbsolute(6);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorColAbsolute(4);
t.setCursorPos(t.screen.cursor.y + 1, 4);
t.eraseDisplay(alloc, .complete, true);

{
Expand All @@ -3706,10 +3762,10 @@ test "Terminal: eraseDisplay protected below" {
t.carriageReturn();
try t.linefeed();
for ("123456789") |c| try t.print(c);
t.setCursorColAbsolute(6);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorColAbsolute(4);
t.setCursorPos(t.screen.cursor.y + 1, 4);
t.eraseDisplay(alloc, .below, true);

{
Expand All @@ -3728,10 +3784,10 @@ test "Terminal: eraseDisplay protected above" {
t.carriageReturn();
try t.linefeed();
for ("123456789") |c| try t.print(c);
t.setCursorColAbsolute(6);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorColAbsolute(8);
t.setCursorPos(t.screen.cursor.y + 1, 8);
t.eraseDisplay(alloc, .above, true);

{
Expand Down Expand Up @@ -4044,3 +4100,69 @@ test "Terminal: cursorUp resets wrap" {
try testing.expectEqualStrings("ABCDX", str);
}
}

test "Terminal: cursorRight resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorRight(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}

test "Terminal: cursorRight to the edge of screen" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.cursorRight(100);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}

test "Terminal: cursorRight left of right margin" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.scrolling_region.right = 2;
t.cursorRight(100);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}

test "Terminal: cursorRight right of right margin" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);

t.scrolling_region.right = 2;
t.screen.cursor.x = 3;
t.cursorRight(100);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
24 changes: 24 additions & 0 deletions src/terminal/stream.zig
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,18 @@ pub fn Stream(comptime Handler: type) type {
},
) else log.warn("unimplemented CSI callback: {}", .{action}),

// HPR - Cursor Horizontal Position Relative
'a' => if (@hasDecl(T, "setCursorColRelative")) try self.handler.setCursorColRelative(
switch (action.params.len) {
0 => 1,
1 => action.params[0],
else => {
log.warn("invalid HPR command: {}", .{action});
return;
},
},
) else log.warn("unimplemented CSI callback: {}", .{action}),

// Repeat Previous Char (REP)
'b' => if (@hasDecl(T, "printRepeat")) try self.handler.printRepeat(
switch (action.params.len) {
Expand Down Expand Up @@ -474,6 +486,18 @@ pub fn Stream(comptime Handler: type) type {
},
) else log.warn("unimplemented CSI callback: {}", .{action}),

// VPR - Cursor Vertical Position Relative
'e' => if (@hasDecl(T, "setCursorRowRelative")) try self.handler.setCursorRowRelative(
switch (action.params.len) {
0 => 1,
1 => action.params[0],
else => {
log.warn("invalid VPR command: {}", .{action});
return;
},
},
) else log.warn("unimplemented CSI callback: {}", .{action}),

// TBC - Tab Clear
// TODO: test
'g' => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(
Expand Down
Loading