From 1c03770e2be4700ee60db01750a323005ef5dc8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Mar 2026 15:57:39 -0700 Subject: [PATCH] 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. --- include/ghostty/vt/modes.h | 52 +++++++++++++++++ include/ghostty/vt/terminal.h | 39 ++++++++++++- src/lib_vt.zig | 2 + src/terminal/c/main.zig | 2 + src/terminal/c/terminal.zig | 106 ++++++++++++++++++++++++++++++++++ src/terminal/modes.zig | 3 + 6 files changed, 203 insertions(+), 1 deletion(-) diff --git a/include/ghostty/vt/modes.h b/include/ghostty/vt/modes.h index a269f5864..b1fcbf097 100644 --- a/include/ghostty/vt/modes.h +++ b/include/ghostty/vt/modes.h @@ -53,6 +53,58 @@ extern "C" { #endif +/** @name ANSI Mode Tags + * Mode tags for standard ANSI modes. + * @{ + */ +#define GHOSTTY_MODE_KAM (ghostty_mode_tag_new(2, true)) /**< Keyboard action (disable keyboard) */ +#define GHOSTTY_MODE_INSERT (ghostty_mode_tag_new(4, true)) /**< Insert mode */ +#define GHOSTTY_MODE_SRM (ghostty_mode_tag_new(12, true)) /**< Send/receive mode */ +#define GHOSTTY_MODE_LINEFEED (ghostty_mode_tag_new(20, true)) /**< Linefeed/new line mode */ +/** @} */ + +/** @name DEC Private Mode Tags + * Mode tags for DEC private modes (?-prefixed). + * @{ + */ +#define GHOSTTY_MODE_DECCKM (ghostty_mode_tag_new(1, false)) /**< Cursor keys */ +#define GHOSTTY_MODE_132_COLUMN (ghostty_mode_tag_new(3, false)) /**< 132/80 column mode */ +#define GHOSTTY_MODE_SLOW_SCROLL (ghostty_mode_tag_new(4, false)) /**< Slow scroll */ +#define GHOSTTY_MODE_REVERSE_COLORS (ghostty_mode_tag_new(5, false)) /**< Reverse video */ +#define GHOSTTY_MODE_ORIGIN (ghostty_mode_tag_new(6, false)) /**< Origin mode */ +#define GHOSTTY_MODE_WRAPAROUND (ghostty_mode_tag_new(7, false)) /**< Auto-wrap mode */ +#define GHOSTTY_MODE_AUTOREPEAT (ghostty_mode_tag_new(8, false)) /**< Auto-repeat keys */ +#define GHOSTTY_MODE_X10_MOUSE (ghostty_mode_tag_new(9, false)) /**< X10 mouse reporting */ +#define GHOSTTY_MODE_CURSOR_BLINKING (ghostty_mode_tag_new(12, false)) /**< Cursor blink */ +#define GHOSTTY_MODE_CURSOR_VISIBLE (ghostty_mode_tag_new(25, false)) /**< Cursor visible (DECTCEM) */ +#define GHOSTTY_MODE_ENABLE_MODE_3 (ghostty_mode_tag_new(40, false)) /**< Allow 132 column mode */ +#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_tag_new(45, false)) /**< Reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_tag_new(47, false)) /**< Alternate screen (legacy) */ +#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_tag_new(66, false)) /**< Application keypad */ +#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_tag_new(69, false)) /**< Left/right margin mode */ +#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_tag_new(1000, false)) /**< Normal mouse tracking */ +#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_tag_new(1002, false)) /**< Button-event mouse tracking */ +#define GHOSTTY_MODE_ANY_MOUSE (ghostty_mode_tag_new(1003, false)) /**< Any-event mouse tracking */ +#define GHOSTTY_MODE_FOCUS_EVENT (ghostty_mode_tag_new(1004, false)) /**< Focus in/out events */ +#define GHOSTTY_MODE_UTF8_MOUSE (ghostty_mode_tag_new(1005, false)) /**< UTF-8 mouse format */ +#define GHOSTTY_MODE_SGR_MOUSE (ghostty_mode_tag_new(1006, false)) /**< SGR mouse format */ +#define GHOSTTY_MODE_ALT_SCROLL (ghostty_mode_tag_new(1007, false)) /**< Alternate scroll mode */ +#define GHOSTTY_MODE_URXVT_MOUSE (ghostty_mode_tag_new(1015, false)) /**< URxvt mouse format */ +#define GHOSTTY_MODE_SGR_PIXELS_MOUSE (ghostty_mode_tag_new(1016, false)) /**< SGR-Pixels mouse format */ +#define GHOSTTY_MODE_NUMLOCK_KEYPAD (ghostty_mode_tag_new(1035, false)) /**< Ignore keypad with NumLock */ +#define GHOSTTY_MODE_ALT_ESC_PREFIX (ghostty_mode_tag_new(1036, false)) /**< Alt key sends ESC prefix */ +#define GHOSTTY_MODE_ALT_SENDS_ESC (ghostty_mode_tag_new(1039, false)) /**< Alt sends escape */ +#define GHOSTTY_MODE_REVERSE_WRAP_EXT (ghostty_mode_tag_new(1045, false)) /**< Extended reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN (ghostty_mode_tag_new(1047, false)) /**< Alternate screen */ +#define GHOSTTY_MODE_SAVE_CURSOR (ghostty_mode_tag_new(1048, false)) /**< Save cursor (DECSC) */ +#define GHOSTTY_MODE_ALT_SCREEN_SAVE (ghostty_mode_tag_new(1049, false)) /**< Alt screen + save cursor + clear */ +#define GHOSTTY_MODE_BRACKETED_PASTE (ghostty_mode_tag_new(2004, false)) /**< Bracketed paste mode */ +#define GHOSTTY_MODE_SYNC_OUTPUT (ghostty_mode_tag_new(2026, false)) /**< Synchronized output */ +#define GHOSTTY_MODE_GRAPHEME_CLUSTER (ghostty_mode_tag_new(2027, false)) /**< Grapheme cluster mode */ +#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_tag_new(2031, false)) /**< Report color scheme */ +#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_tag_new(2048, false)) /**< In-band size reports */ +/** @} */ + /** * A packed 16-bit terminal mode tag. * diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 8c91920a0..d3dc9cffa 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -7,10 +7,12 @@ #ifndef GHOSTTY_VT_TERMINAL_H #define GHOSTTY_VT_TERMINAL_H +#include #include #include #include #include +#include #ifdef __cplusplus extern "C" { @@ -192,7 +194,42 @@ void ghostty_terminal_vt_write(GhosttyTerminal terminal, * @ingroup terminal */ void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal, - GhosttyTerminalScrollViewport behavior); + GhosttyTerminalScrollViewport behavior); + +/** + * Get the current value of a terminal mode. + * + * Returns the value of the mode identified by the given mode tag. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param tag The mode tag 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 tag does not correspond to a known mode + * + * @ingroup terminal + */ +GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal, + GhosttyModeTag tag, + bool* out_value); + +/** + * Set the value of a terminal mode. + * + * Sets the mode identified by the given mode tag to the specified value. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param tag The mode tag 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 tag does not correspond to a known mode + * + * @ingroup terminal + */ +GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, + GhosttyModeTag tag, + bool value); /** @} */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 9d8e94bba..0951ae38e 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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()) { diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 133acbd1f..1b2ae79a9 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 0af791f91..c3c084c52 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -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( diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 8d6bd63d9..0d8cde5b7 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -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