a modern tui library written in zig
1const std = @import("std");
2const xev = @import("xev");
3
4const Tty = @import("main.zig").Tty;
5const Winsize = @import("main.zig").Winsize;
6const Vaxis = @import("Vaxis.zig");
7const Parser = @import("Parser.zig");
8const Key = @import("Key.zig");
9const Mouse = @import("Mouse.zig");
10const Color = @import("Cell.zig").Color;
11
12const log = std.log.scoped(.vaxis_xev);
13
14pub const Event = union(enum) {
15 key_press: Key,
16 key_release: Key,
17 mouse: Mouse,
18 focus_in,
19 focus_out,
20 paste_start, // bracketed paste start
21 paste_end, // bracketed paste end
22 paste: []const u8, // osc 52 paste, caller must free
23 color_report: Color.Report, // osc 4, 10, 11, 12 response
24 color_scheme: Color.Scheme,
25 winsize: Winsize,
26};
27
28pub fn TtyWatcher(comptime Userdata: type) type {
29 return struct {
30 const Self = @This();
31
32 file: xev.File,
33 tty: *Tty,
34
35 read_buf: [4096]u8,
36 read_buf_start: usize,
37 read_cmp: xev.Completion,
38
39 winsize_wakeup: xev.Async,
40 winsize_cmp: xev.Completion,
41
42 callback: *const fn (
43 ud: ?*Userdata,
44 loop: *xev.Loop,
45 watcher: *Self,
46 event: Event,
47 ) xev.CallbackAction,
48
49 ud: ?*Userdata,
50 vx: *Vaxis,
51 parser: Parser,
52
53 pub fn init(
54 self: *Self,
55 tty: *Tty,
56 vaxis: *Vaxis,
57 loop: *xev.Loop,
58 userdata: ?*Userdata,
59 callback: *const fn (
60 ud: ?*Userdata,
61 loop: *xev.Loop,
62 watcher: *Self,
63 event: Event,
64 ) xev.CallbackAction,
65 ) !void {
66 self.* = .{
67 .tty = tty,
68 .file = xev.File.initFd(tty.fd),
69 .read_buf = undefined,
70 .read_buf_start = 0,
71 .read_cmp = .{},
72
73 .winsize_wakeup = try xev.Async.init(),
74 .winsize_cmp = .{},
75
76 .callback = callback,
77 .ud = userdata,
78 .vx = vaxis,
79 .parser = .{ .grapheme_data = &vaxis.unicode.grapheme_data },
80 };
81
82 self.file.read(
83 loop,
84 &self.read_cmp,
85 .{ .slice = &self.read_buf },
86 Self,
87 self,
88 Self.ttyReadCallback,
89 );
90 self.winsize_wakeup.wait(
91 loop,
92 &self.winsize_cmp,
93 Self,
94 self,
95 winsizeCallback,
96 );
97 const handler: Tty.SignalHandler = .{
98 .context = self,
99 .callback = Self.signalCallback,
100 };
101 try Tty.notifyWinsize(handler);
102 }
103
104 fn signalCallback(ptr: *anyopaque) void {
105 const self: *Self = @ptrCast(@alignCast(ptr));
106 self.winsize_wakeup.notify() catch |err| {
107 log.warn("couldn't wake up winsize callback: {}", .{err});
108 };
109 }
110
111 fn ttyReadCallback(
112 ud: ?*Self,
113 loop: *xev.Loop,
114 c: *xev.Completion,
115 _: xev.File,
116 buf: xev.ReadBuffer,
117 r: xev.ReadError!usize,
118 ) xev.CallbackAction {
119 const n = r catch |err| {
120 log.err("read error: {}", .{err});
121 return .disarm;
122 };
123 const self = ud orelse unreachable;
124
125 // reset read start state
126 self.read_buf_start = 0;
127
128 var seq_start: usize = 0;
129 parse_loop: while (seq_start < n) {
130 const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| {
131 log.err("couldn't parse input: {}", .{err});
132 return .disarm;
133 };
134 if (result.n == 0) {
135 // copy the read to the beginning. We don't use memcpy because
136 // this could be overlapping, and it's also rare
137 const initial_start = seq_start;
138 while (seq_start < n) : (seq_start += 1) {
139 self.read_buf[seq_start - initial_start] = self.read_buf[seq_start];
140 }
141 self.read_buf_start = seq_start - initial_start + 1;
142 return .rearm;
143 }
144 seq_start += n;
145 const event_inner = result.event orelse {
146 log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]});
147 continue :parse_loop;
148 };
149
150 // Capture events we want to bubble up
151 const event: ?Event = switch (event_inner) {
152 .key_press => |key| .{ .key_press = key },
153 .key_release => |key| .{ .key_release = key },
154 .mouse => |mouse| .{ .mouse = mouse },
155 .focus_in => .focus_in,
156 .focus_out => .focus_out,
157 .paste_start => .paste_start,
158 .paste_end => .paste_end,
159 .paste => |paste| .{ .paste = paste },
160 .color_report => |report| .{ .color_report = report },
161 .color_scheme => |scheme| .{ .color_scheme = scheme },
162 .winsize => |ws| .{ .winsize = ws },
163
164 // capability events which we handle below
165 .cap_kitty_keyboard,
166 .cap_kitty_graphics,
167 .cap_rgb,
168 .cap_unicode,
169 .cap_sgr_pixels,
170 .cap_color_scheme_updates,
171 .cap_da1,
172 => null, // handled below
173 };
174
175 if (event) |ev| {
176 const action = self.callback(self.ud, loop, self, ev);
177 switch (action) {
178 .disarm => return .disarm,
179 else => continue :parse_loop,
180 }
181 }
182
183 switch (event_inner) {
184 .key_press,
185 .key_release,
186 .mouse,
187 .focus_in,
188 .focus_out,
189 .paste_start,
190 .paste_end,
191 .paste,
192 .color_report,
193 .color_scheme,
194 .winsize,
195 => unreachable, // handled above
196
197 .cap_kitty_keyboard => {
198 log.info("kitty keyboard capability detected", .{});
199 self.vx.caps.kitty_keyboard = true;
200 },
201 .cap_kitty_graphics => {
202 if (!self.vx.caps.kitty_graphics) {
203 log.info("kitty graphics capability detected", .{});
204 self.vx.caps.kitty_graphics = true;
205 }
206 },
207 .cap_rgb => {
208 log.info("rgb capability detected", .{});
209 self.vx.caps.rgb = true;
210 },
211 .cap_unicode => {
212 log.info("unicode capability detected", .{});
213 self.vx.caps.unicode = .unicode;
214 self.vx.screen.width_method = .unicode;
215 },
216 .cap_sgr_pixels => {
217 log.info("pixel mouse capability detected", .{});
218 self.vx.caps.sgr_pixels = true;
219 },
220 .cap_color_scheme_updates => {
221 log.info("color_scheme_updates capability detected", .{});
222 self.vx.caps.color_scheme_updates = true;
223 },
224 .cap_da1 => {
225 self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| {
226 log.err("couldn't enable features: {}", .{err});
227 };
228 },
229 }
230 }
231
232 self.file.read(
233 loop,
234 c,
235 .{ .slice = &self.read_buf },
236 Self,
237 self,
238 Self.ttyReadCallback,
239 );
240 return .disarm;
241 }
242
243 fn winsizeCallback(
244 ud: ?*Self,
245 l: *xev.Loop,
246 c: *xev.Completion,
247 r: xev.Async.WaitError!void,
248 ) xev.CallbackAction {
249 _ = r catch |err| {
250 log.err("async error: {}", .{err});
251 return .disarm;
252 };
253 const self = ud orelse unreachable; // no userdata
254 const winsize = Tty.getWinsize(self.tty.fd) catch |err| {
255 log.err("couldn't get winsize: {}", .{err});
256 return .disarm;
257 };
258 const ret = self.callback(self.ud, l, self, .{ .winsize = winsize });
259 if (ret == .disarm) return .disarm;
260
261 self.winsize_wakeup.wait(
262 l,
263 c,
264 Self,
265 self,
266 winsizeCallback,
267 );
268 return .disarm;
269 }
270 };
271}