a73x

b2d1bc2e

Wire terminal effect callbacks

a73x   2026-04-08 13:09

Commit message
Wire terminal effect callbacks

src/main.zig
Old New
@@ -14,6 +14,19 @@ const GridSize = struct {
14 rows: u16, 14 rows: u16,
15 }; 15 };
16 16
17 fn writePtyFromTerminal(_: *vt.Terminal, ctx: ?*anyopaque, data: []const u8) void {
18 const p: *pty.Pty = @ptrCast(@alignCast(ctx orelse return));
19 _ = p.write(data) catch |err| {
20 std.log.warn("pty write callback failed: {}", .{err});
21 return;
22 };
23 }
24
25 fn updateWindowTitle(_: *vt.Terminal, ctx: ?*anyopaque, title: ?[:0]const u8) void {
26 const window: *wayland_client.Window = @ptrCast(@alignCast(ctx orelse return));
27 window.setTitle(title);
28 }
29
17 pub fn main() !void { 30 pub fn main() !void {
18 var gpa: std.heap.DebugAllocator(.{}) = .init; 31 var gpa: std.heap.DebugAllocator(.{}) = .init;
19 defer _ = gpa.deinit(); 32 defer _ = gpa.deinit();
@@ -106,6 +119,13 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
106 .max_scrollback = 1000, 119 .max_scrollback = 1000,
107 }); 120 });
108 defer term.deinit(); 121 defer term.deinit();
122 term.setTitleChangedCallback(window, &updateWindowTitle);
123 term.setReportedSize(.{
124 .rows = rows,
125 .columns = cols,
126 .cell_width = cell_w,
127 .cell_height = cell_h,
128 });
109 129
110 // === pty === 130 // === pty ===
111 const shell: [:0]const u8 = blk: { 131 const shell: [:0]const u8 = blk: {
@@ -116,6 +136,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
116 136
117 var p = try pty.Pty.spawn(.{ .cols = cols, .rows = rows, .shell = shell }); 137 var p = try pty.Pty.spawn(.{ .cols = cols, .rows = rows, .shell = shell });
118 defer p.deinit(); 138 defer p.deinit();
139 term.setWritePtyCallback(&p, &writePtyFromTerminal);
119 140
120 // === instance buffer === 141 // === instance buffer ===
121 var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; 142 var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty;
@@ -183,6 +204,12 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
183 try p.resize(new_grid.cols, new_grid.rows); 204 try p.resize(new_grid.cols, new_grid.rows);
184 cols = new_grid.cols; 205 cols = new_grid.cols;
185 rows = new_grid.rows; 206 rows = new_grid.rows;
207 term.setReportedSize(.{
208 .rows = rows,
209 .columns = cols,
210 .cell_width = cell_w,
211 .cell_height = cell_h,
212 });
186 } else { 213 } else {
187 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 214 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
188 try ctx.recreateSwapchain(window.width, window.height); 215 try ctx.recreateSwapchain(window.width, window.height);
src/vt.zig
Old New
@@ -49,20 +49,39 @@
49 49
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 const DeviceAttributesCallback = std.meta.Child(
53 @FieldType(ghostty_vt.TerminalStream.Handler.Effects, "device_attributes"),
54 );
55 const DeviceAttributesReturn = @typeInfo(
56 std.meta.Child(DeviceAttributesCallback),
57 ).@"fn".return_type.?;
52 58
53 pub const InputAction = ghostty_vt.input.KeyAction; 59 pub const InputAction = ghostty_vt.input.KeyAction;
54 pub const InputKey = ghostty_vt.input.Key; 60 pub const InputKey = ghostty_vt.input.Key;
55 pub const InputMods = ghostty_vt.input.KeyMods; 61 pub const InputMods = ghostty_vt.input.KeyMods;
62 pub const Size = ghostty_vt.size_report.Size;
56 pub const CellColors = struct { 63 pub const CellColors = struct {
57 fg: [4]f32, 64 fg: [4]f32,
58 bg: [4]f32, 65 bg: [4]f32,
59 }; 66 };
60 67
61 pub const Terminal = struct { 68 pub const Terminal = struct {
69 pub const WritePtyFn = *const fn (*Terminal, ?*anyopaque, []const u8) void;
70 pub const TitleChangedFn = *const fn (*Terminal, ?*anyopaque, ?[:0]const u8) void;
71
72 const Hooks = struct {
73 write_pty_ctx: ?*anyopaque = null,
74 write_pty: ?WritePtyFn = null,
75 title_changed_ctx: ?*anyopaque = null,
76 title_changed: ?TitleChangedFn = null,
77 reported_size: ?Size = null,
78 };
79
62 alloc: std.mem.Allocator, 80 alloc: std.mem.Allocator,
63 inner: ghostty_vt.Terminal, 81 inner: ghostty_vt.Terminal,
64 stream: ghostty_vt.TerminalStream, 82 stream: ghostty_vt.TerminalStream,
65 render_state: ghostty_vt.RenderState, 83 render_state: ghostty_vt.RenderState,
84 hooks: Hooks = .{},
66 85
67 pub const InitOptions = struct { 86 pub const InitOptions = struct {
68 cols: u16, 87 cols: u16,
@@ -94,6 +113,7 @@ pub const Terminal = struct {
94 .inner = inner, 113 .inner = inner,
95 .stream = stream, 114 .stream = stream,
96 .render_state = render_state, 115 .render_state = render_state,
116 .hooks = .{},
97 }; 117 };
98 } 118 }
99 119
@@ -101,6 +121,12 @@ pub const Terminal = struct {
101 /// to its final memory location. Must be called once before write(). 121 /// to its final memory location. Must be called once before write().
102 fn fixupStreamPointer(self: *Terminal) void { 122 fn fixupStreamPointer(self: *Terminal) void {
103 self.stream.handler.terminal = &self.inner; 123 self.stream.handler.terminal = &self.inner;
124 self.stream.handler.effects.write_pty = &streamWritePty;
125 self.stream.handler.effects.title_changed = &streamTitleChanged;
126 self.stream.handler.effects.size = &streamSize;
127 self.stream.handler.effects.device_attributes = &streamDeviceAttributes;
128 self.stream.handler.effects.xtversion = &streamXtversion;
129 self.stream.handler.effects.color_scheme = &streamColorScheme;
104 } 130 }
105 131
106 pub fn deinit(self: *Terminal) void { 132 pub fn deinit(self: *Terminal) void {
@@ -109,10 +135,35 @@ pub const Terminal = struct {
109 } 135 }
110 136
111 pub fn write(self: *Terminal, bytes: []const u8) void { 137 pub fn write(self: *Terminal, bytes: []const u8) void {
112 self.stream.handler.terminal = &self.inner; 138 self.fixupStreamPointer();
113 self.stream.nextSlice(bytes); 139 self.stream.nextSlice(bytes);
114 } 140 }
115 141
142 pub fn setWritePtyCallback(
143 self: *Terminal,
144 ctx: ?*anyopaque,
145 callback: ?WritePtyFn,
146 ) void {
147 self.hooks.write_pty_ctx = ctx;
148 self.hooks.write_pty = callback;
149 self.fixupStreamPointer();
150 }
151
152 pub fn setTitleChangedCallback(
153 self: *Terminal,
154 ctx: ?*anyopaque,
155 callback: ?TitleChangedFn,
156 ) void {
157 self.hooks.title_changed_ctx = ctx;
158 self.hooks.title_changed = callback;
159 self.fixupStreamPointer();
160 }
161
162 pub fn setReportedSize(self: *Terminal, size: Size) void {
163 self.hooks.reported_size = size;
164 self.fixupStreamPointer();
165 }
166
116 pub fn encodeKey( 167 pub fn encodeKey(
117 self: *const Terminal, 168 self: *const Terminal,
118 key: InputKey, 169 key: InputKey,
@@ -182,6 +233,40 @@ fn rgbToFloat4(rgb: ghostty_vt.color.RGB) [4]f32 {
182 }; 233 };
183 } 234 }
184 235
236 fn terminalFromHandler(handler: *ghostty_vt.TerminalStream.Handler) *Terminal {
237 const inner: *ghostty_vt.Terminal = handler.terminal;
238 return @fieldParentPtr("inner", inner);
239 }
240
241 fn streamWritePty(handler: *ghostty_vt.TerminalStream.Handler, data: [:0]const u8) void {
242 const self = terminalFromHandler(handler);
243 const callback = self.hooks.write_pty orelse return;
244 callback(self, self.hooks.write_pty_ctx, data);
245 }
246
247 fn streamTitleChanged(handler: *ghostty_vt.TerminalStream.Handler) void {
248 const self = terminalFromHandler(handler);
249 const callback = self.hooks.title_changed orelse return;
250 callback(self, self.hooks.title_changed_ctx, self.inner.getTitle());
251 }
252
253 fn streamSize(handler: *ghostty_vt.TerminalStream.Handler) ?Size {
254 const self = terminalFromHandler(handler);
255 return self.hooks.reported_size;
256 }
257
258 fn streamDeviceAttributes(_: *ghostty_vt.TerminalStream.Handler) DeviceAttributesReturn {
259 return .{};
260 }
261
262 fn streamXtversion(_: *ghostty_vt.TerminalStream.Handler) []const u8 {
263 return "waystty";
264 }
265
266 fn streamColorScheme(_: *ghostty_vt.TerminalStream.Handler) ?ghostty_vt.device_status.ColorScheme {
267 return null;
268 }
269
185 test "Terminal init/deinit" { 270 test "Terminal init/deinit" {
186 var term = try Terminal.init(std.testing.allocator, .{ 271 var term = try Terminal.init(std.testing.allocator, .{
187 .cols = 80, 272 .cols = 80,
@@ -246,3 +331,153 @@ test "Terminal resolves ANSI fg and bg colors from render state" {
246 try std.testing.expectEqualDeep(rgbToFloat4(palette[1]), colors.fg); 331 try std.testing.expectEqualDeep(rgbToFloat4(palette[1]), colors.fg);
247 try std.testing.expectEqualDeep(rgbToFloat4(palette[4]), colors.bg); 332 try std.testing.expectEqualDeep(rgbToFloat4(palette[4]), colors.bg);
248 } 333 }
334
335 test "Terminal title callback fires on OSC 2" {
336 var term = try Terminal.init(std.testing.allocator, .{
337 .cols = 80,
338 .rows = 24,
339 });
340 defer term.deinit();
341
342 const S = struct {
343 var title: ?[]const u8 = null;
344
345 fn onTitle(_: *Terminal, _: ?*anyopaque, value: ?[:0]const u8) void {
346 title = if (value) |v| v else null;
347 }
348 };
349 S.title = null;
350
351 term.setTitleChangedCallback(null, &S.onTitle);
352 term.write("\x1b]2;waystty\x1b\\");
353
354 try std.testing.expectEqualStrings("waystty", S.title.?);
355 }
356
357 test "Terminal title callback fires on empty OSC 2" {
358 var term = try Terminal.init(std.testing.allocator, .{
359 .cols = 80,
360 .rows = 24,
361 });
362 defer term.deinit();
363
364 const S = struct {
365 var saw_callback = false;
366 var title_is_null = false;
367
368 fn onTitle(_: *Terminal, _: ?*anyopaque, value: ?[:0]const u8) void {
369 saw_callback = true;
370 title_is_null = value == null;
371 }
372 };
373 S.saw_callback = false;
374 S.title_is_null = false;
375
376 term.setTitleChangedCallback(null, &S.onTitle);
377 term.write("\x1b]2;\x1b\\");
378
379 try std.testing.expect(S.saw_callback);
380 try std.testing.expect(S.title_is_null);
381 }
382
383 test "Terminal write_pty callback emits cursor position reports" {
384 var term = try Terminal.init(std.testing.allocator, .{
385 .cols = 80,
386 .rows = 24,
387 });
388 defer term.deinit();
389
390 const S = struct {
391 var written: [128]u8 = undefined;
392 var written_len: usize = 0;
393
394 fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void {
395 @memcpy(written[0..data.len], data);
396 written_len = data.len;
397 }
398 };
399 S.written_len = 0;
400
401 term.setWritePtyCallback(null, &S.onWrite);
402 term.write("\x1b[6n");
403
404 try std.testing.expectEqualStrings("\x1b[1;1R", S.written[0..S.written_len]);
405 }
406
407 test "Terminal reports waystty for XTVERSION queries" {
408 var term = try Terminal.init(std.testing.allocator, .{
409 .cols = 80,
410 .rows = 24,
411 });
412 defer term.deinit();
413
414 const S = struct {
415 var written: [128]u8 = undefined;
416 var written_len: usize = 0;
417
418 fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void {
419 @memcpy(written[0..data.len], data);
420 written_len = data.len;
421 }
422 };
423 S.written_len = 0;
424
425 term.setWritePtyCallback(null, &S.onWrite);
426 term.write("\x1b[>0q");
427
428 try std.testing.expectEqualStrings("\x1bP>|waystty\x1b\\", S.written[0..S.written_len]);
429 }
430
431 test "Terminal reports configured size queries" {
432 var term = try Terminal.init(std.testing.allocator, .{
433 .cols = 80,
434 .rows = 24,
435 });
436 defer term.deinit();
437
438 const S = struct {
439 var written: [128]u8 = undefined;
440 var written_len: usize = 0;
441
442 fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void {
443 @memcpy(written[0..data.len], data);
444 written_len = data.len;
445 }
446 };
447 S.written_len = 0;
448
449 term.setWritePtyCallback(null, &S.onWrite);
450 term.setReportedSize(.{
451 .rows = 24,
452 .columns = 80,
453 .cell_width = 9,
454 .cell_height = 18,
455 });
456 term.write("\x1b[18t");
457
458 try std.testing.expectEqualStrings("\x1b[8;24;80t", S.written[0..S.written_len]);
459 }
460
461 test "Terminal reports default device attributes queries" {
462 var term = try Terminal.init(std.testing.allocator, .{
463 .cols = 80,
464 .rows = 24,
465 });
466 defer term.deinit();
467
468 const S = struct {
469 var buf: [64]u8 = undefined;
470 var written_len: usize = 0;
471
472 fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void {
473 written_len = data.len;
474 @memcpy(buf[0..data.len], data);
475 }
476 };
477 S.written_len = 0;
478
479 term.setWritePtyCallback(null, &S.onWrite);
480 term.write("\x1b[c");
481
482 try std.testing.expectEqualStrings("\x1b[?62;22c", S.buf[0..S.written_len]);
483 }
src/wayland.zig
Old New
@@ -127,6 +127,10 @@ pub const Window = struct {
127 self.surface.destroy(); 127 self.surface.destroy();
128 self.alloc.destroy(self); 128 self.alloc.destroy(self);
129 } 129 }
130
131 pub fn setTitle(self: *Window, title: ?[:0]const u8) void {
132 self.xdg_toplevel.setTitle((title orelse "waystty"));
133 }
130 }; 134 };
131 135
132 pub const Connection = struct { 136 pub const Connection = struct {