this repo has no description
13
fork

Configure Feed

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

1# Usage 2 3## Custom Event Loops 4 5Vaxis provides an abstract enough API to allow the usage of a custom event loop. 6An event loop implementation is responsible for three primary tasks: 7 81. Read raw bytes from the TTY 92. Pass bytes to the Vaxis input event parser 103. Handle the returned events 11 12Everything after this can be left up to user code, or brought into an event loop 13to be a more abstract application layer. One important part of handling the 14events is to update the Vaxis struct with discovered terminal capabilities. This 15lets Vaxis know what features it can use. For example, the Kitty Keyboard 16protocol, in-band-resize reports, and Unicode width measurements are just a few 17examples. 18 19### `libxev` 20 21Below is an example [`libxev`](https://github.com/mitchellh/libxev) event loop. 22Note that this code is not necessarily up-to-date with the latest `libxev` 23release and is shown here merely as a proof of concept. 24 25```zig 26const std = @import("std"); 27const xev = @import("xev"); 28 29const Tty = @import("main.zig").Tty; 30const Winsize = @import("main.zig").Winsize; 31const Vaxis = @import("Vaxis.zig"); 32const Parser = @import("Parser.zig"); 33const Key = @import("Key.zig"); 34const Mouse = @import("Mouse.zig"); 35const Color = @import("Cell.zig").Color; 36 37const log = std.log.scoped(.vaxis_xev); 38 39pub const Event = union(enum) { 40 key_press: Key, 41 key_release: Key, 42 mouse: Mouse, 43 focus_in, 44 focus_out, 45 paste_start, // bracketed paste start 46 paste_end, // bracketed paste end 47 paste: []const u8, // osc 52 paste, caller must free 48 color_report: Color.Report, // osc 4, 10, 11, 12 response 49 color_scheme: Color.Scheme, 50 winsize: Winsize, 51}; 52 53pub fn TtyWatcher(comptime Userdata: type) type { 54 return struct { 55 const Self = @This(); 56 57 file: xev.File, 58 tty: *Tty, 59 60 read_buf: [4096]u8, 61 read_buf_start: usize, 62 read_cmp: xev.Completion, 63 64 winsize_wakeup: xev.Async, 65 winsize_cmp: xev.Completion, 66 67 callback: *const fn ( 68 ud: ?*Userdata, 69 loop: *xev.Loop, 70 watcher: *Self, 71 event: Event, 72 ) xev.CallbackAction, 73 74 ud: ?*Userdata, 75 vx: *Vaxis, 76 parser: Parser, 77 78 pub fn init( 79 self: *Self, 80 tty: *Tty, 81 vaxis: *Vaxis, 82 loop: *xev.Loop, 83 userdata: ?*Userdata, 84 callback: *const fn ( 85 ud: ?*Userdata, 86 loop: *xev.Loop, 87 watcher: *Self, 88 event: Event, 89 ) xev.CallbackAction, 90 ) !void { 91 self.* = .{ 92 .tty = tty, 93 .file = xev.File.initFd(tty.fd), 94 .read_buf = undefined, 95 .read_buf_start = 0, 96 .read_cmp = .{}, 97 98 .winsize_wakeup = try xev.Async.init(), 99 .winsize_cmp = .{}, 100 101 .callback = callback, 102 .ud = userdata, 103 .vx = vaxis, 104 .parser = .{ .grapheme_data = &vaxis.unicode.width_data.g_data }, 105 }; 106 107 self.file.read( 108 loop, 109 &self.read_cmp, 110 .{ .slice = &self.read_buf }, 111 Self, 112 self, 113 Self.ttyReadCallback, 114 ); 115 self.winsize_wakeup.wait( 116 loop, 117 &self.winsize_cmp, 118 Self, 119 self, 120 winsizeCallback, 121 ); 122 const handler: Tty.SignalHandler = .{ 123 .context = self, 124 .callback = Self.signalCallback, 125 }; 126 try Tty.notifyWinsize(handler); 127 } 128 129 fn signalCallback(ptr: *anyopaque) void { 130 const self: *Self = @ptrCast(@alignCast(ptr)); 131 self.winsize_wakeup.notify() catch |err| { 132 log.warn("couldn't wake up winsize callback: {}", .{err}); 133 }; 134 } 135 136 fn ttyReadCallback( 137 ud: ?*Self, 138 loop: *xev.Loop, 139 c: *xev.Completion, 140 _: xev.File, 141 buf: xev.ReadBuffer, 142 r: xev.ReadError!usize, 143 ) xev.CallbackAction { 144 const n = r catch |err| { 145 log.err("read error: {}", .{err}); 146 return .disarm; 147 }; 148 const self = ud orelse unreachable; 149 150 // reset read start state 151 self.read_buf_start = 0; 152 153 var seq_start: usize = 0; 154 parse_loop: while (seq_start < n) { 155 const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| { 156 log.err("couldn't parse input: {}", .{err}); 157 return .disarm; 158 }; 159 if (result.n == 0) { 160 // copy the read to the beginning. We don't use memcpy because 161 // this could be overlapping, and it's also rare 162 const initial_start = seq_start; 163 while (seq_start < n) : (seq_start += 1) { 164 self.read_buf[seq_start - initial_start] = self.read_buf[seq_start]; 165 } 166 self.read_buf_start = seq_start - initial_start + 1; 167 return .rearm; 168 } 169 seq_start += n; 170 const event_inner = result.event orelse { 171 log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]}); 172 continue :parse_loop; 173 }; 174 175 // Capture events we want to bubble up 176 const event: ?Event = switch (event_inner) { 177 .key_press => |key| .{ .key_press = key }, 178 .key_release => |key| .{ .key_release = key }, 179 .mouse => |mouse| .{ .mouse = mouse }, 180 .focus_in => .focus_in, 181 .focus_out => .focus_out, 182 .paste_start => .paste_start, 183 .paste_end => .paste_end, 184 .paste => |paste| .{ .paste = paste }, 185 .color_report => |report| .{ .color_report = report }, 186 .color_scheme => |scheme| .{ .color_scheme = scheme }, 187 .winsize => |ws| .{ .winsize = ws }, 188 189 // capability events which we handle below 190 .cap_kitty_keyboard, 191 .cap_kitty_graphics, 192 .cap_rgb, 193 .cap_unicode, 194 .cap_sgr_pixels, 195 .cap_color_scheme_updates, 196 .cap_da1, 197 => null, // handled below 198 }; 199 200 if (event) |ev| { 201 const action = self.callback(self.ud, loop, self, ev); 202 switch (action) { 203 .disarm => return .disarm, 204 else => continue :parse_loop, 205 } 206 } 207 208 switch (event_inner) { 209 .key_press, 210 .key_release, 211 .mouse, 212 .focus_in, 213 .focus_out, 214 .paste_start, 215 .paste_end, 216 .paste, 217 .color_report, 218 .color_scheme, 219 .winsize, 220 => unreachable, // handled above 221 222 .cap_kitty_keyboard => { 223 log.info("kitty keyboard capability detected", .{}); 224 self.vx.caps.kitty_keyboard = true; 225 }, 226 .cap_kitty_graphics => { 227 if (!self.vx.caps.kitty_graphics) { 228 log.info("kitty graphics capability detected", .{}); 229 self.vx.caps.kitty_graphics = true; 230 } 231 }, 232 .cap_rgb => { 233 log.info("rgb capability detected", .{}); 234 self.vx.caps.rgb = true; 235 }, 236 .cap_unicode => { 237 log.info("unicode capability detected", .{}); 238 self.vx.caps.unicode = .unicode; 239 self.vx.screen.width_method = .unicode; 240 }, 241 .cap_sgr_pixels => { 242 log.info("pixel mouse capability detected", .{}); 243 self.vx.caps.sgr_pixels = true; 244 }, 245 .cap_color_scheme_updates => { 246 log.info("color_scheme_updates capability detected", .{}); 247 self.vx.caps.color_scheme_updates = true; 248 }, 249 .cap_da1 => { 250 self.vx.enableDetectedFeatures(self.tty.writer()) catch |err| { 251 log.err("couldn't enable features: {}", .{err}); 252 }; 253 }, 254 } 255 } 256 257 self.file.read( 258 loop, 259 c, 260 .{ .slice = &self.read_buf }, 261 Self, 262 self, 263 Self.ttyReadCallback, 264 ); 265 return .disarm; 266 } 267 268 fn winsizeCallback( 269 ud: ?*Self, 270 l: *xev.Loop, 271 c: *xev.Completion, 272 r: xev.Async.WaitError!void, 273 ) xev.CallbackAction { 274 _ = r catch |err| { 275 log.err("async error: {}", .{err}); 276 return .disarm; 277 }; 278 const self = ud orelse unreachable; // no userdata 279 const winsize = Tty.getWinsize(self.tty.fd) catch |err| { 280 log.err("couldn't get winsize: {}", .{err}); 281 return .disarm; 282 }; 283 const ret = self.callback(self.ud, l, self, .{ .winsize = winsize }); 284 if (ret == .disarm) return .disarm; 285 286 self.winsize_wakeup.wait( 287 l, 288 c, 289 Self, 290 self, 291 winsizeCallback, 292 ); 293 return .disarm; 294 } 295 }; 296} 297``` 298 299### zig-aio 300 301Below is an example [`zig-aio`](https://github.com/Cloudef/zig-aio) event loop. 302Note that this code is not necessarily up-to-date with the latest `zig-aio` 303release and is shown here merely as a proof of concept. 304 305```zig 306const builtin = @import("builtin"); 307const std = @import("std"); 308const vaxis = @import("vaxis"); 309const handleEventGeneric = vaxis.loop.handleEventGeneric; 310const log = std.log.scoped(.vaxis_aio); 311 312const Yield = enum { no_state, took_event }; 313 314/// zig-aio based event loop 315/// <https://github.com/Cloudef/zig-aio> 316pub fn LoopWithModules(T: type, aio: type, coro: type) type { 317 return struct { 318 const Event = T; 319 320 winsize_task: ?coro.Task.Generic2(winsizeTask) = null, 321 reader_task: ?coro.Task.Generic2(ttyReaderTask) = null, 322 queue: std.BoundedArray(T, 512) = .{}, 323 source: aio.EventSource, 324 fatal: bool = false, 325 326 pub fn init() !@This() { 327 return .{ .source = try aio.EventSource.init() }; 328 } 329 330 pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { 331 vx.deviceStatusReport(tty.writer()) catch {}; 332 if (self.winsize_task) |task| task.cancel(); 333 if (self.reader_task) |task| task.cancel(); 334 self.source.deinit(); 335 self.* = undefined; 336 } 337 338 fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void { 339 const Context = struct { 340 loop: *@TypeOf(self.*), 341 tty: *vaxis.Tty, 342 winsize: ?vaxis.Winsize = null, 343 fn cb(ptr: *anyopaque) void { 344 std.debug.assert(coro.current() == null); 345 const ctx: *@This() = @ptrCast(@alignCast(ptr)); 346 ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return; 347 ctx.loop.source.notify(); 348 } 349 }; 350 351 // keep on stack 352 var ctx: Context = .{ .loop = self, .tty = tty }; 353 if (builtin.target.os.tag != .windows) { 354 if (@hasField(Event, "winsize")) { 355 const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; 356 try vaxis.Tty.notifyWinsize(handler); 357 } 358 } 359 360 while (true) { 361 try coro.io.single(aio.WaitEventSource{ .source = &self.source }); 362 if (ctx.winsize) |winsize| { 363 if (!@hasField(Event, "winsize")) unreachable; 364 ctx.loop.postEvent(.{ .winsize = winsize }) catch {}; 365 ctx.winsize = null; 366 } 367 } 368 } 369 370 fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void { 371 self.winsizeInner(tty) catch |err| { 372 if (err != error.Canceled) log.err("winsize: {}", .{err}); 373 self.fatal = true; 374 }; 375 } 376 377 fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event { 378 var state: vaxis.Tty.EventState = .{}; 379 while (true) { 380 var bytes_read: usize = 0; 381 var input_record: vaxis.Tty.INPUT_RECORD = undefined; 382 try coro.io.single(aio.ReadTty{ 383 .tty = .{ .handle = tty.stdin }, 384 .buffer = std.mem.asBytes(&input_record), 385 .out_read = &bytes_read, 386 }); 387 388 if (try tty.eventFromRecord(&input_record, &state)) |ev| { 389 return ev; 390 } 391 } 392 } 393 394 fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void { 395 var cache: vaxis.GraphemeCache = .{}; 396 while (true) { 397 const event = try windowsReadEvent(tty); 398 try handleEventGeneric(self, vx, &cache, Event, event, null); 399 } 400 } 401 402 fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void { 403 // initialize a grapheme cache 404 var cache: vaxis.GraphemeCache = .{}; 405 406 // get our initial winsize 407 const winsize = try vaxis.Tty.getWinsize(tty.fd); 408 if (@hasField(Event, "winsize")) { 409 try self.postEvent(.{ .winsize = winsize }); 410 } 411 412 var parser: vaxis.Parser = .{ 413 .grapheme_data = &vx.unicode.width_data.g_data, 414 }; 415 416 const file: std.fs.File = .{ .handle = tty.fd }; 417 while (true) { 418 var buf: [4096]u8 = undefined; 419 var n: usize = undefined; 420 var read_start: usize = 0; 421 try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n }); 422 var seq_start: usize = 0; 423 while (seq_start < n) { 424 const result = try parser.parse(buf[seq_start..n], paste_allocator); 425 if (result.n == 0) { 426 // copy the read to the beginning. We don't use memcpy because 427 // this could be overlapping, and it's also rare 428 const initial_start = seq_start; 429 while (seq_start < n) : (seq_start += 1) { 430 buf[seq_start - initial_start] = buf[seq_start]; 431 } 432 read_start = seq_start - initial_start + 1; 433 continue; 434 } 435 read_start = 0; 436 seq_start += result.n; 437 438 const event = result.event orelse continue; 439 try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator); 440 } 441 } 442 } 443 444 fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void { 445 return switch (builtin.target.os.tag) { 446 .windows => self.ttyReaderWindows(vx, tty), 447 else => self.ttyReaderPosix(vx, tty, paste_allocator), 448 } catch |err| { 449 if (err != error.Canceled) log.err("ttyReader: {}", .{err}); 450 self.fatal = true; 451 }; 452 } 453 454 /// Spawns tasks to handle winsize signal and tty 455 pub fn spawn( 456 self: *@This(), 457 scheduler: *coro.Scheduler, 458 vx: *vaxis.Vaxis, 459 tty: *vaxis.Tty, 460 paste_allocator: ?std.mem.Allocator, 461 spawn_options: coro.Scheduler.SpawnOptions, 462 ) coro.Scheduler.SpawnError!void { 463 if (self.reader_task) |_| unreachable; // programming error 464 // This is required even if app doesn't care about winsize 465 // It is because it consumes the EventSource, so it can wakeup the scheduler 466 // Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update 467 self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options); 468 self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options); 469 } 470 471 pub const PopEventError = error{TtyCommunicationSevered}; 472 473 /// Call this in a while loop in the main event handler until it returns null 474 pub fn popEvent(self: *@This()) PopEventError!?T { 475 if (self.fatal) return error.TtyCommunicationSevered; 476 defer self.winsize_task.?.wakeupIf(Yield.took_event); 477 defer self.reader_task.?.wakeupIf(Yield.took_event); 478 return self.queue.popOrNull(); 479 } 480 481 pub const PostEventError = error{Overflow}; 482 483 pub fn postEvent(self: *@This(), event: T) !void { 484 if (coro.current()) |_| { 485 while (true) { 486 self.queue.insert(0, event) catch { 487 // wait for the app to take event 488 try coro.yield(Yield.took_event); 489 continue; 490 }; 491 break; 492 } 493 } else { 494 // queue can be full, app could handle this error by spinning the scheduler 495 try self.queue.insert(0, event); 496 } 497 // wakes up the scheduler, so custom events update UI 498 self.source.notify(); 499 } 500 }; 501} 502```