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

View File

@ -511,29 +511,41 @@ pub fn Stream(comptime Handler: type) type {
) else log.warn("unimplemented CSI callback: {}", .{action}),
// SM - Set Mode
'h' => if (@hasDecl(T, "setMode")) {
for (action.params) |mode| {
if (modes.hasSupport(mode)) {
try self.handler.setMode(
@enumFromInt(mode),
true,
);
'h' => if (@hasDecl(T, "setMode")) mode: {
const ansi_mode = ansi: {
if (action.intermediates.len == 0) break :ansi true;
if (action.intermediates.len == 1 and
action.intermediates[0] == '?') break :ansi 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, true);
} else {
log.warn("unimplemented mode: {}", .{mode});
log.warn("unimplemented mode: {}", .{mode_int});
}
}
} else log.warn("unimplemented CSI callback: {}", .{action}),
// RM - Reset Mode
'l' => if (@hasDecl(T, "setMode")) {
for (action.params) |mode| {
if (modes.hasSupport(mode)) {
try self.handler.setMode(
@enumFromInt(mode),
false,
);
'l' => if (@hasDecl(T, "setMode")) mode: {
const ansi_mode = ansi: {
if (action.intermediates.len == 0) break :ansi true;
if (action.intermediates.len == 1 and
action.intermediates[0] == '?') break :ansi 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 {
log.warn("unimplemented mode: {}", .{mode});
log.warn("unimplemented mode: {}", .{mode_int});
}
}
} else log.warn("unimplemented CSI callback: {}", .{action}),
@ -646,15 +658,20 @@ pub fn Stream(comptime Handler: type) type {
// DECRQM - Request Mode
'p' => switch (action.intermediates.len) {
2 => decrqm: {
if (action.intermediates[0] != '?' and
action.intermediates[1] != '$')
{
const ansi_mode = ansi: {
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(
"ignoring unimplemented CSI p with intermediates: {s}",
.{action.intermediates},
);
break :decrqm;
}
};
if (action.params.len != 1) {
log.warn("invalid DECRQM command: {}", .{action});
@ -662,7 +679,7 @@ pub fn Stream(comptime Handler: type) type {
}
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});
},
@ -746,15 +763,13 @@ pub fn Stream(comptime Handler: type) type {
1 => switch (action.intermediates[0]) {
// Restore Mode
'?' => if (@hasDecl(T, "restoreMode")) {
for (action.params) |mode| {
if (modes.hasSupport(mode)) {
try self.handler.restoreMode(
@enumFromInt(mode),
);
for (action.params) |mode_int| {
if (modes.modeFromInt(mode_int, false)) |mode| {
try self.handler.restoreMode(mode);
} else {
log.warn(
"unimplemented restore mode: {}",
.{mode},
.{mode_int},
);
}
}
@ -776,15 +791,13 @@ pub fn Stream(comptime Handler: type) type {
's' => switch (action.intermediates.len) {
1 => switch (action.intermediates[0]) {
'?' => if (@hasDecl(T, "saveMode")) {
for (action.params) |mode| {
if (modes.hasSupport(mode)) {
try self.handler.saveMode(
@enumFromInt(mode),
);
for (action.params) |mode_int| {
if (modes.modeFromInt(mode_int, false)) |mode| {
try self.handler.saveMode(mode);
} else {
log.warn(
"unimplemented save mode: {}",
.{mode},
.{mode_int},
);
}
}
@ -1219,7 +1232,7 @@ test "stream: cursor right (CUF)" {
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 {
mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)),
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);
}
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" {
const H = struct {
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.
const code: u8 = code: {
if (!terminal.modes.hasSupport(mode_raw)) break :code 0;
if (self.terminal.modes.get(@enumFromInt(mode_raw))) break :code 1;
const mode = terminal.modes.modeFromInt(mode_raw, ansi) orelse break :code 0;
if (self.terminal.modes.get(mode)) break :code 1;
break :code 2;
};
var msg: termio.Message = .{ .write_small = .{} };
const resp = try std.fmt.bufPrint(
&msg.write_small.data,
"\x1B[?{};{}$y",
"\x1B[{s}{};{}$y",
.{
if (ansi) "" else "?",
mode_raw,
code,
},