From 25679f3ae736d94a93d4778fbe3aa095633cd5cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Mar 2026 15:47:57 -0700 Subject: [PATCH 1/4] vt: add C API header for terminal mode tags Add modes.h with GhosttyModeTag, a uint16_t typedef matching the Zig ModeTag packed struct layout (bits 0-14 for the mode value, bit 15 for the ANSI flag). Three inline helper functions provide construction and inspection: ghostty_mode_tag_new, ghostty_mode_tag_value, and ghostty_mode_tag_ansi. --- include/ghostty/vt.h | 1 + include/ghostty/vt/modes.h | 108 +++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 include/ghostty/vt/modes.h diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index cc02a2886..34a7f2d4b 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -99,6 +99,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/include/ghostty/vt/modes.h b/include/ghostty/vt/modes.h new file mode 100644 index 000000000..a269f5864 --- /dev/null +++ b/include/ghostty/vt/modes.h @@ -0,0 +1,108 @@ +/** + * @file modes.h + * + * Terminal mode tag utilities - pack and unpack ANSI/DEC mode identifiers. + */ + +#ifndef GHOSTTY_VT_MODES_H +#define GHOSTTY_VT_MODES_H + +/** @defgroup modes Mode Utilities + * + * Utilities for working with terminal mode tags. A mode tag is a compact + * 16-bit representation of a terminal mode identifier that encodes both + * the numeric mode value (up to 15 bits) and whether the mode is an ANSI + * mode or a DEC private mode (?-prefixed). + * + * The packed layout (least-significant bit first) is: + * - Bits 0–14: mode value (u15) + * - Bit 15: ANSI flag (0 = DEC private mode, 1 = ANSI mode) + * + * ## Example + * + * @code{.c} + * #include + * #include + * + * int main() { + * // Create a tag for DEC mode 25 (cursor visible) + * GhosttyModeTag tag = ghostty_mode_tag_new(25, false); + * printf("value=%u ansi=%d packed=0x%04x\n", + * ghostty_mode_tag_value(tag), + * ghostty_mode_tag_ansi(tag), + * tag); + * + * // Create a tag for ANSI mode 4 (insert mode) + * GhosttyModeTag ansi_tag = ghostty_mode_tag_new(4, true); + * printf("value=%u ansi=%d packed=0x%04x\n", + * ghostty_mode_tag_value(ansi_tag), + * ghostty_mode_tag_ansi(ansi_tag), + * ansi_tag); + * + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A packed 16-bit terminal mode tag. + * + * Encodes a mode value (bits 0–14) and an ANSI flag (bit 15) into a + * single 16-bit integer. Use the inline helper functions to construct + * and inspect mode tags rather than manipulating bits directly. + */ +typedef uint16_t GhosttyModeTag; + +/** + * Create a mode tag from a mode value and ANSI flag. + * + * @param value The numeric mode value (0–32767) + * @param ansi true for an ANSI mode, false for a DEC private mode + * @return The packed mode tag + * + * @ingroup modes + */ +static inline GhosttyModeTag ghostty_mode_tag_new(uint16_t value, bool ansi) { + return (GhosttyModeTag)((value & 0x7FFF) | ((uint16_t)ansi << 15)); +} + +/** + * Extract the numeric mode value from a mode tag. + * + * @param tag The mode tag + * @return The mode value (0–32767) + * + * @ingroup modes + */ +static inline uint16_t ghostty_mode_tag_value(GhosttyModeTag tag) { + return tag & 0x7FFF; +} + +/** + * Check whether a mode tag represents an ANSI mode. + * + * @param tag The mode tag + * @return true if this is an ANSI mode, false if it is a DEC private mode + * + * @ingroup modes + */ +static inline bool ghostty_mode_tag_ansi(GhosttyModeTag tag) { + return (tag >> 15) != 0; +} + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_MODES_H */ From 1c03770e2be4700ee60db01750a323005ef5dc8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Mar 2026 15:57:39 -0700 Subject: [PATCH 2/4] 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 From a460743b2ac036577ac46ee9c34946b37b214e67 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Mar 2026 16:09:02 -0700 Subject: [PATCH 3/4] vt: add mode report encoding to C API Add ghostty_mode_report_encode() which encodes a DECRPM response sequence into a caller-provided buffer. The function takes a mode tag, a report state integer, an output buffer, and writes the appropriate CSI sequence (with ? prefix for DEC private modes). The Zig-side ReportState is a non-exhaustive c_int enum that uses std.meta.intToEnum for safe conversion to the internal type, returning GHOSTTY_INVALID_VALUE on overflow. The C header exposes a GhosttyModeReportState enum with named constants for the five standard DECRPM state values. --- include/ghostty/vt/modes.h | 79 ++++++++++++++++++++++++++++ src/lib_vt.zig | 1 + src/terminal/c/main.zig | 4 ++ src/terminal/c/modes.zig | 103 +++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 src/terminal/c/modes.zig diff --git a/include/ghostty/vt/modes.h b/include/ghostty/vt/modes.h index b1fcbf097..f7b48637c 100644 --- a/include/ghostty/vt/modes.h +++ b/include/ghostty/vt/modes.h @@ -43,12 +43,44 @@ * } * @endcode * + * ## DECRPM Report Encoding + * + * Use ghostty_mode_report_encode() to encode a DECRPM response into a + * caller-provided buffer: + * + * @code{.c} + * #include + * #include + * + * int main() { + * char buf[32]; + * size_t written = 0; + * + * // Encode a report that DEC mode 25 (cursor visible) is set + * GhosttyResult result = ghostty_mode_report_encode( + * GHOSTTY_MODE_CURSOR_VISIBLE, + * GHOSTTY_MODE_REPORT_SET, + * buf, sizeof(buf), &written); + * + * if (result == GHOSTTY_SUCCESS) { + * printf("Encoded %zu bytes: ", written); + * fwrite(buf, 1, written, stdout); + * printf("\n"); // prints: ESC[?25;1$y + * } + * + * return 0; + * } + * @endcode + * * @{ */ #include +#include #include +#include + #ifdef __cplusplus extern "C" { #endif @@ -151,6 +183,53 @@ static inline bool ghostty_mode_tag_ansi(GhosttyModeTag tag) { return (tag >> 15) != 0; } +/** + * DECRPM report state values. + * + * These correspond to the Ps2 parameter in a DECRPM response + * sequence (CSI ? Ps1 ; Ps2 $ y). + */ +typedef enum { + /** Mode is not recognized */ + GHOSTTY_MODE_REPORT_NOT_RECOGNIZED = 0, + /** Mode is set (enabled) */ + GHOSTTY_MODE_REPORT_SET = 1, + /** Mode is reset (disabled) */ + GHOSTTY_MODE_REPORT_RESET = 2, + /** Mode is permanently set */ + GHOSTTY_MODE_REPORT_PERMANENTLY_SET = 3, + /** Mode is permanently reset */ + GHOSTTY_MODE_REPORT_PERMANENTLY_RESET = 4, +} GhosttyModeReportState; + +/** + * Encode a DECRPM (DEC Private Mode Report) response sequence. + * + * Writes a mode report escape sequence into the provided buffer. + * The generated sequence has the form: + * - DEC private mode: CSI ? Ps1 ; Ps2 $ y + * - ANSI mode: CSI Ps1 ; Ps2 $ y + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param tag The mode tag identifying the mode to report on + * @param state The report state for this mode + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GhosttyResult ghostty_mode_report_encode( + GhosttyModeTag tag, + GhosttyModeReportState state, + char* buf, + size_t buf_len, + size_t* out_written); + #ifdef __cplusplus } #endif diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 0951ae38e..16b47a6be 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -165,6 +165,7 @@ comptime { @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); @export(&c.focus_encode, .{ .name = "ghostty_focus_encode" }); + @export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" }); @export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); @export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" }); @export(&c.sgr_new, .{ .name = "ghostty_sgr_new" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 1b2ae79a9..c8388ef08 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,6 +1,7 @@ pub const color = @import("color.zig"); pub const focus = @import("focus.zig"); pub const formatter = @import("formatter.zig"); +pub const modes = @import("modes.zig"); pub const osc = @import("osc.zig"); pub const key_event = @import("key_event.zig"); pub const key_encode = @import("key_encode.zig"); @@ -23,6 +24,8 @@ pub const color_rgb_get = color.rgb_get; pub const focus_encode = focus.encode; +pub const mode_report_encode = modes.report_encode; + pub const formatter_terminal_new = formatter.terminal_new; pub const formatter_format_buf = formatter.format_buf; pub const formatter_format_alloc = formatter.format_alloc; @@ -97,6 +100,7 @@ test { _ = color; _ = focus; _ = formatter; + _ = modes; _ = osc; _ = key_event; _ = key_encode; diff --git a/src/terminal/c/modes.zig b/src/terminal/c/modes.zig new file mode 100644 index 000000000..d0d5fedf0 --- /dev/null +++ b/src/terminal/c/modes.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const modes = @import("../modes.zig"); +const Result = @import("result.zig").Result; + +/// C: GhosttyModeReportState +pub const ReportState = enum(c_int) { + _, + + fn toZig(self: ReportState) ?modes.Report.State { + return std.meta.intToEnum( + modes.Report.State, + @intFromEnum(self), + ) catch null; + } +}; + +pub fn report_encode( + tag: modes.ModeTag.Backing, + state: ReportState, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(.c) Result { + const mode_tag: modes.ModeTag = @bitCast(tag); + const report: modes.Report = .{ + .tag = mode_tag, + .state = state.toZig() orelse return .invalid_value, + }; + + var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{}); + report.encode(&writer) catch |err| switch (err) { + error.WriteFailed => { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + report.encode(&discarding.writer) catch unreachable; + out_written.* = @intCast(discarding.count); + return .out_of_space; + }, + }; + + out_written.* = writer.end; + return .success; +} + +test "encode DEC mode set" { + var buf: [modes.Report.max_size]u8 = undefined; + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false }); + const result = report_encode(tag, @enumFromInt(1), &buf, buf.len, &written); + try std.testing.expectEqual(.success, result); + try std.testing.expectEqualStrings("\x1B[?1;1$y", buf[0..written]); +} + +test "encode DEC mode reset" { + var buf: [modes.Report.max_size]u8 = undefined; + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false }); + const result = report_encode(tag, @enumFromInt(2), &buf, buf.len, &written); + try std.testing.expectEqual(.success, result); + try std.testing.expectEqualStrings("\x1B[?1;2$y", buf[0..written]); +} + +test "encode ANSI mode" { + var buf: [modes.Report.max_size]u8 = undefined; + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 4, .ansi = true }); + const result = report_encode(tag, @enumFromInt(1), &buf, buf.len, &written); + try std.testing.expectEqual(.success, result); + try std.testing.expectEqualStrings("\x1B[4;1$y", buf[0..written]); +} + +test "encode not recognized" { + var buf: [modes.Report.max_size]u8 = undefined; + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 9999, .ansi = false }); + const result = report_encode(tag, @enumFromInt(0), &buf, buf.len, &written); + try std.testing.expectEqual(.success, result); + try std.testing.expectEqualStrings("\x1B[?9999;0$y", buf[0..written]); +} + +test "encode with insufficient buffer" { + var buf: [1]u8 = undefined; + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false }); + const result = report_encode(tag, @enumFromInt(1), &buf, buf.len, &written); + try std.testing.expectEqual(.out_of_space, result); + try std.testing.expect(written > 1); +} + +test "encode with invalid state" { + var buf: [modes.Report.max_size]u8 = undefined; + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false }); + const result = report_encode(tag, @enumFromInt(99), &buf, buf.len, &written); + try std.testing.expectEqual(.invalid_value, result); +} + +test "encode with null buffer" { + var written: usize = 0; + const tag: modes.ModeTag.Backing = @bitCast(modes.ModeTag{ .value = 1, .ansi = false }); + const result = report_encode(tag, @enumFromInt(1), null, 0, &written); + try std.testing.expectEqual(.out_of_space, result); + try std.testing.expect(written > 0); +} From bfaab044684e0209c9996e5b7b6bc77a778b1b89 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 16 Mar 2026 16:15:36 -0700 Subject: [PATCH 4/4] vt: rename mode tag to mode --- include/ghostty/vt/modes.h | 142 +++++++++++++++++----------------- include/ghostty/vt/terminal.h | 16 ++-- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/include/ghostty/vt/modes.h b/include/ghostty/vt/modes.h index f7b48637c..4454b2a50 100644 --- a/include/ghostty/vt/modes.h +++ b/include/ghostty/vt/modes.h @@ -1,7 +1,7 @@ /** * @file modes.h * - * Terminal mode tag utilities - pack and unpack ANSI/DEC mode identifiers. + * Terminal mode utilities - pack and unpack ANSI/DEC mode identifiers. */ #ifndef GHOSTTY_VT_MODES_H @@ -9,7 +9,7 @@ /** @defgroup modes Mode Utilities * - * Utilities for working with terminal mode tags. A mode tag is a compact + * Utilities for working with terminal modes. A mode is a compact * 16-bit representation of a terminal mode identifier that encodes both * the numeric mode value (up to 15 bits) and whether the mode is an ANSI * mode or a DEC private mode (?-prefixed). @@ -25,18 +25,18 @@ * #include * * int main() { - * // Create a tag for DEC mode 25 (cursor visible) - * GhosttyModeTag tag = ghostty_mode_tag_new(25, false); + * // Create a mode for DEC mode 25 (cursor visible) + * GhosttyMode tag = ghostty_mode_new(25, false); * printf("value=%u ansi=%d packed=0x%04x\n", - * ghostty_mode_tag_value(tag), - * ghostty_mode_tag_ansi(tag), + * ghostty_mode_value(tag), + * ghostty_mode_ansi(tag), * tag); * - * // Create a tag for ANSI mode 4 (insert mode) - * GhosttyModeTag ansi_tag = ghostty_mode_tag_new(4, true); + * // Create a mode for ANSI mode 4 (insert mode) + * GhosttyMode ansi_tag = ghostty_mode_new(4, true); * printf("value=%u ansi=%d packed=0x%04x\n", - * ghostty_mode_tag_value(ansi_tag), - * ghostty_mode_tag_ansi(ansi_tag), + * ghostty_mode_value(ansi_tag), + * ghostty_mode_ansi(ansi_tag), * ansi_tag); * * return 0; @@ -85,102 +85,102 @@ extern "C" { #endif -/** @name ANSI Mode Tags - * Mode tags for standard ANSI modes. +/** @name ANSI Modes + * Modes 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 */ +#define GHOSTTY_MODE_KAM (ghostty_mode_new(2, true)) /**< Keyboard action (disable keyboard) */ +#define GHOSTTY_MODE_INSERT (ghostty_mode_new(4, true)) /**< Insert mode */ +#define GHOSTTY_MODE_SRM (ghostty_mode_new(12, true)) /**< Send/receive mode */ +#define GHOSTTY_MODE_LINEFEED (ghostty_mode_new(20, true)) /**< Linefeed/new line mode */ /** @} */ -/** @name DEC Private Mode Tags - * Mode tags for DEC private modes (?-prefixed). +/** @name DEC Private Modes + * Modes 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 */ +#define GHOSTTY_MODE_DECCKM (ghostty_mode_new(1, false)) /**< Cursor keys */ +#define GHOSTTY_MODE_132_COLUMN (ghostty_mode_new(3, false)) /**< 132/80 column mode */ +#define GHOSTTY_MODE_SLOW_SCROLL (ghostty_mode_new(4, false)) /**< Slow scroll */ +#define GHOSTTY_MODE_REVERSE_COLORS (ghostty_mode_new(5, false)) /**< Reverse video */ +#define GHOSTTY_MODE_ORIGIN (ghostty_mode_new(6, false)) /**< Origin mode */ +#define GHOSTTY_MODE_WRAPAROUND (ghostty_mode_new(7, false)) /**< Auto-wrap mode */ +#define GHOSTTY_MODE_AUTOREPEAT (ghostty_mode_new(8, false)) /**< Auto-repeat keys */ +#define GHOSTTY_MODE_X10_MOUSE (ghostty_mode_new(9, false)) /**< X10 mouse reporting */ +#define GHOSTTY_MODE_CURSOR_BLINKING (ghostty_mode_new(12, false)) /**< Cursor blink */ +#define GHOSTTY_MODE_CURSOR_VISIBLE (ghostty_mode_new(25, false)) /**< Cursor visible (DECTCEM) */ +#define GHOSTTY_MODE_ENABLE_MODE_3 (ghostty_mode_new(40, false)) /**< Allow 132 column mode */ +#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_new(45, false)) /**< Reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_new(47, false)) /**< Alternate screen (legacy) */ +#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_new(66, false)) /**< Application keypad */ +#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_new(69, false)) /**< Left/right margin mode */ +#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_new(1000, false)) /**< Normal mouse tracking */ +#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_new(1002, false)) /**< Button-event mouse tracking */ +#define GHOSTTY_MODE_ANY_MOUSE (ghostty_mode_new(1003, false)) /**< Any-event mouse tracking */ +#define GHOSTTY_MODE_FOCUS_EVENT (ghostty_mode_new(1004, false)) /**< Focus in/out events */ +#define GHOSTTY_MODE_UTF8_MOUSE (ghostty_mode_new(1005, false)) /**< UTF-8 mouse format */ +#define GHOSTTY_MODE_SGR_MOUSE (ghostty_mode_new(1006, false)) /**< SGR mouse format */ +#define GHOSTTY_MODE_ALT_SCROLL (ghostty_mode_new(1007, false)) /**< Alternate scroll mode */ +#define GHOSTTY_MODE_URXVT_MOUSE (ghostty_mode_new(1015, false)) /**< URxvt mouse format */ +#define GHOSTTY_MODE_SGR_PIXELS_MOUSE (ghostty_mode_new(1016, false)) /**< SGR-Pixels mouse format */ +#define GHOSTTY_MODE_NUMLOCK_KEYPAD (ghostty_mode_new(1035, false)) /**< Ignore keypad with NumLock */ +#define GHOSTTY_MODE_ALT_ESC_PREFIX (ghostty_mode_new(1036, false)) /**< Alt key sends ESC prefix */ +#define GHOSTTY_MODE_ALT_SENDS_ESC (ghostty_mode_new(1039, false)) /**< Alt sends escape */ +#define GHOSTTY_MODE_REVERSE_WRAP_EXT (ghostty_mode_new(1045, false)) /**< Extended reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN (ghostty_mode_new(1047, false)) /**< Alternate screen */ +#define GHOSTTY_MODE_SAVE_CURSOR (ghostty_mode_new(1048, false)) /**< Save cursor (DECSC) */ +#define GHOSTTY_MODE_ALT_SCREEN_SAVE (ghostty_mode_new(1049, false)) /**< Alt screen + save cursor + clear */ +#define GHOSTTY_MODE_BRACKETED_PASTE (ghostty_mode_new(2004, false)) /**< Bracketed paste mode */ +#define GHOSTTY_MODE_SYNC_OUTPUT (ghostty_mode_new(2026, false)) /**< Synchronized output */ +#define GHOSTTY_MODE_GRAPHEME_CLUSTER (ghostty_mode_new(2027, false)) /**< Grapheme cluster mode */ +#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_new(2031, false)) /**< Report color scheme */ +#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_new(2048, false)) /**< In-band size reports */ /** @} */ /** - * A packed 16-bit terminal mode tag. + * A packed 16-bit terminal mode. * * Encodes a mode value (bits 0–14) and an ANSI flag (bit 15) into a * single 16-bit integer. Use the inline helper functions to construct - * and inspect mode tags rather than manipulating bits directly. + * and inspect modes rather than manipulating bits directly. */ -typedef uint16_t GhosttyModeTag; +typedef uint16_t GhosttyMode; /** - * Create a mode tag from a mode value and ANSI flag. + * Create a mode from a mode value and ANSI flag. * * @param value The numeric mode value (0–32767) * @param ansi true for an ANSI mode, false for a DEC private mode - * @return The packed mode tag + * @return The packed mode * * @ingroup modes */ -static inline GhosttyModeTag ghostty_mode_tag_new(uint16_t value, bool ansi) { - return (GhosttyModeTag)((value & 0x7FFF) | ((uint16_t)ansi << 15)); +static inline GhosttyMode ghostty_mode_new(uint16_t value, bool ansi) { + return (GhosttyMode)((value & 0x7FFF) | ((uint16_t)ansi << 15)); } /** - * Extract the numeric mode value from a mode tag. + * Extract the numeric mode value from a mode. * - * @param tag The mode tag + * @param mode The mode * @return The mode value (0–32767) * * @ingroup modes */ -static inline uint16_t ghostty_mode_tag_value(GhosttyModeTag tag) { - return tag & 0x7FFF; +static inline uint16_t ghostty_mode_value(GhosttyMode mode) { + return mode & 0x7FFF; } /** - * Check whether a mode tag represents an ANSI mode. + * Check whether a mode represents an ANSI mode. * - * @param tag The mode tag + * @param mode The mode * @return true if this is an ANSI mode, false if it is a DEC private mode * * @ingroup modes */ -static inline bool ghostty_mode_tag_ansi(GhosttyModeTag tag) { - return (tag >> 15) != 0; +static inline bool ghostty_mode_ansi(GhosttyMode mode) { + return (mode >> 15) != 0; } /** @@ -214,7 +214,7 @@ typedef enum { * and writes the required buffer size to @p out_written. The caller can * then retry with a sufficiently sized buffer. * - * @param tag The mode tag identifying the mode to report on + * @param mode The mode identifying the mode to report on * @param state The report state for this mode * @param buf Output buffer to write the encoded sequence into (may be NULL) * @param buf_len Size of the output buffer in bytes @@ -224,7 +224,7 @@ typedef enum { * is too small */ GhosttyResult ghostty_mode_report_encode( - GhosttyModeTag tag, + GhosttyMode mode, GhosttyModeReportState state, char* buf, size_t buf_len, diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index d3dc9cffa..5be6db001 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -199,36 +199,36 @@ void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal, /** * Get the current value of a terminal mode. * - * Returns the value of the mode identified by the given mode tag. + * Returns the value of the mode identified by the given mode. * * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) - * @param tag The mode tag identifying the mode to query + * @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 tag does not correspond to a known mode + * is NULL or the mode does not correspond to a known mode * * @ingroup terminal */ GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal, - GhosttyModeTag tag, + GhosttyMode mode, bool* out_value); /** * Set the value of a terminal mode. * - * Sets the mode identified by the given mode tag to the specified value. + * Sets the mode identified by the given mode 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 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 tag does not correspond to a known mode + * is NULL or the mode does not correspond to a known mode * * @ingroup terminal */ GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, - GhosttyModeTag tag, + GhosttyMode mode, bool value); /** @} */