a modern tui library written in zig
at v0.5.1 12 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3 4const grapheme = @import("grapheme"); 5 6const GraphemeCache = @import("GraphemeCache.zig"); 7const Parser = @import("Parser.zig"); 8const Queue = @import("queue.zig").Queue; 9const vaxis = @import("main.zig"); 10const Tty = vaxis.Tty; 11const Vaxis = @import("Vaxis.zig"); 12 13const log = std.log.scoped(.vaxis); 14 15pub fn Loop(comptime T: type) type { 16 return struct { 17 const Self = @This(); 18 19 const Event = T; 20 21 tty: *Tty, 22 vaxis: *Vaxis, 23 24 queue: Queue(T, 512) = .{}, 25 thread: ?std.Thread = null, 26 should_quit: bool = false, 27 28 /// Initialize the event loop. This is an intrusive init so that we have 29 /// a stable pointer to register signal callbacks with posix TTYs 30 pub fn init(self: *Self) !void { 31 switch (builtin.os.tag) { 32 .windows => {}, 33 else => { 34 const handler: Tty.SignalHandler = .{ 35 .context = self, 36 .callback = Self.winsizeCallback, 37 }; 38 try Tty.notifyWinsize(handler); 39 }, 40 } 41 } 42 43 /// spawns the input thread to read input from the tty 44 pub fn start(self: *Self) !void { 45 if (self.thread) |_| return; 46 self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{ 47 self, 48 &self.vaxis.unicode.grapheme_data, 49 self.vaxis.opts.system_clipboard_allocator, 50 }); 51 } 52 53 /// stops reading from the tty. 54 pub fn stop(self: *Self) void { 55 // If we don't have a thread, we have nothing to stop 56 if (self.thread == null) return; 57 self.should_quit = true; 58 // trigger a read 59 self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {}; 60 61 if (self.thread) |thread| { 62 thread.join(); 63 self.thread = null; 64 self.should_quit = false; 65 } 66 } 67 68 /// returns the next available event, blocking until one is available 69 pub fn nextEvent(self: *Self) T { 70 return self.queue.pop(); 71 } 72 73 /// blocks until an event is available. Useful when your application is 74 /// operating on a poll + drain architecture (see tryEvent) 75 pub fn pollEvent(self: *Self) void { 76 self.queue.poll(); 77 } 78 79 /// returns an event if one is available, otherwise null. Non-blocking. 80 pub fn tryEvent(self: *Self) ?T { 81 return self.queue.tryPop(); 82 } 83 84 /// posts an event into the event queue. Will block if there is not 85 /// capacity for the event 86 pub fn postEvent(self: *Self, event: T) void { 87 self.queue.push(event); 88 } 89 90 pub fn tryPostEvent(self: *Self, event: T) bool { 91 return self.queue.tryPush(event); 92 } 93 94 pub fn winsizeCallback(ptr: *anyopaque) void { 95 const self: *Self = @ptrCast(@alignCast(ptr)); 96 // We will be receiving winsize updates in-band 97 if (self.vaxis.state.in_band_resize) return; 98 99 const winsize = Tty.getWinsize(self.tty.fd) catch return; 100 if (@hasField(Event, "winsize")) { 101 self.postEvent(.{ .winsize = winsize }); 102 } 103 } 104 105 /// read input from the tty. This is run in a separate thread 106 fn ttyRun( 107 self: *Self, 108 grapheme_data: *const grapheme.GraphemeData, 109 paste_allocator: ?std.mem.Allocator, 110 ) !void { 111 // initialize a grapheme cache 112 var cache: GraphemeCache = .{}; 113 114 switch (builtin.os.tag) { 115 .windows => { 116 var parser: Parser = .{ 117 .grapheme_data = grapheme_data, 118 }; 119 while (!self.should_quit) { 120 const event = try self.tty.nextEvent(&parser, paste_allocator); 121 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); 122 } 123 }, 124 else => { 125 // get our initial winsize 126 const winsize = try Tty.getWinsize(self.tty.fd); 127 if (@hasField(Event, "winsize")) { 128 self.postEvent(.{ .winsize = winsize }); 129 } 130 131 var parser: Parser = .{ 132 .grapheme_data = grapheme_data, 133 }; 134 135 // initialize the read buffer 136 var buf: [1024]u8 = undefined; 137 var read_start: usize = 0; 138 // read loop 139 read_loop: while (!self.should_quit) { 140 const n = try self.tty.read(buf[read_start..]); 141 var seq_start: usize = 0; 142 while (seq_start < n) { 143 const result = try parser.parse(buf[seq_start..n], paste_allocator); 144 if (result.n == 0) { 145 // copy the read to the beginning. We don't use memcpy because 146 // this could be overlapping, and it's also rare 147 const initial_start = seq_start; 148 while (seq_start < n) : (seq_start += 1) { 149 buf[seq_start - initial_start] = buf[seq_start]; 150 } 151 read_start = seq_start - initial_start + 1; 152 continue :read_loop; 153 } 154 read_start = 0; 155 seq_start += result.n; 156 157 const event = result.event orelse continue; 158 try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator); 159 } 160 } 161 }, 162 } 163 } 164 }; 165} 166 167// Use return on the self.postEvent's so it can either return error union or void 168pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void { 169 switch (builtin.os.tag) { 170 .windows => { 171 switch (event) { 172 .winsize => |ws| { 173 if (@hasField(Event, "winsize")) { 174 return self.postEvent(.{ .winsize = ws }); 175 } 176 }, 177 .key_press => |key| { 178 if (@hasField(Event, "key_press")) { 179 // HACK: yuck. there has to be a better way 180 var mut_key = key; 181 if (key.text) |text| { 182 mut_key.text = cache.put(text); 183 } 184 return self.postEvent(.{ .key_press = mut_key }); 185 } 186 }, 187 .key_release => |key| { 188 if (@hasField(Event, "key_release")) { 189 // HACK: yuck. there has to be a better way 190 var mut_key = key; 191 if (key.text) |text| { 192 mut_key.text = cache.put(text); 193 } 194 return self.postEvent(.{ .key_release = mut_key }); 195 } 196 }, 197 .cap_da1 => { 198 std.Thread.Futex.wake(&vx.query_futex, 10); 199 }, 200 .mouse => {}, // Unsupported currently 201 else => {}, 202 } 203 }, 204 else => { 205 switch (event) { 206 .key_press => |key| { 207 if (@hasField(Event, "key_press")) { 208 // HACK: yuck. there has to be a better way 209 var mut_key = key; 210 if (key.text) |text| { 211 mut_key.text = cache.put(text); 212 } 213 return self.postEvent(.{ .key_press = mut_key }); 214 } 215 }, 216 .key_release => |key| { 217 if (@hasField(Event, "key_release")) { 218 // HACK: yuck. there has to be a better way 219 var mut_key = key; 220 if (key.text) |text| { 221 mut_key.text = cache.put(text); 222 } 223 return self.postEvent(.{ .key_release = mut_key }); 224 } 225 }, 226 .mouse => |mouse| { 227 if (@hasField(Event, "mouse")) { 228 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 229 } 230 }, 231 .focus_in => { 232 if (@hasField(Event, "focus_in")) { 233 return self.postEvent(.focus_in); 234 } 235 }, 236 .focus_out => { 237 if (@hasField(Event, "focus_out")) { 238 return self.postEvent(.focus_out); 239 } 240 }, 241 .paste_start => { 242 if (@hasField(Event, "paste_start")) { 243 return self.postEvent(.paste_start); 244 } 245 }, 246 .paste_end => { 247 if (@hasField(Event, "paste_end")) { 248 return self.postEvent(.paste_end); 249 } 250 }, 251 .paste => |text| { 252 if (@hasField(Event, "paste")) { 253 return self.postEvent(.{ .paste = text }); 254 } else { 255 if (paste_allocator) |_| 256 paste_allocator.?.free(text); 257 } 258 }, 259 .color_report => |report| { 260 if (@hasField(Event, "color_report")) { 261 return self.postEvent(.{ .color_report = report }); 262 } 263 }, 264 .color_scheme => |scheme| { 265 if (@hasField(Event, "color_scheme")) { 266 return self.postEvent(.{ .color_scheme = scheme }); 267 } 268 }, 269 .cap_kitty_keyboard => { 270 log.info("kitty keyboard capability detected", .{}); 271 vx.caps.kitty_keyboard = true; 272 }, 273 .cap_kitty_graphics => { 274 if (!vx.caps.kitty_graphics) { 275 log.info("kitty graphics capability detected", .{}); 276 vx.caps.kitty_graphics = true; 277 } 278 }, 279 .cap_rgb => { 280 log.info("rgb capability detected", .{}); 281 vx.caps.rgb = true; 282 }, 283 .cap_unicode => { 284 log.info("unicode capability detected", .{}); 285 vx.caps.unicode = .unicode; 286 vx.screen.width_method = .unicode; 287 }, 288 .cap_sgr_pixels => { 289 log.info("pixel mouse capability detected", .{}); 290 vx.caps.sgr_pixels = true; 291 }, 292 .cap_color_scheme_updates => { 293 log.info("color_scheme_updates capability detected", .{}); 294 vx.caps.color_scheme_updates = true; 295 }, 296 .cap_da1 => { 297 std.Thread.Futex.wake(&vx.query_futex, 10); 298 }, 299 .winsize => |winsize| { 300 vx.state.in_band_resize = true; 301 if (@hasField(Event, "winsize")) { 302 return self.postEvent(.{ .winsize = winsize }); 303 } 304 305 switch (builtin.os.tag) { 306 .windows => {}, 307 // Reset the signal handler if we are receiving in_band_resize 308 else => self.tty.resetSignalHandler(), 309 } 310 }, 311 } 312 }, 313 } 314}