terminal: EL (erase line) xterm audit

Fix multi-cell char handling
Fix bg SGR respecting in non-protected cases
Fix protected attribute logic
This commit is contained in:
Mitchell Hashimoto
2023-10-08 09:03:04 -07:00
parent 4301760384
commit 02b134f97e
2 changed files with 601 additions and 40 deletions

View File

@ -1227,58 +1227,70 @@ test "Terminal: eraseDisplay complete" {
pub fn eraseLine(
self: *Terminal,
mode: csi.EraseLine,
protected: bool,
protected_req: bool,
) void {
const tracy = trace(@src());
defer tracy.end();
// We always need a row no matter what
// We always fill with the background
const pen: Screen.Cell = if (!self.screen.cursor.pen.attrs.has_bg) .{} else .{
.bg = self.screen.cursor.pen.bg,
.attrs = .{ .has_bg = true },
};
// Get our start/end positions depending on mode.
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
// Non-protected erase is much faster because we can just memset
// a contiguous block of memory.
if (!protected) {
switch (mode) {
.right => {
row.fillSlice(self.screen.cursor.pen, self.screen.cursor.x, self.cols);
self.screen.cursor.pending_wrap = false;
},
.left => {
row.fillSlice(self.screen.cursor.pen, 0, self.screen.cursor.x + 1);
self.screen.cursor.pending_wrap = false;
},
.complete => {
row.fill(self.screen.cursor.pen);
self.screen.cursor.pending_wrap = false;
},
else => log.err("unimplemented erase line mode: {}", .{mode}),
}
return;
}
// Protected mode we have to iterate over the cells to check their
// protection status and erase them individually.
const start, const end = switch (mode) {
.right => .{ self.screen.cursor.x, row.lenCells() },
.left => .{ 0, self.screen.cursor.x + 1 },
.right => right: {
var x = self.screen.cursor.x;
// If our X is a wide spacer tail then we need to erase the
// previous cell too so we don't split a multi-cell character.
if (x > 0) {
const cell = row.getCellPtr(x);
if (cell.attrs.wide_spacer_tail) x -= 1;
}
break :right .{ x, row.lenCells() };
},
.left => left: {
var x = self.screen.cursor.x;
// If our x is a wide char we need to delete the tail too.
const cell = row.getCellPtr(x);
if (cell.attrs.wide) {
if (row.getCellPtr(x + 1).attrs.wide_spacer_tail) {
x += 1;
}
}
break :left .{ 0, x + 1 };
},
.complete => .{ 0, row.lenCells() },
else => {
log.err("unimplemented erase line mode: {}", .{mode});
return;
},
};
// All modes will clear the pending wrap state
// All modes will clear the pending wrap state and we know we have
// a valid mode at this point.
self.screen.cursor.pending_wrap = false;
const pen: Screen.Cell = if (!self.screen.cursor.pen.attrs.has_bg) .{} else .{
.bg = self.screen.cursor.pen.bg,
.attrs = .{ .has_bg = true },
};
// We respect protected attributes if explicitly requested (probably
// a DECSEL sequence) or if our last protected mode was ISO even if its
// not currently set.
const protected = self.screen.protected_mode == .iso or protected_req;
// If we're not respecting protected attributes, we can use a fast-path
// to fill the entire line.
if (!protected) {
row.fillSlice(self.screen.cursor.pen, start, end);
return;
}
for (start..end) |x| {
const cell = row.getCellPtr(x);
@ -3724,6 +3736,7 @@ test "Terminal: eraseChars protected attributes ignored with dec most recent" {
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);
@ -3820,6 +3833,22 @@ test "Terminal: setProtectedMode" {
try testing.expect(!t.screen.cursor.pen.attrs.protected);
}
test "Terminal: eraseLine simple erase right" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 3);
t.eraseLine(.right, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB", str);
}
}
test "Terminal: eraseLine resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
@ -3838,7 +3867,104 @@ test "Terminal: eraseLine resets wrap" {
}
}
test "Terminal: eraseLine protected right" {
test "Terminal: eraseLine right preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
const pen: Screen.Cell = .{
.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 },
.attrs = .{ .has_bg = true },
};
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.screen.cursor.pen = pen;
t.eraseLine(.right, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
for (1..5) |x| {
const cell = t.screen.getCell(.active, 0, x);
try testing.expectEqual(pen, cell);
}
}
}
test "Terminal: eraseLine right wide character" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 5);
defer t.deinit(alloc);
for ("AB") |c| try t.print(c);
try t.print('橋');
for ("DE") |c| try t.print(c);
t.setCursorPos(1, 4);
t.eraseLine(.right, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB", str);
}
}
test "Terminal: eraseLine right 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.eraseLine(.right, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseLine right 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, 2);
t.eraseLine(.right, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
test "Terminal: eraseLine right 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, 2);
t.eraseLine(.right, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
test "Terminal: eraseLine right protected requested" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 5);
defer t.deinit(alloc);
@ -3857,7 +3983,140 @@ test "Terminal: eraseLine protected right" {
}
}
test "Terminal: eraseLine protected left" {
// ------------------- SPLIT
test "Terminal: eraseLine simple erase left" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 3);
t.eraseLine(.left, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" DE", str);
}
}
test "Terminal: eraseLine left 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.eraseLine(.left, false);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('B');
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" B", str);
}
}
test "Terminal: eraseLine left preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
const pen: Screen.Cell = .{
.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 },
.attrs = .{ .has_bg = true },
};
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.screen.cursor.pen = pen;
t.eraseLine(.left, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" CDE", str);
for (0..2) |x| {
const cell = t.screen.getCell(.active, 0, x);
try testing.expectEqual(pen, cell);
}
}
}
test "Terminal: eraseLine left wide character" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 5);
defer t.deinit(alloc);
for ("AB") |c| try t.print(c);
try t.print('橋');
for ("DE") |c| try t.print(c);
t.setCursorPos(1, 3);
t.eraseLine(.left, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" DE", str);
}
}
test "Terminal: eraseLine left 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.eraseLine(.left, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseLine left 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, 2);
t.eraseLine(.left, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: eraseLine left 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, 2);
t.eraseLine(.left, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: eraseLine left protected requested" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 5);
defer t.deinit(alloc);
@ -3876,7 +4135,86 @@ test "Terminal: eraseLine protected left" {
}
}
test "Terminal: eraseLine protected complete" {
test "Terminal: eraseLine complete preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
const pen: Screen.Cell = .{
.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 },
.attrs = .{ .has_bg = true },
};
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.screen.cursor.pen = pen;
t.eraseLine(.complete, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
for (0..5) |x| {
const cell = t.screen.getCell(.active, 0, x);
try testing.expectEqual(pen, cell);
}
}
}
test "Terminal: eraseLine complete 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.eraseLine(.complete, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseLine complete 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, 2);
t.eraseLine(.complete, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: eraseLine complete 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, 2);
t.eraseLine(.complete, false);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: eraseLine complete protected requested" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 5);
defer t.deinit(alloc);

223
website/app/vt/el/page.mdx Normal file
View File

@ -0,0 +1,223 @@
import VTSequence from "@/components/VTSequence";
# Erase Line (EL)
<VTSequence sequence={["CSI", "Pn", "K"]} />
Erase line contents with behavior depending on the command `n`.
If `n` is unset, the value of `n` is 0. The only valid values for `n` are
0, 1, or 2. If any other value of `n` is given, do not execute this sequence.
The remainder of the sequence documentation assumes a valid value of `n`.
For all valid values of `n`, this sequence unsets the pending wrap state.
The cursor position will remain unchanged under all circumstances throughout
this sequence.
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. Otherwise, protected attributes will be
respected. For more details on this specific logic for protected attribute
handling, see [Erase Character (ECH)](/vt/ech).
For all operations, if a multi-cell character would be split, erase the full multi-cell
character. For example, if "橋" is printed and the erase would only erase the
first or second cell of the two-cell character, both cells should be erased.
If `n` is `0`, perform an **erase line right** operation. Erase line right
is equivalent to [Erase Character (ECH)](/vt/ech) with `n` set to the total
remaining columns from the cursor to the end of the line (and including
the cursor).
If `n` is `1`, perform an **erase line left** operation. This replaces
the `n` cells left of and including the cursor with a blank character and
colors the background according to the current SGR state. The leftmost
column that can be blanked is the first column of the screen. The
[left margin](#TODO) has no effect on this operation.
If `n` is `2`, **erase the entire line**. This is the equivalent of
erase left (`n = 1`) and erase right (`n = 0`) both being executed.
## Validation
### EL V-1: Simple Erase Right
```bash
printf "ABCDE"
printf "\033[3G"
printf "\033[0K"
```
```
|ABc_____|
```
### EL V-2: Erase Right Resets Pending Wrap
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[0K"
printf "X"
```
```
|_______Xc
```
### EL V-3: Erase Right SGR State
```bash
printf "ABC"
printf "\033[2G"
printf "\033[41m"
printf "\033[0K"
```
```
|Ac______|
```
The cells from `c` onwards should have a red background all the way to
the right edge of the screen.
### EL V-4: Erase Right Multi-cell Character
```bash
printf "AB橋DE"
printf "\033[4G"
printf "\033[0K"
```
```
|AB_c____|
```
### EL V-5: Erase Right Left/Right Scroll Region Ignored
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABCDE"
printf "\033[?69h" # enable left/right margins
printf "\033[1;3s" # scroll region left/right
printf "\033[2G"
printf "\033[0K"
```
```
|Ac________|
```
### EL V-6: Erase Right Protected Attributes Ignored with DECSCA
```bash
printf "\033V"
printf "ABCDE"
printf "\033[1\"q"
printf "\033[0\"q"
printf "\033[2G"
printf "\033[0K"
```
```
|Ac________|
```
### EL V-7: Protected Attributes Respected without DECSCA
```bash
printf "\033[1\"q"
printf "ABCDE"
printf "\033V"
printf "\033[2G"
printf "\033[0K"
printf "\033[1K"
printf "\033[2K"
```
```
|ABCDE_____|
```
### EL V-8: Simple Erase Left
```bash
printf "ABCDE"
printf "\033[3G"
printf "\033[1K"
```
```
|__cDE___|
```
### EL V-9: Erase Left SGR State
```bash
printf "ABC"
printf "\033[2G"
printf "\033[41m"
printf "\033[1K"
```
```
|_cC_____|
```
The cells from `c` to the left should have a red background.
### EL V-10: Erase Left Multi-cell Character
```bash
printf "AB橋DE"
printf "\033[3G"
printf "\033[1K"
```
```
|__c_DE__|
```
### EL V-11: Erase Left Protected Attributes Ignored with DECSCA
```bash
printf "\033V"
printf "ABCDE"
printf "\033[1\"q"
printf "\033[0\"q"
printf "\033[2G"
printf "\033[1K"
```
```
|_cCDE_____|
```
### EL V-12: Simple Erase Complete
```bash
printf "ABCDE"
printf "\033[3G"
printf "\033[2K"
```
```
|__c_______|
```
### EL V-13: Erase Complete SGR State
```bash
printf "ABC"
printf "\033[2G"
printf "\033[41m"
printf "\033[2K"
```
```
|_c______|
```
The entire line should have a red background.