this repo has no description
13
fork

Configure Feed

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

core!: move libxev and zig-aio event loops to USAGE.md

Create USAGE.md to serve as a friendly guide for usage.

Move the libxev and zig-aio event loops to serve as an example of how to
implement a custom event loop in USAGE.md. Remove dependency on these
libraries and all associated examples and source code.

The motivation for this change is to provide a simpler library. The
power to create a custom event loop will always be kept, however the
support burden of maintaining three event loops is not where I want to
focus my energy. The code is small enough that it's easy enough to be
copy/pasted into a project or provided as a third-party library.

+513 -807
+513
USAGE.md
··· 1 + # Usage 2 + 3 + ## Custom Event Loops 4 + 5 + Vaxis provides an abstract enough API to allow the usage of a custom event loop. 6 + An event loop implementation is responsible for three primary tasks: 7 + 8 + 1. Read raw bytes from the TTY 9 + 2. Pass bytes to the Vaxis input event parser 10 + 3. Handle the returned events 11 + 12 + Everything after this can be left up to user code, or brought into an event loop 13 + to be a more abstract application layer. One important part of handling the 14 + events is to update the Vaxis struct with discovered terminal capabilities. This 15 + let's Vaxis know what features it can use. For example, the Kitty Keyboard 16 + protocol, in-band-resize reports, and Unicode width measurements are just a few 17 + examples. 18 + 19 + ### `libxev` 20 + 21 + Below is an example [`libxev`](https://github.com/mitchellh/libxev) event loop. 22 + Note that this code is not necessarily up-to-date with the latest `libxev` 23 + release and is shown here merely as a proof of concept. 24 + 25 + ```zig 26 + const std = @import("std"); 27 + const xev = @import("xev"); 28 + 29 + const Tty = @import("main.zig").Tty; 30 + const Winsize = @import("main.zig").Winsize; 31 + const Vaxis = @import("Vaxis.zig"); 32 + const Parser = @import("Parser.zig"); 33 + const Key = @import("Key.zig"); 34 + const Mouse = @import("Mouse.zig"); 35 + const Color = @import("Cell.zig").Color; 36 + 37 + const log = std.log.scoped(.vaxis_xev); 38 + 39 + pub 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 + 53 + pub 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.anyWriter()) 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 + 301 + Below is an example [`zig-aio`](https://github.com/Cloudef/zig-aio) event loop. 302 + Note that this code is not necessarily up-to-date with the latest `zig-aio` 303 + release and is shown here merely as a proof of concept. 304 + 305 + ```zig 306 + const build_options = @import("build_options"); 307 + const builtin = @import("builtin"); 308 + const std = @import("std"); 309 + const vaxis = @import("main.zig"); 310 + const handleEventGeneric = @import("Loop.zig").handleEventGeneric; 311 + const log = std.log.scoped(.vaxis_aio); 312 + 313 + const Yield = enum { no_state, took_event }; 314 + 315 + pub fn Loop(T: type) type { 316 + if (!build_options.aio) { 317 + @compileError( 318 + \\build_options.aio is not enabled. 319 + \\Use `LoopWithModules` instead to provide `aio` and `coro` modules from outside vaxis. 320 + ); 321 + } 322 + return LoopWithModules(T, @import("aio"), @import("coro")); 323 + } 324 + 325 + /// zig-aio based event loop 326 + /// <https://github.com/Cloudef/zig-aio> 327 + pub fn LoopWithModules(T: type, aio: type, coro: type) type { 328 + return struct { 329 + const Event = T; 330 + 331 + winsize_task: ?coro.Task.Generic2(winsizeTask) = null, 332 + reader_task: ?coro.Task.Generic2(ttyReaderTask) = null, 333 + queue: std.BoundedArray(T, 512) = .{}, 334 + source: aio.EventSource, 335 + fatal: bool = false, 336 + 337 + pub fn init() !@This() { 338 + return .{ .source = try aio.EventSource.init() }; 339 + } 340 + 341 + pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { 342 + vx.deviceStatusReport(tty.anyWriter()) catch {}; 343 + if (self.winsize_task) |task| task.cancel(); 344 + if (self.reader_task) |task| task.cancel(); 345 + self.source.deinit(); 346 + self.* = undefined; 347 + } 348 + 349 + fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void { 350 + const Context = struct { 351 + loop: *@TypeOf(self.*), 352 + tty: *vaxis.Tty, 353 + winsize: ?vaxis.Winsize = null, 354 + fn cb(ptr: *anyopaque) void { 355 + std.debug.assert(coro.current() == null); 356 + const ctx: *@This() = @ptrCast(@alignCast(ptr)); 357 + ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return; 358 + ctx.loop.source.notify(); 359 + } 360 + }; 361 + 362 + // keep on stack 363 + var ctx: Context = .{ .loop = self, .tty = tty }; 364 + if (builtin.target.os.tag != .windows) { 365 + if (@hasField(Event, "winsize")) { 366 + const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; 367 + try vaxis.Tty.notifyWinsize(handler); 368 + } 369 + } 370 + 371 + while (true) { 372 + try coro.io.single(aio.WaitEventSource{ .source = &self.source }); 373 + if (ctx.winsize) |winsize| { 374 + if (!@hasField(Event, "winsize")) unreachable; 375 + ctx.loop.postEvent(.{ .winsize = winsize }) catch {}; 376 + ctx.winsize = null; 377 + } 378 + } 379 + } 380 + 381 + fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void { 382 + self.winsizeInner(tty) catch |err| { 383 + if (err != error.Canceled) log.err("winsize: {}", .{err}); 384 + self.fatal = true; 385 + }; 386 + } 387 + 388 + fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event { 389 + var state: vaxis.Tty.EventState = .{}; 390 + while (true) { 391 + var bytes_read: usize = 0; 392 + var input_record: vaxis.Tty.INPUT_RECORD = undefined; 393 + try coro.io.single(aio.ReadTty{ 394 + .tty = .{ .handle = tty.stdin }, 395 + .buffer = std.mem.asBytes(&input_record), 396 + .out_read = &bytes_read, 397 + }); 398 + 399 + if (try tty.eventFromRecord(&input_record, &state)) |ev| { 400 + return ev; 401 + } 402 + } 403 + } 404 + 405 + fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void { 406 + var cache: vaxis.GraphemeCache = .{}; 407 + while (true) { 408 + const event = try windowsReadEvent(tty); 409 + try handleEventGeneric(self, vx, &cache, Event, event, null); 410 + } 411 + } 412 + 413 + fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void { 414 + // initialize a grapheme cache 415 + var cache: vaxis.GraphemeCache = .{}; 416 + 417 + // get our initial winsize 418 + const winsize = try vaxis.Tty.getWinsize(tty.fd); 419 + if (@hasField(Event, "winsize")) { 420 + try self.postEvent(.{ .winsize = winsize }); 421 + } 422 + 423 + var parser: vaxis.Parser = .{ 424 + .grapheme_data = &vx.unicode.width_data.g_data, 425 + }; 426 + 427 + const file: std.fs.File = .{ .handle = tty.fd }; 428 + while (true) { 429 + var buf: [4096]u8 = undefined; 430 + var n: usize = undefined; 431 + var read_start: usize = 0; 432 + try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n }); 433 + var seq_start: usize = 0; 434 + while (seq_start < n) { 435 + const result = try parser.parse(buf[seq_start..n], paste_allocator); 436 + if (result.n == 0) { 437 + // copy the read to the beginning. We don't use memcpy because 438 + // this could be overlapping, and it's also rare 439 + const initial_start = seq_start; 440 + while (seq_start < n) : (seq_start += 1) { 441 + buf[seq_start - initial_start] = buf[seq_start]; 442 + } 443 + read_start = seq_start - initial_start + 1; 444 + continue; 445 + } 446 + read_start = 0; 447 + seq_start += result.n; 448 + 449 + const event = result.event orelse continue; 450 + try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator); 451 + } 452 + } 453 + } 454 + 455 + fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void { 456 + return switch (builtin.target.os.tag) { 457 + .windows => self.ttyReaderWindows(vx, tty), 458 + else => self.ttyReaderPosix(vx, tty, paste_allocator), 459 + } catch |err| { 460 + if (err != error.Canceled) log.err("ttyReader: {}", .{err}); 461 + self.fatal = true; 462 + }; 463 + } 464 + 465 + /// Spawns tasks to handle winsize signal and tty 466 + pub fn spawn( 467 + self: *@This(), 468 + scheduler: *coro.Scheduler, 469 + vx: *vaxis.Vaxis, 470 + tty: *vaxis.Tty, 471 + paste_allocator: ?std.mem.Allocator, 472 + spawn_options: coro.Scheduler.SpawnOptions, 473 + ) coro.Scheduler.SpawnError!void { 474 + if (self.reader_task) |_| unreachable; // programming error 475 + // This is required even if app doesn't care about winsize 476 + // It is because it consumes the EventSource, so it can wakeup the scheduler 477 + // Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update 478 + self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options); 479 + self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options); 480 + } 481 + 482 + pub const PopEventError = error{TtyCommunicationSevered}; 483 + 484 + /// Call this in a while loop in the main event handler until it returns null 485 + pub fn popEvent(self: *@This()) PopEventError!?T { 486 + if (self.fatal) return error.TtyCommunicationSevered; 487 + defer self.winsize_task.?.wakeupIf(Yield.took_event); 488 + defer self.reader_task.?.wakeupIf(Yield.took_event); 489 + return self.queue.popOrNull(); 490 + } 491 + 492 + pub const PostEventError = error{Overflow}; 493 + 494 + pub fn postEvent(self: *@This(), event: T) !void { 495 + if (coro.current()) |_| { 496 + while (true) { 497 + self.queue.insert(0, event) catch { 498 + // wait for the app to take event 499 + try coro.yield(Yield.took_event); 500 + continue; 501 + }; 502 + break; 503 + } 504 + } else { 505 + // queue can be full, app could handle this error by spinning the scheduler 506 + try self.queue.insert(0, event); 507 + } 508 + // wakes up the scheduler, so custom events update UI 509 + self.source.notify(); 510 + } 511 + }; 512 + } 513 + ```
-18
build.zig
··· 1 1 const std = @import("std"); 2 2 3 3 pub fn build(b: *std.Build) void { 4 - const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true; 5 4 const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true; 6 - const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false; 7 5 8 6 const options = b.addOptions(); 9 - options.addOption(bool, "libxev", include_libxev); 10 7 options.addOption(bool, "images", include_images); 11 - options.addOption(bool, "aio", include_aio); 12 8 13 9 const options_mod = options.createModule(); 14 10 ··· 25 21 .optimize = optimize, 26 22 .target = target, 27 23 }) else null; 28 - const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{ 29 - .optimize = optimize, 30 - .target = target, 31 - }) else null; 32 - const aio_dep = if (include_aio) b.lazyDependency("aio", .{ 33 - .optimize = optimize, 34 - .target = target, 35 - }) else null; 36 24 37 25 // Module 38 26 const vaxis_mod = b.addModule("vaxis", .{ ··· 44 32 vaxis_mod.addImport("grapheme", zg_dep.module("grapheme")); 45 33 vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth")); 46 34 if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg")); 47 - if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev")); 48 - if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio")); 49 - if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro")); 50 35 vaxis_mod.addImport("build_options", options_mod); 51 36 52 37 // Examples ··· 75 60 .optimize = optimize, 76 61 }); 77 62 example.root_module.addImport("vaxis", vaxis_mod); 78 - if (xev_dep) |dep| example.root_module.addImport("xev", dep.module("xev")); 79 - if (aio_dep) |dep| example.root_module.addImport("aio", dep.module("aio")); 80 - if (aio_dep) |dep| example.root_module.addImport("coro", dep.module("coro")); 81 63 82 64 const example_run = b.addRunArtifact(example); 83 65 example_step.dependOn(&example_run.step);
-10
build.zig.zon
··· 12 12 .url = "git+https://codeberg.org/dude_the_builder/zg#7ddce488e074c3f052949ba513a340446cea86e9", 13 13 .hash = "12205e2e7108385a1ec280b6365166e902a690d6a0e8fcc015fdea62d63cf79be8ec", 14 14 }, 15 - .libxev = .{ 16 - .url = "git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b", 17 - .hash = "12207b7a5b538ffb7fb18f954ae17d2f8490b6e3778a9e30564ad82c58ee8da52361", 18 - .lazy = true, 19 - }, 20 - .aio = .{ 21 - .url = "git+https://github.com/Cloudef/zig-aio#b5a407344379508466c5dcbe4c74438a6166e2ca", 22 - .hash = "1220a55aedabdd10578d0c514719ea39ae1bc6d7ed990f508dc100db7f0ccf391437", 23 - .lazy = true, 24 - }, 25 15 }, 26 16 .paths = .{ 27 17 "LICENSE",
-172
examples/aio.zig
··· 1 - const builtin = @import("builtin"); 2 - const std = @import("std"); 3 - const vaxis = @import("vaxis"); 4 - const aio = @import("aio"); 5 - const coro = @import("coro"); 6 - 7 - pub const panic = vaxis.panic_handler; 8 - 9 - const Event = union(enum) { 10 - key_press: vaxis.Key, 11 - winsize: vaxis.Winsize, 12 - }; 13 - 14 - const Loop = vaxis.aio.Loop(Event); 15 - 16 - const Video = enum { no_state, ready, end }; 17 - const Audio = enum { no_state, ready, end }; 18 - 19 - fn downloadTask(allocator: std.mem.Allocator, url: []const u8) ![]const u8 { 20 - var client: std.http.Client = .{ .allocator = allocator }; 21 - defer client.deinit(); 22 - var body = std.ArrayList(u8).init(allocator); 23 - _ = try client.fetch(.{ 24 - .location = .{ .url = url }, 25 - .response_storage = .{ .dynamic = &body }, 26 - .max_append_size = 1.6e+7, 27 - }); 28 - return try body.toOwnedSlice(); 29 - } 30 - 31 - fn audioTask(allocator: std.mem.Allocator) !void { 32 - // signals end of audio in case there's a error 33 - errdefer coro.yield(Audio.end) catch {}; 34 - 35 - // var child = std.process.Child.init(&.{ "aplay", "-Dplug:default", "-q", "-f", "S16_LE", "-r", "8000" }, allocator); 36 - var child = std.process.Child.init(&.{ "mpv", "--audio-samplerate=16000", "--audio-channels=mono", "--audio-format=s16", "-" }, allocator); 37 - child.stdin_behavior = .Pipe; 38 - child.stdout_behavior = .Ignore; 39 - child.stderr_behavior = .Ignore; 40 - try child.spawn(); 41 - defer _ = child.kill() catch {}; 42 - 43 - const sound = blk: { 44 - var tpool = try coro.ThreadPool.init(allocator, .{}); 45 - defer tpool.deinit(); 46 - break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" }, .{}); 47 - }; 48 - defer allocator.free(sound); 49 - 50 - try coro.yield(Audio.ready); 51 - 52 - var audio_off: usize = 0; 53 - while (audio_off < sound.len) { 54 - var written: usize = 0; 55 - try coro.io.single(aio.Write{ .file = child.stdin.?, .buffer = sound[audio_off..], .out_written = &written }); 56 - audio_off += written; 57 - } 58 - 59 - // the audio is already fed to the player and the defer 60 - // would kill the child, so stay here chilling 61 - coro.yield(Audio.end) catch {}; 62 - } 63 - 64 - fn videoTask(writer: std.io.AnyWriter) !void { 65 - // signals end of video 66 - defer coro.yield(Video.end) catch {}; 67 - 68 - var socket: std.posix.socket_t = undefined; 69 - try coro.io.single(aio.Socket{ 70 - .domain = std.posix.AF.INET, 71 - .flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC, 72 - .protocol = std.posix.IPPROTO.TCP, 73 - .out_socket = &socket, 74 - }); 75 - defer std.posix.close(socket); 76 - 77 - const address = std.net.Address.initIp4(.{ 44, 224, 41, 160 }, 1987); 78 - try coro.io.single(aio.Connect{ 79 - .socket = socket, 80 - .addr = &address.any, 81 - .addrlen = address.getOsSockLen(), 82 - }); 83 - 84 - try coro.yield(Video.ready); 85 - 86 - var buf: [1024]u8 = undefined; 87 - while (true) { 88 - var read: usize = 0; 89 - try coro.io.single(aio.Recv{ .socket = socket, .buffer = &buf, .out_read = &read }); 90 - if (read == 0) break; 91 - _ = try writer.write(buf[0..read]); 92 - } 93 - } 94 - 95 - fn loadingTask(vx: *vaxis.Vaxis, writer: std.io.AnyWriter) !void { 96 - var color_idx: u8 = 30; 97 - var dir: enum { up, down } = .up; 98 - 99 - while (true) { 100 - try coro.io.single(aio.Timeout{ .ns = 8 * std.time.ns_per_ms }); 101 - 102 - const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ color_idx, color_idx, color_idx } } }; 103 - const segment: vaxis.Segment = .{ .text = vaxis.logo, .style = style }; 104 - 105 - const win = vx.window(); 106 - win.clear(); 107 - 108 - var loc = vaxis.widgets.alignment.center(win, 28, 4); 109 - _ = try loc.printSegment(segment, .{ .wrap = .grapheme }); 110 - 111 - switch (dir) { 112 - .up => { 113 - color_idx += 1; 114 - if (color_idx == 255) dir = .down; 115 - }, 116 - .down => { 117 - color_idx -= 1; 118 - if (color_idx == 30) dir = .up; 119 - }, 120 - } 121 - 122 - try vx.render(writer); 123 - } 124 - } 125 - 126 - pub fn main() !void { 127 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 128 - defer _ = gpa.deinit(); 129 - const allocator = gpa.allocator(); 130 - 131 - var tty = try vaxis.Tty.init(); 132 - defer tty.deinit(); 133 - 134 - var vx = try vaxis.init(allocator, .{}); 135 - defer vx.deinit(allocator, tty.anyWriter()); 136 - 137 - var scheduler = try coro.Scheduler.init(allocator, .{}); 138 - defer scheduler.deinit(); 139 - 140 - var loop = try Loop.init(); 141 - try loop.spawn(&scheduler, &vx, &tty, null, .{}); 142 - defer loop.deinit(&vx, &tty); 143 - 144 - try vx.enterAltScreen(tty.anyWriter()); 145 - try vx.queryTerminalSend(tty.anyWriter()); 146 - 147 - var buffered_tty_writer = tty.bufferedWriter(); 148 - const loading = try scheduler.spawn(loadingTask, .{ &vx, buffered_tty_writer.writer().any() }, .{}); 149 - const audio = try scheduler.spawn(audioTask, .{allocator}, .{}); 150 - const video = try scheduler.spawn(videoTask, .{buffered_tty_writer.writer().any()}, .{}); 151 - 152 - main: while (try scheduler.tick(.blocking) > 0) { 153 - while (try loop.popEvent()) |event| switch (event) { 154 - .key_press => |key| { 155 - if (key.matches('c', .{ .ctrl = true })) { 156 - break :main; 157 - } 158 - }, 159 - .winsize => |ws| try vx.resize(allocator, buffered_tty_writer.writer().any(), ws), 160 - }; 161 - 162 - if (audio.state(Video) == .ready and video.state(Audio) == .ready) { 163 - loading.cancel(); 164 - audio.wakeup(); 165 - video.wakeup(); 166 - } else if (audio.state(Audio) == .end and video.state(Video) == .end) { 167 - break :main; 168 - } 169 - 170 - try buffered_tty_writer.flush(); 171 - } 172 - }
-127
examples/xev.zig
··· 1 - const std = @import("std"); 2 - const vaxis = @import("vaxis"); 3 - const xev = @import("xev"); 4 - const Cell = vaxis.Cell; 5 - 6 - pub const panic = vaxis.panic_handler; 7 - 8 - const App = struct { 9 - const lower_limit: u8 = 30; 10 - const next_ms: u64 = 8; 11 - 12 - allocator: std.mem.Allocator, 13 - vx: *vaxis.Vaxis, 14 - buffered_writer: std.io.BufferedWriter(4096, std.io.AnyWriter), 15 - color_idx: u8, 16 - dir: enum { 17 - up, 18 - down, 19 - }, 20 - 21 - fn draw(self: *App) !void { 22 - const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ self.color_idx, self.color_idx, self.color_idx } } }; 23 - 24 - const segment: vaxis.Segment = .{ 25 - .text = vaxis.logo, 26 - .style = style, 27 - }; 28 - const win = self.vx.window(); 29 - win.clear(); 30 - const center = vaxis.widgets.alignment.center(win, 28, 4); 31 - _ = try center.printSegment(segment, .{ .wrap = .grapheme }); 32 - switch (self.dir) { 33 - .up => { 34 - self.color_idx += 1; 35 - if (self.color_idx == 255) self.dir = .down; 36 - }, 37 - .down => { 38 - self.color_idx -= 1; 39 - if (self.color_idx == lower_limit) self.dir = .up; 40 - }, 41 - } 42 - try self.vx.render(self.buffered_writer.writer().any()); 43 - try self.buffered_writer.flush(); 44 - } 45 - }; 46 - 47 - pub fn main() !void { 48 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 49 - defer { 50 - const deinit_status = gpa.deinit(); 51 - //fail test; can't try in defer as defer is executed after we return 52 - if (deinit_status == .leak) { 53 - std.log.err("memory leak", .{}); 54 - } 55 - } 56 - const alloc = gpa.allocator(); 57 - 58 - var tty = try vaxis.Tty.init(); 59 - defer tty.deinit(); 60 - 61 - var vx = try vaxis.init(alloc, .{}); 62 - defer vx.deinit(alloc, tty.anyWriter()); 63 - 64 - var pool = xev.ThreadPool.init(.{}); 65 - var loop = try xev.Loop.init(.{ 66 - .thread_pool = &pool, 67 - }); 68 - defer loop.deinit(); 69 - 70 - var app: App = .{ 71 - .allocator = alloc, 72 - .buffered_writer = tty.bufferedWriter(), 73 - .color_idx = App.lower_limit, 74 - .dir = .up, 75 - .vx = &vx, 76 - }; 77 - 78 - var vx_loop: vaxis.xev.TtyWatcher(App) = undefined; 79 - try vx_loop.init(&tty, &vx, &loop, &app, eventCallback); 80 - 81 - try vx.enterAltScreen(tty.anyWriter()); 82 - // send queries asynchronously 83 - try vx.queryTerminalSend(tty.anyWriter()); 84 - 85 - const timer = try xev.Timer.init(); 86 - var timer_cmp: xev.Completion = .{}; 87 - timer.run(&loop, &timer_cmp, App.next_ms, App, &app, timerCallback); 88 - 89 - try loop.run(.until_done); 90 - } 91 - 92 - fn eventCallback( 93 - ud: ?*App, 94 - loop: *xev.Loop, 95 - watcher: *vaxis.xev.TtyWatcher(App), 96 - event: vaxis.xev.Event, 97 - ) xev.CallbackAction { 98 - const app = ud orelse unreachable; 99 - switch (event) { 100 - .key_press => |key| { 101 - if (key.matches('c', .{ .ctrl = true })) { 102 - loop.stop(); 103 - return .disarm; 104 - } 105 - }, 106 - .winsize => |ws| watcher.vx.resize(app.allocator, watcher.tty.anyWriter(), ws) catch @panic("TODO"), 107 - else => {}, 108 - } 109 - return .rearm; 110 - } 111 - 112 - fn timerCallback( 113 - ud: ?*App, 114 - l: *xev.Loop, 115 - c: *xev.Completion, 116 - r: xev.Timer.RunError!void, 117 - ) xev.CallbackAction { 118 - _ = r catch @panic("timer error"); 119 - 120 - var app = ud orelse return .disarm; 121 - app.draw() catch @panic("couldn't draw"); 122 - 123 - const timer = try xev.Timer.init(); 124 - timer.run(l, c, App.next_ms, App, ud, timerCallback); 125 - 126 - return .disarm; 127 - }
-207
src/aio.zig
··· 1 - const build_options = @import("build_options"); 2 - const builtin = @import("builtin"); 3 - const std = @import("std"); 4 - const vaxis = @import("main.zig"); 5 - const handleEventGeneric = @import("Loop.zig").handleEventGeneric; 6 - const log = std.log.scoped(.vaxis_aio); 7 - 8 - const Yield = enum { no_state, took_event }; 9 - 10 - pub fn Loop(T: type) type { 11 - if (!build_options.aio) { 12 - @compileError( 13 - \\build_options.aio is not enabled. 14 - \\Use `LoopWithModules` instead to provide `aio` and `coro` modules from outside vaxis. 15 - ); 16 - } 17 - return LoopWithModules(T, @import("aio"), @import("coro")); 18 - } 19 - 20 - /// zig-aio based event loop 21 - /// <https://github.com/Cloudef/zig-aio> 22 - pub fn LoopWithModules(T: type, aio: type, coro: type) type { 23 - return struct { 24 - const Event = T; 25 - 26 - winsize_task: ?coro.Task.Generic2(winsizeTask) = null, 27 - reader_task: ?coro.Task.Generic2(ttyReaderTask) = null, 28 - queue: std.BoundedArray(T, 512) = .{}, 29 - source: aio.EventSource, 30 - fatal: bool = false, 31 - 32 - pub fn init() !@This() { 33 - return .{ .source = try aio.EventSource.init() }; 34 - } 35 - 36 - pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { 37 - vx.deviceStatusReport(tty.anyWriter()) catch {}; 38 - if (self.winsize_task) |task| task.cancel(); 39 - if (self.reader_task) |task| task.cancel(); 40 - self.source.deinit(); 41 - self.* = undefined; 42 - } 43 - 44 - fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void { 45 - const Context = struct { 46 - loop: *@TypeOf(self.*), 47 - tty: *vaxis.Tty, 48 - winsize: ?vaxis.Winsize = null, 49 - fn cb(ptr: *anyopaque) void { 50 - std.debug.assert(coro.current() == null); 51 - const ctx: *@This() = @ptrCast(@alignCast(ptr)); 52 - ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return; 53 - ctx.loop.source.notify(); 54 - } 55 - }; 56 - 57 - // keep on stack 58 - var ctx: Context = .{ .loop = self, .tty = tty }; 59 - if (builtin.target.os.tag != .windows) { 60 - if (@hasField(Event, "winsize")) { 61 - const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; 62 - try vaxis.Tty.notifyWinsize(handler); 63 - } 64 - } 65 - 66 - while (true) { 67 - try coro.io.single(aio.WaitEventSource{ .source = &self.source }); 68 - if (ctx.winsize) |winsize| { 69 - if (!@hasField(Event, "winsize")) unreachable; 70 - ctx.loop.postEvent(.{ .winsize = winsize }) catch {}; 71 - ctx.winsize = null; 72 - } 73 - } 74 - } 75 - 76 - fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void { 77 - self.winsizeInner(tty) catch |err| { 78 - if (err != error.Canceled) log.err("winsize: {}", .{err}); 79 - self.fatal = true; 80 - }; 81 - } 82 - 83 - fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event { 84 - var state: vaxis.Tty.EventState = .{}; 85 - while (true) { 86 - var bytes_read: usize = 0; 87 - var input_record: vaxis.Tty.INPUT_RECORD = undefined; 88 - try coro.io.single(aio.ReadTty{ 89 - .tty = .{ .handle = tty.stdin }, 90 - .buffer = std.mem.asBytes(&input_record), 91 - .out_read = &bytes_read, 92 - }); 93 - 94 - if (try tty.eventFromRecord(&input_record, &state)) |ev| { 95 - return ev; 96 - } 97 - } 98 - } 99 - 100 - fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void { 101 - var cache: vaxis.GraphemeCache = .{}; 102 - while (true) { 103 - const event = try windowsReadEvent(tty); 104 - try handleEventGeneric(self, vx, &cache, Event, event, null); 105 - } 106 - } 107 - 108 - fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void { 109 - // initialize a grapheme cache 110 - var cache: vaxis.GraphemeCache = .{}; 111 - 112 - // get our initial winsize 113 - const winsize = try vaxis.Tty.getWinsize(tty.fd); 114 - if (@hasField(Event, "winsize")) { 115 - try self.postEvent(.{ .winsize = winsize }); 116 - } 117 - 118 - var parser: vaxis.Parser = .{ 119 - .grapheme_data = &vx.unicode.width_data.g_data, 120 - }; 121 - 122 - const file: std.fs.File = .{ .handle = tty.fd }; 123 - while (true) { 124 - var buf: [4096]u8 = undefined; 125 - var n: usize = undefined; 126 - var read_start: usize = 0; 127 - try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n }); 128 - var seq_start: usize = 0; 129 - while (seq_start < n) { 130 - const result = try parser.parse(buf[seq_start..n], paste_allocator); 131 - if (result.n == 0) { 132 - // copy the read to the beginning. We don't use memcpy because 133 - // this could be overlapping, and it's also rare 134 - const initial_start = seq_start; 135 - while (seq_start < n) : (seq_start += 1) { 136 - buf[seq_start - initial_start] = buf[seq_start]; 137 - } 138 - read_start = seq_start - initial_start + 1; 139 - continue; 140 - } 141 - read_start = 0; 142 - seq_start += result.n; 143 - 144 - const event = result.event orelse continue; 145 - try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator); 146 - } 147 - } 148 - } 149 - 150 - fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void { 151 - return switch (builtin.target.os.tag) { 152 - .windows => self.ttyReaderWindows(vx, tty), 153 - else => self.ttyReaderPosix(vx, tty, paste_allocator), 154 - } catch |err| { 155 - if (err != error.Canceled) log.err("ttyReader: {}", .{err}); 156 - self.fatal = true; 157 - }; 158 - } 159 - 160 - /// Spawns tasks to handle winsize signal and tty 161 - pub fn spawn( 162 - self: *@This(), 163 - scheduler: *coro.Scheduler, 164 - vx: *vaxis.Vaxis, 165 - tty: *vaxis.Tty, 166 - paste_allocator: ?std.mem.Allocator, 167 - spawn_options: coro.Scheduler.SpawnOptions, 168 - ) coro.Scheduler.SpawnError!void { 169 - if (self.reader_task) |_| unreachable; // programming error 170 - // This is required even if app doesn't care about winsize 171 - // It is because it consumes the EventSource, so it can wakeup the scheduler 172 - // Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update 173 - self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options); 174 - self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options); 175 - } 176 - 177 - pub const PopEventError = error{TtyCommunicationSevered}; 178 - 179 - /// Call this in a while loop in the main event handler until it returns null 180 - pub fn popEvent(self: *@This()) PopEventError!?T { 181 - if (self.fatal) return error.TtyCommunicationSevered; 182 - defer self.winsize_task.?.wakeupIf(Yield.took_event); 183 - defer self.reader_task.?.wakeupIf(Yield.took_event); 184 - return self.queue.popOrNull(); 185 - } 186 - 187 - pub const PostEventError = error{Overflow}; 188 - 189 - pub fn postEvent(self: *@This(), event: T) !void { 190 - if (coro.current()) |_| { 191 - while (true) { 192 - self.queue.insert(0, event) catch { 193 - // wait for the app to take event 194 - try coro.yield(Yield.took_event); 195 - continue; 196 - }; 197 - break; 198 - } 199 - } else { 200 - // queue can be full, app could handle this error by spinning the scheduler 201 - try self.queue.insert(0, event); 202 - } 203 - // wakes up the scheduler, so custom events update UI 204 - self.source.notify(); 205 - } 206 - }; 207 - }
-2
src/main.zig
··· 5 5 pub const Vaxis = @import("Vaxis.zig"); 6 6 7 7 pub const Loop = @import("Loop.zig").Loop; 8 - pub const xev = @import("xev.zig"); 9 - pub const aio = @import("aio.zig"); 10 8 11 9 pub const zigimg = @import("zigimg"); 12 10
-271
src/xev.zig
··· 1 - const std = @import("std"); 2 - const xev = @import("xev"); 3 - 4 - const Tty = @import("main.zig").Tty; 5 - const Winsize = @import("main.zig").Winsize; 6 - const Vaxis = @import("Vaxis.zig"); 7 - const Parser = @import("Parser.zig"); 8 - const Key = @import("Key.zig"); 9 - const Mouse = @import("Mouse.zig"); 10 - const Color = @import("Cell.zig").Color; 11 - 12 - const log = std.log.scoped(.vaxis_xev); 13 - 14 - pub const Event = union(enum) { 15 - key_press: Key, 16 - key_release: Key, 17 - mouse: Mouse, 18 - focus_in, 19 - focus_out, 20 - paste_start, // bracketed paste start 21 - paste_end, // bracketed paste end 22 - paste: []const u8, // osc 52 paste, caller must free 23 - color_report: Color.Report, // osc 4, 10, 11, 12 response 24 - color_scheme: Color.Scheme, 25 - winsize: Winsize, 26 - }; 27 - 28 - pub fn TtyWatcher(comptime Userdata: type) type { 29 - return struct { 30 - const Self = @This(); 31 - 32 - file: xev.File, 33 - tty: *Tty, 34 - 35 - read_buf: [4096]u8, 36 - read_buf_start: usize, 37 - read_cmp: xev.Completion, 38 - 39 - winsize_wakeup: xev.Async, 40 - winsize_cmp: xev.Completion, 41 - 42 - callback: *const fn ( 43 - ud: ?*Userdata, 44 - loop: *xev.Loop, 45 - watcher: *Self, 46 - event: Event, 47 - ) xev.CallbackAction, 48 - 49 - ud: ?*Userdata, 50 - vx: *Vaxis, 51 - parser: Parser, 52 - 53 - pub fn init( 54 - self: *Self, 55 - tty: *Tty, 56 - vaxis: *Vaxis, 57 - loop: *xev.Loop, 58 - userdata: ?*Userdata, 59 - callback: *const fn ( 60 - ud: ?*Userdata, 61 - loop: *xev.Loop, 62 - watcher: *Self, 63 - event: Event, 64 - ) xev.CallbackAction, 65 - ) !void { 66 - self.* = .{ 67 - .tty = tty, 68 - .file = xev.File.initFd(tty.fd), 69 - .read_buf = undefined, 70 - .read_buf_start = 0, 71 - .read_cmp = .{}, 72 - 73 - .winsize_wakeup = try xev.Async.init(), 74 - .winsize_cmp = .{}, 75 - 76 - .callback = callback, 77 - .ud = userdata, 78 - .vx = vaxis, 79 - .parser = .{ .grapheme_data = &vaxis.unicode.width_data.g_data }, 80 - }; 81 - 82 - self.file.read( 83 - loop, 84 - &self.read_cmp, 85 - .{ .slice = &self.read_buf }, 86 - Self, 87 - self, 88 - Self.ttyReadCallback, 89 - ); 90 - self.winsize_wakeup.wait( 91 - loop, 92 - &self.winsize_cmp, 93 - Self, 94 - self, 95 - winsizeCallback, 96 - ); 97 - const handler: Tty.SignalHandler = .{ 98 - .context = self, 99 - .callback = Self.signalCallback, 100 - }; 101 - try Tty.notifyWinsize(handler); 102 - } 103 - 104 - fn signalCallback(ptr: *anyopaque) void { 105 - const self: *Self = @ptrCast(@alignCast(ptr)); 106 - self.winsize_wakeup.notify() catch |err| { 107 - log.warn("couldn't wake up winsize callback: {}", .{err}); 108 - }; 109 - } 110 - 111 - fn ttyReadCallback( 112 - ud: ?*Self, 113 - loop: *xev.Loop, 114 - c: *xev.Completion, 115 - _: xev.File, 116 - buf: xev.ReadBuffer, 117 - r: xev.ReadError!usize, 118 - ) xev.CallbackAction { 119 - const n = r catch |err| { 120 - log.err("read error: {}", .{err}); 121 - return .disarm; 122 - }; 123 - const self = ud orelse unreachable; 124 - 125 - // reset read start state 126 - self.read_buf_start = 0; 127 - 128 - var seq_start: usize = 0; 129 - parse_loop: while (seq_start < n) { 130 - const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| { 131 - log.err("couldn't parse input: {}", .{err}); 132 - return .disarm; 133 - }; 134 - if (result.n == 0) { 135 - // copy the read to the beginning. We don't use memcpy because 136 - // this could be overlapping, and it's also rare 137 - const initial_start = seq_start; 138 - while (seq_start < n) : (seq_start += 1) { 139 - self.read_buf[seq_start - initial_start] = self.read_buf[seq_start]; 140 - } 141 - self.read_buf_start = seq_start - initial_start + 1; 142 - return .rearm; 143 - } 144 - seq_start += n; 145 - const event_inner = result.event orelse { 146 - log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]}); 147 - continue :parse_loop; 148 - }; 149 - 150 - // Capture events we want to bubble up 151 - const event: ?Event = switch (event_inner) { 152 - .key_press => |key| .{ .key_press = key }, 153 - .key_release => |key| .{ .key_release = key }, 154 - .mouse => |mouse| .{ .mouse = mouse }, 155 - .focus_in => .focus_in, 156 - .focus_out => .focus_out, 157 - .paste_start => .paste_start, 158 - .paste_end => .paste_end, 159 - .paste => |paste| .{ .paste = paste }, 160 - .color_report => |report| .{ .color_report = report }, 161 - .color_scheme => |scheme| .{ .color_scheme = scheme }, 162 - .winsize => |ws| .{ .winsize = ws }, 163 - 164 - // capability events which we handle below 165 - .cap_kitty_keyboard, 166 - .cap_kitty_graphics, 167 - .cap_rgb, 168 - .cap_unicode, 169 - .cap_sgr_pixels, 170 - .cap_color_scheme_updates, 171 - .cap_da1, 172 - => null, // handled below 173 - }; 174 - 175 - if (event) |ev| { 176 - const action = self.callback(self.ud, loop, self, ev); 177 - switch (action) { 178 - .disarm => return .disarm, 179 - else => continue :parse_loop, 180 - } 181 - } 182 - 183 - switch (event_inner) { 184 - .key_press, 185 - .key_release, 186 - .mouse, 187 - .focus_in, 188 - .focus_out, 189 - .paste_start, 190 - .paste_end, 191 - .paste, 192 - .color_report, 193 - .color_scheme, 194 - .winsize, 195 - => unreachable, // handled above 196 - 197 - .cap_kitty_keyboard => { 198 - log.info("kitty keyboard capability detected", .{}); 199 - self.vx.caps.kitty_keyboard = true; 200 - }, 201 - .cap_kitty_graphics => { 202 - if (!self.vx.caps.kitty_graphics) { 203 - log.info("kitty graphics capability detected", .{}); 204 - self.vx.caps.kitty_graphics = true; 205 - } 206 - }, 207 - .cap_rgb => { 208 - log.info("rgb capability detected", .{}); 209 - self.vx.caps.rgb = true; 210 - }, 211 - .cap_unicode => { 212 - log.info("unicode capability detected", .{}); 213 - self.vx.caps.unicode = .unicode; 214 - self.vx.screen.width_method = .unicode; 215 - }, 216 - .cap_sgr_pixels => { 217 - log.info("pixel mouse capability detected", .{}); 218 - self.vx.caps.sgr_pixels = true; 219 - }, 220 - .cap_color_scheme_updates => { 221 - log.info("color_scheme_updates capability detected", .{}); 222 - self.vx.caps.color_scheme_updates = true; 223 - }, 224 - .cap_da1 => { 225 - self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| { 226 - log.err("couldn't enable features: {}", .{err}); 227 - }; 228 - }, 229 - } 230 - } 231 - 232 - self.file.read( 233 - loop, 234 - c, 235 - .{ .slice = &self.read_buf }, 236 - Self, 237 - self, 238 - Self.ttyReadCallback, 239 - ); 240 - return .disarm; 241 - } 242 - 243 - fn winsizeCallback( 244 - ud: ?*Self, 245 - l: *xev.Loop, 246 - c: *xev.Completion, 247 - r: xev.Async.WaitError!void, 248 - ) xev.CallbackAction { 249 - _ = r catch |err| { 250 - log.err("async error: {}", .{err}); 251 - return .disarm; 252 - }; 253 - const self = ud orelse unreachable; // no userdata 254 - const winsize = Tty.getWinsize(self.tty.fd) catch |err| { 255 - log.err("couldn't get winsize: {}", .{err}); 256 - return .disarm; 257 - }; 258 - const ret = self.callback(self.ud, l, self, .{ .winsize = winsize }); 259 - if (ret == .disarm) return .disarm; 260 - 261 - self.winsize_wakeup.wait( 262 - l, 263 - c, 264 - Self, 265 - self, 266 - winsizeCallback, 267 - ); 268 - return .disarm; 269 - } 270 - }; 271 - }