this repo has no description
13
fork

Configure Feed

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

at b698a3641da7f3886bde4aeda7fcecc8d3642580 412 lines 17 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.writer()) 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_multi_cursor => { 366 log.info("multi cursor capability detected", .{}); 367 vx.caps.multi_cursor = true; 368 }, 369 .cap_da1 => { 370 std.Thread.Futex.wake(&vx.query_futex, 10); 371 vx.queries_done.store(true, .unordered); 372 }, 373 .winsize => |winsize| { 374 vx.state.in_band_resize = true; 375 switch (builtin.os.tag) { 376 .windows => {}, 377 // Reset the signal handler if we are receiving in_band_resize 378 else => Tty.resetSignalHandler(), 379 } 380 if (@hasField(Event, "winsize")) { 381 return self.postEvent(.{ .winsize = winsize }); 382 } 383 }, 384 } 385 }, 386 } 387} 388 389test Loop { 390 const Event = union(enum) { 391 key_press: vaxis.Key, 392 winsize: vaxis.Winsize, 393 focus_in, 394 foo: u8, 395 }; 396 397 var tty = try vaxis.Tty.init(&.{}); 398 defer tty.deinit(); 399 400 var vx = try vaxis.init(std.testing.allocator, .{}); 401 defer vx.deinit(std.testing.allocator, tty.writer()); 402 403 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 404 try loop.init(); 405 406 try loop.start(); 407 defer loop.stop(); 408 409 // Optionally enter the alternate screen 410 try vx.enterAltScreen(tty.writer()); 411 try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_ms); 412}