From 02b134f97e4059ff2a27079fbcfc266f6b0f6a27 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 09:03:04 -0700 Subject: [PATCH] terminal: EL (erase line) xterm audit Fix multi-cell char handling Fix bg SGR respecting in non-protected cases Fix protected attribute logic --- src/terminal/Terminal.zig | 418 +++++++++++++++++++++++++++++++++---- website/app/vt/el/page.mdx | 223 ++++++++++++++++++++ 2 files changed, 601 insertions(+), 40 deletions(-) create mode 100644 website/app/vt/el/page.mdx diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c8e33c057..5bc769f1d 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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); diff --git a/website/app/vt/el/page.mdx b/website/app/vt/el/page.mdx new file mode 100644 index 000000000..d34297391 --- /dev/null +++ b/website/app/vt/el/page.mdx @@ -0,0 +1,223 @@ +import VTSequence from "@/components/VTSequence"; + +# Erase Line (EL) + + + +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.