diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 7e88373dc..2c783974b 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -695,6 +695,24 @@ typedef void (*GhosttyTerminalWritePtyFn)(GhosttyTerminal terminal, typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal, void* userdata); +/** + * A terminal mode and boolean value used for mode configuration and queries. + * + * For GHOSTTY_TERMINAL_DATA_MODE, initialize `mode` before calling + * ghostty_terminal_get(). On success, `value` contains the current mode value. + * + * This struct has a frozen layout and will not gain fields in future versions. + * + * @ingroup terminal + */ +typedef struct { + /** Mode to configure or query. */ + GhosttyMode mode; + + /** Value to set, or the current value returned by a query. */ + bool value; +} GhosttyTerminalModeConfig; + /** * Terminal option identifiers. * @@ -1060,6 +1078,31 @@ typedef enum GHOSTTY_ENUM_TYPED { * Input type: bool* */ GHOSTTY_TERMINAL_OPT_TITLE_REPORT = 32, + + /** + * Set the reset default for a terminal mode. + * + * This unconditionally updates both the current value and the value restored + * by a full terminal reset (RIS). + * + * Some recognized modes represent transitions or mirror additional terminal + * state and cannot safely be configured as reset defaults. Those modes return + * GHOSTTY_INVALID_VALUE. A NULL value pointer also returns + * GHOSTTY_INVALID_VALUE. + * + * Input type: GhosttyTerminalModeConfig* + */ + GHOSTTY_TERMINAL_OPT_MODE_DEFAULT = 33, + + /** + * Set the current value of a terminal mode. + * + * This does not change the value restored by a full terminal reset (RIS). + * A NULL value pointer or unknown mode returns GHOSTTY_INVALID_VALUE. + * + * Input type: GhosttyTerminalModeConfig* + */ + GHOSTTY_TERMINAL_OPT_MODE = 34, GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalOption; @@ -1420,6 +1463,17 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: size_t * */ GHOSTTY_TERMINAL_DATA_CONTINUATION_MAX_BYTES = 36, + + /** + * Get the current value of a terminal mode. + * + * The caller must initialize the `mode` field. On success, the `value` field + * is updated with the current value. A NULL pointer or unknown mode returns + * GHOSTTY_INVALID_VALUE. + * + * Input/output type: GhosttyTerminalModeConfig * + */ + GHOSTTY_TERMINAL_DATA_MODE = 37, GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalData; @@ -1709,41 +1763,6 @@ GHOSTTY_API GhosttyResult ghostty_terminal_compress( GhosttyTerminalCompressionMode mode, GhosttyTerminalCompressionResult* out_result); -/** - * Get the current value of a terminal mode. - * - * Returns the value of the mode identified by the given mode. - * - * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) - * @param mode The mode identifying the mode to query - * @param[out] out_value On success, set to true if the mode is set, false - * if it is reset - * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal - * is NULL or the mode does not correspond to a known mode - * - * @ingroup terminal - */ -GHOSTTY_API GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal, - GhosttyMode mode, - bool* out_value); - -/** - * Set the value of a terminal mode. - * - * Sets the mode identified by the given mode to the specified value. - * - * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) - * @param mode The mode identifying the mode to set - * @param value true to set the mode, false to reset it - * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal - * is NULL or the mode does not correspond to a known mode - * - * @ingroup terminal - */ -GHOSTTY_API GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, - GhosttyMode mode, - bool value); - /** * Get data from a terminal instance. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 58be6b3e6..ab04d5c4a 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -284,8 +284,6 @@ comptime { @export(&c.terminal_scroll_viewport, .{ .name = "ghostty_terminal_scroll_viewport" }); @export(&c.terminal_compression_activity, .{ .name = "ghostty_terminal_compression_activity" }); @export(&c.terminal_compress, .{ .name = "ghostty_terminal_compress" }); - @export(&c.terminal_mode_get, .{ .name = "ghostty_terminal_mode_get" }); - @export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" }); @export(&c.terminal_get, .{ .name = "ghostty_terminal_get" }); @export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" }); @export(&c.terminal_continuation_write, .{ .name = "ghostty_terminal_continuation_write" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index a217cffc7..d6f9e6656 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -187,8 +187,6 @@ pub const terminal_vt_write = terminal.vt_write; pub const terminal_scroll_viewport = terminal.scroll_viewport; pub const terminal_compression_activity = terminal.compression_activity; pub const terminal_compress = terminal.compress; -pub const terminal_mode_get = terminal.mode_get; -pub const terminal_mode_set = terminal.mode_set; pub const terminal_get = terminal.get; pub const terminal_get_multi = terminal.get_multi; pub const terminal_continuation_write = terminal.continuation_write; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 98acbd84c..63c996c0a 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -148,6 +148,19 @@ pub const ProgressReport = extern struct { progress: i8, }; +/// A terminal mode and boolean value used for mode configuration. +/// +/// C: GhosttyTerminalModeConfig +pub const ModeConfig = extern struct { + mode: modes.ModeTag.Backing, + value: bool, + + fn toMode(self: ModeConfig) ?modes.Mode { + const tag: modes.ModeTag = @bitCast(self.mode); + return modes.modeFromInt(tag.value, tag.ansi); + } +}; + /// C callback state for terminal effects. Trampolines are always /// installed on the stream handler; they check these fields and /// no-op when the corresponding callback is null. @@ -890,6 +903,8 @@ pub const Option = enum(c_int) { progress_report = 30, continuation_max_bytes = 31, title_report = 32, + mode_default = 33, + mode = 34, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -926,6 +941,7 @@ pub const Option = enum(c_int) { .selection => ?*const selection_c.CSelection, .default_cursor_style => ?*const TerminalCursorStyle, .default_cursor_blink => ?*const bool, + .mode, .mode_default => ?*const ModeConfig, }; } }; @@ -1090,6 +1106,18 @@ fn setTyped( wrapper, if (value) |ptr| ptr.* else default_continuation_max_bytes, ), + .mode, .mode_default => { + const config = (value orelse return .invalid_value).*; + const mode = config.toMode() orelse return .invalid_value; + switch (option) { + .mode => wrapper.terminal.modes.set(mode, config.value), + .mode_default => { + if (!modes.defaultConfigurable(mode)) return .invalid_value; + wrapper.terminal.modes.setDefault(mode, config.value); + }, + else => unreachable, + } + }, } return .success; } @@ -1160,30 +1188,6 @@ pub fn reset(terminal_: Terminal) callconv(lib.calling_conv) void { t.fullReset(); } -pub fn mode_get( - terminal_: Terminal, - tag: modes.ModeTag.Backing, - out_value: *bool, -) callconv(lib.calling_conv) Result { - const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; - const mode_tag: modes.ModeTag = @bitCast(tag); - const mode = modes.modeFromInt(mode_tag.value, mode_tag.ansi) orelse return .invalid_value; - out_value.* = t.modes.get(mode); - return .success; -} - -pub fn mode_set( - terminal_: Terminal, - tag: modes.ModeTag.Backing, - value: bool, -) callconv(lib.calling_conv) Result { - const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; - const mode_tag: modes.ModeTag = @bitCast(tag); - const mode = modes.modeFromInt(mode_tag.value, mode_tag.ansi) orelse return .invalid_value; - t.modes.set(mode, value); - return .success; -} - /// C: GhosttyKittyGraphics pub const KittyGraphics = kitty_gfx_c.KittyGraphics; @@ -1232,6 +1236,7 @@ pub const TerminalData = enum(c_int) { scrollback_max_bytes = 34, scrollback_max_lines = 35, continuation_max_bytes = 36, + mode = 37, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { @@ -1271,6 +1276,7 @@ pub const TerminalData = enum(c_int) { .kitty_image_medium_temp_file => lib.String, .kitty_graphics => KittyGraphics, .selection => selection_c.CSelection, + .mode => ModeConfig, }; } }; @@ -1287,12 +1293,14 @@ pub fn get( }; } + const out_ptr = out orelse return .invalid_value; + return switch (data) { .invalid => .invalid_value, inline else => |comptime_data| getTyped( terminal_, comptime_data, - @ptrCast(@alignCast(out)), + @ptrCast(@alignCast(out_ptr)), ), }; } @@ -1401,6 +1409,10 @@ fn getTyped( out.* = max; }, .continuation_max_bytes => out.* = continuationMaxBytes(wrapper), + .mode => { + const mode = out.toMode() orelse return .invalid_value; + out.value = t.modes.get(mode); + }, } return .success; @@ -2230,7 +2242,7 @@ test "resize shrinks both axes with cursor at bottom" { try testing.expectEqual(23, t.?.terminal.rows); } -test "mode_get and mode_set" { +test "set and get mode" { var t: Terminal = null; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, @@ -2240,40 +2252,31 @@ test "mode_get and mode_set" { )); defer free(t); - var value: bool = undefined; - // DEC mode 25 (cursor_visible) defaults to true const cursor_visible: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }); - try testing.expectEqual(Result.success, mode_get(t, cursor_visible, &value)); - try testing.expect(value); + var config: ModeConfig = .{ .mode = cursor_visible, .value = undefined }; + try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config))); + try testing.expect(config.value); // Set it to false - try testing.expectEqual(Result.success, mode_set(t, cursor_visible, false)); - try testing.expectEqual(Result.success, mode_get(t, cursor_visible, &value)); - try testing.expect(!value); + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config))); + try testing.expect(!config.value); // ANSI mode 4 (insert) defaults to false const insert: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 4, .ansi = true }); - try testing.expectEqual(Result.success, mode_get(t, insert, &value)); - try testing.expect(!value); + config.mode = insert; + try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config))); + try testing.expect(!config.value); - try testing.expectEqual(Result.success, mode_set(t, insert, true)); - try testing.expectEqual(Result.success, mode_get(t, insert, &value)); - try testing.expect(value); + config.value = true; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + try testing.expectEqual(Result.success, get(t, .mode, @ptrCast(&config))); + try testing.expect(config.value); } -test "mode_get null" { - var value: bool = undefined; - const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }); - try testing.expectEqual(Result.invalid_value, mode_get(null, tag, &value)); -} - -test "mode_set null" { - const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }); - try testing.expectEqual(Result.invalid_value, mode_set(null, tag, true)); -} - -test "mode_get unknown mode" { +test "set mode default updates current and reset value" { var t: Terminal = null; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, @@ -2283,12 +2286,49 @@ test "mode_get unknown mode" { )); defer free(t); - var value: bool = undefined; - const unknown: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }); - try testing.expectEqual(Result.invalid_value, mode_get(t, unknown, &value)); + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ + .value = 2027, + .ansi = false, + }); + var config: ModeConfig = .{ .mode = tag, .value = true }; + try testing.expectEqual(Result.success, set( + t, + .mode_default, + @ptrCast(&config), + )); + try testing.expect(t.?.terminal.modes.get(.grapheme_cluster)); + try testing.expect(t.?.terminal.modes.default.grapheme_cluster); + + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + try testing.expect(!t.?.terminal.modes.get(.grapheme_cluster)); + try testing.expect(t.?.terminal.modes.default.grapheme_cluster); + + // Setting the default also unconditionally replaces the current value. + config.value = true; + try testing.expectEqual(Result.success, set( + t, + .mode_default, + @ptrCast(&config), + )); + try testing.expect(t.?.terminal.modes.get(.grapheme_cluster)); + + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + vt_write(t, "\x1bc", 2); + try testing.expect(t.?.terminal.modes.get(.grapheme_cluster)); + + config.value = false; + try testing.expectEqual(Result.success, set( + t, + .mode_default, + @ptrCast(&config), + )); + try testing.expect(!t.?.terminal.modes.get(.grapheme_cluster)); + try testing.expect(!t.?.terminal.modes.default.grapheme_cluster); } -test "mode_set unknown mode" { +test "set mode default validates configuration" { var t: Terminal = null; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, @@ -2298,8 +2338,56 @@ test "mode_set unknown mode" { )); defer free(t); - const unknown: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }); - try testing.expectEqual(Result.invalid_value, mode_set(t, unknown, true)); + try testing.expectEqual(Result.invalid_value, set( + t, + .mode_default, + null, + )); + + var config: ModeConfig = .{ + .mode = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }), + .value = true, + }; + try testing.expectEqual(Result.invalid_value, set( + t, + .mode_default, + @ptrCast(&config), + )); + + config.mode = @bitCast(modes.ModeTag{ .value = 1047, .ansi = false }); + try testing.expectEqual(Result.invalid_value, set( + t, + .mode_default, + @ptrCast(&config), + )); +} + +test "set and get mode null" { + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }); + var config: ModeConfig = .{ .mode = tag, .value = true }; + try testing.expectEqual(Result.invalid_value, set(null, .mode, @ptrCast(&config))); + try testing.expectEqual(Result.invalid_value, get(null, .mode, @ptrCast(&config))); +} + +test "set and get mode validate configuration" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + 80, + 24, + )); + defer free(t); + + try testing.expectEqual(Result.invalid_value, set(t, .mode, null)); + try testing.expectEqual(Result.invalid_value, get(t, .mode, null)); + + var config: ModeConfig = .{ + .mode = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }), + .value = true, + }; + try testing.expectEqual(Result.invalid_value, set(t, .mode, @ptrCast(&config))); + try testing.expectEqual(Result.invalid_value, get(t, .mode, @ptrCast(&config))); } test "vt_write" { @@ -2464,8 +2552,11 @@ test "get cursor_visible" { try testing.expect(visible); // DEC mode 25 controls cursor visibility - const cursor_visible_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }); - try testing.expectEqual(Result.success, mode_set(t, cursor_visible_mode, false)); + var config: ModeConfig = .{ + .mode = @bitCast(modes.ModeTag{ .value = 25, .ansi = false }), + .value = false, + }; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible))); try testing.expect(!visible); } @@ -2521,34 +2612,50 @@ test "get mouse_tracking" { try testing.expect(!tracking); // Enable X10 mouse (DEC mode 9) - const x10_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9, .ansi = false }); - try testing.expectEqual(Result.success, mode_set(t, x10_mode, true)); + var config: ModeConfig = .{ + .mode = @bitCast(modes.ModeTag{ .value = 9, .ansi = false }), + .value = true, + }; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking))); try testing.expect(tracking); // Disable X10, enable normal mouse (DEC mode 1000) - try testing.expectEqual(Result.success, mode_set(t, x10_mode, false)); - const normal_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1000, .ansi = false }); - try testing.expectEqual(Result.success, mode_set(t, normal_mode, true)); + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + config = .{ + .mode = @bitCast(modes.ModeTag{ .value = 1000, .ansi = false }), + .value = true, + }; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking))); try testing.expect(tracking); // Disable normal, enable button mouse (DEC mode 1002) - try testing.expectEqual(Result.success, mode_set(t, normal_mode, false)); - const button_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1002, .ansi = false }); - try testing.expectEqual(Result.success, mode_set(t, button_mode, true)); + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + config = .{ + .mode = @bitCast(modes.ModeTag{ .value = 1002, .ansi = false }), + .value = true, + }; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking))); try testing.expect(tracking); // Disable button, enable any mouse (DEC mode 1003) - try testing.expectEqual(Result.success, mode_set(t, button_mode, false)); - const any_mode: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1003, .ansi = false }); - try testing.expectEqual(Result.success, mode_set(t, any_mode, true)); + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); + config = .{ + .mode = @bitCast(modes.ModeTag{ .value = 1003, .ansi = false }), + .value = true, + }; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking))); try testing.expect(tracking); // Disable all - should be false again - try testing.expectEqual(Result.success, mode_set(t, any_mode, false)); + config.value = false; + try testing.expectEqual(Result.success, set(t, .mode, @ptrCast(&config))); try testing.expectEqual(Result.success, get(t, .mouse_tracking, @ptrCast(&tracking))); try testing.expect(!tracking); } diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index 43f553d8b..40a5b63e0 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -71,6 +71,7 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: { .{ "GhosttyStyle", StructInfo.init(style_c.Style) }, .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) }, .{ "GhosttyTerminalDesktopNotification", StructInfo.init(terminal.DesktopNotification) }, + .{ "GhosttyTerminalModeConfig", StructInfo.init(terminal.ModeConfig) }, .{ "GhosttyTerminalProgressReport", StructInfo.init(terminal.ProgressReport) }, .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) }, .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) }, @@ -221,6 +222,7 @@ test "json parses" { try std.testing.expect(root.contains("GhosttyClipboardContent")); try std.testing.expect(root.contains("GhosttyClipboardWrite")); try std.testing.expect(root.contains("GhosttyFormatterTerminalOptions")); + try std.testing.expect(root.contains("GhosttyTerminalModeConfig")); try std.testing.expect(root.contains("GhosttyReader")); try std.testing.expect(root.contains("GhosttyWriter")); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 621d536e4..c545b3304 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -34,22 +34,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 @@ -110,6 +106,24 @@ 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: { @@ -239,8 +253,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. @@ -256,46 +286,46 @@ 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 }, + .{ .name = "report_visibility", .value = 2033, .default_configurable = false }, .{ .name = "in_band_size_reports", .value = 2048 }, }; @@ -327,6 +357,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 });