From dace9b5da2996f608b81585ae44fe8c51a544211 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 15:25:28 -0700 Subject: [PATCH 1/5] website: IL --- website/app/vt/il/page.mdx | 119 +++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 website/app/vt/il/page.mdx diff --git a/website/app/vt/il/page.mdx b/website/app/vt/il/page.mdx new file mode 100644 index 000000000..dc19e3297 --- /dev/null +++ b/website/app/vt/il/page.mdx @@ -0,0 +1,119 @@ +import VTSequence from "@/components/VTSequence"; + +# Insert Line (IL) + + + +Inserts `n` lines at the current cursor position and shifts existing +lines down. + +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. + +If the current cursor position is outside of the current scroll region, +this sequence does nothing. The cursor is outside of the current scroll +region if it is above the [top margin](#TODO), below the [bottom margin](#TODO), +left of the [left margin](#TODO), or right of the [right margin](#TODO). + +This sequence unsets the pending wrap state. + +This sequence moves the cursor column to the left margin. + +From the current cursor row down `n` lines, insert blank lines colored +with a background color according to the current SGR state. When a line is +inserted, shift all existing content down one line. The bottommost row +is the bottom margin. If content is shifted beyond the bottom margin, +it is lost and the existing content beyond the bottom margin is preserved +and not shifted. + +If a [left margin](#TODO) or [right margin](#TODO) is set, only the cells +within and including the margins are blanked (when inserted) or shifted. +Other existing contents to the left of the left margin or right of the +right margin remains untouched. + +If a multi-cell character would be split, erase the full multi-cell +character. For example, if "橋" is printed to the left of the left margin +and shifting the line down as a result of IL would split the character, +the cell should be erased. + +## Validation + +### IL V-1: Simple Insert Line + +```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[L" +``` + +``` +|ABC_____| +|c_______| +|DEF_____| +|GHI_____| +``` + +### IL V-2: Cursor Outside of Scroll Region + +```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[3;4r" # scroll region top/bottom +printf "\033[2;2H" +printf "\033[L" +``` + +``` +|ABC_____| +|DEF_____| +|GHI_____| +``` + +### IL V-3: Top/Bottom Scroll Regions + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "123\n" +printf "\033[1;3r" # scroll region top/bottom +printf "\033[2;2H" +printf "\033[L" +``` + +``` +|ABC_____| +|_c______| +|DEF_____| +|123_____| +``` + +### IL V-4: Left/Right Scroll Regions + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC123\n" +printf "DEF456\n" +printf "GHI789\n" +printf "\033[?69h" # enable left/right margins +printf "\033[2;4s" # scroll region left/right +printf "\033[2;2H" +printf "\033[L" +``` + +``` +|ABC123__| +|Dc__56__| +|GEF489__| +|_HI7____| +``` From 38f968e368a50f5924be15945e1bc6eb902d693b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 15:27:09 -0700 Subject: [PATCH 2/5] terminal: IL preserves bg sgr, partial left/right margin support --- src/terminal/Terminal.zig | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 941b93dec..cdf044be4 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1564,8 +1564,14 @@ pub fn insertLines(self: *Terminal, count: usize) !void { // Rare, but happens if (count == 0) return; + // If the cursor is outside the scroll region we do nothing. + if (self.screen.cursor.y < self.scrolling_region.top or + self.screen.cursor.y > self.scrolling_region.bottom or + self.screen.cursor.x < self.scrolling_region.left or + self.screen.cursor.x > self.scrolling_region.right) return; + // Move the cursor to the left margin - self.screen.cursor.x = 0; + self.screen.cursor.x = self.scrolling_region.left; self.screen.cursor.pending_wrap = false; // Remaining rows from our cursor @@ -1582,14 +1588,19 @@ pub fn insertLines(self: *Terminal, count: usize) !void { // Ensure we have the lines populated to the end while (y > top) : (y -= 1) { - try self.screen.copyRow(.{ .active = y }, .{ .active = y - adjusted_count }); + const src = self.screen.getRow(.{ .active = y - adjusted_count }); + const dst = self.screen.getRow(.{ .active = y }); + try dst.copyRow(src); } // Insert count blank lines y = self.screen.cursor.y; while (y < self.screen.cursor.y + adjusted_count) : (y += 1) { const row = self.screen.getRow(.{ .active = y }); - row.clear(self.screen.cursor.pen); + row.fillSlice(.{ + .bg = self.screen.cursor.pen.bg, + .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, + }, self.scrolling_region.left, self.scrolling_region.right + 1); } } @@ -2648,6 +2659,101 @@ test "Terminal: deleteLines resets wrap" { } } +test "Terminal: insertLines simple" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.setCursorPos(2, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); + } +} + +test "Terminal: insertLines outside of scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.setScrollingRegion(3, 4); + t.setCursorPos(2, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nDEF\nGHI", str); + } +} + +test "Terminal: insertLines top/bottom scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("123"); + t.setScrollingRegion(1, 3); + t.setCursorPos(2, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\n\nDEF\n123", str); + } +} + +// test "Terminal: insertLines left/right scroll region" { +// const alloc = testing.allocator; +// var t = try init(alloc, 10, 10); +// defer t.deinit(alloc); +// +// try t.printString("ABC123"); +// t.carriageReturn(); +// try t.linefeed(); +// try t.printString("DEF456"); +// t.carriageReturn(); +// try t.linefeed(); +// try t.printString("GHI789"); +// t.scrolling_region.left = 1; +// t.scrolling_region.right = 3; +// t.setCursorPos(2, 2); +// try t.insertLines(1); +// +// { +// var str = try t.plainString(testing.allocator); +// defer testing.allocator.free(str); +// try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str); +// } +// } + test "Terminal: insertLines" { const alloc = testing.allocator; var t = try init(alloc, 2, 5); From 76bbb7c361166964de43422c749c67f56eb08e04 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 20:51:00 -0700 Subject: [PATCH 3/5] terminal: insert lines (IL) handles left/right scroll regions --- src/terminal/Screen.zig | 21 ++++++++++++++++ src/terminal/Terminal.zig | 50 ++++++++++++++++++++------------------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 6fd50b54e..93194393b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -498,6 +498,27 @@ pub const Row = struct { } } + /// Copy a single cell from column x in src to column x in this row. + pub fn copyCell(self: Row, src: Row, x: usize) !void { + const dst_cell = self.getCellPtr(x); + const src_cell = src.getCellPtr(x); + + // If our destination has graphemes, we have to clear them. + if (dst_cell.attrs.grapheme) self.clearGraphemes(x); + dst_cell.* = src_cell.*; + + // If the source doesn't have any graphemes, then we can just copy. + if (!src_cell.attrs.grapheme) return; + + // Source cell has graphemes. Copy them. + const src_key = src.getId() + x + 1; + const src_data = src.screen.graphemes.get(src_key) orelse return; + const dst_key = self.getId() + x + 1; + const dst_gop = try self.screen.graphemes.getOrPut(self.screen.alloc, dst_key); + dst_gop.value_ptr.* = try src_data.copy(self.screen.alloc); + self.storage[0].header.flags.grapheme = true; + } + /// Copy the row src into this row. The row can be from another screen. pub fn copyRow(self: Row, src: Row) !void { // If we have graphemes, clear first to unset them. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index cdf044be4..b09ca943e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1590,7 +1590,9 @@ pub fn insertLines(self: *Terminal, count: usize) !void { while (y > top) : (y -= 1) { const src = self.screen.getRow(.{ .active = y - adjusted_count }); const dst = self.screen.getRow(.{ .active = y }); - try dst.copyRow(src); + for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| { + try dst.copyCell(src, x); + } } // Insert count blank lines @@ -2730,29 +2732,29 @@ test "Terminal: insertLines top/bottom scroll region" { } } -// test "Terminal: insertLines left/right scroll region" { -// const alloc = testing.allocator; -// var t = try init(alloc, 10, 10); -// defer t.deinit(alloc); -// -// try t.printString("ABC123"); -// t.carriageReturn(); -// try t.linefeed(); -// try t.printString("DEF456"); -// t.carriageReturn(); -// try t.linefeed(); -// try t.printString("GHI789"); -// t.scrolling_region.left = 1; -// t.scrolling_region.right = 3; -// t.setCursorPos(2, 2); -// try t.insertLines(1); -// -// { -// var str = try t.plainString(testing.allocator); -// defer testing.allocator.free(str); -// try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str); -// } -// } +test "Terminal: insertLines left/right scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + try t.printString("ABC123"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF456"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI789"); + t.scrolling_region.left = 1; + t.scrolling_region.right = 3; + t.setCursorPos(2, 2); + try t.insertLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str); + } +} test "Terminal: insertLines" { const alloc = testing.allocator; From 4b9560aa31f15d916855f31a8c5f656d9c7fbc73 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 21:28:41 -0700 Subject: [PATCH 4/5] terminal: DL --- src/terminal/Terminal.zig | 127 +++++++++++++++++++++++++++++++++---- website/app/vt/dl/page.mdx | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 website/app/vt/dl/page.mdx diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b09ca943e..29ee05aab 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1626,23 +1626,58 @@ pub fn deleteLines(self: *Terminal, count: usize) !void { const tracy = trace(@src()); defer tracy.end(); - // If our cursor is outside of the scroll region, do nothing. + // If the cursor is outside the scroll region we do nothing. if (self.screen.cursor.y < self.scrolling_region.top or - self.screen.cursor.y > self.scrolling_region.bottom) + self.screen.cursor.y > self.scrolling_region.bottom or + self.screen.cursor.x < self.scrolling_region.left or + self.screen.cursor.x > self.scrolling_region.right) return; + + // Move the cursor to the left margin + self.screen.cursor.x = self.scrolling_region.left; + self.screen.cursor.pending_wrap = false; + + // If this is a full line margin then we can do a faster scroll. + if (self.scrolling_region.left == 0 and + self.scrolling_region.right == self.cols - 1) { + self.screen.scrollRegionUp( + .{ .active = self.screen.cursor.y }, + .{ .active = self.scrolling_region.bottom }, + @min(count, self.scrolling_region.bottom - self.screen.cursor.y), + ); return; } - // Move the cursor to the left margin - self.screen.cursor.x = 0; - self.screen.cursor.pending_wrap = false; + // Left/right margin is set, we need to do a slower scroll. + // Remaining rows from our cursor in the region, 1-indexed. + const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1; - // Perform the scroll - self.screen.scrollRegionUp( - .{ .active = self.screen.cursor.y }, - .{ .active = self.scrolling_region.bottom }, - @min(count, self.scrolling_region.bottom - self.screen.cursor.y), - ); + // If our count is greater than the remaining amount, we can just + // clear the region using insertLines. + if (count >= rem) { + try self.insertLines(count); + return; + } + + // The amount of lines we need to scroll up. + const scroll_amount = rem - count; + const scroll_top = self.scrolling_region.bottom - scroll_amount; + for (self.screen.cursor.y..scroll_top + 1) |y| { + const src = self.screen.getRow(.{ .active = y + count }); + const dst = self.screen.getRow(.{ .active = y }); + for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| { + try dst.copyCell(src, x); + } + } + + // Insert blank lines + for (scroll_top + 1..self.scrolling_region.bottom + 1) |y| { + const row = self.screen.getRow(.{ .active = y }); + row.fillSlice(.{ + .bg = self.screen.cursor.pen.bg, + .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, + }, self.scrolling_region.left, self.scrolling_region.right + 1); + } } /// Scroll the text down by one row. @@ -2661,6 +2696,76 @@ test "Terminal: deleteLines resets wrap" { } } +test "Terminal: deleteLines simple" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.setCursorPos(2, 2); + try t.deleteLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nGHI", str); + } +} + +test "Terminal: deleteLines left/right scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + try t.printString("ABC123"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF456"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI789"); + t.scrolling_region.left = 1; + t.scrolling_region.right = 3; + t.setCursorPos(2, 2); + try t.deleteLines(1); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC123\nDHI756\nG 89", str); + } +} + +test "Terminal: deleteLines left/right scroll region high count" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + try t.printString("ABC123"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF456"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI789"); + t.scrolling_region.left = 1; + t.scrolling_region.right = 3; + t.setCursorPos(2, 2); + try t.deleteLines(100); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC123\nD 56\nG 89", str); + } +} + test "Terminal: insertLines simple" { const alloc = testing.allocator; var t = try init(alloc, 5, 5); diff --git a/website/app/vt/dl/page.mdx b/website/app/vt/dl/page.mdx new file mode 100644 index 000000000..00c3e6288 --- /dev/null +++ b/website/app/vt/dl/page.mdx @@ -0,0 +1,113 @@ +import VTSequence from "@/components/VTSequence"; + +# Delete Line (DL) + + + +Deletes `n` lines at the current cursor position and shifts existing +lines up. + +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. + +If the current cursor position is outside of the current scroll region, +this sequence does nothing. The cursor is outside of the current scroll +region if it is above the [top margin](#TODO), below the [bottom margin](#TODO), +left of the [left margin](#TODO), or right of the [right margin](#TODO). + +This sequence unsets the pending wrap state. + +This sequence moves the cursor column to the left margin. + +Remove the top `n` lines of the current scroll region, and shift existing +lines up. The space created at the bottom of the scroll region should be +blank with the background color set according to the current SGR state. + +If a [left margin](#TODO) or [right margin](#TODO) is set, only the cells +within and including the margins are deleted or shifted. +Other existing contents to the left of the left margin or right of the +right margin remains untouched. + +If a multi-cell character would be split, erase the full multi-cell +character. For example, if "橋" is printed to the left of the left margin +and shifting the line down as a result of DL would split the character, +the cell should be erased. + +## Validation + +### DL V-1: Simple Delete Line + +```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[M" +``` + +``` +|ABC_____| +|GHI_____| +``` + +### DL V-2: Cursor Outside of Scroll Region + +```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[3;4r" # scroll region top/bottom +printf "\033[2;2H" +printf "\033[M" +``` + +``` +|ABC_____| +|DEF_____| +|GHI_____| +``` + +### DL V-3: Top/Bottom Scroll Regions + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC\n" +printf "DEF\n" +printf "GHI\n" +printf "123\n" +printf "\033[1;3r" # scroll region top/bottom +printf "\033[2;2H" +printf "\033[M" +``` + +``` +|ABC_____| +|GHI_____| +|________| +|123_____| +``` + +### DL V-4: Left/Right Scroll Regions + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "ABC123\n" +printf "DEF456\n" +printf "GHI789\n" +printf "\033[?69h" # enable left/right margins +printf "\033[2;4s" # scroll region left/right +printf "\033[2;2H" +printf "\033[M" +``` + +``` +|ABC123__| +|DHI756__| +|G___89__| +``` From 1176b65a95a4f15109a83eedb779a36c09367b4d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Oct 2023 21:39:29 -0700 Subject: [PATCH 5/5] terminal: test IND preserves background sgr --- src/terminal/Terminal.zig | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 29ee05aab..9312a1623 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -3177,6 +3177,32 @@ test "Terminal: index bottom of primary screen" { } } +test "Terminal: index bottom of primary screen 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 }, + }; + + t.setCursorPos(5, 1); + try t.print('A'); + t.screen.cursor.pen = pen; + try t.index(); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n\n\nA", str); + for (0..5) |x| { + const cell = t.screen.getCell(.active, 4, x); + try testing.expectEqual(pen, cell); + } + } +} + test "Terminal: index inside scroll region" { const alloc = testing.allocator; var t = try init(alloc, 5, 5);