From f445de7568fe623ed439172e40ec77c0f1ab77ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 22 Jul 2022 09:57:52 -0700 Subject: [PATCH] CSI: Insert Blanks (ESC [ n @) --- src/Window.zig | 6 ++- src/terminal/Terminal.zig | 110 +++++++++++++++++++++++++++++++++++++- src/terminal/stream.zig | 8 +++ 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index de451bdb5..cd27d4f6b 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -717,7 +717,7 @@ pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void { } pub fn eraseLine(self: *Window, mode: terminal.EraseLine) !void { - try self.terminal.eraseLine(mode); + self.terminal.eraseLine(mode); } pub fn deleteChars(self: *Window, count: usize) !void { @@ -732,6 +732,10 @@ pub fn insertLines(self: *Window, count: usize) !void { self.terminal.insertLines(count); } +pub fn insertBlanks(self: *Window, count: usize) !void { + self.terminal.insertBlanks(count); +} + pub fn deleteLines(self: *Window, count: usize) !void { self.terminal.deleteLines(count); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 96b3b2677..b41047ab6 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -431,7 +431,7 @@ pub fn eraseDisplay( pub fn eraseLine( self: *Terminal, mode: csi.EraseLine, -) !void { +) void { switch (mode) { .right => { const row = self.screen.getRow(self.cursor.y); @@ -605,6 +605,54 @@ pub fn linefeed(self: *Terminal) void { self.index(); } +/// Inserts spaces at current cursor position moving existing cell contents +/// to the right. The contents of the count right-most columns in the scroll +/// region are lost. The cursor position is not changed. +/// +/// This unsets the pending wrap state without wrapping. +/// +/// The inserted cells are colored according to the current SGR state. +pub fn insertBlanks(self: *Terminal, count: usize) void { + // Unset pending wrap state without wrapping + self.cursor.pending_wrap = false; + + // If our count is larger than the remaining amount, we just erase right. + if (count > self.cols - self.cursor.x) { + self.eraseLine(.right); + return; + } + + // Get the current row + const row = self.screen.getRow(self.cursor.y); + + // Determine our indexes. + const start = self.cursor.x; + const pivot = self.cursor.x + count; + + // 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 = row.len - pivot; + if (copyable > 0) { + // This is the index of the final copyable value that we need to copy. + const copyable_end = start + copyable - 1; + + // Shift count cells. We have to do this backwards since we're not + // allocated new space, otherwise we'll copy duplicates. + var i: usize = 0; + while (i < copyable) : (i += 1) { + const to = row.len - 1 - i; + const from = copyable_end - i; + row[to] = row[from]; + } + } + + // Insert zero + var pen = self.cursor.pen; + pen.char = ' '; // NOTE: this should be 0 but we need space for tests + std.mem.set(Screen.Cell, row[start..pivot], pen); +} + /// Insert amount lines at the current cursor row. The contents of the line /// at the current cursor row and below (to the bottom-most line in the /// scrolling region) are shifted down by amount lines. The contents of the @@ -1308,3 +1356,63 @@ test "Terminal: DECALN" { try testing.expectEqualStrings("EE\nEE", str); } } + +test "Terminal: insertBlanks" { + // NOTE: this is not verified with conformance tests, so these + // tests might actually be verifying wrong behavior. + const alloc = testing.allocator; + var t = try init(alloc, 5, 2); + defer t.deinit(alloc); + + try t.print('A'); + try t.print('B'); + 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 pushes off end" { + // NOTE: this is not verified with conformance tests, so these + // tests might actually be verifying wrong behavior. + const alloc = testing.allocator; + var t = try init(alloc, 3, 2); + defer t.deinit(alloc); + + try t.print('A'); + try t.print('B'); + 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(" A", str); + } +} + +test "Terminal: insertBlanks more than size" { + // NOTE: this is not verified with conformance tests, so these + // tests might actually be verifying wrong behavior. + const alloc = testing.allocator; + var t = try init(alloc, 3, 2); + defer t.deinit(alloc); + + try t.print('A'); + try t.print('B'); + try t.print('C'); + t.setCursorPos(1, 1); + t.insertBlanks(5); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 834f07006..7f456fc98 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -376,6 +376,14 @@ pub fn Stream(comptime Handler: type) type { try self.handler.csiUnimplemented(action) else log.warn("unimplemented CSI action: {}", .{action}), + + // ICH - Insert Blanks + // TODO: test + '@' => if (@hasDecl(T, "insertBlanks")) switch (action.params.len) { + 0 => try self.handler.insertBlanks(1), + 1 => try self.handler.insertBlanks(action.params[0]), + else => log.warn("invalid ICH command: {}", .{action}), + } else log.warn("unimplemented CSI callback: {}", .{action}), } }