diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 4f8fef88e..e07ccefaf 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -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 #include +#include #include #include #include diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h new file mode 100644 index 000000000..f2e68fa96 --- /dev/null +++ b/include/ghostty/vt/terminal.h @@ -0,0 +1,88 @@ +/** + * @file terminal.h + * + * Terminal lifecycle management. + */ + +#ifndef GHOSTTY_VT_TERMINAL_H +#define GHOSTTY_VT_TERMINAL_H + +#include +#include +#include +#include + +#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 */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 426660621..9af5d9d70 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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()) { diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 706f235c7..ec08ec72c 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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; } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index bc92597f5..e77769e24 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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"); diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig new file mode 100644 index 000000000..14d66ca0a --- /dev/null +++ b/src/terminal/c/terminal.zig @@ -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); +}