mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
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:
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
},
|
||||
|
Reference in New Issue
Block a user