Merge pull request #636 from mitchellh/ech

xterm audit: ECH
This commit is contained in:
Mitchell Hashimoto
2023-10-07 22:44:42 -07:00
committed by GitHub
3 changed files with 337 additions and 8 deletions

View File

@ -56,6 +56,7 @@ const Allocator = std.mem.Allocator;
const utf8proc = @import("utf8proc");
const trace = @import("tracy").trace;
const ansi = @import("ansi.zig");
const sgr = @import("sgr.zig");
const color = @import("color.zig");
const kitty = @import("kitty.zig");
@ -932,6 +933,13 @@ saved_charset: CharsetState = .{},
/// independent to each screen (primary and alternate)
saved_origin_mode: bool = false,
/// The current or most recent protected mode. Once a protection mode is
/// set, this will never become "off" again until the screen is reset.
/// The current state of whether protection attributes should be set is
/// set on the Cell pen; this is only used to determine the most recent
/// protection mode since some sequences such as ECH depend on this.
protected_mode: ansi.ProtectedMode = .off,
/// Initialize a new screen.
pub fn init(
alloc: Allocator,

View File

@ -1320,7 +1320,6 @@ pub fn deleteChars(self: *Terminal, count: usize) !void {
}
}
// TODO: test, docs
pub fn eraseChars(self: *Terminal, count: usize) void {
const tracy = trace(@src());
defer tracy.end();
@ -1330,13 +1329,39 @@ pub fn eraseChars(self: *Terminal, count: usize) void {
// Our last index is at most the end of the number of chars we have
// in the current line.
const end = @min(self.cols, self.screen.cursor.x + count);
// Shift
var pen = self.screen.cursor.pen;
pen.char = 0;
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
const end = end: {
var end = @min(self.cols, self.screen.cursor.x + count);
// If our last cell is a wide char then we need to also clear the
// cell beyond it since we can't just split a wide char.
if (end != self.cols) {
const last = row.getCellPtr(end - 1);
if (last.attrs.wide) end += 1;
}
break :end end;
};
const pen: Screen.Cell = .{
.bg = self.screen.cursor.pen.bg,
.attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg },
};
// If we never had a protection mode, then we can assume no cells
// are protected and go with the fast path. If the last protection
// mode was not ISO we also always ignore protection attributes.
if (self.screen.protected_mode != .iso) {
row.fillSlice(pen, self.screen.cursor.x, end);
}
// We had a protection mode at some point. We must go through each
// cell and check its protection attribute.
for (self.screen.cursor.x..end) |x| {
const cell = row.getCellPtr(x);
if (cell.attrs.protected) continue;
cell.* = pen;
}
}
/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1.
@ -1874,15 +1899,20 @@ pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void {
switch (mode) {
.off => {
self.screen.cursor.pen.attrs.protected = false;
// screen.protected_mode is NEVER reset to ".off" because
// logic such as eraseChars depends on knowing what the
// _most recent_ mode was.
},
// TODO: ISO/DEC have very subtle differences, so we should track that.
.iso => {
self.screen.cursor.pen.attrs.protected = true;
self.screen.protected_mode = .iso;
},
.dec => {
self.screen.cursor.pen.attrs.protected = true;
self.screen.protected_mode = .dec;
},
}
}
@ -1898,6 +1928,7 @@ pub fn fullReset(self: *Terminal, alloc: Allocator) void {
self.screen.saved_cursor = .{};
self.screen.selection = null;
self.screen.kitty_keyboard = .{};
self.screen.protected_mode = .off;
self.scrolling_region = .{
.top = 0,
.bottom = self.rows - 1,
@ -3577,6 +3608,139 @@ test "Terminal: eraseChars resets wrap" {
}
}
test "Terminal: eraseChars simple operation" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(2);
try t.print('X');
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X C", str);
}
}
test "Terminal: eraseChars beyond screen edge" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
for (" ABC") |c| try t.print(c);
t.setCursorPos(1, 4);
t.eraseChars(10);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" A", str);
}
}
test "Terminal: eraseChars preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);
const pen: Screen.Cell = .{
.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 },
.attrs = .{ .has_bg = true },
};
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.screen.cursor.pen = pen;
t.eraseChars(2);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
{
const cell = t.screen.getCell(.active, 0, 0);
try testing.expectEqual(pen, cell);
}
{
const cell = t.screen.getCell(.active, 0, 1);
try testing.expectEqual(pen, cell);
}
}
}
test "Terminal: eraseChars wide character" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
try t.print('橋');
for ("BC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(1);
try t.print('X');
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X BC", str);
}
}
test "Terminal: eraseChars protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(2);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseChars protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(1, 1);
t.eraseChars(2);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: eraseChars protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(2);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
// 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.

157
website/app/vt/ech/page.mdx Normal file
View File

@ -0,0 +1,157 @@
import VTSequence from "@/components/VTSequence";
# Erase Character (ECH)
<VTSequence sequence={["CSI", "Pn", "X"]} />
Blank `n` cells beginning with (including) and to the right of the cursor.
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.
The rightmost column that can be erased is the rightmost column of the screen.
The [right margin](#) has no effect on this sequence.
This sequence always unsets the pending wrap state.
For `n` cells up to the rightmost column, blank the cell by replacing it
with an empty character with the background color colored according to the
current SGR state. No other SGR attributes are preserved.
If a multi-cell character would be split, erase the full multi-cell
character. For example, if "橋" is printed and ECH `n = 1` is issued,
the full character should be erased even though it takes up two cells.
Both erased cells are colored with the current background color according
to the current SGR state.
If [Select Character Selection Attribute (DECSCA)](#TODO) is enabled
or was the most recently enabled protection mode on the currently active screen,
protected attributes are ignored as if they were never set and the cells
with them are erased. It does not matter if DECSCA is currently disabled,
protected attributes are still ignored so long as DECSCA was the
_most recently enabled_ protection mode.
If DECSCA is not currently enabled and was not the most recently enabled protection
mode on the currently active screen, cells with the protected attribute set are
respected and not erased but still count towards `n`. It does not matter if the
protection attribute for a cell was originally set from DECSCA.
## Validation
### ECH V-1: Simple Operation
```bash
printf "ABC"
printf "\033[1G"
printf "\033[2X"
```
```
|c_C_____|
```
### ECH V-2: Erasing Beyond Edge of Screen
```bash
cols=$(tput cols)
printf "\033[${cols}G"
printf "\033[2D"
printf "ABC"
printf "\033[D"
printf "\033[10X"
```
```
|_____Ac_|
```
### ECH V-3: Reset Pending Wrap State
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[X"
printf "X"
```
```
|_______Xc
```
### ECH V-4: SGR State
```bash
printf "ABC"
printf "\033[1G"
printf "\033[41m"
printf "\033[2X"
```
```
|c_C_____|
```
The `c_` cells should both have a red background. All other cells
remain unchanged in style.
### ECH V-5: Multi-cell Character
```bash
printf "橋BC"
printf "\033[1G"
printf "\033[X"
printf "X"
```
```
|XcBC____|
```
### ECH V-6: Left/Right Scroll Region Ignored
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[1;3s" # scroll region left/right
printf "\033[4G"
printf "ABC"
printf "\033[1G"
printf "\033[4X"
```
```
|c___BC____|
```
### ECH V-7: Protected Attributes Ignored with DECSCA
```bash
printf "\033V"
printf "ABC"
printf "\033[1\"q"
printf "\033[0\"q"
printf "\033[1G"
printf "\033[2X"
```
```
|c_C_______|
```
### ECH V-8: Protected Attributes Respected without DECSCA
```bash
printf "\033[1\"q"
printf "ABC"
printf "\033V"
printf "\033[1G"
printf "\033[2X"
```
```
|ABC_______|
```
The cursor remains at `A`.