this repo has no description
13
fork

Configure Feed

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

at 990fe29b38e57de69733025ab580b8917c2d092f 400 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 // initialize a grapheme cache 114 var cache: GraphemeCache = .{}; 115 116 switch (builtin.os.tag) { 117 .windows => { 118 var parser: Parser = .{ 119 .grapheme_data = grapheme_data, 120 }; 121 while (!self.should_quit) { 122 const event = try self.tty.nextEvent(&parser, paste_allocator); 123 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); 124 } 125 }, 126 else => { 127 // get our initial winsize 128 const winsize = try Tty.getWinsize(self.tty.fd); 129 if (@hasField(Event, "winsize")) { 130 self.postEvent(.{ .winsize = winsize }); 131 } 132 133 var parser: Parser = .{ 134 .grapheme_data = grapheme_data, 135 }; 136 137 // initialize the read buffer 138 var buf: [1024]u8 = undefined; 139 var read_start: usize = 0; 140 // read loop 141 read_loop: while (!self.should_quit) { 142 const n = try self.tty.read(buf[read_start..]); 143 var seq_start: usize = 0; 144 while (seq_start < n) { 145 const result = try parser.parse(buf[seq_start..n], paste_allocator); 146 if (result.n == 0) { 147 // copy the read to the beginning. We don't use memcpy because 148 // this could be overlapping, and it's also rare 149 const initial_start = seq_start; 150 while (seq_start < n) : (seq_start += 1) { 151 buf[seq_start - initial_start] = buf[seq_start]; 152 } 153 read_start = seq_start - initial_start + 1; 154 continue :read_loop; 155 } 156 read_start = 0; 157 seq_start += result.n; 158 159 const event = result.event orelse continue; 160 try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator); 161 } 162 } 163 }, 164 } 165 } 166 }; 167} 168 169// Use return on the self.postEvent's so it can either return error union or void 170pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void { 171 switch (builtin.os.tag) { 172 .windows => { 173 switch (event) { 174 .winsize => |ws| { 175 if (@hasField(Event, "winsize")) { 176 return self.postEvent(.{ .winsize = ws }); 177 } 178 }, 179 .key_press => |key| { 180 // Check for a cursor position response for our explicit width query. This will 181 // always be an F3 key with shift = true, and we must be looking for queries 182 if (key.codepoint == vaxis.Key.f3 and 183 key.mods.shift and 184 !vx.queries_done.load(.unordered)) 185 { 186 log.info("explicit width capability detected", .{}); 187 vx.caps.explicit_width = true; 188 vx.caps.unicode = .unicode; 189 vx.screen.width_method = .unicode; 190 return; 191 } 192 // Check for a cursor position response for our scaled text query. This will 193 // always be an F3 key with alt = true, and we must be looking for queries 194 if (key.codepoint == vaxis.Key.f3 and 195 key.mods.alt and 196 !vx.queries_done.load(.unordered)) 197 { 198 log.info("scaled text capability detected", .{}); 199 vx.caps.scaled_text = true; 200 return; 201 } 202 if (@hasField(Event, "key_press")) { 203 // HACK: yuck. there has to be a better way 204 var mut_key = key; 205 if (key.text) |text| { 206 mut_key.text = cache.put(text); 207 } 208 return self.postEvent(.{ .key_press = mut_key }); 209 } 210 }, 211 .key_release => |key| { 212 if (@hasField(Event, "key_release")) { 213 // HACK: yuck. there has to be a better way 214 var mut_key = key; 215 if (key.text) |text| { 216 mut_key.text = cache.put(text); 217 } 218 return self.postEvent(.{ .key_release = mut_key }); 219 } 220 }, 221 .cap_da1 => { 222 std.Thread.Futex.wake(&vx.query_futex, 10); 223 vx.queries_done.store(true, .unordered); 224 }, 225 .mouse => |mouse| { 226 if (@hasField(Event, "mouse")) { 227 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 228 } 229 }, 230 .focus_in => { 231 if (@hasField(Event, "focus_in")) { 232 return self.postEvent(.focus_in); 233 } 234 }, 235 .focus_out => { 236 if (@hasField(Event, "focus_out")) { 237 return self.postEvent(.focus_out); 238 } 239 }, // Unsupported currently 240 else => {}, 241 } 242 }, 243 else => { 244 switch (event) { 245 .key_press => |key| { 246 // Check for a cursor position response for our explicity width query. This will 247 // always be an F3 key with shift = true, and we must be looking for queries 248 if (key.codepoint == vaxis.Key.f3 and 249 key.mods.shift and 250 !vx.queries_done.load(.unordered)) 251 { 252 log.info("explicit width capability detected", .{}); 253 vx.caps.explicit_width = true; 254 vx.caps.unicode = .unicode; 255 vx.screen.width_method = .unicode; 256 return; 257 } 258 // Check for a cursor position response for our scaled text query. This will 259 // always be an F3 key with alt = true, and we must be looking for queries 260 if (key.codepoint == vaxis.Key.f3 and 261 key.mods.alt and 262 !vx.queries_done.load(.unordered)) 263 { 264 log.info("scaled text capability detected", .{}); 265 vx.caps.scaled_text = true; 266 return; 267 } 268 if (@hasField(Event, "key_press")) { 269 // HACK: yuck. there has to be a better way 270 var mut_key = key; 271 if (key.text) |text| { 272 mut_key.text = cache.put(text); 273 } 274 return self.postEvent(.{ .key_press = mut_key }); 275 } 276 }, 277 .key_release => |key| { 278 if (@hasField(Event, "key_release")) { 279 // HACK: yuck. there has to be a better way 280 var mut_key = key; 281 if (key.text) |text| { 282 mut_key.text = cache.put(text); 283 } 284 return self.postEvent(.{ .key_release = mut_key }); 285 } 286 }, 287 .mouse => |mouse| { 288 if (@hasField(Event, "mouse")) { 289 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 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_da1 => { 358 std.Thread.Futex.wake(&vx.query_futex, 10); 359 vx.queries_done.store(true, .unordered); 360 }, 361 .winsize => |winsize| { 362 vx.state.in_band_resize = true; 363 switch (builtin.os.tag) { 364 .windows => {}, 365 // Reset the signal handler if we are receiving in_band_resize 366 else => Tty.resetSignalHandler(), 367 } 368 if (@hasField(Event, "winsize")) { 369 return self.postEvent(.{ .winsize = winsize }); 370 } 371 }, 372 } 373 }, 374 } 375} 376 377test Loop { 378 const Event = union(enum) { 379 key_press: vaxis.Key, 380 winsize: vaxis.Winsize, 381 focus_in, 382 foo: u8, 383 }; 384 385 var tty = try vaxis.Tty.init(); 386 defer tty.deinit(); 387 388 var vx = try vaxis.init(std.testing.allocator, .{}); 389 defer vx.deinit(std.testing.allocator, tty.anyWriter()); 390 391 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 392 try loop.init(); 393 394 try loop.start(); 395 defer loop.stop(); 396 397 // Optionally enter the alternate screen 398 try vx.enterAltScreen(tty.anyWriter()); 399 try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms); 400}