this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 50d7b413fb695dfa76dd250cc45fda8e9c2f06fa 408 lines 16 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3 4const Graphemes = @import("Graphemes"); 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 if (!builtin.is_test) { 35 const handler: Tty.SignalHandler = .{ 36 .context = self, 37 .callback = Self.winsizeCallback, 38 }; 39 try Tty.notifyWinsize(handler); 40 } 41 }, 42 } 43 } 44 45 /// spawns the input thread to read input from the tty 46 pub fn start(self: *Self) !void { 47 if (self.thread) |_| return; 48 self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{ 49 self, 50 &self.vaxis.unicode.width_data.graphemes, 51 self.vaxis.opts.system_clipboard_allocator, 52 }); 53 } 54 55 /// stops reading from the tty. 56 pub fn stop(self: *Self) void { 57 // If we don't have a thread, we have nothing to stop 58 if (self.thread == null) return; 59 self.should_quit = true; 60 // trigger a read 61 self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {}; 62 63 if (self.thread) |thread| { 64 thread.join(); 65 self.thread = null; 66 self.should_quit = false; 67 } 68 } 69 70 /// returns the next available event, blocking until one is available 71 pub fn nextEvent(self: *Self) T { 72 return self.queue.pop(); 73 } 74 75 /// blocks until an event is available. Useful when your application is 76 /// operating on a poll + drain architecture (see tryEvent) 77 pub fn pollEvent(self: *Self) void { 78 self.queue.poll(); 79 } 80 81 /// returns an event if one is available, otherwise null. Non-blocking. 82 pub fn tryEvent(self: *Self) ?T { 83 return self.queue.tryPop(); 84 } 85 86 /// posts an event into the event queue. Will block if there is not 87 /// capacity for the event 88 pub fn postEvent(self: *Self, event: T) void { 89 self.queue.push(event); 90 } 91 92 pub fn tryPostEvent(self: *Self, event: T) bool { 93 return self.queue.tryPush(event); 94 } 95 96 pub fn winsizeCallback(ptr: *anyopaque) void { 97 const self: *Self = @ptrCast(@alignCast(ptr)); 98 // We will be receiving winsize updates in-band 99 if (self.vaxis.state.in_band_resize) return; 100 101 const winsize = Tty.getWinsize(self.tty.fd) catch return; 102 if (@hasField(Event, "winsize")) { 103 self.postEvent(.{ .winsize = winsize }); 104 } 105 } 106 107 /// read input from the tty. This is run in a separate thread 108 fn ttyRun( 109 self: *Self, 110 grapheme_data: *const Graphemes, 111 paste_allocator: ?std.mem.Allocator, 112 ) !void { 113 // Return early if we're in test mode to avoid infinite loops 114 if (builtin.is_test) return; 115 116 // initialize a grapheme cache 117 var cache: GraphemeCache = .{}; 118 119 switch (builtin.os.tag) { 120 .windows => { 121 var parser: Parser = .{ 122 .grapheme_data = grapheme_data, 123 }; 124 while (!self.should_quit) { 125 const event = try self.tty.nextEvent(&parser, paste_allocator); 126 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); 127 } 128 }, 129 else => { 130 // get our initial winsize 131 const winsize = try Tty.getWinsize(self.tty.fd); 132 if (@hasField(Event, "winsize")) { 133 self.postEvent(.{ .winsize = winsize }); 134 } 135 136 var parser: Parser = .{ 137 .grapheme_data = grapheme_data, 138 }; 139 140 // initialize the read buffer 141 var buf: [1024]u8 = undefined; 142 var read_start: usize = 0; 143 // read loop 144 read_loop: while (!self.should_quit) { 145 const n = try self.tty.read(buf[read_start..]); 146 var seq_start: usize = 0; 147 while (seq_start < n) { 148 const result = try parser.parse(buf[seq_start..n], paste_allocator); 149 if (result.n == 0) { 150 // copy the read to the beginning. We don't use memcpy because 151 // this could be overlapping, and it's also rare 152 const initial_start = seq_start; 153 while (seq_start < n) : (seq_start += 1) { 154 buf[seq_start - initial_start] = buf[seq_start]; 155 } 156 read_start = seq_start - initial_start + 1; 157 continue :read_loop; 158 } 159 read_start = 0; 160 seq_start += result.n; 161 162 const event = result.event orelse continue; 163 try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator); 164 } 165 } 166 }, 167 } 168 } 169 }; 170} 171 172// Use return on the self.postEvent's so it can either return error union or void 173pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void { 174 switch (builtin.os.tag) { 175 .windows => { 176 switch (event) { 177 .winsize => |ws| { 178 if (@hasField(Event, "winsize")) { 179 return self.postEvent(.{ .winsize = ws }); 180 } 181 }, 182 .key_press => |key| { 183 // Check for a cursor position response for our explicit width query. This will 184 // always be an F3 key with shift = true, and we must be looking for queries 185 if (key.codepoint == vaxis.Key.f3 and 186 key.mods.shift and 187 !vx.queries_done.load(.unordered)) 188 { 189 log.info("explicit width capability detected", .{}); 190 vx.caps.explicit_width = true; 191 vx.caps.unicode = .unicode; 192 vx.screen.width_method = .unicode; 193 return; 194 } 195 // Check for a cursor position response for our scaled text query. This will 196 // always be an F3 key with alt = true, and we must be looking for queries 197 if (key.codepoint == vaxis.Key.f3 and 198 key.mods.alt and 199 !vx.queries_done.load(.unordered)) 200 { 201 log.info("scaled text capability detected", .{}); 202 vx.caps.scaled_text = true; 203 return; 204 } 205 if (@hasField(Event, "key_press")) { 206 // HACK: yuck. there has to be a better way 207 var mut_key = key; 208 if (key.text) |text| { 209 mut_key.text = cache.put(text); 210 } 211 return self.postEvent(.{ .key_press = mut_key }); 212 } 213 }, 214 .key_release => |key| { 215 if (@hasField(Event, "key_release")) { 216 // HACK: yuck. there has to be a better way 217 var mut_key = key; 218 if (key.text) |text| { 219 mut_key.text = cache.put(text); 220 } 221 return self.postEvent(.{ .key_release = mut_key }); 222 } 223 }, 224 .cap_da1 => { 225 std.Thread.Futex.wake(&vx.query_futex, 10); 226 vx.queries_done.store(true, .unordered); 227 }, 228 .mouse => |mouse| { 229 if (@hasField(Event, "mouse")) { 230 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 231 } 232 }, 233 .focus_in => { 234 if (@hasField(Event, "focus_in")) { 235 return self.postEvent(.focus_in); 236 } 237 }, 238 .focus_out => { 239 if (@hasField(Event, "focus_out")) { 240 return self.postEvent(.focus_out); 241 } 242 }, // Unsupported currently 243 else => {}, 244 } 245 }, 246 else => { 247 switch (event) { 248 .key_press => |key| { 249 // Check for a cursor position response for our explicity width query. This will 250 // always be an F3 key with shift = true, and we must be looking for queries 251 if (key.codepoint == vaxis.Key.f3 and 252 key.mods.shift and 253 !vx.queries_done.load(.unordered)) 254 { 255 log.info("explicit width capability detected", .{}); 256 vx.caps.explicit_width = true; 257 vx.caps.unicode = .unicode; 258 vx.screen.width_method = .unicode; 259 return; 260 } 261 // Check for a cursor position response for our scaled text query. This will 262 // always be an F3 key with alt = true, and we must be looking for queries 263 if (key.codepoint == vaxis.Key.f3 and 264 key.mods.alt and 265 !vx.queries_done.load(.unordered)) 266 { 267 log.info("scaled text capability detected", .{}); 268 vx.caps.scaled_text = true; 269 return; 270 } 271 if (@hasField(Event, "key_press")) { 272 // HACK: yuck. there has to be a better way 273 var mut_key = key; 274 if (key.text) |text| { 275 mut_key.text = cache.put(text); 276 } 277 return self.postEvent(.{ .key_press = mut_key }); 278 } 279 }, 280 .key_release => |key| { 281 if (@hasField(Event, "key_release")) { 282 // HACK: yuck. there has to be a better way 283 var mut_key = key; 284 if (key.text) |text| { 285 mut_key.text = cache.put(text); 286 } 287 return self.postEvent(.{ .key_release = mut_key }); 288 } 289 }, 290 .mouse => |mouse| { 291 if (@hasField(Event, "mouse")) { 292 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 293 } 294 }, 295 .mouse_leave => { 296 if (@hasField(Event, "mouse_leave")) { 297 return self.postEvent(.mouse_leave); 298 } 299 }, 300 .focus_in => { 301 if (@hasField(Event, "focus_in")) { 302 return self.postEvent(.focus_in); 303 } 304 }, 305 .focus_out => { 306 if (@hasField(Event, "focus_out")) { 307 return self.postEvent(.focus_out); 308 } 309 }, 310 .paste_start => { 311 if (@hasField(Event, "paste_start")) { 312 return self.postEvent(.paste_start); 313 } 314 }, 315 .paste_end => { 316 if (@hasField(Event, "paste_end")) { 317 return self.postEvent(.paste_end); 318 } 319 }, 320 .paste => |text| { 321 if (@hasField(Event, "paste")) { 322 return self.postEvent(.{ .paste = text }); 323 } else { 324 if (paste_allocator) |_| 325 paste_allocator.?.free(text); 326 } 327 }, 328 .color_report => |report| { 329 if (@hasField(Event, "color_report")) { 330 return self.postEvent(.{ .color_report = report }); 331 } 332 }, 333 .color_scheme => |scheme| { 334 if (@hasField(Event, "color_scheme")) { 335 return self.postEvent(.{ .color_scheme = scheme }); 336 } 337 }, 338 .cap_kitty_keyboard => { 339 log.info("kitty keyboard capability detected", .{}); 340 vx.caps.kitty_keyboard = true; 341 }, 342 .cap_kitty_graphics => { 343 if (!vx.caps.kitty_graphics) { 344 log.info("kitty graphics capability detected", .{}); 345 vx.caps.kitty_graphics = true; 346 } 347 }, 348 .cap_rgb => { 349 log.info("rgb capability detected", .{}); 350 vx.caps.rgb = true; 351 }, 352 .cap_unicode => { 353 log.info("unicode capability detected", .{}); 354 vx.caps.unicode = .unicode; 355 vx.screen.width_method = .unicode; 356 }, 357 .cap_sgr_pixels => { 358 log.info("pixel mouse capability detected", .{}); 359 vx.caps.sgr_pixels = true; 360 }, 361 .cap_color_scheme_updates => { 362 log.info("color_scheme_updates capability detected", .{}); 363 vx.caps.color_scheme_updates = true; 364 }, 365 .cap_da1 => { 366 std.Thread.Futex.wake(&vx.query_futex, 10); 367 vx.queries_done.store(true, .unordered); 368 }, 369 .winsize => |winsize| { 370 vx.state.in_band_resize = true; 371 switch (builtin.os.tag) { 372 .windows => {}, 373 // Reset the signal handler if we are receiving in_band_resize 374 else => Tty.resetSignalHandler(), 375 } 376 if (@hasField(Event, "winsize")) { 377 return self.postEvent(.{ .winsize = winsize }); 378 } 379 }, 380 } 381 }, 382 } 383} 384 385test Loop { 386 const Event = union(enum) { 387 key_press: vaxis.Key, 388 winsize: vaxis.Winsize, 389 focus_in, 390 foo: u8, 391 }; 392 393 var tty = try vaxis.Tty.init(&.{}); 394 defer tty.deinit(); 395 396 var vx = try vaxis.init(std.testing.allocator, .{}); 397 defer vx.deinit(std.testing.allocator, tty.anyWriter()); 398 399 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 400 try loop.init(); 401 402 try loop.start(); 403 defer loop.stop(); 404 405 // Optionally enter the alternate screen 406 try vx.enterAltScreen(tty.anyWriter()); 407 try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms); 408}