vt: expose ghostty_terminal_new/free

This commit is contained in:
Mitchell Hashimoto
2026-03-13 12:57:46 -07:00
parent e75f8956c5
commit 302e68fd3d
6 changed files with 210 additions and 1 deletions

View File

@@ -28,6 +28,7 @@
* @section groups_sec API Reference
*
* The API is organized into the following groups:
* - @ref terminal "Terminal Lifecycle" - Create and destroy terminal instances
* - @ref key "Key Encoding" - Encode key events into terminal sequences
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
@@ -74,6 +75,7 @@ extern "C" {
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/key.h>

View File

@@ -0,0 +1,88 @@
/**
* @file terminal.h
*
* Terminal lifecycle management.
*/
#ifndef GHOSTTY_VT_TERMINAL_H
#define GHOSTTY_VT_TERMINAL_H
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup terminal Terminal Lifecycle
*
* Minimal API for creating and destroying terminal instances.
*
* This currently only exposes lifecycle operations. Additional terminal
* APIs will be added over time.
*
* @{
*/
/**
* Opaque handle to a terminal instance.
*
* @ingroup terminal
*/
typedef struct GhosttyTerminal* GhosttyTerminal;
/**
* Terminal initialization options.
*
* @ingroup terminal
*/
typedef struct {
/** Terminal width in cells. Must be greater than zero. */
uint16_t cols;
/** Terminal height in cells. Must be greater than zero. */
uint16_t rows;
/** Maximum number of lines to keep in scrollback history. */
size_t max_scrollback;
// TODO: Consider ABI compatibility implications of this struct.
// We may want to artificially pad it significantly to support
// future options.
} GhosttyTerminalOptions;
/**
* Create a new terminal instance.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param terminal Pointer to store the created terminal handle
* @param options Terminal initialization options
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_new(const GhosttyAllocator* allocator,
GhosttyTerminal* terminal,
GhosttyTerminalOptions options);
/**
* Free a terminal instance.
*
* Releases all resources associated with the terminal. After this call,
* the terminal handle becomes invalid and must not be used.
*
* @param terminal The terminal handle to free (may be NULL)
*
* @ingroup terminal
*/
void ghostty_terminal_free(GhosttyTerminal terminal);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_TERMINAL_H */

View File

@@ -143,6 +143,8 @@ comptime {
@export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" });
@export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" });
@export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });
// On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) {

View File

@@ -271,7 +271,7 @@ pub fn vtHandler(self: *Terminal) ReadonlyHandler {
}
/// The general allocator we should use for this terminal.
fn gpa(self: *Terminal) Allocator {
pub fn gpa(self: *Terminal) Allocator {
return self.screens.active.alloc;
}

View File

@@ -4,6 +4,7 @@ pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.zig");
pub const paste = @import("paste.zig");
pub const sgr = @import("sgr.zig");
pub const terminal = @import("terminal.zig");
// The full C API, unexported.
pub const osc_new = osc.new;
@@ -52,6 +53,9 @@ pub const key_encoder_encode = key_encode.encode;
pub const paste_is_safe = paste.is_safe;
pub const terminal_new = terminal.new;
pub const terminal_free = terminal.free;
test {
_ = color;
_ = osc;
@@ -59,6 +63,7 @@ test {
_ = key_encode;
_ = paste;
_ = sgr;
_ = terminal;
// We want to make sure we run the tests for the C allocator interface.
_ = @import("../../lib/allocator.zig");

112
src/terminal/c/terminal.zig Normal file
View File

@@ -0,0 +1,112 @@
const std = @import("std");
const testing = std.testing;
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
const size = @import("../size.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyTerminal
pub const Terminal = ?*ZigTerminal;
/// C: GhosttyTerminalOptions
pub const Options = extern struct {
cols: size.CellCountInt,
rows: size.CellCountInt,
max_scrollback: usize,
};
const NewError = error{
InvalidValue,
OutOfMemory,
};
pub fn new(
alloc_: ?*const CAllocator,
result: *Terminal,
opts: Options,
) callconv(.c) Result {
result.* = new_(alloc_, opts) catch |err| {
result.* = null;
return switch (err) {
error.InvalidValue => .invalid_value,
error.OutOfMemory => .out_of_memory,
};
};
return .success;
}
fn new_(
alloc_: ?*const CAllocator,
opts: Options,
) NewError!*ZigTerminal {
if (opts.cols == 0 or opts.rows == 0) return error.InvalidValue;
const alloc = lib_alloc.default(alloc_);
const ptr = alloc.create(ZigTerminal) catch
return error.OutOfMemory;
errdefer alloc.destroy(ptr);
ptr.* = try .init(alloc, .{
.cols = opts.cols,
.rows = opts.rows,
.max_scrollback = opts.max_scrollback,
});
return ptr;
}
pub fn free(terminal_: Terminal) callconv(.c) void {
const t = terminal_ orelse return;
const alloc = t.gpa();
t.deinit(alloc);
alloc.destroy(t);
}
test "new/free" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
try testing.expect(t != null);
free(t);
}
test "new invalid value" {
var t: Terminal = null;
try testing.expectEqual(Result.invalid_value, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 0,
.rows = 24,
.max_scrollback = 10_000,
},
));
try testing.expect(t == null);
try testing.expectEqual(Result.invalid_value, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 0,
.max_scrollback = 10_000,
},
));
try testing.expect(t == null);
}
test "free null" {
free(null);
}