diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/modes.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/modes.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/modes.zig | 196 |
1 files changed, 136 insertions, 60 deletions
diff --git a/vendor/libghostty-vt/src/terminal/modes.zig b/vendor/libghostty-vt/src/terminal/modes.zig index c92f97b0..ed5f1b72 100644 --- a/vendor/libghostty-vt/src/terminal/modes.zig +++ b/vendor/libghostty-vt/src/terminal/modes.zig @@ -8,6 +8,8 @@ //! to ensure all our various types and logic remain in sync. const std = @import("std"); +const builtin = @import("builtin"); +const build_options = @import("terminal_options"); const testing = std.testing; /// A struct that maintains the state of all the settable modes. @@ -34,22 +36,18 @@ pub const ModeState = struct { /// 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; - }, - } + setPacked(&self.values, mode, value); + } + + /// Set the reset default and current value for a mode. + pub fn setDefault(self: *ModeState, mode: Mode, value: bool) void { + setPacked(&self.values, mode, value); + setPacked(&self.default, mode, value); } /// Get the value of a mode. pub fn get(self: *const ModeState, mode: Mode) bool { - switch (mode) { - inline else => |mode_comptime| { - const entry = comptime entryForMode(mode_comptime); - return @field(self.values, entry.name); - }, - } + return getPacked(&self.values, mode); } /// Save the state of the given mode. This can then be restored @@ -79,6 +77,19 @@ pub const ModeState = struct { /// Return a DECRPM report for the given mode tag. If the tag does /// not correspond to a known mode, the report state is .not_recognized. pub fn getReport(self: *const ModeState, tag: ModeTag) Report { + // DECECM (Erase Color Mode, DEC private mode 117) controls whether erasing + // and scrolling use the default background or the active background color. + // Ghostty's behavior is fixed equivalent to DECECM reset, and DECRQM has a + // "permanently reset" response for recognized modes that cannot be changed. + // Report that instead of "not recognized" so applications can query and adapt + // to Ghostty's erase-color behavior. + // + // See VT520/VT525 Programmer Information, "Erase Color" and DECRQM/DECRPM: + // https://web.mit.edu/dosathena/doc/www/ek-vt520-rm.pdf + + if (!tag.ansi and tag.value == 117) { + return .{ .tag = tag, .state = .permanently_reset }; + } const mode = modeFromInt(tag.value, tag.ansi) orelse return .{ .tag = tag, .state = .not_recognized, @@ -97,49 +108,53 @@ pub const ModeState = struct { } }; +fn setPacked(values: *ModePacked, mode: Mode, value: bool) void { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + @field(values, entry.name) = value; + }, + } +} + +fn getPacked(values: *const ModePacked, mode: Mode) bool { + switch (mode) { + inline else => |mode_comptime| { + const entry = comptime entryForMode(mode_comptime); + return @field(values, entry.name); + }, + } +} + /// 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_ptr = &entry.default, - .is_comptime = false, - .alignment = 0, - }; + var names: [entries.len][]const u8 = undefined; + var types = [_]type{bool} ** entries.len; + var attrs: [entries.len]StructField.Attributes = undefined; + + for (entries, &names, &attrs) |entry, *name, *attr| { + name.* = entry.name; + attr.* = .{ .default_value_ptr = &entry.default }; } - break :packed_struct @Type(.{ .@"struct" = .{ - .layout = .@"packed", - .fields = &fields, - .decls = &.{}, - .is_tuple = false, - } }); + break :packed_struct @Struct(.@"packed", null, &names, &types, &attrs); }; /// 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, - })), - }; + var names: [entries.len][]const u8 = undefined; + var values: [entries.len]ModeTag.Backing = undefined; + for (entries, &names, &values) |entry, *name, *value| { + name.* = entry.name; + value.* = @bitCast(ModeTag{ + .value = entry.value, + .ansi = entry.ansi, + }); } - break :mode_enum @Type(.{ .@"enum" = .{ - .tag_type = ModeTag.Backing, - .fields = &fields, - .decls = &.{}, - .is_exhaustive = true, - } }); + break :mode_enum @Enum(ModeTag.Backing, .exhaustive, &names, &values); }; /// The tag type for our enum is a u16 but we use a packed struct @@ -240,8 +255,24 @@ const ModeEntry = struct { /// 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, + + /// Whether an embedder may safely configure this bit as reset policy. + /// Modes that perform a transition or mirror additional terminal state + /// must use their semantic configuration API instead. + default_configurable: bool = true, }; +/// Return whether a mode can safely be configured as a reset default by an +/// embedder. This excludes modes whose set/reset operations have side effects +/// beyond changing the mode bit. +pub fn defaultConfigurable(mode: Mode) bool { + switch (mode) { + inline else => |mode_comptime| { + return comptime entryForMode(mode_comptime).default_configurable; + }, + } +} + /// 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. @@ -257,46 +288,58 @@ const entries: []const ModeEntry = &.{ // DEC .{ .name = "cursor_keys", .value = 1 }, // DECCKM - .{ .name = "132_column", .value = 3 }, + .{ .name = "132_column", .value = 3, .default_configurable = false }, .{ .name = "slow_scroll", .value = 4 }, .{ .name = "reverse_colors", .value = 5 }, - .{ .name = "origin", .value = 6 }, + .{ .name = "origin", .value = 6, .default_configurable = false }, .{ .name = "wraparound", .value = 7, .default = true }, .{ .name = "autorepeat", .value = 8 }, - .{ .name = "mouse_event_x10", .value = 9 }, - .{ .name = "cursor_blinking", .value = 12 }, + .{ .name = "mouse_event_x10", .value = 9, .default_configurable = false }, + .{ .name = "cursor_blinking", .value = 12, .default_configurable = false }, .{ .name = "cursor_visible", .value = 25, .default = true }, .{ .name = "enable_mode_3", .value = 40 }, .{ .name = "reverse_wrap", .value = 45 }, - .{ .name = "alt_screen_legacy", .value = 47 }, + .{ .name = "alt_screen_legacy", .value = 47, .default_configurable = false }, .{ .name = "keypad_keys", .value = 66 }, // DEC Backarrow Key Mode (DECBKM) // See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170 // If `false` (the default), `backspace` emits 0x7f // If `true`, `backspace` emits 0x08 .{ .name = "backarrow_key_mode", .value = 67 }, - .{ .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 = "enable_left_and_right_margin", .value = 69, .default_configurable = false }, + .{ .name = "mouse_event_normal", .value = 1000, .default_configurable = false }, + .{ .name = "mouse_event_button", .value = 1002, .default_configurable = false }, + .{ .name = "mouse_event_any", .value = 1003, .default_configurable = false }, .{ .name = "focus_event", .value = 1004 }, - .{ .name = "mouse_format_utf8", .value = 1005 }, - .{ .name = "mouse_format_sgr", .value = 1006 }, + .{ .name = "mouse_format_utf8", .value = 1005, .default_configurable = false }, + .{ .name = "mouse_format_sgr", .value = 1006, .default_configurable = false }, .{ .name = "mouse_alternate_scroll", .value = 1007, .default = true }, - .{ .name = "mouse_format_urxvt", .value = 1015 }, - .{ .name = "mouse_format_sgr_pixels", .value = 1016 }, + .{ .name = "mouse_format_urxvt", .value = 1015, .default_configurable = false }, + .{ .name = "mouse_format_sgr_pixels", .value = 1016, .default_configurable = false }, .{ .name = "ignore_keypad_with_numlock", .value = 1035, .default = true }, .{ .name = "alt_esc_prefix", .value = 1036, .default = true }, .{ .name = "alt_sends_escape", .value = 1039 }, .{ .name = "reverse_wrap_extended", .value = 1045 }, - .{ .name = "alt_screen", .value = 1047 }, - .{ .name = "save_cursor", .value = 1048 }, - .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 }, + .{ .name = "alt_screen", .value = 1047, .default_configurable = false }, + .{ .name = "save_cursor", .value = 1048, .default_configurable = false }, + .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049, .default_configurable = false }, .{ .name = "bracketed_paste", .value = 2004 }, - .{ .name = "synchronized_output", .value = 2026 }, + .{ .name = "synchronized_output", .value = 2026, .default_configurable = false }, .{ .name = "grapheme_cluster", .value = 2027 }, .{ .name = "report_color_scheme", .value = 2031 }, + .{ .name = "report_visibility", .value = 2033, .default_configurable = false }, .{ .name = "in_band_size_reports", .value = 2048 }, + // Kitty clipboard protocol paste events. When set, a user-initiated + // paste sends an unsolicited OSC 5522 targets listing with a + // one-time password instead of pasting the text. + // See https://sw.kovidgoyal.net/kitty/clipboard/ + .{ + .name = "kitty_paste_events", + .value = 5522, + // The macOS app and libghostty-vt can both serve the follow-up + // Kitty clipboard read that a paste event grants. + .disabled = build_options.artifact != .lib and builtin.os.tag != .macos, + }, }; test { @@ -327,6 +370,32 @@ test ModeState { try testing.expect(state.get(.cursor_keys)); } +test "ModeState set default updates current and reset value" { + var state: ModeState = .{}; + + state.setDefault(.grapheme_cluster, true); + try testing.expect(state.get(.grapheme_cluster)); + try testing.expect(state.default.grapheme_cluster); + + state.set(.grapheme_cluster, false); + try testing.expect(!state.get(.grapheme_cluster)); + try testing.expect(state.default.grapheme_cluster); + + state.setDefault(.grapheme_cluster, true); + try testing.expect(state.get(.grapheme_cluster)); + + state.set(.grapheme_cluster, false); + state.reset(); + try testing.expect(state.get(.grapheme_cluster)); +} + +test "default configurable modes" { + try testing.expect(defaultConfigurable(.grapheme_cluster)); + try testing.expect(defaultConfigurable(.wraparound)); + try testing.expect(!defaultConfigurable(.alt_screen)); + try testing.expect(!defaultConfigurable(.cursor_blinking)); +} + test "getReport known DEC mode" { var state: ModeState = .{}; const report = state.getReport(.{ .value = 1 }); @@ -347,6 +416,13 @@ test "getReport known ANSI mode" { try testing.expectEqual(true, report.tag.ansi); } +test "getReport DECECM permanently reset" { + const state: ModeState = .{}; + const report = state.getReport(.{ .value = 117, .ansi = false }); + try testing.expectEqual(Report.State.permanently_reset, report.state); + try testing.expectEqual(false, report.tag.ansi); +} + test "getReport unknown mode" { const state: ModeState = .{}; const report = state.getReport(.{ .value = 9999 }); |
