terminal: handle ansi vs dec mode

Previously, we just ignored ansi vs dec modes (`?`-prefix) and just
responded to both requests most of the time using the number as the
unique value. This _kind of works_ because almost all DEC modes do not
overlap with ANSI modes, but some overlap (i.e. `insert`, ANSI mode 4).

This commit properly separates ANSI vs DEC modes and updates all of our
terminal sequences to handle both (where applicable -- some sequences
are explicitly DEC-only).
This commit is contained in:
Mitchell Hashimoto
2023-10-09 15:57:56 -07:00
parent 5e5b1f81d3
commit 9c45c6a3d1
3 changed files with 123 additions and 52 deletions

View File

@ -103,27 +103,45 @@ pub const Mode = mode_enum: {
for (entries, 0..) |entry, i| { for (entries, 0..) |entry, i| {
fields[i] = .{ fields[i] = .{
.name = entry.name, .name = entry.name,
.value = @as(ModeTag.Backing, @bitCast(ModeTag{
.value = entry.value, .value = entry.value,
.ansi = entry.ansi,
})),
}; };
} }
break :mode_enum @Type(.{ .Enum = .{ break :mode_enum @Type(.{ .Enum = .{
.tag_type = u16, .tag_type = ModeTag.Backing,
.fields = &fields, .fields = &fields,
.decls = &.{}, .decls = &.{},
.is_exhaustive = true, .is_exhaustive = true,
} }); } });
}; };
/// Returns true if we support the given mode. If this is true then /// The tag type for our enum is a u16 but we use a packed struct
/// you can use `@enumFromInt` to get the Mode value. We don't do /// in order to pack the ansi bit into the tag.
/// this directly due to a Zig compiler bug. const ModeTag = packed struct(u16) {
pub fn hasSupport(v: u16) bool { const Backing = @typeInfo(@This()).Struct.backing_integer.?;
inline for (@typeInfo(Mode).Enum.fields) |field| { value: u15,
if (field.value == v) return true; ansi: bool = false,
test "order" {
const t: ModeTag = .{ .value = 1 };
const int: Backing = @bitCast(t);
try std.testing.expectEqual(@as(Backing, 1), int);
}
};
pub fn modeFromInt(v: u16, ansi: bool) ?Mode {
inline for (entries) |entry| {
if (entry.value == v and entry.ansi == ansi) {
const tag: ModeTag = .{ .ansi = ansi, .value = entry.value };
const int: ModeTag.Backing = @bitCast(tag);
return @enumFromInt(int);
}
} }
return false; return null;
} }
fn entryForMode(comptime mode: Mode) ModeEntry { fn entryForMode(comptime mode: Mode) ModeEntry {
@ -141,15 +159,17 @@ const ModeEntry = struct {
name: []const u8, name: []const u8,
value: comptime_int, value: comptime_int,
default: bool = false, default: bool = false,
ansi: bool = false,
}; };
/// The full list of available entries. For documentation see how /// The full list of available entries. For documentation see how
/// they're used within Ghostty or google their values. It is not /// they're used within Ghostty or google their values. It is not
/// valuable to redocument them all here. /// valuable to redocument them all here.
const entries: []const ModeEntry = &.{ const entries: []const ModeEntry = &.{
.{ .name = "insert", .value = 4, .ansi = true },
.{ .name = "cursor_keys", .value = 1 }, .{ .name = "cursor_keys", .value = 1 },
.{ .name = "132_column", .value = 3 }, .{ .name = "132_column", .value = 3 },
.{ .name = "insert", .value = 4 },
.{ .name = "reverse_colors", .value = 5 }, .{ .name = "reverse_colors", .value = 5 },
.{ .name = "origin", .value = 6 }, .{ .name = "origin", .value = 6 },
.{ .name = "wraparound", .value = 7, .default = true }, .{ .name = "wraparound", .value = 7, .default = true },
@ -182,10 +202,11 @@ test {
_ = ModePacked; _ = ModePacked;
} }
test hasSupport { test modeFromInt {
try testing.expect(hasSupport(1)); try testing.expect(modeFromInt(4, true).? == .insert);
try testing.expect(hasSupport(2004)); try testing.expect(modeFromInt(9, true) == null);
try testing.expect(!hasSupport(8888)); try testing.expect(modeFromInt(9, false).? == .mouse_event_x10);
try testing.expect(modeFromInt(12, true) == null);
} }
test ModeState { test ModeState {

View File

@ -511,29 +511,41 @@ pub fn Stream(comptime Handler: type) type {
) else log.warn("unimplemented CSI callback: {}", .{action}), ) else log.warn("unimplemented CSI callback: {}", .{action}),
// SM - Set Mode // SM - Set Mode
'h' => if (@hasDecl(T, "setMode")) { 'h' => if (@hasDecl(T, "setMode")) mode: {
for (action.params) |mode| { const ansi_mode = ansi: {
if (modes.hasSupport(mode)) { if (action.intermediates.len == 0) break :ansi true;
try self.handler.setMode( if (action.intermediates.len == 1 and
@enumFromInt(mode), action.intermediates[0] == '?') break :ansi false;
true,
); log.warn("invalid set mode command: {}", .{action});
break :mode;
};
for (action.params) |mode_int| {
if (modes.modeFromInt(mode_int, ansi_mode)) |mode| {
try self.handler.setMode(mode, true);
} else { } else {
log.warn("unimplemented mode: {}", .{mode}); log.warn("unimplemented mode: {}", .{mode_int});
} }
} }
} else log.warn("unimplemented CSI callback: {}", .{action}), } else log.warn("unimplemented CSI callback: {}", .{action}),
// RM - Reset Mode // RM - Reset Mode
'l' => if (@hasDecl(T, "setMode")) { 'l' => if (@hasDecl(T, "setMode")) mode: {
for (action.params) |mode| { const ansi_mode = ansi: {
if (modes.hasSupport(mode)) { if (action.intermediates.len == 0) break :ansi true;
try self.handler.setMode( if (action.intermediates.len == 1 and
@enumFromInt(mode), action.intermediates[0] == '?') break :ansi false;
false,
); log.warn("invalid set mode command: {}", .{action});
break :mode;
};
for (action.params) |mode_int| {
if (modes.modeFromInt(mode_int, ansi_mode)) |mode| {
try self.handler.setMode(mode, false);
} else { } else {
log.warn("unimplemented mode: {}", .{mode}); log.warn("unimplemented mode: {}", .{mode_int});
} }
} }
} else log.warn("unimplemented CSI callback: {}", .{action}), } else log.warn("unimplemented CSI callback: {}", .{action}),
@ -646,15 +658,20 @@ pub fn Stream(comptime Handler: type) type {
// DECRQM - Request Mode // DECRQM - Request Mode
'p' => switch (action.intermediates.len) { 'p' => switch (action.intermediates.len) {
2 => decrqm: { 2 => decrqm: {
if (action.intermediates[0] != '?' and const ansi_mode = ansi: {
action.intermediates[1] != '$') switch (action.intermediates.len) {
{ 1 => if (action.intermediates[0] == '$') break :ansi true,
2 => if (action.intermediates[0] == '?' and
action.intermediates[1] == '$') break :ansi false,
else => {},
}
log.warn( log.warn(
"ignoring unimplemented CSI p with intermediates: {s}", "ignoring unimplemented CSI p with intermediates: {s}",
.{action.intermediates}, .{action.intermediates},
); );
break :decrqm; break :decrqm;
} };
if (action.params.len != 1) { if (action.params.len != 1) {
log.warn("invalid DECRQM command: {}", .{action}); log.warn("invalid DECRQM command: {}", .{action});
@ -662,7 +679,7 @@ pub fn Stream(comptime Handler: type) type {
} }
if (@hasDecl(T, "requestMode")) { if (@hasDecl(T, "requestMode")) {
try self.handler.requestMode(action.params[0]); try self.handler.requestMode(action.params[0], ansi_mode);
} else log.warn("unimplemented DECRQM callback: {}", .{action}); } else log.warn("unimplemented DECRQM callback: {}", .{action});
}, },
@ -746,15 +763,13 @@ pub fn Stream(comptime Handler: type) type {
1 => switch (action.intermediates[0]) { 1 => switch (action.intermediates[0]) {
// Restore Mode // Restore Mode
'?' => if (@hasDecl(T, "restoreMode")) { '?' => if (@hasDecl(T, "restoreMode")) {
for (action.params) |mode| { for (action.params) |mode_int| {
if (modes.hasSupport(mode)) { if (modes.modeFromInt(mode_int, false)) |mode| {
try self.handler.restoreMode( try self.handler.restoreMode(mode);
@enumFromInt(mode),
);
} else { } else {
log.warn( log.warn(
"unimplemented restore mode: {}", "unimplemented restore mode: {}",
.{mode}, .{mode_int},
); );
} }
} }
@ -776,15 +791,13 @@ pub fn Stream(comptime Handler: type) type {
's' => switch (action.intermediates.len) { 's' => switch (action.intermediates.len) {
1 => switch (action.intermediates[0]) { 1 => switch (action.intermediates[0]) {
'?' => if (@hasDecl(T, "saveMode")) { '?' => if (@hasDecl(T, "saveMode")) {
for (action.params) |mode| { for (action.params) |mode_int| {
if (modes.hasSupport(mode)) { if (modes.modeFromInt(mode_int, false)) |mode| {
try self.handler.saveMode( try self.handler.saveMode(mode);
@enumFromInt(mode),
);
} else { } else {
log.warn( log.warn(
"unimplemented save mode: {}", "unimplemented save mode: {}",
.{mode}, .{mode_int},
); );
} }
} }
@ -1219,7 +1232,7 @@ test "stream: cursor right (CUF)" {
try testing.expectEqual(@as(u16, 0), s.handler.amount); try testing.expectEqual(@as(u16, 0), s.handler.amount);
} }
test "stream: set mode (SM) and reset mode (RM)" { test "stream: dec set mode (SM) and reset mode (RM)" {
const H = struct { const H = struct {
mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)), mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)),
pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void {
@ -1236,6 +1249,42 @@ test "stream: set mode (SM) and reset mode (RM)" {
try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode); try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);
} }
test "stream: ansi set mode (SM) and reset mode (RM)" {
const H = struct {
mode: ?modes.Mode = null,
pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void {
self.mode = null;
if (v) self.mode = mode;
}
};
var s: Stream(H) = .{ .handler = .{} };
try s.nextSlice("\x1B[4h");
try testing.expectEqual(@as(modes.Mode, .insert), s.handler.mode.?);
try s.nextSlice("\x1B[4l");
try testing.expect(s.handler.mode == null);
}
test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" {
const H = struct {
mode: ?modes.Mode = null,
pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void {
self.mode = null;
if (v) self.mode = mode;
}
};
var s: Stream(H) = .{ .handler = .{} };
try s.nextSlice("\x1B[6h");
try testing.expect(s.handler.mode == null);
try s.nextSlice("\x1B[6l");
try testing.expect(s.handler.mode == null);
}
test "stream: restore mode" { test "stream: restore mode" {
const H = struct { const H = struct {
const Self = @This(); const Self = @This();

View File

@ -1408,19 +1408,20 @@ const StreamHandler = struct {
} }
} }
pub fn requestMode(self: *StreamHandler, mode_raw: u16) !void { pub fn requestMode(self: *StreamHandler, mode_raw: u16, ansi: bool) !void {
// Get the mode value and respond. // Get the mode value and respond.
const code: u8 = code: { const code: u8 = code: {
if (!terminal.modes.hasSupport(mode_raw)) break :code 0; const mode = terminal.modes.modeFromInt(mode_raw, ansi) orelse break :code 0;
if (self.terminal.modes.get(@enumFromInt(mode_raw))) break :code 1; if (self.terminal.modes.get(mode)) break :code 1;
break :code 2; break :code 2;
}; };
var msg: termio.Message = .{ .write_small = .{} }; var msg: termio.Message = .{ .write_small = .{} };
const resp = try std.fmt.bufPrint( const resp = try std.fmt.bufPrint(
&msg.write_small.data, &msg.write_small.data,
"\x1B[?{};{}$y", "\x1B[{s}{};{}$y",
.{ .{
if (ansi) "" else "?",
mode_raw, mode_raw,
code, code,
}, },