mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
243 lines
8.4 KiB
Zig
243 lines
8.4 KiB
Zig
//! This file contains all the terminal modes that we support
|
|
//! and various support types for them: an enum of supported modes,
|
|
//! a packed struct to store mode values, a more generalized state
|
|
//! struct to store values plus handle save/restore, and much more.
|
|
//!
|
|
//! There is pretty heavy comptime usage and type generation here.
|
|
//! I don't love to have this sort of complexity but its a good way
|
|
//! to ensure all our various types and logic remain in sync.
|
|
|
|
const std = @import("std");
|
|
const testing = std.testing;
|
|
|
|
/// A struct that maintains the state of all the settable modes.
|
|
pub const ModeState = struct {
|
|
/// The values of the current modes.
|
|
values: ModePacked = .{},
|
|
|
|
/// The saved values. We only allow saving each mode once.
|
|
/// This is in line with other terminals that implement XTSAVE
|
|
/// and XTRESTORE. We can improve this in the future if it becomes
|
|
/// a real-world issue but we need to be aware of a DoS vector.
|
|
saved: ModePacked = .{},
|
|
|
|
/// Set a mode to a value.
|
|
pub fn set(self: *ModeState, mode: Mode, value: bool) void {
|
|
switch (mode) {
|
|
inline else => |mode_comptime| {
|
|
const entry = comptime entryForMode(mode_comptime);
|
|
@field(self.values, entry.name) = value;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get the value of a mode.
|
|
pub fn get(self: *ModeState, mode: Mode) bool {
|
|
switch (mode) {
|
|
inline else => |mode_comptime| {
|
|
const entry = comptime entryForMode(mode_comptime);
|
|
return @field(self.values, entry.name);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Save the state of the given mode. This can then be restored
|
|
/// with restore. This will only be accurate if the previous
|
|
/// mode was saved exactly once and not restored. Otherwise this
|
|
/// will just keep restoring the last stored value in memory.
|
|
pub fn save(self: *ModeState, mode: Mode) void {
|
|
switch (mode) {
|
|
inline else => |mode_comptime| {
|
|
const entry = comptime entryForMode(mode_comptime);
|
|
@field(self.saved, entry.name) = @field(self.values, entry.name);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// See save. This will return the restored value.
|
|
pub fn restore(self: *ModeState, mode: Mode) bool {
|
|
switch (mode) {
|
|
inline else => |mode_comptime| {
|
|
const entry = comptime entryForMode(mode_comptime);
|
|
@field(self.values, entry.name) = @field(self.saved, entry.name);
|
|
return @field(self.values, entry.name);
|
|
},
|
|
}
|
|
}
|
|
|
|
test {
|
|
// We have this here so that we explicitly fail when we change the
|
|
// size of modes. The size of modes is NOT particularly important,
|
|
// we just want to be mentally aware when it happens.
|
|
try std.testing.expectEqual(4, @sizeOf(ModePacked));
|
|
}
|
|
};
|
|
|
|
/// A packed struct of all the settable modes. This shouldn't
|
|
/// be used directly but rather through the ModeState struct.
|
|
pub const ModePacked = packed_struct: {
|
|
const StructField = std.builtin.Type.StructField;
|
|
var fields: [entries.len]StructField = undefined;
|
|
for (entries, 0..) |entry, i| {
|
|
fields[i] = .{
|
|
.name = entry.name,
|
|
.type = bool,
|
|
.default_value = &entry.default,
|
|
.is_comptime = false,
|
|
.alignment = 0,
|
|
};
|
|
}
|
|
|
|
break :packed_struct @Type(.{ .Struct = .{
|
|
.layout = .Packed,
|
|
.fields = &fields,
|
|
.decls = &.{},
|
|
.is_tuple = false,
|
|
} });
|
|
};
|
|
|
|
/// An enum(u16) of the available modes. See entries for available values.
|
|
pub const Mode = mode_enum: {
|
|
const EnumField = std.builtin.Type.EnumField;
|
|
var fields: [entries.len]EnumField = undefined;
|
|
for (entries, 0..) |entry, i| {
|
|
fields[i] = .{
|
|
.name = entry.name,
|
|
.value = @as(ModeTag.Backing, @bitCast(ModeTag{
|
|
.value = entry.value,
|
|
.ansi = entry.ansi,
|
|
})),
|
|
};
|
|
}
|
|
|
|
break :mode_enum @Type(.{ .Enum = .{
|
|
.tag_type = ModeTag.Backing,
|
|
.fields = &fields,
|
|
.decls = &.{},
|
|
.is_exhaustive = 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 (comptime !entry.disabled) {
|
|
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 null;
|
|
}
|
|
|
|
fn entryForMode(comptime mode: Mode) ModeEntry {
|
|
@setEvalBranchQuota(10_000);
|
|
const name = @tagName(mode);
|
|
for (entries) |entry| {
|
|
if (std.mem.eql(u8, entry.name, name)) return entry;
|
|
}
|
|
|
|
unreachable;
|
|
}
|
|
|
|
/// A single entry of a possible mode we support. This is used to
|
|
/// dynamically define the enum and other tables.
|
|
const ModeEntry = struct {
|
|
name: []const u8,
|
|
value: comptime_int,
|
|
default: bool = false,
|
|
|
|
/// True if this is an ANSI mode, false if its a DEC mode (?-prefixed).
|
|
ansi: bool = false,
|
|
|
|
/// If true, this mode is disabled and Ghostty will not allow it to be
|
|
/// set or queried. The mode enum still has it, allowing Ghostty developers
|
|
/// to develop a mode without exposing it to real users.
|
|
disabled: 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 = &.{
|
|
// ANSI
|
|
.{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM
|
|
.{ .name = "insert", .value = 4, .ansi = true },
|
|
.{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM
|
|
.{ .name = "linefeed", .value = 20, .ansi = true },
|
|
|
|
// DEC
|
|
.{ .name = "cursor_keys", .value = 1 }, // DECCKM
|
|
.{ .name = "132_column", .value = 3 },
|
|
.{ .name = "reverse_colors", .value = 5 },
|
|
.{ .name = "origin", .value = 6 },
|
|
.{ .name = "wraparound", .value = 7, .default = true },
|
|
.{ .name = "mouse_event_x10", .value = 9 },
|
|
.{ .name = "cursor_blinking", .value = 12 },
|
|
.{ .name = "cursor_visible", .value = 25, .default = true },
|
|
.{ .name = "enable_mode_3", .value = 40 },
|
|
.{ .name = "reverse_wrap", .value = 45 },
|
|
.{ .name = "keypad_keys", .value = 66 },
|
|
.{ .name = "enable_left_and_right_margin", .value = 69 },
|
|
.{ .name = "mouse_event_normal", .value = 1000 },
|
|
.{ .name = "mouse_event_button", .value = 1002 },
|
|
.{ .name = "mouse_event_any", .value = 1003 },
|
|
.{ .name = "focus_event", .value = 1004 },
|
|
.{ .name = "mouse_format_utf8", .value = 1005 },
|
|
.{ .name = "mouse_format_sgr", .value = 1006 },
|
|
.{ .name = "mouse_alternate_scroll", .value = 1007, .default = true },
|
|
.{ .name = "mouse_format_urxvt", .value = 1015 },
|
|
.{ .name = "mouse_format_sgr_pixels", .value = 1016 },
|
|
.{ .name = "alt_esc_prefix", .value = 1036, .default = true },
|
|
.{ .name = "alt_sends_escape", .value = 1039 },
|
|
.{ .name = "reverse_wrap_extended", .value = 1045 },
|
|
.{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 },
|
|
.{ .name = "bracketed_paste", .value = 2004 },
|
|
.{ .name = "synchronized_output", .value = 2026 },
|
|
.{ .name = "grapheme_cluster", .value = 2027 },
|
|
};
|
|
|
|
test {
|
|
_ = Mode;
|
|
_ = ModePacked;
|
|
}
|
|
|
|
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(14, true) == null);
|
|
}
|
|
|
|
test ModeState {
|
|
var state: ModeState = .{};
|
|
|
|
// Normal set/get
|
|
try testing.expect(!state.get(.cursor_keys));
|
|
state.set(.cursor_keys, true);
|
|
try testing.expect(state.get(.cursor_keys));
|
|
|
|
// Save/restore
|
|
state.save(.cursor_keys);
|
|
state.set(.cursor_keys, false);
|
|
try testing.expect(!state.get(.cursor_keys));
|
|
try testing.expect(state.restore(.cursor_keys));
|
|
try testing.expect(state.get(.cursor_keys));
|
|
}
|