diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 35e724abaa..54ee0598cc 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1581,8 +1581,20 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // Unset pending wrap state without wrapping self.screen.cursor.pending_wrap = false; + // If our cursor is outside the margins then do nothing. We DO reset + // wrap state still so this must remain below the above logic. + if (self.screen.cursor.x < self.scrolling_region.left or + self.screen.cursor.x > self.scrolling_region.right) return; + + // The limit we can shift to is our right margin. We add 1 since the + // math around this is 1-indexed. + const right_limit = self.scrolling_region.right + 1; + // If our count is larger than the remaining amount, we just erase right. - if (count > self.cols - self.screen.cursor.x) { + // We only do this if we can erase the entire line (no right margin). + if (right_limit == self.cols and + count > right_limit - self.screen.cursor.x) + { self.eraseLine(.right, false); return; } @@ -1597,7 +1609,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // This is the number of spaces we have left to shift existing data. // If count is bigger than the available space left after the cursor, // we may have no space at all for copying. - const copyable = self.screen.cols - pivot; + const copyable = right_limit - pivot; if (copyable > 0) { // This is the index of the final copyable value that we need to copy. const copyable_end = start + copyable - 1; @@ -1606,14 +1618,19 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // allocated new space, otherwise we'll copy duplicates. var i: usize = 0; while (i < copyable) : (i += 1) { - const to = self.screen.cols - 1 - i; + const to = right_limit - 1 - i; const from = copyable_end - i; - row.getCellPtr(to).* = row.getCell(from); + const src = row.getCell(from); + const dst = row.getCellPtr(to); + dst.* = src; } } - // Insert zero - row.fillSlice(.{}, start, pivot); + // Insert blanks. The blanks preserve the background color. + row.fillSlice(.{ + .bg = self.screen.cursor.pen.bg, + .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, + }, start, pivot); } /// Insert amount lines at the current cursor row. The contents of the line @@ -3128,6 +3145,103 @@ test "Terminal: insertBlanks more than size" { } } +test "Terminal: insertBlanks no scroll region, fits" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.setCursorPos(1, 1); + t.insertBlanks(2); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" ABC", str); + } +} + +test "Terminal: insertBlanks 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.insertBlanks(2); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" ABC", str); + const cell = t.screen.getCell(.active, 0, 0); + try testing.expectEqual(pen, cell); + } +} + +test "Terminal: insertBlanks shift off screen" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 10); + defer t.deinit(alloc); + + for (" ABC") |c| try t.print(c); + t.setCursorPos(1, 3); + t.insertBlanks(2); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" X A", str); + } +} + +test "Terminal: insertBlanks inside left/right scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + t.scrolling_region.left = 2; + t.scrolling_region.right = 4; + t.setCursorPos(1, 3); + for ("ABC") |c| try t.print(c); + t.setCursorPos(1, 3); + t.insertBlanks(2); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" X A", str); + } +} + +test "Terminal: insertBlanks outside left/right scroll region" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + t.scrolling_region.left = 2; + t.scrolling_region.right = 4; + t.setCursorPos(1, 3); + for ("ABC") |c| try t.print(c); + t.setCursorPos(1, 1); + t.insertBlanks(2); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("X ABC", str); + } +} + test "Terminal: insert mode with space" { const alloc = testing.allocator; var t = try init(alloc, 10, 2); diff --git a/website/app/vt/ich/page.mdx b/website/app/vt/ich/page.mdx new file mode 100644 index 0000000000..5586757d68 --- /dev/null +++ b/website/app/vt/ich/page.mdx @@ -0,0 +1,128 @@ +import VTSequence from "@/components/VTSequence"; + +# Insert Character (ICH) + + + +Insert `n` blank characters at the current cursor position and shift +existing cell contents right. + +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. + +This sequence always unsets the pending wrap state. + +If the cursor position is outside of the [left and right margins](#TODO), +this sequence does not change the screen, but the pending wrap state is +still reset. + +Existing cells shifted beyond the right margin are deleted. Inserted cells +are blank with the background color colored according to the current SGR state. + +If a multi-cell character (such as "橋") is shifted so that the cell is split +in half, the multi-cell character can either be clipped or erased. Typical +behavior is to clip at the right edge of the screen and erase at a right +margin, but either behavior is acceptable. + +## Validation + +### ICH V-1: No Scroll Region, Fits on Screen + +```bash +printf "ABC" +printf "\033[1G" +printf "\033[2@" +``` + +``` +|XcABC_____| +``` + +### ICH V-2: SGR State + +```bash +printf "ABC" +printf "\033[1G" +printf "\033[41m" +printf "\033[2@" +printf "X" +``` + +``` +|c_ABC_____| +``` + +The `c_` cells should both have a red background. The `ABC` cells should +remain unchanged in style. + +### ICH V-3: Shifting Content Off the Screen + +```bash +cols=$(tput cols) +printf "\033[${cols}G" +printf "\033[2D" +printf "ABC" +printf "\033[2D" +printf "\033[2@" +printf "X" +``` + +``` +|_______XcA| +``` + +### ICH V-4: Inside Left/Right Scroll Region + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "\033[?69h" # enable left/right margins +printf "\033[3;5s" # scroll region left/right +printf "\033[3G" +printf "ABC" +printf "\033[3G" +printf "\033[2@" +printf "X" +``` + +``` +|__XcA_____| +``` + +### ICH V-5: Outside Left/Right Scroll Region + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "\033[?69h" # enable left/right margins +printf "\033[3;5s" # scroll region left/right +printf "\033[3G" +printf "ABC" +printf "\033[1G" +printf "\033[2@" +printf "X" +``` + +``` +|XcABC_____| +``` + +### ICH V-6: Split Wide Character + +```bash +cols=$(tput cols) +printf "\033[${cols}G" +printf "\033[1D" +printf "橋" +printf "\033[2D" +printf "\033[@" +printf "X" +``` + +``` +|_______Xc_| +``` + +In this case, it is valid for the last cell to be blank or to clip the +multi-cell character. xterm clips the character but many other terminals +erase the cell.