5a01c1f3
feat(vt): Terminal facade wrapping ghostty-vt
a73x 2026-04-08 06:08
Wraps ghostty_vt.Terminal + TerminalStream + RenderState into a single Terminal struct with init/deinit/write/resize/snapshot/cols/rows API.
diff --git a/src/vt.zig b/src/vt.zig index 512d818..b506046 100644 --- a/src/vt.zig +++ b/src/vt.zig @@ -1,4 +1,4 @@ //! Thin wrapper around the ghostty-vt Zig module. //! Thin facade over the ghostty-vt Zig module. //! //! ## ghostty-vt API notes (as of ghostty-1.3.2-dev) //! @@ -50,7 +50,111 @@ const std = @import("std"); const ghostty_vt = @import("ghostty-vt"); test "ghostty-vt module imports" { // Smoke test — just reference the module so the import is verified _ = ghostty_vt; pub const Terminal = struct { alloc: std.mem.Allocator, inner: ghostty_vt.Terminal, stream: ghostty_vt.TerminalStream, render_state: ghostty_vt.RenderState, pub const InitOptions = struct { cols: u16, rows: u16, max_scrollback: u32 = 1000, }; pub fn init(alloc: std.mem.Allocator, opts: InitOptions) !Terminal { var inner = try ghostty_vt.Terminal.init(alloc, .{ .cols = @intCast(opts.cols), .rows = @intCast(opts.rows), .max_scrollback = @intCast(opts.max_scrollback), }); errdefer inner.deinit(alloc); // TerminalStream.init takes a Handler value. Handler contains // a pointer to the terminal, so we store inner first then set // the pointer during return below — but we need a stable address. // We initialise with a placeholder and fix up the pointer after // the struct is placed in its final location by the caller. const stream: ghostty_vt.TerminalStream = .init(.{ .terminal = &inner, }); const render_state: ghostty_vt.RenderState = .empty; return .{ .alloc = alloc, .inner = inner, .stream = stream, .render_state = render_state, }; } /// Fix up the internal stream pointer after the Terminal has been moved /// to its final memory location. Must be called once before write(). fn fixupStreamPointer(self: *Terminal) void { self.stream.handler.terminal = &self.inner; } pub fn deinit(self: *Terminal) void { self.render_state.deinit(self.alloc); self.inner.deinit(self.alloc); } pub fn write(self: *Terminal, bytes: []const u8) void { self.stream.handler.terminal = &self.inner; self.stream.nextSlice(bytes); } pub fn resize(self: *Terminal, new_cols: u16, new_rows: u16) !void { try self.inner.resize(self.alloc, @intCast(new_cols), @intCast(new_rows)); } pub fn snapshot(self: *Terminal) !void { try self.render_state.update(self.alloc, &self.inner); } /// Number of columns in the terminal grid. pub fn cols(self: *const Terminal) u16 { return @intCast(self.render_state.cols); } /// Number of rows in the terminal grid. pub fn rows(self: *const Terminal) u16 { return @intCast(self.render_state.rows); } }; test "Terminal init/deinit" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); } test "Terminal.write feeds plain text" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); term.write("Hello"); try term.snapshot(); // The test just verifies the call sequence works without crashing. // We don't yet have a way to read back individual cells in this task. } test "Terminal.resize" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); try term.resize(120, 40); try term.snapshot(); try std.testing.expectEqual(@as(u16, 120), term.cols()); try std.testing.expectEqual(@as(u16, 40), term.rows()); }