b2d1bc2e
Wire terminal effect callbacks
a73x 2026-04-08 13:09
diff --git a/src/main.zig b/src/main.zig index 1060bb3..e057439 100644 --- a/src/main.zig +++ b/src/main.zig @@ -14,6 +14,19 @@ const GridSize = struct { rows: u16, }; fn writePtyFromTerminal(_: *vt.Terminal, ctx: ?*anyopaque, data: []const u8) void { const p: *pty.Pty = @ptrCast(@alignCast(ctx orelse return)); _ = p.write(data) catch |err| { std.log.warn("pty write callback failed: {}", .{err}); return; }; } fn updateWindowTitle(_: *vt.Terminal, ctx: ?*anyopaque, title: ?[:0]const u8) void { const window: *wayland_client.Window = @ptrCast(@alignCast(ctx orelse return)); window.setTitle(title); } pub fn main() !void { var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); @@ -106,6 +119,13 @@ fn runTerminal(alloc: std.mem.Allocator) !void { .max_scrollback = 1000, }); defer term.deinit(); term.setTitleChangedCallback(window, &updateWindowTitle); term.setReportedSize(.{ .rows = rows, .columns = cols, .cell_width = cell_w, .cell_height = cell_h, }); // === pty === const shell: [:0]const u8 = blk: { @@ -116,6 +136,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { var p = try pty.Pty.spawn(.{ .cols = cols, .rows = rows, .shell = shell }); defer p.deinit(); term.setWritePtyCallback(&p, &writePtyFromTerminal); // === instance buffer === var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; @@ -183,6 +204,12 @@ fn runTerminal(alloc: std.mem.Allocator) !void { try p.resize(new_grid.cols, new_grid.rows); cols = new_grid.cols; rows = new_grid.rows; term.setReportedSize(.{ .rows = rows, .columns = cols, .cell_width = cell_w, .cell_height = cell_h, }); } else { _ = try ctx.vkd.deviceWaitIdle(ctx.device); try ctx.recreateSwapchain(window.width, window.height); diff --git a/src/vt.zig b/src/vt.zig index e0f67b4..a320058 100644 --- a/src/vt.zig +++ b/src/vt.zig @@ -49,20 +49,39 @@ const std = @import("std"); const ghostty_vt = @import("ghostty-vt"); const DeviceAttributesCallback = std.meta.Child( @FieldType(ghostty_vt.TerminalStream.Handler.Effects, "device_attributes"), ); const DeviceAttributesReturn = @typeInfo( std.meta.Child(DeviceAttributesCallback), ).@"fn".return_type.?; pub const InputAction = ghostty_vt.input.KeyAction; pub const InputKey = ghostty_vt.input.Key; pub const InputMods = ghostty_vt.input.KeyMods; pub const Size = ghostty_vt.size_report.Size; pub const CellColors = struct { fg: [4]f32, bg: [4]f32, }; pub const Terminal = struct { pub const WritePtyFn = *const fn (*Terminal, ?*anyopaque, []const u8) void; pub const TitleChangedFn = *const fn (*Terminal, ?*anyopaque, ?[:0]const u8) void; const Hooks = struct { write_pty_ctx: ?*anyopaque = null, write_pty: ?WritePtyFn = null, title_changed_ctx: ?*anyopaque = null, title_changed: ?TitleChangedFn = null, reported_size: ?Size = null, }; alloc: std.mem.Allocator, inner: ghostty_vt.Terminal, stream: ghostty_vt.TerminalStream, render_state: ghostty_vt.RenderState, hooks: Hooks = .{}, pub const InitOptions = struct { cols: u16, @@ -94,6 +113,7 @@ pub const Terminal = struct { .inner = inner, .stream = stream, .render_state = render_state, .hooks = .{}, }; } @@ -101,6 +121,12 @@ pub const Terminal = struct { /// to its final memory location. Must be called once before write(). fn fixupStreamPointer(self: *Terminal) void { self.stream.handler.terminal = &self.inner; self.stream.handler.effects.write_pty = &streamWritePty; self.stream.handler.effects.title_changed = &streamTitleChanged; self.stream.handler.effects.size = &streamSize; self.stream.handler.effects.device_attributes = &streamDeviceAttributes; self.stream.handler.effects.xtversion = &streamXtversion; self.stream.handler.effects.color_scheme = &streamColorScheme; } pub fn deinit(self: *Terminal) void { @@ -109,10 +135,35 @@ pub const Terminal = struct { } pub fn write(self: *Terminal, bytes: []const u8) void { self.stream.handler.terminal = &self.inner; self.fixupStreamPointer(); self.stream.nextSlice(bytes); } pub fn setWritePtyCallback( self: *Terminal, ctx: ?*anyopaque, callback: ?WritePtyFn, ) void { self.hooks.write_pty_ctx = ctx; self.hooks.write_pty = callback; self.fixupStreamPointer(); } pub fn setTitleChangedCallback( self: *Terminal, ctx: ?*anyopaque, callback: ?TitleChangedFn, ) void { self.hooks.title_changed_ctx = ctx; self.hooks.title_changed = callback; self.fixupStreamPointer(); } pub fn setReportedSize(self: *Terminal, size: Size) void { self.hooks.reported_size = size; self.fixupStreamPointer(); } pub fn encodeKey( self: *const Terminal, key: InputKey, @@ -182,6 +233,40 @@ fn rgbToFloat4(rgb: ghostty_vt.color.RGB) [4]f32 { }; } fn terminalFromHandler(handler: *ghostty_vt.TerminalStream.Handler) *Terminal { const inner: *ghostty_vt.Terminal = handler.terminal; return @fieldParentPtr("inner", inner); } fn streamWritePty(handler: *ghostty_vt.TerminalStream.Handler, data: [:0]const u8) void { const self = terminalFromHandler(handler); const callback = self.hooks.write_pty orelse return; callback(self, self.hooks.write_pty_ctx, data); } fn streamTitleChanged(handler: *ghostty_vt.TerminalStream.Handler) void { const self = terminalFromHandler(handler); const callback = self.hooks.title_changed orelse return; callback(self, self.hooks.title_changed_ctx, self.inner.getTitle()); } fn streamSize(handler: *ghostty_vt.TerminalStream.Handler) ?Size { const self = terminalFromHandler(handler); return self.hooks.reported_size; } fn streamDeviceAttributes(_: *ghostty_vt.TerminalStream.Handler) DeviceAttributesReturn { return .{}; } fn streamXtversion(_: *ghostty_vt.TerminalStream.Handler) []const u8 { return "waystty"; } fn streamColorScheme(_: *ghostty_vt.TerminalStream.Handler) ?ghostty_vt.device_status.ColorScheme { return null; } test "Terminal init/deinit" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, @@ -246,3 +331,153 @@ test "Terminal resolves ANSI fg and bg colors from render state" { try std.testing.expectEqualDeep(rgbToFloat4(palette[1]), colors.fg); try std.testing.expectEqualDeep(rgbToFloat4(palette[4]), colors.bg); } test "Terminal title callback fires on OSC 2" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); const S = struct { var title: ?[]const u8 = null; fn onTitle(_: *Terminal, _: ?*anyopaque, value: ?[:0]const u8) void { title = if (value) |v| v else null; } }; S.title = null; term.setTitleChangedCallback(null, &S.onTitle); term.write("\x1b]2;waystty\x1b\\"); try std.testing.expectEqualStrings("waystty", S.title.?); } test "Terminal title callback fires on empty OSC 2" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); const S = struct { var saw_callback = false; var title_is_null = false; fn onTitle(_: *Terminal, _: ?*anyopaque, value: ?[:0]const u8) void { saw_callback = true; title_is_null = value == null; } }; S.saw_callback = false; S.title_is_null = false; term.setTitleChangedCallback(null, &S.onTitle); term.write("\x1b]2;\x1b\\"); try std.testing.expect(S.saw_callback); try std.testing.expect(S.title_is_null); } test "Terminal write_pty callback emits cursor position reports" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); const S = struct { var written: [128]u8 = undefined; var written_len: usize = 0; fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void { @memcpy(written[0..data.len], data); written_len = data.len; } }; S.written_len = 0; term.setWritePtyCallback(null, &S.onWrite); term.write("\x1b[6n"); try std.testing.expectEqualStrings("\x1b[1;1R", S.written[0..S.written_len]); } test "Terminal reports waystty for XTVERSION queries" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); const S = struct { var written: [128]u8 = undefined; var written_len: usize = 0; fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void { @memcpy(written[0..data.len], data); written_len = data.len; } }; S.written_len = 0; term.setWritePtyCallback(null, &S.onWrite); term.write("\x1b[>0q"); try std.testing.expectEqualStrings("\x1bP>|waystty\x1b\\", S.written[0..S.written_len]); } test "Terminal reports configured size queries" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); const S = struct { var written: [128]u8 = undefined; var written_len: usize = 0; fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void { @memcpy(written[0..data.len], data); written_len = data.len; } }; S.written_len = 0; term.setWritePtyCallback(null, &S.onWrite); term.setReportedSize(.{ .rows = 24, .columns = 80, .cell_width = 9, .cell_height = 18, }); term.write("\x1b[18t"); try std.testing.expectEqualStrings("\x1b[8;24;80t", S.written[0..S.written_len]); } test "Terminal reports default device attributes queries" { var term = try Terminal.init(std.testing.allocator, .{ .cols = 80, .rows = 24, }); defer term.deinit(); const S = struct { var buf: [64]u8 = undefined; var written_len: usize = 0; fn onWrite(_: *Terminal, _: ?*anyopaque, data: []const u8) void { written_len = data.len; @memcpy(buf[0..data.len], data); } }; S.written_len = 0; term.setWritePtyCallback(null, &S.onWrite); term.write("\x1b[c"); try std.testing.expectEqualStrings("\x1b[?62;22c", S.buf[0..S.written_len]); } diff --git a/src/wayland.zig b/src/wayland.zig index 0b9d5fa..6e4b2cf 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -127,6 +127,10 @@ pub const Window = struct { self.surface.destroy(); self.alloc.destroy(self); } pub fn setTitle(self: *Window, title: ?[:0]const u8) void { self.xdg_toplevel.setTitle((title orelse "waystty")); } }; pub const Connection = struct {