mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
libghostty: add configurable mode defaults, remove mode_set/get
ABI BREAKING: This removes `ghostty_terminal_mode_get` and `_mode_set`. We can now represent these operations completely with standard `ghostty_terminal_get` and `ghostty_terminal_set`, which makes it much more flexible to preserve ABI in the future. This is all centered around a new `GhosttyTerminalModeConfig` structure that is an in or out parameter depending on use case. This also adds a new `GHOSTTY_TERMINAL_OPT_MODE_DEFAULT` option that can be used to set the _default_ value of mode that happens when a RIS event (full reset) is sent.
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user