this repo has no description
13
fork

Configure Feed

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

at 5e39d991f73f59df546c0e0c467a34c2df4e0822 448 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 ConnectionResetByPeer, 118 EndOfStream, 119 InputOutput, 120 InvalidCharacter, 121 InvalidColorSpec, 122 InvalidPadding, 123 InvalidUTF8, 124 IoctlError, 125 IsDir, 126 LockViolation, 127 NoSpaceLeft, 128 NotOpenForReading, 129 OutOfMemory, 130 Overflow, 131 SocketUnconnected, 132 SystemResources, 133 Unexpected, 134 WouldBlock, 135 }; 136 137 /// read input from the tty. This is run in a separate thread 138 fn ttyRun(self: *Self, paste_allocator: ?std.mem.Allocator) void { 139 self._ttyRun(paste_allocator) catch {}; 140 } 141 142 fn _ttyRun( 143 self: *Self, 144 paste_allocator: ?std.mem.Allocator, 145 ) TtyRunError!void { 146 // Return early if we're in test mode to avoid infinite loops 147 if (builtin.is_test) return; 148 149 // initialize a grapheme cache 150 var cache: GraphemeCache = .{}; 151 152 switch (builtin.os.tag) { 153 .windows => { 154 var parser: Parser = .{}; 155 while (!self.should_quit) { 156 const event = try self.tty.nextEvent(&parser, paste_allocator); 157 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); 158 } 159 }, 160 else => { 161 // get our initial winsize 162 const winsize = try self.tty.getWinsize(); 163 if (@hasField(Event, "winsize")) { 164 try self.postEvent(.{ .winsize = winsize }); 165 } 166 167 var parser: Parser = .{}; 168 169 // initialize the read buffer 170 var buf: [1024]u8 = undefined; 171 var read_start: usize = 0; 172 // read loop 173 read_loop: while (!self.should_quit) { 174 const n = try self.tty.read(buf[read_start..]); 175 var seq_start: usize = 0; 176 while (seq_start < n) { 177 const result = try parser.parse(buf[seq_start..n], paste_allocator); 178 if (result.n == 0) { 179 // copy the read to the beginning. We don't use memcpy because 180 // this could be overlapping, and it's also rare 181 const initial_start = seq_start; 182 while (seq_start < n) : (seq_start += 1) { 183 buf[seq_start - initial_start] = buf[seq_start]; 184 } 185 read_start = seq_start - initial_start + 1; 186 continue :read_loop; 187 } 188 read_start = 0; 189 seq_start += result.n; 190 191 const event = result.event orelse continue; 192 try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator); 193 } 194 } 195 }, 196 } 197 } 198 }; 199} 200 201// Use return on the self.postEvent's so it can either return error union or void 202pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void { 203 switch (builtin.os.tag) { 204 .windows => { 205 switch (event) { 206 .winsize => |ws| { 207 if (@hasField(Event, "winsize")) { 208 return self.postEvent(.{ .winsize = ws }); 209 } 210 }, 211 .key_press => |key| { 212 // Check for a cursor position response for our explicit width query. This will 213 // always be an F3 key with shift = true, and we must be looking for queries 214 if (key.codepoint == vaxis.Key.f3 and 215 key.mods.shift and 216 !vx.queries_done.load(.unordered)) 217 { 218 log.info("explicit width capability detected", .{}); 219 vx.caps.explicit_width = true; 220 vx.caps.unicode = .unicode; 221 vx.screen.width_method = .unicode; 222 return; 223 } 224 // Check for a cursor position response for our scaled text query. This will 225 // always be an F3 key with alt = true, and we must be looking for queries 226 if (key.codepoint == vaxis.Key.f3 and 227 key.mods.alt and 228 !vx.queries_done.load(.unordered)) 229 { 230 log.info("scaled text capability detected", .{}); 231 vx.caps.scaled_text = true; 232 return; 233 } 234 if (@hasField(Event, "key_press")) { 235 // HACK: yuck. there has to be a better way 236 var mut_key = key; 237 if (key.text) |text| { 238 mut_key.text = cache.put(text); 239 } 240 return self.postEvent(.{ .key_press = mut_key }); 241 } 242 }, 243 .key_release => |key| { 244 if (@hasField(Event, "key_release")) { 245 // HACK: yuck. there has to be a better way 246 var mut_key = key; 247 if (key.text) |text| { 248 mut_key.text = cache.put(text); 249 } 250 return self.postEvent(.{ .key_release = mut_key }); 251 } 252 }, 253 .cap_da1 => { 254 std.Thread.Futex.wake(&vx.query_futex, 10); 255 vx.queries_done.store(true, .unordered); 256 }, 257 .mouse => |mouse| { 258 if (@hasField(Event, "mouse")) { 259 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 260 } 261 }, 262 .focus_in => { 263 if (@hasField(Event, "focus_in")) { 264 return self.postEvent(.focus_in); 265 } 266 }, 267 .focus_out => { 268 if (@hasField(Event, "focus_out")) { 269 return self.postEvent(.focus_out); 270 } 271 }, // Unsupported currently 272 else => {}, 273 } 274 }, 275 else => { 276 switch (event) { 277 .key_press => |key| { 278 // Check for a cursor position response for our explicitly width query. This will 279 // always be an F3 key with shift = true, and we must be looking for queries 280 if (key.codepoint == vaxis.Key.f3 and 281 key.mods.shift and 282 !vx.queries_done.load(.unordered)) 283 { 284 log.info("explicit width capability detected", .{}); 285 vx.caps.explicit_width = true; 286 vx.caps.unicode = .unicode; 287 vx.screen.width_method = .unicode; 288 return; 289 } 290 // Check for a cursor position response for our scaled text query. This will 291 // always be an F3 key with alt = true, and we must be looking for queries 292 if (key.codepoint == vaxis.Key.f3 and 293 key.mods.alt and 294 !vx.queries_done.load(.unordered)) 295 { 296 log.info("scaled text capability detected", .{}); 297 vx.caps.scaled_text = true; 298 return; 299 } 300 if (@hasField(Event, "key_press")) { 301 // HACK: yuck. there has to be a better way 302 var mut_key = key; 303 if (key.text) |text| { 304 mut_key.text = cache.put(text); 305 } 306 return self.postEvent(.{ .key_press = mut_key }); 307 } 308 }, 309 .key_release => |key| { 310 if (@hasField(Event, "key_release")) { 311 // HACK: yuck. there has to be a better way 312 var mut_key = key; 313 if (key.text) |text| { 314 mut_key.text = cache.put(text); 315 } 316 return self.postEvent(.{ .key_release = mut_key }); 317 } 318 }, 319 .mouse => |mouse| { 320 if (@hasField(Event, "mouse")) { 321 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 322 } 323 }, 324 .mouse_leave => { 325 if (@hasField(Event, "mouse_leave")) { 326 return self.postEvent(.mouse_leave); 327 } 328 }, 329 .focus_in => { 330 if (@hasField(Event, "focus_in")) { 331 return self.postEvent(.focus_in); 332 } 333 }, 334 .focus_out => { 335 if (@hasField(Event, "focus_out")) { 336 return self.postEvent(.focus_out); 337 } 338 }, 339 .paste_start => { 340 if (@hasField(Event, "paste_start")) { 341 return self.postEvent(.paste_start); 342 } 343 }, 344 .paste_end => { 345 if (@hasField(Event, "paste_end")) { 346 return self.postEvent(.paste_end); 347 } 348 }, 349 .paste => |text| { 350 if (@hasField(Event, "paste")) { 351 return self.postEvent(.{ .paste = text }); 352 } else { 353 if (paste_allocator) |_| 354 paste_allocator.?.free(text); 355 } 356 }, 357 .color_report => |report| { 358 if (@hasField(Event, "color_report")) { 359 return self.postEvent(.{ .color_report = report }); 360 } 361 }, 362 .color_scheme => |scheme| { 363 if (@hasField(Event, "color_scheme")) { 364 return self.postEvent(.{ .color_scheme = scheme }); 365 } 366 }, 367 .cap_kitty_keyboard => { 368 log.info("kitty keyboard capability detected", .{}); 369 vx.caps.kitty_keyboard = true; 370 }, 371 .cap_kitty_graphics => { 372 if (!vx.caps.kitty_graphics) { 373 log.info("kitty graphics capability detected", .{}); 374 vx.caps.kitty_graphics = true; 375 } 376 }, 377 .cap_rgb => { 378 log.info("rgb capability detected", .{}); 379 vx.caps.rgb = true; 380 }, 381 .cap_unicode => { 382 log.info("unicode capability detected", .{}); 383 vx.caps.unicode = .unicode; 384 vx.screen.width_method = .unicode; 385 }, 386 .cap_sgr_pixels => { 387 log.info("pixel mouse capability detected", .{}); 388 vx.caps.sgr_pixels = true; 389 }, 390 .cap_color_scheme_updates => { 391 log.info("color_scheme_updates capability detected", .{}); 392 vx.caps.color_scheme_updates = true; 393 }, 394 .cap_multi_cursor => { 395 log.info("multi cursor capability detected", .{}); 396 vx.caps.multi_cursor = true; 397 }, 398 .cap_da1 => { 399 std.Io.futexWake(vx.io, std.atomic.Value(u32), &vx.query_futex, 10); 400 vx.queries_done.store(true, .unordered); 401 }, 402 .winsize => |winsize| { 403 vx.state.in_band_resize = true; 404 switch (builtin.os.tag) { 405 .windows => {}, 406 // Reset the signal handler if we are receiving in_band_resize 407 else => Tty.resetSignalHandler(), 408 } 409 if (@hasField(Event, "winsize")) { 410 return self.postEvent(.{ .winsize = winsize }); 411 } 412 }, 413 } 414 }, 415 } 416} 417 418test Loop { 419 const io = std.testing.io; 420 var env_map = try std.testing.environ.createMap(std.testing.allocator); 421 defer env_map.deinit(); 422 423 const Event = union(enum) { 424 key_press: vaxis.Key, 425 winsize: vaxis.Winsize, 426 focus_in, 427 foo: u8, 428 }; 429 430 var tty = try vaxis.Tty.init(io, &.{}); 431 defer tty.deinit(); 432 433 var vx = try vaxis.init(io, std.testing.allocator, &env_map, .{}); 434 defer vx.deinit(std.testing.allocator, tty.writer()); 435 436 var loop: vaxis.Loop(Event) = .init(io, &tty, &vx); 437 438 try loop.start(); 439 defer loop.stop(); 440 441 // Optionally enter the alternate screen 442 try vx.enterAltScreen(tty.writer()); 443 try vx.queryTerminal(tty.writer(), .fromSeconds(1)); 444} 445 446test { 447 std.testing.refAllDecls(@This()); 448}