a73x

5a01c1f3

feat(vt): Terminal facade wrapping ghostty-vt

a73x   2026-04-08 06:08

Commit message
feat(vt): Terminal facade wrapping ghostty-vt

Wraps ghostty_vt.Terminal + TerminalStream + RenderState into a single
Terminal struct with init/deinit/write/resize/snapshot/cols/rows API.

src/vt.zig
Old New
@@ -1,4 +1,4 @@
1 //! Thin wrapper around the ghostty-vt Zig module. 1 //! Thin facade over the ghostty-vt Zig module.
2 //! 2 //!
3 //! ## ghostty-vt API notes (as of ghostty-1.3.2-dev) 3 //! ## ghostty-vt API notes (as of ghostty-1.3.2-dev)
4 //! 4 //!
@@ -50,7 +50,111 @@
50 const std = @import("std"); 50 const std = @import("std");
51 const ghostty_vt = @import("ghostty-vt"); 51 const ghostty_vt = @import("ghostty-vt");
52 52
53 test "ghostty-vt module imports" { 53 pub const Terminal = struct {
54 // Smoke test — just reference the module so the import is verified 54 alloc: std.mem.Allocator,
55 _ = ghostty_vt; 55 inner: ghostty_vt.Terminal,
56 stream: ghostty_vt.TerminalStream,
57 render_state: ghostty_vt.RenderState,
58
59 pub const InitOptions = struct {
60 cols: u16,
61 rows: u16,
62 max_scrollback: u32 = 1000,
63 };
64
65 pub fn init(alloc: std.mem.Allocator, opts: InitOptions) !Terminal {
66 var inner = try ghostty_vt.Terminal.init(alloc, .{
67 .cols = @intCast(opts.cols),
68 .rows = @intCast(opts.rows),
69 .max_scrollback = @intCast(opts.max_scrollback),
70 });
71 errdefer inner.deinit(alloc);
72
73 // TerminalStream.init takes a Handler value. Handler contains
74 // a pointer to the terminal, so we store inner first then set
75 // the pointer during return below — but we need a stable address.
76 // We initialise with a placeholder and fix up the pointer after
77 // the struct is placed in its final location by the caller.
78 const stream: ghostty_vt.TerminalStream = .init(.{
79 .terminal = &inner,
80 });
81
82 const render_state: ghostty_vt.RenderState = .empty;
83
84 return .{
85 .alloc = alloc,
86 .inner = inner,
87 .stream = stream,
88 .render_state = render_state,
89 };
90 }
91
92 /// Fix up the internal stream pointer after the Terminal has been moved
93 /// to its final memory location. Must be called once before write().
94 fn fixupStreamPointer(self: *Terminal) void {
95 self.stream.handler.terminal = &self.inner;
96 }
97
98 pub fn deinit(self: *Terminal) void {
99 self.render_state.deinit(self.alloc);
100 self.inner.deinit(self.alloc);
101 }
102
103 pub fn write(self: *Terminal, bytes: []const u8) void {
104 self.stream.handler.terminal = &self.inner;
105 self.stream.nextSlice(bytes);
106 }
107
108 pub fn resize(self: *Terminal, new_cols: u16, new_rows: u16) !void {
109 try self.inner.resize(self.alloc, @intCast(new_cols), @intCast(new_rows));
110 }
111
112 pub fn snapshot(self: *Terminal) !void {
113 try self.render_state.update(self.alloc, &self.inner);
114 }
115
116 /// Number of columns in the terminal grid.
117 pub fn cols(self: *const Terminal) u16 {
118 return @intCast(self.render_state.cols);
119 }
120
121 /// Number of rows in the terminal grid.
122 pub fn rows(self: *const Terminal) u16 {
123 return @intCast(self.render_state.rows);
124 }
125 };
126
127 test "Terminal init/deinit" {
128 var term = try Terminal.init(std.testing.allocator, .{
129 .cols = 80,
130 .rows = 24,
131 });
132 defer term.deinit();
133 }
134
135 test "Terminal.write feeds plain text" {
136 var term = try Terminal.init(std.testing.allocator, .{
137 .cols = 80,
138 .rows = 24,
139 });
140 defer term.deinit();
141
142 term.write("Hello");
143 try term.snapshot();
144
145 // The test just verifies the call sequence works without crashing.
146 // We don't yet have a way to read back individual cells in this task.
147 }
148
149 test "Terminal.resize" {
150 var term = try Terminal.init(std.testing.allocator, .{
151 .cols = 80,
152 .rows = 24,
153 });
154 defer term.deinit();
155
156 try term.resize(120, 40);
157 try term.snapshot();
158 try std.testing.expectEqual(@as(u16, 120), term.cols());
159 try std.testing.expectEqual(@as(u16, 40), term.rows());
56 } 160 }