a modern tui library written in zig
at v0.4.1 271 lines 9.3 kB view raw
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}