Merge pull request #631 from mitchellh/xt-cuf

xterm audit: CUF, CUP, HPA, VPA, HPR, VPR
This commit is contained in:
Mitchell Hashimoto
2023-10-07 09:44:51 -07:00
committed by GitHub
10 changed files with 496 additions and 69 deletions

View File

@ -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,
@ -986,7 +986,7 @@ 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 });
@ -994,34 +994,6 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {
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,
@ -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
@ -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);
@ -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.
@ -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);
{
@ -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);
{
@ -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);
{
@ -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);
{
@ -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);
{
@ -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);
{
@ -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);
}
}

View File

@ -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) {
@ -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(

View File

@ -1313,18 +1313,27 @@ const StreamHandler = struct {
}
pub fn setCursorCol(self: *StreamHandler, col: u16) !void {
self.terminal.setCursorColAbsolute(col);
self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, col);
}
pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void {
self.terminal.setCursorPos(
self.terminal.screen.cursor.y + 1,
self.terminal.screen.cursor.x + 1 + offset,
);
}
pub fn setCursorRow(self: *StreamHandler, row: u16) !void {
if (self.terminal.modes.get(.origin)) {
// TODO
log.err("setCursorRow: unimplemented origin mode handling, misrendering may occur", .{});
}
self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1);
}
pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void {
self.terminal.setCursorPos(
self.terminal.screen.cursor.y + 1 + offset,
self.terminal.screen.cursor.x + 1,
);
}
pub fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void {
self.terminal.setCursorPos(row, col);
}

View File

@ -0,0 +1,83 @@
import VTSequence from "@/components/VTSequence";
# Cursor Forward (CUF)
<VTSequence sequence={["CSI", "Pn", "C"]} />
Move the cursor `n` cells right.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
This sequence always unsets the pending wrap state.
The rightmost boundary the cursor can move to is determined by the current
cursor column and the [right margin](#TODO). If the cursor begins to the right
of the right margin, modify the right margin to be the rightmost column
of the screen for the duration of the sequence. The rightmost column the cursor
can be on is the right margin.
Move the cursor `n` cells to the right up to and including the rightmost boundary.
This sequence never wraps or modifies cell content. This sequence is not affected
by any terminal modes.
## Validation
### CUF V-1: Pending Wrap is Unset
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[C" # move forward one
printf "XYZ"
```
```
|_________X|
|YZ________|
```
### CUF V-2: Rightmost Boundary with Reverse Wrap Disabled
```bash
printf "A"
printf "\033[500C" # forward larger than screen width
printf "B"
```
```
|A________Bc
```
### CUF V-3: Left of the Right Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[1G" # move to left
printf "\033[500C" # forward larger than screen width
printf "X"
```
```
|____X_____|
```
### CUF V-4: Right of the Right Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[6G" # move to right of margin
printf "\033[500C" # forward larger than screen width
printf "X"
```
```
|_________X|
```

127
website/app/vt/cup/page.mdx Normal file
View File

@ -0,0 +1,127 @@
import VTSequence from "@/components/VTSequence";
# Cursor Position (CUP)
<VTSequence sequence={["CSI", "Py", ";", "Px", "H"]} />
Move the cursor to row `y` and column `x`.
The parameters `y` and `x` must be integers greater than or equal to 1.
If either is less than or equal to 0, adjust that parameter to be 1.
The values `y` and `x` are both one-based. For example, the top row is row 1
and the leftmost column on the screen is column 1.
This sequence always unsets the pending wrap state.
If [origin mode](#TODO) is **NOT** set, the cursor is moved exactly to the
row and column specified by `y` and `x`. The maxium value for `y` is the
bottom row of the screen and the maximum value for `x` is the rightmost
column of the screen.
If [origin mode](#TODO) is set, the cursor position is set relative
to the top-left corner of the scroll region. `y = 1` corresponds to
the [top margin](#TODO) and `x = 1` corresponds to the [left margin](#TODO).
The maximum value for `y` is the [bottom margin](#TODO) and the maximum
value for `x` is the [right margin](#TODO).
When origin mode is set, it is impossible set a cursor position using
this sequence outside the boundaries of the scroll region.
## Validation
### CUP V-1: Normal Usage
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[2;3H"
printf "A"
```
```
|__________|
|__Ac______|
```
### CUP V-2: Off the Screen
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[500;500H"
printf "A"
```
```
|__________|
|__________|
|_________Ac
```
### CUP V-3: Relative to Origin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[2;3r" # scroll region top/bottom
printf "\033[?6h" # origin mode
printf "\033[1;1H" # move to top-left
printf "X"
```
```
|__________|
|X_________|
```
### CUP V-4: Relative to Origin with Left/Right Margins
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[2;3r" # scroll region top/bottom
printf "\033[?6h" # origin mode
printf "\033[1;1H" # move to top-left
printf "X"
```
```
|__________|
|__X_______|
```
### CUP V-5: Limits with Scroll Region and Origin Mode
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[2;3r" # scroll region top/bottom
printf "\033[?6h" # origin mode
printf "\033[500;500H" # move to top-left
printf "X"
```
```
|__________|
|__________|
|____X_____|
```
### CUP V-6: Pending Wrap is Unset
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[1;1H"
printf "X"
```
```
|Xc_______X|
```

View File

@ -0,0 +1,14 @@
import VTSequence from "@/components/VTSequence";
# Horizontal Position Absolute (HPA)
<VTSequence sequence={["CSI", "Px", "`"]} />
This sequence performs [cursor position (CUP)](/vt/cup) with `x` set
to the parameterized value and `y` set to the current cursor position.
There is no additional or different behavior for using `HPA`.
Because this invokes `CUP`, the cursor row (`x`) can change if it is
outside the bounds of the `CUP` operation. For example, if
[origin mode](#TODO) is set and the current cursor position is outside
of the scroll region, the row will be adjusted.

View File

@ -0,0 +1,17 @@
import VTSequence from "@/components/VTSequence";
# Horizontal Position Relative (HPR)
<VTSequence sequence={["CSI", "Px", "a"]} />
This sequence performs [cursor position (CUP)](/vt/cup) with `x` set
to the current cursor column plus `x` and `y` set to the current cursor row.
There is no additional or different behavior for using `HPR`.
The parameter `x` must be an integer greater than or equal to 1. If `x` is less than
or equal to 0, adjust `x` to be 1. If `x` is omitted, `x` defaults to 1.
Because this invokes `CUP`, the cursor row (`y`) can change if it is
outside the bounds of the `CUP` operation. For example, if
[origin mode](#TODO) is set and the current cursor position is outside
of the scroll region, the row will be adjusted.

View File

@ -0,0 +1,14 @@
import VTSequence from "@/components/VTSequence";
# Vertical Position Absolute (VPA)
<VTSequence sequence={["CSI", "Py", "d"]} />
This sequence performs [cursor position (CUP)](/vt/cup) with `y` set
to the parameterized value and `x` set to the current cursor position.
There is no additional or different behavior for using `VPA`.
Because this invokes `CUP`, the cursor column (`y`) can change if it is
outside the bounds of the `CUP` operation. For example, if
[origin mode](#TODO) is set and the current cursor position is outside
of the scroll region, the column will be adjusted.

View File

@ -0,0 +1,17 @@
import VTSequence from "@/components/VTSequence";
# Vertical Position Relative (VPR)
<VTSequence sequence={["CSI", "Py", "e"]} />
This sequence performs [cursor position (CUP)](/vt/cup) with `y` set
to the current cursor row plus `y` and `x` set to the current cursor column.
There is no additional or different behavior for using `VPR`.
The parameter `y` must be an integer greater than or equal to 1. If `y` is less than
or equal to 0, adjust `y` to be 1. If `y` is omitted, `y` defaults to 1.
Because this invokes `CUP`, the cursor column (`x`) can change if it is
outside the bounds of the `CUP` operation. For example, if
[origin mode](#TODO) is set and the current cursor position is outside
of the scroll region, the column will be adjusted.

View File

@ -29,7 +29,7 @@ export default function VTSequence({
}
function VTElem({ elem }: { elem: string }) {
const param = elem === "Pn";
const param = elem.length > 1 && elem[0] === "P";
elem = param ? elem[1] : elem;
const specialChar = special[elem] ?? elem.charCodeAt(0);
const hex = specialChar.toString(16).padStart(2, "0").toUpperCase();