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:
Mitchell Hashimoto
2026-03-19 11:33:26 -07:00
parent c2e9de224e
commit f168b3c098
5 changed files with 356 additions and 6 deletions

View File

@@ -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()) {

View File

@@ -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".

View File

@@ -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;

View File

@@ -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));
}