mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
vt: add ghostty_terminal_get for reading terminal state
Add a typed data query API to the terminal C interface, following the same OutType pattern used by the OSC command data API. The new ghostty_terminal_get function takes a GhosttyTerminalData tag and an output pointer, returning GhosttyResult. Currently exposes cols, rows, cursor x/y position, and cursor pending wrap state. The GhosttyTerminalData enum is placed with the other types in the header (before functions) per the ordering convention.
This commit is contained in:
@@ -97,6 +97,119 @@ typedef struct {
|
||||
GhosttyTerminalScrollViewportValue value;
|
||||
} GhosttyTerminalScrollViewport;
|
||||
|
||||
/**
|
||||
* Terminal screen identifier.
|
||||
*
|
||||
* Identifies which screen buffer is active in the terminal.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum {
|
||||
/** The primary (normal) screen. */
|
||||
GHOSTTY_TERMINAL_SCREEN_PRIMARY = 0,
|
||||
|
||||
/** The alternate screen. */
|
||||
GHOSTTY_TERMINAL_SCREEN_ALTERNATE = 1,
|
||||
} GhosttyTerminalScreen;
|
||||
|
||||
/**
|
||||
* Scrollbar state for the terminal viewport.
|
||||
*
|
||||
* Represents the scrollable area dimensions needed to render a scrollbar.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Total size of the scrollable area in rows. */
|
||||
uint64_t total;
|
||||
|
||||
/** Offset into the total area that the viewport is at. */
|
||||
uint64_t offset;
|
||||
|
||||
/** Length of the visible area in rows. */
|
||||
uint64_t len;
|
||||
} GhosttyTerminalScrollbar;
|
||||
|
||||
/**
|
||||
* Terminal data types.
|
||||
*
|
||||
* These values specify what type of data to extract from a terminal
|
||||
* using `ghostty_terminal_get`.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum {
|
||||
/** Invalid data type. Never results in any data extraction. */
|
||||
GHOSTTY_TERMINAL_DATA_INVALID = 0,
|
||||
|
||||
/**
|
||||
* Terminal width in cells.
|
||||
*
|
||||
* Output type: uint16_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_COLS = 1,
|
||||
|
||||
/**
|
||||
* Terminal height in cells.
|
||||
*
|
||||
* Output type: uint16_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_ROWS = 2,
|
||||
|
||||
/**
|
||||
* Cursor column position (0-indexed).
|
||||
*
|
||||
* Output type: uint16_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_CURSOR_X = 3,
|
||||
|
||||
/**
|
||||
* Cursor row position within the active area (0-indexed).
|
||||
*
|
||||
* Output type: uint16_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_CURSOR_Y = 4,
|
||||
|
||||
/**
|
||||
* Whether the cursor has a pending wrap (next print will soft-wrap).
|
||||
*
|
||||
* Output type: bool *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP = 5,
|
||||
|
||||
/**
|
||||
* The currently active screen.
|
||||
*
|
||||
* Output type: GhosttyTerminalScreen *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN = 6,
|
||||
|
||||
/**
|
||||
* Whether the cursor is visible (DEC mode 25).
|
||||
*
|
||||
* Output type: bool *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE = 7,
|
||||
|
||||
/**
|
||||
* Current Kitty keyboard protocol flags.
|
||||
*
|
||||
* Output type: GhosttyKittyKeyFlags * (uint8_t *)
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS = 8,
|
||||
|
||||
/**
|
||||
* Scrollbar state for the terminal viewport.
|
||||
*
|
||||
* This may be expensive to calculate depending on where the viewport
|
||||
* is (arbitrary pins are expensive). The caller should take care to only
|
||||
* call this as needed and not too frequently.
|
||||
*
|
||||
* Output type: GhosttyTerminalScrollbar *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_SCROLLBAR = 9,
|
||||
} GhosttyTerminalData;
|
||||
|
||||
/**
|
||||
* Create a new terminal instance.
|
||||
*
|
||||
@@ -228,8 +341,28 @@ GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal,
|
||||
* @ingroup terminal
|
||||
*/
|
||||
GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal,
|
||||
GhosttyMode mode,
|
||||
bool value);
|
||||
GhosttyMode mode,
|
||||
bool value);
|
||||
|
||||
/**
|
||||
* Get data from a terminal instance.
|
||||
*
|
||||
* Extracts typed data from the given terminal based on the specified
|
||||
* data type. The output pointer must be of the appropriate type for the
|
||||
* requested data kind. Valid data types and output types are documented
|
||||
* in the `GhosttyTerminalData` enum.
|
||||
*
|
||||
* @param terminal The terminal handle (may be NULL)
|
||||
* @param data The type of data to extract
|
||||
* @param out Pointer to store the extracted data (type depends on data parameter)
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
|
||||
* is NULL or the data type is invalid
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal,
|
||||
GhosttyTerminalData data,
|
||||
void *out);
|
||||
|
||||
/** @} */
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ comptime {
|
||||
@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" });
|
||||
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
|
||||
|
||||
// On Wasm we need to export our allocator convenience functions.
|
||||
if (builtin.target.cpu.arch.isWasm()) {
|
||||
|
||||
@@ -9,15 +9,19 @@ const ScreenSet = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const build_options = @import("terminal_options");
|
||||
const lib = @import("../lib/main.zig");
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Screen = @import("Screen.zig");
|
||||
|
||||
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
|
||||
|
||||
/// The possible keys for screens in the screen set.
|
||||
pub const Key = enum(u1) {
|
||||
primary,
|
||||
alternate,
|
||||
};
|
||||
pub const Key = lib.Enum(lib_target, &.{
|
||||
"primary",
|
||||
"alternate",
|
||||
});
|
||||
|
||||
/// The key value of the currently active screen. Useful for simple
|
||||
/// comparisons, e.g. "is this screen the primary screen".
|
||||
|
||||
@@ -98,6 +98,7 @@ 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;
|
||||
pub const terminal_get = terminal.get;
|
||||
|
||||
test {
|
||||
_ = color;
|
||||
|
||||
@@ -3,10 +3,15 @@ const testing = std.testing;
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const ZigTerminal = @import("../Terminal.zig");
|
||||
const ScreenSet = @import("../ScreenSet.zig");
|
||||
const PageList = @import("../PageList.zig");
|
||||
const kitty = @import("../kitty/key.zig");
|
||||
const modes = @import("../modes.zig");
|
||||
const size = @import("../size.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const log = std.log.scoped(.terminal_c);
|
||||
|
||||
/// C: GhosttyTerminal
|
||||
pub const Terminal = ?*ZigTerminal;
|
||||
|
||||
@@ -123,6 +128,81 @@ pub fn mode_set(
|
||||
return .success;
|
||||
}
|
||||
|
||||
/// C: GhosttyTerminalScreen
|
||||
pub const TerminalScreen = ScreenSet.Key;
|
||||
|
||||
/// C: GhosttyTerminalScrollbar
|
||||
pub const TerminalScrollbar = PageList.Scrollbar.C;
|
||||
|
||||
/// C: GhosttyTerminalData
|
||||
pub const TerminalData = enum(c_int) {
|
||||
invalid = 0,
|
||||
cols = 1,
|
||||
rows = 2,
|
||||
cursor_x = 3,
|
||||
cursor_y = 4,
|
||||
cursor_pending_wrap = 5,
|
||||
active_screen = 6,
|
||||
cursor_visible = 7,
|
||||
kitty_keyboard_flags = 8,
|
||||
scrollbar = 9,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: TerminalData) type {
|
||||
return switch (self) {
|
||||
.invalid => void,
|
||||
.cols, .rows, .cursor_x, .cursor_y => size.CellCountInt,
|
||||
.cursor_pending_wrap, .cursor_visible => bool,
|
||||
.active_screen => TerminalScreen,
|
||||
.kitty_keyboard_flags => u8,
|
||||
.scrollbar => TerminalScrollbar,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn get(
|
||||
terminal_: Terminal,
|
||||
data: TerminalData,
|
||||
out: ?*anyopaque,
|
||||
) callconv(.c) Result {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(TerminalData, @intFromEnum(data)) catch {
|
||||
log.warn("terminal_get invalid data value={d}", .{@intFromEnum(data)});
|
||||
return .invalid_value;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (data) {
|
||||
inline else => |comptime_data| getTyped(
|
||||
terminal_,
|
||||
comptime_data,
|
||||
@ptrCast(@alignCast(out)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn getTyped(
|
||||
terminal_: Terminal,
|
||||
comptime data: TerminalData,
|
||||
out: *data.OutType(),
|
||||
) Result {
|
||||
const t = terminal_ orelse return .invalid_value;
|
||||
switch (data) {
|
||||
.invalid => return .invalid_value,
|
||||
.cols => out.* = t.cols,
|
||||
.rows => out.* = t.rows,
|
||||
.cursor_x => out.* = t.screens.active.cursor.x,
|
||||
.cursor_y => out.* = t.screens.active.cursor.y,
|
||||
.cursor_pending_wrap => out.* = t.screens.active.cursor.pending_wrap,
|
||||
.active_screen => out.* = t.screens.active_key,
|
||||
.cursor_visible => out.* = t.modes.get(.cursor_visible),
|
||||
.kitty_keyboard_flags => out.* = @as(u8, t.screens.active.kitty_keyboard.current().int()),
|
||||
.scrollbar => out.* = t.screens.active.pages.scrollbar().cval(),
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn free(terminal_: Terminal) callconv(.c) void {
|
||||
const t = terminal_ orelse return;
|
||||
|
||||
@@ -397,3 +477,134 @@ test "vt_write" {
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("Hello", str);
|
||||
}
|
||||
|
||||
test "get cols and rows" {
|
||||
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 cols: size.CellCountInt = undefined;
|
||||
var rows: size.CellCountInt = undefined;
|
||||
try testing.expectEqual(Result.success, get(t, .cols, @ptrCast(&cols)));
|
||||
try testing.expectEqual(Result.success, get(t, .rows, @ptrCast(&rows)));
|
||||
try testing.expectEqual(80, cols);
|
||||
try testing.expectEqual(24, rows);
|
||||
}
|
||||
|
||||
test "get cursor position" {
|
||||
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);
|
||||
|
||||
vt_write(t, "Hello", 5);
|
||||
|
||||
var x: size.CellCountInt = undefined;
|
||||
var y: size.CellCountInt = undefined;
|
||||
try testing.expectEqual(Result.success, get(t, .cursor_x, @ptrCast(&x)));
|
||||
try testing.expectEqual(Result.success, get(t, .cursor_y, @ptrCast(&y)));
|
||||
try testing.expectEqual(5, x);
|
||||
try testing.expectEqual(0, y);
|
||||
}
|
||||
|
||||
test "get null" {
|
||||
var cols: size.CellCountInt = undefined;
|
||||
try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols)));
|
||||
}
|
||||
|
||||
test "get cursor_visible" {
|
||||
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 visible: bool = undefined;
|
||||
try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&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));
|
||||
try testing.expectEqual(Result.success, get(t, .cursor_visible, @ptrCast(&visible)));
|
||||
try testing.expect(!visible);
|
||||
}
|
||||
|
||||
test "get active_screen" {
|
||||
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 screen: TerminalScreen = undefined;
|
||||
try testing.expectEqual(Result.success, get(t, .active_screen, @ptrCast(&screen)));
|
||||
try testing.expectEqual(.primary, screen);
|
||||
}
|
||||
|
||||
test "get kitty_keyboard_flags" {
|
||||
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 flags: u8 = undefined;
|
||||
try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
|
||||
try testing.expectEqual(0, flags);
|
||||
|
||||
// Push kitty flags via VT sequence: CSI > 3 u (push disambiguate | report_events)
|
||||
vt_write(t, "\x1b[>3u", 5);
|
||||
|
||||
try testing.expectEqual(Result.success, get(t, .kitty_keyboard_flags, @ptrCast(&flags)));
|
||||
try testing.expectEqual(3, flags);
|
||||
}
|
||||
|
||||
test "get invalid" {
|
||||
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);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, get(t, .invalid, null));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user