From 2a390785f5d8276d4f34bc30b700a65301f8835c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Sep 2023 09:38:42 -0700 Subject: [PATCH 1/5] terminal: add protected mode flag to cursor pen --- src/terminal/Screen.zig | 1 + src/terminal/Terminal.zig | 34 ++++++++++++++++++++++++++++++++++ src/terminal/ansi.zig | 8 ++++++++ src/terminal/main.zig | 1 + 4 files changed, 44 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index a949d858d..73f39d1ee 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -232,6 +232,7 @@ pub const Cell = struct { strikethrough: bool = false, underline: sgr.Attribute.Underline = .none, underline_color: bool = false, + protected: bool = false, /// True if this is a wide character. This char takes up /// two cells. The following cell ALWAYS is a space. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index eae35385a..62cf2c071 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1728,6 +1728,24 @@ pub fn kittyGraphics( return kitty.graphics.execute(alloc, self, cmd); } +/// Set the character protection mode for the terminal. +pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void { + switch (mode) { + .off => { + self.screen.cursor.pen.attrs.protected = false; + }, + + // TODO: ISO/DEC have very subtle differences, so we should track that. + .iso => { + self.screen.cursor.pen.attrs.protected = true; + }, + + .dec => { + self.screen.cursor.pen.attrs.protected = true; + }, + } +} + /// Full reset pub fn fullReset(self: *Terminal, alloc: Allocator) void { self.primaryScreen(alloc, .{ .clear_on_exit = true, .cursor_save = true }); @@ -2953,3 +2971,19 @@ test "Terminal: saveCursor with screen change" { try testing.expect(t.screen.charset.gr == .G3); try testing.expect(t.modes.get(.origin)); } + +test "Terminal: setProtectedMode" { + const alloc = testing.allocator; + var t = try init(alloc, 3, 3); + defer t.deinit(alloc); + + try testing.expect(!t.screen.cursor.pen.attrs.protected); + t.setProtectedMode(.off); + try testing.expect(!t.screen.cursor.pen.attrs.protected); + t.setProtectedMode(.iso); + try testing.expect(t.screen.cursor.pen.attrs.protected); + t.setProtectedMode(.dec); + try testing.expect(t.screen.cursor.pen.attrs.protected); + t.setProtectedMode(.off); + try testing.expect(!t.screen.cursor.pen.attrs.protected); +} diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index 36d3c7361..96f4b9c0a 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -111,3 +111,11 @@ pub const ModifyKeyFormat = union(enum) { function_keys: void, other_keys: enum { none, numeric_except, numeric }, }; + +/// The protection modes that can be set for the terminal. See DECSCA and +/// ESC V, W. +pub const ProtectedMode = enum { + off, + iso, // ESC V, W + dec, // CSI Ps " q +}; diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 23b032daf..4e8d3819a 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -28,6 +28,7 @@ pub const DeviceAttributeReq = ansi.DeviceAttributeReq; pub const DeviceStatusReq = ansi.DeviceStatusReq; pub const Mode = modes.Mode; pub const ModifyKeyFormat = ansi.ModifyKeyFormat; +pub const ProtectedMode = ansi.ProtectedMode; pub const StatusLineType = ansi.StatusLineType; pub const StatusDisplay = ansi.StatusDisplay; pub const EraseDisplay = csi.EraseDisplay; From 8137a66ef6cc92ae0e00533c4613043f53a0c208 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Sep 2023 09:45:16 -0700 Subject: [PATCH 2/5] terminal: CSI Ps " q for setting DEC protected mode --- src/terminal/stream.zig | 52 +++++++++++++++++++++++++++++++++++++++++ src/termio/Exec.zig | 4 ++++ 2 files changed, 56 insertions(+) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 739768b7c..bc8576ad8 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -651,6 +651,29 @@ pub fn Stream(comptime Handler: type) type { }, ) else log.warn("unimplemented CSI callback: {}", .{action}); }, + + // DECSCA + '"' => { + if (@hasDecl(T, "setProtectedMode")) { + const mode_: ?ansi.ProtectedMode = switch (action.params.len) { + else => null, + 0 => .off, + 1 => switch (action.params[0]) { + 0, 2 => .off, + 1 => .dec, + else => null, + }, + }; + + const mode = mode_ orelse { + log.warn("invalid set protected mode command: {}", .{action}); + return; + }; + + try self.handler.setProtectedMode(mode); + } else log.warn("unimplemented CSI callback: {}", .{action}); + }, + // XTVERSION '>' => { if (@hasDecl(T, "reportXtversion")) try self.handler.reportXtversion(); @@ -1202,3 +1225,32 @@ test "stream: pop kitty keyboard with no params defaults to 1" { for ("\x1B[ Date: Mon, 25 Sep 2023 09:58:30 -0700 Subject: [PATCH 3/5] terminal: DECSED, DECSEL parsing, tests --- src/terminal/stream.zig | 213 +++++++++++++++++++++++++++++++++------- src/termio/Exec.zig | 7 +- 2 files changed, 183 insertions(+), 37 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index bc8576ad8..6a8acad9c 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -259,45 +259,58 @@ pub fn Stream(comptime Handler: type) type { ) else log.warn("unimplemented CSI callback: {}", .{action}), // Erase Display - // TODO: test - 'J' => if (@hasDecl(T, "eraseDisplay")) try self.handler.eraseDisplay( - switch (action.params.len) { - 0 => .below, - 1 => mode: { - // TODO: use meta to get enum max - if (action.params[0] > 3) { - log.warn("invalid erase display command: {}", .{action}); - return; - } + 'J' => if (@hasDecl(T, "eraseDisplay")) { + const protected_: ?bool = switch (action.intermediates.len) { + 0 => false, + 1 => if (action.intermediates[0] == '?') true else null, + else => null, + }; - break :mode @enumFromInt(action.params[0]); - }, - else => { - log.warn("invalid erase display command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), + const protected = protected_ orelse { + log.warn("invalid erase display command: {}", .{action}); + return; + }; + + const mode_: ?csi.EraseDisplay = switch (action.params.len) { + 0 => .below, + 1 => if (action.params[0] <= 3) @enumFromInt(action.params[0]) else null, + else => null, + }; + + const mode = mode_ orelse { + log.warn("invalid erase display command: {}", .{action}); + return; + }; + + try self.handler.eraseDisplay(mode, protected); + } else log.warn("unimplemented CSI callback: {}", .{action}), // Erase Line - 'K' => if (@hasDecl(T, "eraseLine")) try self.handler.eraseLine( - switch (action.params.len) { - 0 => .right, - 1 => mode: { - // TODO: use meta to get enum max - if (action.params[0] > 3) { - log.warn("invalid erase line command: {}", .{action}); - return; - } + 'K' => if (@hasDecl(T, "eraseLine")) { + const protected_: ?bool = switch (action.intermediates.len) { + 0 => false, + 1 => if (action.intermediates[0] == '?') true else null, + else => null, + }; - break :mode @enumFromInt(action.params[0]); - }, - else => { - log.warn("invalid erase line command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), + const protected = protected_ orelse { + log.warn("invalid erase line command: {}", .{action}); + return; + }; + + const mode_: ?csi.EraseLine = switch (action.params.len) { + 0 => .right, + 1 => if (action.params[0] < 3) @enumFromInt(action.params[0]) else null, + else => null, + }; + + const mode = mode_ orelse { + log.warn("invalid erase line command: {}", .{action}); + return; + }; + + try self.handler.eraseLine(mode, protected); + } else log.warn("unimplemented CSI callback: {}", .{action}), // IL - Insert Lines // TODO: test @@ -1254,3 +1267,133 @@ test "stream: DECSCA" { try testing.expectEqual(ansi.ProtectedMode.dec, s.handler.v.?); } } + +test "stream: DECED, DECSED" { + const H = struct { + const Self = @This(); + mode: ?csi.EraseDisplay = null, + protected: ?bool = null, + + pub fn eraseDisplay( + self: *Self, + mode: csi.EraseDisplay, + protected: bool, + ) !void { + self.mode = mode; + self.protected = protected; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + { + for ("\x1B[?J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?0J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?1J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.above, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?2J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.complete, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?3J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + + { + for ("\x1B[J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[0J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[1J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.above, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[2J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.complete, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[3J") |c| try s.next(c); + try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } +} + +test "stream: DECEL, DECSEL" { + const H = struct { + const Self = @This(); + mode: ?csi.EraseLine = null, + protected: ?bool = null, + + pub fn eraseLine( + self: *Self, + mode: csi.EraseLine, + protected: bool, + ) !void { + self.mode = mode; + self.protected = protected; + } + }; + + var s: Stream(H) = .{ .handler = .{} }; + { + for ("\x1B[?K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?0K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?1K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.left, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + { + for ("\x1B[?2K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?); + try testing.expect(s.handler.protected.?); + } + + { + for ("\x1B[K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[0K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[1K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.left, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } + { + for ("\x1B[2K") |c| try s.next(c); + try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?); + try testing.expect(!s.handler.protected.?); + } +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index f4057b219..c8026f34e 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1304,7 +1304,9 @@ const StreamHandler = struct { self.terminal.setCursorPos(row, col); } - pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay) !void { + pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { + _ = protected; + if (mode == .complete) { // Whenever we erase the full display, scroll to bottom. try self.terminal.scrollViewport(.{ .bottom = {} }); @@ -1314,7 +1316,8 @@ const StreamHandler = struct { self.terminal.eraseDisplay(self.alloc, mode); } - pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine) !void { + pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { + _ = protected; self.terminal.eraseLine(mode); } From f1c771615f8bec7ca688e3b51b60e151a072c06e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Sep 2023 10:13:47 -0700 Subject: [PATCH 4/5] terminal: eraseLine protected, tests --- src/terminal/Terminal.zig | 113 ++++++++++++++++++++++++++++++++------ src/termio/Exec.zig | 3 +- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 62cf2c071..3ec58ca39 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1219,36 +1219,58 @@ test "Terminal: eraseDisplay complete" { } /// Erase the line. -/// TODO: test pub fn eraseLine( self: *Terminal, mode: csi.EraseLine, + protected: bool, ) void { const tracy = trace(@src()); defer tracy.end(); - switch (mode) { - .right => { - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - row.fillSlice(self.screen.cursor.pen, self.screen.cursor.x, self.cols); - }, + // We always need a row no matter what + const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - .left => { - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - row.fillSlice(self.screen.cursor.pen, 0, self.screen.cursor.x + 1); + // 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, + ), - // Unsets pending wrap state - self.screen.cursor.pending_wrap = false; - }, + .left => { + row.fillSlice(self.screen.cursor.pen, 0, self.screen.cursor.x + 1); - .complete => { - const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); - row.fill(self.screen.cursor.pen); - }, + // Unsets pending wrap state + self.screen.cursor.pending_wrap = false; + }, + .complete => row.fill(self.screen.cursor.pen), + + 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 }, + .complete => .{ 0, row.lenCells() }, else => { log.err("unimplemented erase line mode: {}", .{mode}); + return; }, + }; + + for (start..end) |x| { + const cell = row.getCellPtr(x); + if (cell.attrs.protected) continue; + cell.* = self.screen.cursor.pen; } } @@ -1443,7 +1465,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // If our count is larger than the remaining amount, we just erase right. if (count > self.cols - self.screen.cursor.x) { - self.eraseLine(.right); + self.eraseLine(.right, false); return; } @@ -2987,3 +3009,60 @@ test "Terminal: setProtectedMode" { t.setProtectedMode(.off); try testing.expect(!t.screen.cursor.pen.attrs.protected); } + +test "Terminal: eraseLine protected right" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + for ("12345678") |c| try t.print(c); + t.setCursorColAbsolute(6); + t.setProtectedMode(.dec); + try t.print('X'); + t.setCursorColAbsolute(4); + t.eraseLine(.right, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("123 X", str); + } +} + +test "Terminal: eraseLine protected left" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + for ("123456789") |c| try t.print(c); + t.setCursorColAbsolute(6); + t.setProtectedMode(.dec); + try t.print('X'); + t.setCursorColAbsolute(8); + t.eraseLine(.left, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" X 9", str); + } +} + +test "Terminal: eraseLine protected complete" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + for ("123456789") |c| try t.print(c); + t.setCursorColAbsolute(6); + t.setProtectedMode(.dec); + try t.print('X'); + t.setCursorColAbsolute(8); + t.eraseLine(.complete, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" X", str); + } +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index c8026f34e..cc620dacc 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1317,8 +1317,7 @@ const StreamHandler = struct { } pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { - _ = protected; - self.terminal.eraseLine(mode); + self.terminal.eraseLine(mode, protected); } pub fn deleteChars(self: *StreamHandler, count: usize) !void { From 35f89bd28cd6d8876b015b28c2d3143d58dd2732 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Sep 2023 11:15:24 -0700 Subject: [PATCH 5/5] terminal: eraseDisplay protected --- src/terminal/Terminal.zig | 100 ++++++++++++++++++++++++++++++++++---- src/termio/Exec.zig | 4 +- 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 3ec58ca39..ed113f16b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -213,7 +213,7 @@ pub fn alternateScreen( self.screen.selection = null; if (options.clear_on_enter) { - self.eraseDisplay(alloc, .complete); + self.eraseDisplay(alloc, .complete, false); } } @@ -232,7 +232,7 @@ pub fn primaryScreen( // TODO(mitchellh): what happens if we enter alternate screen multiple times? if (self.active_screen == .primary) return; - if (options.clear_on_exit) self.eraseDisplay(alloc, .complete); + if (options.clear_on_exit) self.eraseDisplay(alloc, .complete, false); // Switch the screens const old = self.screen; @@ -279,7 +279,7 @@ pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void { try self.resize(alloc, 0, self.rows); // TODO: do not clear screen flag mode - self.eraseDisplay(alloc, .complete); + self.eraseDisplay(alloc, .complete, false); self.setCursorPos(1, 1); // TODO: left/right margins @@ -1010,6 +1010,7 @@ pub fn eraseDisplay( self: *Terminal, alloc: Allocator, mode: csi.EraseDisplay, + protected: bool, ) void { const tracy = trace(@src()); defer tracy.end(); @@ -1026,7 +1027,18 @@ pub fn eraseDisplay( while (it.next()) |row| { row.setWrapped(false); row.setDirty(true); - row.clear(pen); + + if (!protected) { + row.clear(pen); + continue; + } + + // Protected mode erase + for (0..row.lenCells()) |x| { + const cell = row.getCellPtr(x); + if (cell.attrs.protected) continue; + cell.* = pen; + } } // Unsets pending wrap state @@ -1045,6 +1057,7 @@ pub fn eraseDisplay( 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; } @@ -1058,6 +1071,7 @@ pub fn eraseDisplay( for (0..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; } @@ -1072,6 +1086,7 @@ pub fn eraseDisplay( 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; } @@ -1082,6 +1097,7 @@ pub fn eraseDisplay( x = 0; while (x < self.cols) : (x += 1) { const cell = self.screen.getCellPtr(.active, y, x); + if (protected and cell.attrs.protected) continue; cell.* = pen; cell.char = 0; } @@ -1121,7 +1137,7 @@ test "Terminal: eraseDisplay above" { t.screen.cursor.y = 40; t.screen.cursor.x = 40; // erase above the cursor - t.eraseDisplay(testing.allocator, .above); + t.eraseDisplay(testing.allocator, .above, false); // check it was erased cell = t.screen.getCell(.active, 0, 0); try testing.expect(cell.bg.eql(pink)); @@ -1158,7 +1174,7 @@ test "Terminal: eraseDisplay below" { try testing.expect(cell.char == 'a'); try testing.expect(cell.attrs.bold); // erase below the cursor - t.eraseDisplay(testing.allocator, .below); + t.eraseDisplay(testing.allocator, .below, false); // check it was erased cell = t.screen.getCell(.active, 60, 60); try testing.expect(cell.bg.eql(pink)); @@ -1202,7 +1218,7 @@ test "Terminal: eraseDisplay complete" { // position our cursor between the cells t.screen.cursor.y = 30; // erase everything - t.eraseDisplay(testing.allocator, .complete); + t.eraseDisplay(testing.allocator, .complete, false); // check they were erased cell = t.screen.getCell(.active, 60, 60); try testing.expect(cell.bg.eql(pink)); @@ -1781,8 +1797,8 @@ pub fn fullReset(self: *Terminal, alloc: Allocator) void { self.screen.kitty_keyboard = .{}; self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1 }; self.previous_char = null; - self.eraseDisplay(alloc, .scrollback); - self.eraseDisplay(alloc, .complete); + self.eraseDisplay(alloc, .scrollback, false); + self.eraseDisplay(alloc, .complete, false); self.pwd.clearRetainingCapacity(); } @@ -3066,3 +3082,69 @@ test "Terminal: eraseLine protected complete" { try testing.expectEqualStrings(" X", str); } } + +test "Terminal: eraseDisplay protected complete" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + try t.print('A'); + t.carriageReturn(); + try t.linefeed(); + for ("123456789") |c| try t.print(c); + t.setCursorColAbsolute(6); + t.setProtectedMode(.dec); + try t.print('X'); + t.setCursorColAbsolute(4); + t.eraseDisplay(alloc, .complete, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n X", str); + } +} + +test "Terminal: eraseDisplay protected below" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + try t.print('A'); + t.carriageReturn(); + try t.linefeed(); + for ("123456789") |c| try t.print(c); + t.setCursorColAbsolute(6); + t.setProtectedMode(.dec); + try t.print('X'); + t.setCursorColAbsolute(4); + t.eraseDisplay(alloc, .below, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("A\n123 X", str); + } +} + +test "Terminal: eraseDisplay protected above" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + try t.print('A'); + t.carriageReturn(); + try t.linefeed(); + for ("123456789") |c| try t.print(c); + t.setCursorColAbsolute(6); + t.setProtectedMode(.dec); + try t.print('X'); + t.setCursorColAbsolute(8); + t.eraseDisplay(alloc, .above, true); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n X 9", str); + } +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index cc620dacc..31ec838f5 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1305,15 +1305,13 @@ const StreamHandler = struct { } pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { - _ = protected; - if (mode == .complete) { // Whenever we erase the full display, scroll to bottom. try self.terminal.scrollViewport(.{ .bottom = {} }); try self.queueRender(); } - self.terminal.eraseDisplay(self.alloc, mode); + self.terminal.eraseDisplay(self.alloc, mode, protected); } pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void {