aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/modes.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/modes.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig196
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 });