this repo has no description
13
fork

Configure Feed

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

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