vt: expose terminal modes to C API

Add modes.h with GhosttyModeTag (uint16_t) matching the Zig ModeTag
packed struct layout, along with inline helpers for constructing and
inspecting mode tags. Provide GHOSTTY_MODE_* macros for all 39
built-in modes (4 ANSI, 35 DEC), parenthesized for safety.

Add ghostty_terminal_mode_get and ghostty_terminal_mode_set to
terminal.h, both returning GhosttyResult so that null terminals
and unknown mode tags return GHOSTTY_INVALID_VALUE. The get function
writes its result through a bool out-parameter.

Add a note in the Zig mode entries reminding developers to update
modes.h when adding new modes.
This commit is contained in:
Mitchell Hashimoto
2026-03-16 15:57:39 -07:00
parent 25679f3ae7
commit 1c03770e2b
6 changed files with 203 additions and 1 deletions

View File

@@ -186,6 +186,8 @@ comptime {
@export(&c.terminal_resize, .{ .name = "ghostty_terminal_resize" });
@export(&c.terminal_vt_write, .{ .name = "ghostty_terminal_vt_write" });
@export(&c.terminal_scroll_viewport, .{ .name = "ghostty_terminal_scroll_viewport" });
@export(&c.terminal_mode_get, .{ .name = "ghostty_terminal_mode_get" });
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
// On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) {

View File

@@ -90,6 +90,8 @@ pub const terminal_reset = terminal.reset;
pub const terminal_resize = terminal.resize;
pub const terminal_vt_write = terminal.vt_write;
pub const terminal_scroll_viewport = terminal.scroll_viewport;
pub const terminal_mode_get = terminal.mode_get;
pub const terminal_mode_set = terminal.mode_set;
test {
_ = color;

View File

@@ -3,6 +3,7 @@ const testing = std.testing;
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
const modes = @import("../modes.zig");
const size = @import("../size.zig");
const Result = @import("result.zig").Result;
@@ -98,6 +99,30 @@ pub fn reset(terminal_: Terminal) callconv(.c) void {
t.fullReset();
}
pub fn mode_get(
terminal_: Terminal,
tag: modes.ModeTag.Backing,
out_value: *bool,
) callconv(.c) Result {
const t = terminal_ orelse return .invalid_value;
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(.c) Result {
const t = terminal_ orelse return .invalid_value;
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;
}
pub fn free(terminal_: Terminal) callconv(.c) void {
const t = terminal_ orelse return;
@@ -272,6 +297,87 @@ test "resize invalid value" {
try testing.expectEqual(Result.invalid_value, resize(t, 80, 0));
}
test "mode_get and mode_set" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
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);
// 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);
// 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);
try testing.expectEqual(Result.success, mode_set(t, insert, true));
try testing.expectEqual(Result.success, mode_get(t, insert, &value));
try testing.expect(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" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
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));
}
test "mode_set unknown mode" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
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));
}
test "vt_write" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(

View File

@@ -245,6 +245,9 @@ const ModeEntry = struct {
/// 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.
///
/// NOTE: When adding a new mode entry, also add a corresponding
/// GHOSTTY_MODE_* macro in include/ghostty/vt/modes.h.
const entries: []const ModeEntry = &.{
// ANSI
.{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM