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| {
|
for (entries, 0..) |entry, i| {
|
||||||
fields[i] = .{
|
fields[i] = .{
|
||||||
.name = entry.name,
|
.name = entry.name,
|
||||||
.value = entry.value,
|
.value = @as(ModeTag.Backing, @bitCast(ModeTag{
|
||||||
|
.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 {
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user