this repo has no description
13
fork

Configure Feed

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

at 01605eebf65be21caa85ee9d840047f864553308 380 lines 15 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3 4const grapheme = @import("grapheme"); 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.g_data, 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 grapheme.GraphemeData, 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 explicity 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 if (@hasField(Event, "key_press")) { 193 // HACK: yuck. there has to be a better way 194 var mut_key = key; 195 if (key.text) |text| { 196 mut_key.text = cache.put(text); 197 } 198 return self.postEvent(.{ .key_press = mut_key }); 199 } 200 }, 201 .key_release => |key| { 202 if (@hasField(Event, "key_release")) { 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_release = mut_key }); 209 } 210 }, 211 .cap_da1 => { 212 std.Thread.Futex.wake(&vx.query_futex, 10); 213 vx.queries_done.store(true, .unordered); 214 }, 215 .mouse => |mouse| { 216 if (@hasField(Event, "mouse")) { 217 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 218 } 219 }, 220 .focus_in => { 221 if (@hasField(Event, "focus_in")) { 222 return self.postEvent(.focus_in); 223 } 224 }, 225 .focus_out => { 226 if (@hasField(Event, "focus_out")) { 227 return self.postEvent(.focus_out); 228 } 229 }, // Unsupported currently 230 else => {}, 231 } 232 }, 233 else => { 234 switch (event) { 235 .key_press => |key| { 236 // Check for a cursor position response for our explicity width query. This will 237 // always be an F3 key with shift = true, and we must be looking for queries 238 if (key.codepoint == vaxis.Key.f3 and 239 key.mods.shift and 240 !vx.queries_done.load(.unordered)) 241 { 242 log.info("explicit width capability detected", .{}); 243 vx.caps.explicit_width = true; 244 vx.caps.unicode = .unicode; 245 vx.screen.width_method = .unicode; 246 return; 247 } 248 if (@hasField(Event, "key_press")) { 249 // HACK: yuck. there has to be a better way 250 var mut_key = key; 251 if (key.text) |text| { 252 mut_key.text = cache.put(text); 253 } 254 return self.postEvent(.{ .key_press = mut_key }); 255 } 256 }, 257 .key_release => |key| { 258 if (@hasField(Event, "key_release")) { 259 // HACK: yuck. there has to be a better way 260 var mut_key = key; 261 if (key.text) |text| { 262 mut_key.text = cache.put(text); 263 } 264 return self.postEvent(.{ .key_release = mut_key }); 265 } 266 }, 267 .mouse => |mouse| { 268 if (@hasField(Event, "mouse")) { 269 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 270 } 271 }, 272 .focus_in => { 273 if (@hasField(Event, "focus_in")) { 274 return self.postEvent(.focus_in); 275 } 276 }, 277 .focus_out => { 278 if (@hasField(Event, "focus_out")) { 279 return self.postEvent(.focus_out); 280 } 281 }, 282 .paste_start => { 283 if (@hasField(Event, "paste_start")) { 284 return self.postEvent(.paste_start); 285 } 286 }, 287 .paste_end => { 288 if (@hasField(Event, "paste_end")) { 289 return self.postEvent(.paste_end); 290 } 291 }, 292 .paste => |text| { 293 if (@hasField(Event, "paste")) { 294 return self.postEvent(.{ .paste = text }); 295 } else { 296 if (paste_allocator) |_| 297 paste_allocator.?.free(text); 298 } 299 }, 300 .color_report => |report| { 301 if (@hasField(Event, "color_report")) { 302 return self.postEvent(.{ .color_report = report }); 303 } 304 }, 305 .color_scheme => |scheme| { 306 if (@hasField(Event, "color_scheme")) { 307 return self.postEvent(.{ .color_scheme = scheme }); 308 } 309 }, 310 .cap_kitty_keyboard => { 311 log.info("kitty keyboard capability detected", .{}); 312 vx.caps.kitty_keyboard = true; 313 }, 314 .cap_kitty_graphics => { 315 if (!vx.caps.kitty_graphics) { 316 log.info("kitty graphics capability detected", .{}); 317 vx.caps.kitty_graphics = true; 318 } 319 }, 320 .cap_rgb => { 321 log.info("rgb capability detected", .{}); 322 vx.caps.rgb = true; 323 }, 324 .cap_unicode => { 325 log.info("unicode capability detected", .{}); 326 vx.caps.unicode = .unicode; 327 vx.screen.width_method = .unicode; 328 }, 329 .cap_sgr_pixels => { 330 log.info("pixel mouse capability detected", .{}); 331 vx.caps.sgr_pixels = true; 332 }, 333 .cap_color_scheme_updates => { 334 log.info("color_scheme_updates capability detected", .{}); 335 vx.caps.color_scheme_updates = true; 336 }, 337 .cap_da1 => { 338 std.Thread.Futex.wake(&vx.query_futex, 10); 339 vx.queries_done.store(true, .unordered); 340 }, 341 .winsize => |winsize| { 342 vx.state.in_band_resize = true; 343 switch (builtin.os.tag) { 344 .windows => {}, 345 // Reset the signal handler if we are receiving in_band_resize 346 else => Tty.resetSignalHandler(), 347 } 348 if (@hasField(Event, "winsize")) { 349 return self.postEvent(.{ .winsize = winsize }); 350 } 351 }, 352 } 353 }, 354 } 355} 356 357test Loop { 358 const Event = union(enum) { 359 key_press: vaxis.Key, 360 winsize: vaxis.Winsize, 361 focus_in, 362 foo: u8, 363 }; 364 365 var tty = try vaxis.Tty.init(); 366 defer tty.deinit(); 367 368 var vx = try vaxis.init(std.testing.allocator, .{}); 369 defer vx.deinit(std.testing.allocator, tty.anyWriter()); 370 371 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 372 try loop.init(); 373 374 try loop.start(); 375 defer loop.stop(); 376 377 // Optionally enter the alternate screen 378 try vx.enterAltScreen(tty.anyWriter()); 379 try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms); 380}