From 9c45c6a3d19e66809d546084f94d8dc7446f1af2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Oct 2023 15:57:56 -0700 Subject: [PATCH] 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). --- src/terminal/modes.zig | 49 ++++++++++++----- src/terminal/stream.zig | 117 ++++++++++++++++++++++++++++------------ src/termio/Exec.zig | 9 ++-- 3 files changed, 123 insertions(+), 52 deletions(-) diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 983f2e451..5d2064c7d 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -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 { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index cf7f16ab5..a2500fce0 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -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(); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 3779fc4c0..a929e3c95 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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, },