From eab390344acb84aa5479c304f169b1d6bdeb1f07 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 14:48:06 -0700 Subject: [PATCH] terminal: ED xterm audit Fix multi-cell handling Test all scenarios --- src/terminal/Terminal.zig | 619 ++++++++++++++++++++++++++++--------- website/app/vt/ed/page.mdx | 139 +++++++++ 2 files changed, 620 insertions(+), 138 deletions(-) create mode 100644 website/app/vt/ed/page.mdx diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 5bc769f1d..941b93dec 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -595,6 +595,17 @@ pub fn invokeCharset( } } +/// Print UTF-8 encoded string to the terminal. This string must be +/// a single line, newlines and carriage returns and other control +/// characters are not processed. +/// +/// This is not public because it is only used for tests rigt now. +fn printString(self: *Terminal, str: []const u8) !void { + const view = try std.unicode.Utf8View.init(str); + var it = view.iterator(); + while (it.nextCodepoint()) |cp| try self.print(cp); +} + pub fn print(self: *Terminal, c: u21) !void { const tracy = trace(@src()); defer tracy.end(); @@ -999,7 +1010,7 @@ pub fn eraseDisplay( self: *Terminal, alloc: Allocator, mode: csi.EraseDisplay, - protected: bool, + protected_req: bool, ) void { const tracy = trace(@src()); defer tracy.end(); @@ -1010,6 +1021,11 @@ pub fn eraseDisplay( .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; + switch (mode) { .complete => { var it = self.screen.rowIterator(.active); @@ -1040,16 +1056,10 @@ pub fn eraseDisplay( .below => { // All lines to the right (including the cursor) { + self.eraseLine(.right, protected_req); const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); row.setWrapped(false); row.setDirty(true); - for (self.screen.cursor.x..self.cols) |x| { - if (row.header().flags.grapheme) row.clearGraphemes(x); - const cell = row.getCellPtr(x); - if (protected and cell.attrs.protected) continue; - cell.* = pen; - cell.char = 0; - } } // All lines below @@ -1072,18 +1082,12 @@ pub fn eraseDisplay( .above => { // Erase to the left (including the cursor) - var x: usize = 0; - while (x <= self.screen.cursor.x) : (x += 1) { - const cell = self.screen.getCellPtr(.active, self.screen.cursor.y, x); - if (protected and cell.attrs.protected) continue; - cell.* = pen; - cell.char = 0; - } + self.eraseLine(.left, protected_req); // All lines above var y: usize = 0; while (y < self.screen.cursor.y) : (y += 1) { - x = 0; + var x: usize = 0; while (x < self.cols) : (x += 1) { const cell = self.screen.getCellPtr(.active, y, x); if (protected and cell.attrs.protected) continue; @@ -1103,126 +1107,6 @@ pub fn eraseDisplay( } } -test "Terminal: eraseDisplay above" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen = Screen.Cell{ - .char = 'a', - .bg = pink, - .fg = pink, - .attrs = .{ .bold = true, .has_bg = true }, - }; - const cell_ptr = t.screen.getCellPtr(.active, 0, 0); - cell_ptr.* = t.screen.cursor.pen; - // verify the cell was set - var cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // move the cursor below it - t.screen.cursor.y = 40; - t.screen.cursor.x = 40; - // erase above the cursor - t.eraseDisplay(testing.allocator, .above, false); - // check it was erased - cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); - - // Check that our pen hasn't changed - try testing.expect(t.screen.cursor.pen.attrs.bold); - - // check that another cell got the correct bg - cell = t.screen.getCell(.active, 0, 1); - try testing.expect(cell.bg.eql(pink)); -} - -test "Terminal: eraseDisplay below" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen = Screen.Cell{ - .char = 'a', - .bg = pink, - .fg = pink, - .attrs = .{ .bold = true, .has_bg = true }, - }; - const cell_ptr = t.screen.getCellPtr(.active, 60, 60); - cell_ptr.* = t.screen.cursor.pen; - // verify the cell was set - var cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // erase below the cursor - t.eraseDisplay(testing.allocator, .below, false); - // check it was erased - cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); - - // check that another cell got the correct bg - cell = t.screen.getCell(.active, 0, 1); - try testing.expect(cell.bg.eql(pink)); -} - -test "Terminal: eraseDisplay complete" { - var t = try init(testing.allocator, 80, 80); - defer t.deinit(testing.allocator); - - const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen = Screen.Cell{ - .char = 'a', - .bg = pink, - .fg = pink, - .attrs = .{ .bold = true, .has_bg = true }, - }; - var cell_ptr = t.screen.getCellPtr(.active, 60, 60); - cell_ptr.* = t.screen.cursor.pen; - cell_ptr = t.screen.getCellPtr(.active, 0, 0); - cell_ptr.* = t.screen.cursor.pen; - // verify the cell was set - var cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // verify the cell was set - cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); - try testing.expect(cell.char == 'a'); - try testing.expect(cell.attrs.bold); - // position our cursor between the cells - t.screen.cursor.y = 30; - // erase everything - t.eraseDisplay(testing.allocator, .complete, false); - // check they were erased - cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); - cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); - try testing.expect(cell.char == 0); - try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); -} - /// Erase the line. pub fn eraseLine( self: *Terminal, @@ -3983,8 +3867,6 @@ test "Terminal: eraseLine right protected requested" { } } -// ------------------- SPLIT - test "Terminal: eraseLine simple erase left" { const alloc = testing.allocator; var t = try init(alloc, 5, 5); @@ -4233,6 +4115,467 @@ test "Terminal: eraseLine complete protected requested" { } } +test "Terminal: eraseDisplay simple erase below" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .below, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nD", str); + } +} + +test "Terminal: eraseDisplay erase below preserves SGR bg" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + + const pen: Screen.Cell = .{ + .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, + .attrs = .{ .has_bg = true }, + }; + + t.screen.cursor.pen = pen; + t.eraseDisplay(alloc, .below, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nD", str); + for (1..5) |x| { + const cell = t.screen.getCell(.active, 1, x); + try testing.expectEqual(pen, cell); + } + } +} + +test "Terminal: eraseDisplay below split multi-cell" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("AB橋C"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DE橋F"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GH橋I"); + t.setCursorPos(2, 4); + t.eraseDisplay(alloc, .below, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("AB橋C\nDE", str); + } +} + +test "Terminal: eraseDisplay below 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .below, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nDEF\nGHI", str); + } +} + +test "Terminal: eraseDisplay below 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setProtectedMode(.dec); + t.setProtectedMode(.off); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .below, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nD", str); + } +} + +test "Terminal: eraseDisplay below 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .below, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nD", str); + } +} + +test "Terminal: eraseDisplay simple erase above" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .above, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n F\nGHI", str); + } +} + +test "Terminal: eraseDisplay below protected attributes respected with force" { + 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .below, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nDEF\nGHI", str); + } +} + +test "Terminal: eraseDisplay erase above preserves SGR bg" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + + const pen: Screen.Cell = .{ + .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, + .attrs = .{ .has_bg = true }, + }; + + t.screen.cursor.pen = pen; + t.eraseDisplay(alloc, .above, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n F\nGHI", str); + for (0..2) |x| { + const cell = t.screen.getCell(.active, 1, x); + try testing.expectEqual(pen, cell); + } + } +} + +test "Terminal: eraseDisplay above split multi-cell" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("AB橋C"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DE橋F"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GH橋I"); + t.setCursorPos(2, 3); + t.eraseDisplay(alloc, .above, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n F\nGH橋I", str); + } +} + +test "Terminal: eraseDisplay above 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .above, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nDEF\nGHI", str); + } +} + +test "Terminal: eraseDisplay above 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setProtectedMode(.dec); + t.setProtectedMode(.off); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .above, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n F\nGHI", str); + } +} + +test "Terminal: eraseDisplay above 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .above, false); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n F\nGHI", str); + } +} + +test "Terminal: eraseDisplay above protected attributes respected with force" { + 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.carriageReturn(); + try t.linefeed(); + for ("DEF") |c| try t.print(c); + t.carriageReturn(); + try t.linefeed(); + for ("GHI") |c| try t.print(c); + t.setCursorPos(2, 2); + t.eraseDisplay(alloc, .above, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nDEF\nGHI", str); + } +} +test "Terminal: eraseDisplay above" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; + t.screen.cursor.pen = Screen.Cell{ + .char = 'a', + .bg = pink, + .fg = pink, + .attrs = .{ .bold = true, .has_bg = true }, + }; + const cell_ptr = t.screen.getCellPtr(.active, 0, 0); + cell_ptr.* = t.screen.cursor.pen; + // verify the cell was set + var cell = t.screen.getCell(.active, 0, 0); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.char == 'a'); + try testing.expect(cell.attrs.bold); + // move the cursor below it + t.screen.cursor.y = 40; + t.screen.cursor.x = 40; + // erase above the cursor + t.eraseDisplay(testing.allocator, .above, false); + // check it was erased + cell = t.screen.getCell(.active, 0, 0); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.char == 0); + try testing.expect(!cell.attrs.bold); + try testing.expect(cell.attrs.has_bg); + + // Check that our pen hasn't changed + try testing.expect(t.screen.cursor.pen.attrs.bold); + + // check that another cell got the correct bg + cell = t.screen.getCell(.active, 0, 1); + try testing.expect(cell.bg.eql(pink)); +} + +test "Terminal: eraseDisplay below" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; + t.screen.cursor.pen = Screen.Cell{ + .char = 'a', + .bg = pink, + .fg = pink, + .attrs = .{ .bold = true, .has_bg = true }, + }; + const cell_ptr = t.screen.getCellPtr(.active, 60, 60); + cell_ptr.* = t.screen.cursor.pen; + // verify the cell was set + var cell = t.screen.getCell(.active, 60, 60); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.char == 'a'); + try testing.expect(cell.attrs.bold); + // erase below the cursor + t.eraseDisplay(testing.allocator, .below, false); + // check it was erased + cell = t.screen.getCell(.active, 60, 60); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.char == 0); + try testing.expect(!cell.attrs.bold); + try testing.expect(cell.attrs.has_bg); + + // check that another cell got the correct bg + cell = t.screen.getCell(.active, 0, 1); + try testing.expect(cell.bg.eql(pink)); +} + +test "Terminal: eraseDisplay complete" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; + t.screen.cursor.pen = Screen.Cell{ + .char = 'a', + .bg = pink, + .fg = pink, + .attrs = .{ .bold = true, .has_bg = true }, + }; + var cell_ptr = t.screen.getCellPtr(.active, 60, 60); + cell_ptr.* = t.screen.cursor.pen; + cell_ptr = t.screen.getCellPtr(.active, 0, 0); + cell_ptr.* = t.screen.cursor.pen; + // verify the cell was set + var cell = t.screen.getCell(.active, 60, 60); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.char == 'a'); + try testing.expect(cell.attrs.bold); + // verify the cell was set + cell = t.screen.getCell(.active, 0, 0); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.char == 'a'); + try testing.expect(cell.attrs.bold); + // position our cursor between the cells + t.screen.cursor.y = 30; + // erase everything + t.eraseDisplay(testing.allocator, .complete, false); + // check they were erased + cell = t.screen.getCell(.active, 60, 60); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.char == 0); + try testing.expect(!cell.attrs.bold); + try testing.expect(cell.attrs.has_bg); + cell = t.screen.getCell(.active, 0, 0); + try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.char == 0); + try testing.expect(!cell.attrs.bold); + try testing.expect(cell.attrs.has_bg); +} + test "Terminal: eraseDisplay protected complete" { const alloc = testing.allocator; var t = try init(alloc, 10, 5); diff --git a/website/app/vt/ed/page.mdx b/website/app/vt/ed/page.mdx new file mode 100644 index 000000000..a9323435b --- /dev/null +++ b/website/app/vt/ed/page.mdx @@ -0,0 +1,139 @@ +import VTSequence from "@/components/VTSequence"; + +# Erase Display (ED) + + + +Erase display 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, 2, or 3. 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` except 3, 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. + +This sequence does not respect any scroll regions (top, bottom, left, or +right). The boundaries of the operation are the full visible screen. + +If `n` is `0`, perform an **erase display below** operation. Erase all +cells to the right and below the cursor. The background color of erased cells +is colored according to the current SGR state. + +If `n` is `1`, perform an **erase display above** operation. Erase all +cells to the left and above the cursor. The background color of erased cells +is colored according to the current SGR state. + +If `n` is `2`, **erase the entire display**. This is the equivalent of +erase above (`n = 1`) and erase below (`n = 0`) both being executed. + +If `n` is `3`, **erase only the scrollback region**. This does not affect +the visible display of the screen and does not move the cursor. The scrollback +region is the region of the terminal that is currently above the visible +area of the screen when the screen is scrolled completely to the bottom. + +## Validation + +### ED V-1: Simple Erase Below + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[2;2H" +printf "\033[0J" +``` + +``` +|ABC_____| +|Dc______| +|________| +``` + +### ED V-2: Erase Below SGR State + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[2;2H" +printf "\033[41m" +printf "\033[0J" +``` + +``` +|ABC_____| +|Dc______| +|________| +``` + +All the cells right and below of the cursor should be colored red. + +### ED V-3: Erase Below Multi-Cell Character + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "AB橋C\n" +printf "DE橋F\n" +printf "GH橋I\n" +printf "\033[2;4H" +printf "\033[0J" +``` + +``` +|AB橋C___| +|DE_c____| +|________| +``` + +### ED V-4: Simple Erase Above + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[2;2H" +printf "\033[1J" +``` + +``` +|________| +|_cF_____| +|GHI_____| +``` + +### ED V-5: Simple Erase Complete + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "\033[2;2H" +printf "\033[2J" +``` + +``` +|________| +|_c______| +|________| +```