Merge pull request #536 from mitchellh/protected-mode

DEC Protected Mode (DECSCA, DECSEL, DECSED)
This commit is contained in:
Mitchell Hashimoto
2023-09-25 11:57:05 -07:00
committed by GitHub
6 changed files with 469 additions and 65 deletions

View File

@ -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.

View File

@ -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));
@ -1219,36 +1235,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 +1481,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;
}
@ -1728,6 +1766,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 });
@ -1741,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();
}
@ -2953,3 +3009,142 @@ 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);
}
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);
}
}
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);
}
}

View File

@ -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
};

View File

@ -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;

View File

@ -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
@ -651,6 +664,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 +1238,162 @@ test "stream: pop kitty keyboard with no params defaults to 1" {
for ("\x1B[<u") |c| try s.next(c);
try testing.expectEqual(@as(u16, 1), s.handler.n);
}
test "stream: DECSCA" {
const H = struct {
const Self = @This();
v: ?ansi.ProtectedMode = null,
pub fn setProtectedMode(self: *Self, v: ansi.ProtectedMode) !void {
self.v = v;
}
};
var s: Stream(H) = .{ .handler = .{} };
{
for ("\x1B[\"q") |c| try s.next(c);
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
}
{
for ("\x1B[0\"q") |c| try s.next(c);
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
}
{
for ("\x1B[2\"q") |c| try s.next(c);
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
}
{
for ("\x1B[1\"q") |c| try s.next(c);
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.?);
}
}

View File

@ -1304,18 +1304,18 @@ 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 {
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) !void {
self.terminal.eraseLine(mode);
pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void {
self.terminal.eraseLine(mode, protected);
}
pub fn deleteChars(self: *StreamHandler, count: usize) !void {
@ -1583,6 +1583,10 @@ const StreamHandler = struct {
}
}
pub fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void {
self.terminal.setProtectedMode(mode);
}
pub fn decaln(self: *StreamHandler) !void {
try self.terminal.decaln();
}