this repo has no description
13
fork

Configure Feed

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

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