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