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..4454b2a50 --- /dev/null +++ b/include/ghostty/vt/modes.h @@ -0,0 +1,239 @@ +/** + * @file modes.h + * + * Terminal mode 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 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). + * + * 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 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_value(tag), + * ghostty_mode_ansi(tag), + * tag); + * + * // 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_value(ansi_tag), + * ghostty_mode_ansi(ansi_tag), + * ansi_tag); + * + * return 0; + * } + * @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 + +/** @name ANSI Modes + * Modes for standard ANSI modes. + * @{ + */ +#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 Modes + * Modes for DEC private modes (?-prefixed). + * @{ + */ +#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. + * + * 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 modes rather than manipulating bits directly. + */ +typedef uint16_t GhosttyMode; + +/** + * 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 + * + * @ingroup modes + */ +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. + * + * @param mode The mode + * @return The mode value (0–32767) + * + * @ingroup modes + */ +static inline uint16_t ghostty_mode_value(GhosttyMode mode) { + return mode & 0x7FFF; +} + +/** + * Check whether a mode represents an ANSI mode. + * + * @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_ansi(GhosttyMode mode) { + return (mode >> 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 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 + * @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( + GhosttyMode mode, + GhosttyModeReportState state, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_MODES_H */ diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 8c91920a0..5be6db001 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. + * + * @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 + */ +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 + */ +GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, + GhosttyMode mode, + bool value); /** @} */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 9d8e94bba..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" }); @@ -186,6 +187,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..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; @@ -90,11 +93,14 @@ 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; _ = 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); +} 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