a modern tui library written in zig
at v0.2.1 273 lines 9.4 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(.tty_watcher); 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 const winsize = try Tty.getWinsize(self.tty.fd); 103 _ = self.callback(self.ud, loop, self, .{ .winsize = winsize }); 104 } 105 106 fn signalCallback(ptr: *anyopaque) void { 107 const self: *Self = @ptrCast(@alignCast(ptr)); 108 self.winsize_wakeup.notify() catch |err| { 109 log.warn("couldn't wake up winsize callback: {}", .{err}); 110 }; 111 } 112 113 fn ttyReadCallback( 114 ud: ?*Self, 115 loop: *xev.Loop, 116 c: *xev.Completion, 117 _: xev.File, 118 buf: xev.ReadBuffer, 119 r: xev.ReadError!usize, 120 ) xev.CallbackAction { 121 const n = r catch |err| { 122 log.err("read error: {}", .{err}); 123 return .disarm; 124 }; 125 const self = ud orelse unreachable; 126 127 // reset read start state 128 self.read_buf_start = 0; 129 130 var seq_start: usize = 0; 131 parse_loop: while (seq_start < n) { 132 const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| { 133 log.err("couldn't parse input: {}", .{err}); 134 return .disarm; 135 }; 136 if (result.n == 0) { 137 // copy the read to the beginning. We don't use memcpy because 138 // this could be overlapping, and it's also rare 139 const initial_start = seq_start; 140 while (seq_start < n) : (seq_start += 1) { 141 self.read_buf[seq_start - initial_start] = self.read_buf[seq_start]; 142 } 143 self.read_buf_start = seq_start - initial_start + 1; 144 return .rearm; 145 } 146 seq_start += n; 147 const event_inner = result.event orelse { 148 log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]}); 149 continue :parse_loop; 150 }; 151 152 // Capture events we want to bubble up 153 const event: ?Event = switch (event_inner) { 154 .key_press => |key| .{ .key_press = key }, 155 .key_release => |key| .{ .key_release = key }, 156 .mouse => |mouse| .{ .mouse = mouse }, 157 .focus_in => .focus_in, 158 .focus_out => .focus_out, 159 .paste_start => .paste_start, 160 .paste_end => .paste_end, 161 .paste => |paste| .{ .paste = paste }, 162 .color_report => |report| .{ .color_report = report }, 163 .color_scheme => |scheme| .{ .color_scheme = scheme }, 164 .winsize => |ws| .{ .winsize = ws }, 165 166 // capability events which we handle below 167 .cap_kitty_keyboard, 168 .cap_kitty_graphics, 169 .cap_rgb, 170 .cap_unicode, 171 .cap_sgr_pixels, 172 .cap_color_scheme_updates, 173 .cap_da1, 174 => null, // handled below 175 }; 176 177 if (event) |ev| { 178 const action = self.callback(self.ud, loop, self, ev); 179 switch (action) { 180 .disarm => return .disarm, 181 else => continue :parse_loop, 182 } 183 } 184 185 switch (event_inner) { 186 .key_press, 187 .key_release, 188 .mouse, 189 .focus_in, 190 .focus_out, 191 .paste_start, 192 .paste_end, 193 .paste, 194 .color_report, 195 .color_scheme, 196 .winsize, 197 => unreachable, // handled above 198 199 .cap_kitty_keyboard => { 200 log.info("kitty keyboard capability detected", .{}); 201 self.vx.caps.kitty_keyboard = true; 202 }, 203 .cap_kitty_graphics => { 204 if (!self.vx.caps.kitty_graphics) { 205 log.info("kitty graphics capability detected", .{}); 206 self.vx.caps.kitty_graphics = true; 207 } 208 }, 209 .cap_rgb => { 210 log.info("rgb capability detected", .{}); 211 self.vx.caps.rgb = true; 212 }, 213 .cap_unicode => { 214 log.info("unicode capability detected", .{}); 215 self.vx.caps.unicode = .unicode; 216 self.vx.screen.width_method = .unicode; 217 }, 218 .cap_sgr_pixels => { 219 log.info("pixel mouse capability detected", .{}); 220 self.vx.caps.sgr_pixels = true; 221 }, 222 .cap_color_scheme_updates => { 223 log.info("color_scheme_updates capability detected", .{}); 224 self.vx.caps.color_scheme_updates = true; 225 }, 226 .cap_da1 => { 227 self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| { 228 log.err("couldn't enable features: {}", .{err}); 229 }; 230 }, 231 } 232 } 233 234 self.file.read( 235 loop, 236 c, 237 .{ .slice = &self.read_buf }, 238 Self, 239 self, 240 Self.ttyReadCallback, 241 ); 242 return .disarm; 243 } 244 245 fn winsizeCallback( 246 ud: ?*Self, 247 l: *xev.Loop, 248 c: *xev.Completion, 249 r: xev.Async.WaitError!void, 250 ) xev.CallbackAction { 251 _ = r catch |err| { 252 log.err("async error: {}", .{err}); 253 return .disarm; 254 }; 255 const self = ud orelse unreachable; // no userdata 256 const winsize = Tty.getWinsize(self.tty.fd) catch |err| { 257 log.err("couldn't get winsize: {}", .{err}); 258 return .disarm; 259 }; 260 const ret = self.callback(self.ud, l, self, .{ .winsize = winsize }); 261 if (ret == .disarm) return .disarm; 262 263 self.winsize_wakeup.wait( 264 l, 265 c, 266 Self, 267 self, 268 winsizeCallback, 269 ); 270 return .disarm; 271 } 272 }; 273}