libvaxis

repository·main·Indexed 23 days ago

https://github.com/rockorager/libvaxis

A terminal library for Zig 0.16.0 that provides high-level terminal capabilities without relying on terminfo, using terminal queries to detect features. It supports RGB, Kitty graphics protocol, Kitty Keyboard Protocol, and system integration (OSC 52, 9, 777) across macOS, Windows, and Linux. The library includes a low-level API for full cell control and vxfw, a high-level, Flutter-like framework for TUI applications featuring a widget-based system, event loop, and focus management.

Tokens
14.1K
Snippets
12
Records
15
Agent score
84%

What's inside libvaxis

  1. Overview of libvaxis features

    main

    libvaxis is a terminal library that does not rely on terminfo. Instead, it detects VT features through terminal queries. It supports a wide range of modern terminal capabilities across macOS, Windows, and Linux/Unix-like systems, including:

    • Color & Graphics: RGB, Kitty graphics protocol, and Color Mode Updates (Mode 2031).
    • Input & Interaction: Kitty Keyboard Protocol, Bracketed Paste, Mouse Shapes (OSC 22), and Hyperlinks (OSC 8).
    • System Integration: System Clipboard (OSC 52), System Notifications (OSC 9 and OSC 777).
    • Advanced Terminal Modes: Synchronized Output (Mode 2026), Unicode Core (Mode 2027), In-Band Resize Reports (Mode 2048), and Fancy underlines (undercurl, etc.).
    • Text Rendering: Explicit Width (width modifiers only).
  2. Implement a custom event loop for Vaxis

    main

    Vaxis provides an abstract API that allows you to use your own event loop implementation. To integrate Vaxis into a custom loop, your implementation must perform three primary tasks:

    1. Read raw bytes from the TTY.
    2. Pass bytes to the Vaxis input event parser (using the Parser struct).
    3. Handle the returned events.

    Crucial Requirement: Terminal Capabilities When handling events, you must update the Vaxis struct with discovered terminal capabilities. This allows Vaxis to utilize advanced features like the Kitty Keyboard protocol, in-band-resize reports, and Unicode width measurements. Failing to update these capabilities will prevent Vaxis from using the full feature set of the user's terminal.

  3. How vxfw (Vaxis framework) works

    main

    The vxfw (Vaxis framework) is a high-level, Flutter-like API designed for typical TUI applications. It provides an application runtime that manages the event loop, focus management, and mouse handling.

    Core Concepts

    • Model: Holds your application state. It should implement a widget() method that returns a vxfw.Widget.
    • Widget: A struct containing userdata (your model), an eventHandler, and a drawFn.
    • EventContext: Passed to event handlers. Used to request focus (ctx.requestFocus()), signal a redraw (ctx.consumeAndRedraw()), or quit the application (ctx.quit = true).
    • DrawContext: Passed to the drawFn. It provides:
      • Constraints: min and max size constraints (similar to Flutter). The max constraint can have null width/height.
      • Arena Allocator: A frame-local allocator (ctx.arena) for temporary allocations that only need to live until the next frame.
    • Surface & SubSurface: A Surface represents the rectangular area and properties of a widget. A SubSurface is a Surface with an origin (offset) and z-index, allowing parents to position children within themselves.
    const vaxis = @import("vaxis");
    const vxfw = vaxis.vxfw;
    
    // Example of a Model returning a Widget
    const Model = struct {
        count: u32 = 0,
        button: vxfw.Button,
    
        pub fn widget(self: *Model) vxfw.Widget {
            return .{
                .userdata = self,
                .eventHandler = Model.typeErasedEventHandler,
                .drawFn = Model.typeErasedDrawFn,
            };
        }
    };
  4. Use the Low-level API

    main

    The low-level API provides full control over every cell on the screen and allows you to provide your own event loop. It requires three primitives:

    1. A TTY instance (vaxis.Tty)
    2. An instance of Vaxis (vaxis.Vaxis)
    3. An event loop (e.g., vaxis.Loop)

    Workflow

    1. Initialize the TTY and Vaxis.
    2. Initialize an event loop (e.g., vaxis.Loop(Event)).
    3. Start the loop to begin reading input.
    4. Optionally enter the alternate screen using vx.enterAltScreen(tty.writer()).
    5. Query the terminal for features using vx.queryTerminal(tty.writer(), .fromSeconds(n)).
    6. In your main loop:
      • Get the next event via loop.nextEvent().
      • Handle events (key presses, window resizing, etc.).
      • Clear the window using win.clear().
      • Draw widgets into the window or child windows.
      • Render the screen using vx.render(tty.writer()).

    Note: When handling winsize events, use vx.resize(alloc, tty.writer(), ws) to update the Vaxis instance.

    const std = @import("std");
    const vaxis = @import("vaxis");
    
    pub fn main(init: std.process.Init) !void {
        const io = init.io;
        const alloc = init.gpa;
    
        var buffer: [1024]u8 = undefined;
        var tty = try vaxis.Tty.init(io, &buffer);
        defer tty.deinit();
    
        var vx = try vaxis.init(io, alloc, init.environ_map, .{});
        defer vx.deinit(alloc, tty.writer());
    
        var loop: vaxis.Loop(vaxis.Event) = .init(io, &tty, &vx);
        try loop.start();
        defer loop.stop();
    
        try vx.enterAltScreen(tty.writer());
        try vx.queryTerminal(tty.writer(), .fromSeconds(1));
    
        while (true) {
            const event = try loop.nextEvent();
            switch (event) {
                .key_press => |key| {
                    if (key.matches('c', .{ .ctrl = true })) break else {}
                },
                .winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
                else => {},
            }
    
            const win = vx.window();
            win.clear();
            // ... draw widgets ...
            try vx.render(tty.writer());
        }
    }
  5. Add libvaxis to your project

    main

    To use libvaxis in a Zig project, first fetch the dependency using the Zig CLI:

    zig fetch --save git+https://github.com/rockorager/libvaxis.git

    Then, update your build.zig to include the dependency and import the module.

    Standard Setup

        const vaxis = b.dependency("vaxis", .{
            .target = target,
            .optimize = optimize,
        });
    
        exe.root_module.addImport("vaxis", vaxis.module("vaxis"));

    Setup with ZLS support

    If you require ZLS support, create a module for your executable first:

        // create module
        const exe_mod = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        });
    
        // add vaxis dependency to module
        const vaxis = b.dependency("vaxis", .{
            .target = target,
            .optimize = optimize,
        });
        exe_mod.addImport("vaxis", vaxis.module("vaxis"));
    
        //create executable
        const exe = b.addExecutable(.{
            .name = "project_foo",
            .root_module = exe_mod,
        });
    zig fetch --save git+https://github.com/rockorager/libvaxis.git
  6. Share uucode with your application

    main

    By default, libvaxis pulls in uucode with a fixed set of fields. If your application also uses uucode, you can prevent duplicate tables by building libvaxis against your own uucode module.

    To do this, pass .external_uucode = true to the libvaxis dependency and manually wire your uucode module into the vaxis module.

        const uucode = b.dependency("uucode", .{
            .target = target,
            .optimize = optimize,
            .fields = @as([]const []const u8, &.{ 
                // Add any fields your application needs, plus the fields libvaxis
                // requires.
            }),
        });
    
        const vaxis = b.dependency("vaxis", .{
            .target = target,
            .optimize = optimize,
            .external_uucode = true,
        });
        vaxis.module("vaxis").addImport("uucode", uucode.module("uucode"));
    
        exe.root_module.addImport("vaxis", vaxis.module("vaxis"));
        exe.root_module.addImport("uucode", uucode.module("uucode"));
  7. Initialize and run a vxfw application

    main

    To build a terminal application using the vxfw framework, use the App struct. The lifecycle involves initializing the App with an allocator and I/O, then calling run with a root vxfw.Widget and Options.

    Lifecycle:

    1. init(...): Creates the application on the heap. Requires an allocator, I/O, and an environment map.
    2. run(widget, opts): Starts the event loop, enters the alternate screen, sets up mouse/focus handling, and begins the render loop.
    3. deinit(): Resets terminal state and releases resources. Must be called when the application is complete.

    Note: init creates the App object on the heap to ensure stable pointers for the framework's internal setup.

    const std = @import(
  8. Build a simple button counter with vxfw

    main

    This example demonstrates a complete vxfw application with mouse support and state management.

    const std = @import("std");
    const vaxis = @import("vaxis");
    const vxfw = vaxis.vxfw;
    
    /// Our main application state
    const Model = struct {
        /// State of the counter
        count: u32 = 0,
        /// The button. This widget is stateful and must live between frames
        button: vxfw.Button,
    
        /// Helper function to return a vxfw.Widget struct
        pub fn widget(self: *Model) vxfw.Widget {
            return .{
                .userdata = self,
                .eventHandler = Model.typeErasedEventHandler,
                .drawFn = Model.typeErasedDrawFn,
            };
        }
    
        /// This function will be called from the vxfw runtime.
        fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
            const self: *Model = @ptrCast(@alignCast(ptr));
            switch (event) {
                // The root widget is always sent an init event as the first event. Users of the
                // library can also send this event to other widgets they create if they need to do
                // some initialization.
                .init => return ctx.requestFocus(self.button.widget()),
                .key_press => |key| {
                    if (key.matches('c', .{ .ctrl = true })) {
                        ctx.quit = true;
                        return;
                    },
                },
                // We can request a specific widget gets focus. In this case, we always want to focus
                // our button. Having focus means that key events will be sent up the widget tree to
                // the focused widget, and then bubble back down the tree to the root. Users can tell
                // the runtime the event was handled and the capture or bubble phase will stop
                .focus_in => return ctx.requestFocus(self.button.widget()),
                else => {},
            }
        }
    
        /// This function is called by the vxfw runtime. It will be called on a regular interval, and
        /// only when any event handler has marked the redraw flag in EventContext as true.
        fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
            const self: *Model = @ptrCast(@alignCast(ptr));
            const max_size = ctx.max.size();
    
            // Use the frame arena for temporary allocations
            const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count});
            const text: vxfw.Text = .{ .text = count_text };
    
            const text_child: vxfw.SubSurface = .{
                .origin = .{ .row = 0, .col = 0 },
                .surface = try text.draw(ctx),
            };
    
            const button_child: vxfw.SubSurface = .{
                .origin = .{ .row = 2, .col = 0 },
                .surface = try self.button.draw(ctx.withConstraints(
                    ctx.min,
                    .{ .width = 16, .height = 3 },
                )),
            };
    
            const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
            children[0] = text_child;
            children[1] = button_child;
    
            return .{
                .size = max_size,
                .widget = self.widget(),
                .buffer = &.{},
                .children = children,
            };
        }
    
        /// The onClick callback for our button.
        fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
            const ptr = maybe_ptr orelse return;
            const self: *Model = @ptrCast(@alignCast(ptr));
            self.count +|= 1;
            return ctx.consumeAndRedraw();
        }
    };
    
    pub fn main(init: std.process.Init) !void {
        const io = init.io;
        const alloc = init.gpa;
    
        var buffer: [1024]u8 = undefined;
        var app: vxfw.App = try .init(io, alloc, init.environ_map, &buffer);
        defer app.deinit();
    
        const model = try alloc.create(Model);
        defer alloc.destroy(model);
    
        model.* = .{
            .count = 0,
            .button = .{
                .label = "Click me!",
                .onClick = Model.onClick,
                .userdata = model,
            },
        };
    
        try app.run(model.widget(), .{});
    }
  9. Use libxev as a Vaxis event loop

    main

    This example demonstrates how to implement a TtyWatcher using libxev. The watcher manages reading from the TTY and handling window resize signals.

    Key steps in the implementation:

    • Initialize the TtyWatcher with a Tty, Vaxis instance, and an xev.Loop.
    • Use self.file.read to asynchronously read bytes into a buffer.
    • Use self.parser.parse to convert raw bytes into Vaxis events.
    • Map internal parser events (like .cap_kitty_keyboard or .cap_unicode) to the Vaxis capability fields (e.g., self.vx.caps.kitty_keyboard = true).
    • Handle winsize updates via Tty.notifyWinsize and an async wakeup mechanism.
    const std = @import("std");
    const xev = @import("xev");
    
    const Tty = @import("main.zig").Tty;
    const Winsize = @import("main.zig").Winsize;
    const Vaxis = @import("Vaxis.zig");
    const Parser = @import("Parser.zig");
    const Key = @import("Key.zig");
    const Mouse = @import("Mouse.zig");
    const Color = @import("Cell.zig").Color;
    
    const log = std.log.scoped(.vaxis_xev);
    
    pub const Event = union(enum) {
        key_press: Key,
        key_release: Key,
        mouse: Mouse,
        focus_in,
        focus_out,
        paste_start, // bracketed paste start
        paste_end, // bracketed paste end
        paste: []const u8, // osc 52 paste, caller must free
        color_report: Color.Report, // osc 4, 10, 11, 12 response
        color_scheme: Color.Scheme,
        winsize: Winsize,
    };
    
    pub fn TtyWatcher(comptime Userdata: type) type {
        return struct {
            const Self = @This();
    
            file: xev.File,
            tty: *Tty,
    
            read_buf: [4096]u8,
            read_buf_start: usize,
            read_cmp: xev.Completion,
    
            winsize_wakeup: xev.Async,
            winsize_cmp: xev.Completion,
    
            callback: *const fn (
                ud: ?*Userdata,
                loop: *xev.Loop,
                watcher: *Self,
                event: Event,
            ) xev.CallbackAction,
    
            ud: ?*Userdata,
            vx: *Vaxis,
            parser: Parser,
    
            pub fn init(
                self: *Self,
                tty: *Tty,
                vaxis: *Vaxis,
                loop: *xev.Loop,
                userdata: ?*Userdata,
                callback: *const fn (
                    ud: ?*Userdata,
                    loop: *xev.Loop,
                    watcher: *Self,
                    event: Event,
                ) xev.CallbackAction,
            ) !void {
                self.* = .{
                    .tty = tty,
                    .file = xev.File.initFd(tty.fd),
                    .read_buf = undefined,
                    .read_buf_start = 0,
                    .read_cmp = .{},
    
                    .winsize_wakeup = try xev.Async.init(),
                    .winsize_cmp = .{},
    
                    .callback = callback,
                    .ud = userdata,
                    .vx = vaxis,
                    .parser = .{ .grapheme_data = &vaxis.unicode.width_data.g_data },
                };
    
                self.file.read(
                    loop,
                    &self.read_cmp,
                    .{ .slice = &self.read_buf },
                    Self,
                    self,
                    Self.ttyReadCallback,
                );
                self.winsize_wakeup.wait(
                    loop,
                    &self.winsize_cmp,
                    Self,
                    self,
                    winsizeCallback,
                );
                const handler: Tty.SignalHandler = .{
                    .context = self,
                    .callback = Self.signalCallback,
                };
                try Tty.notifyWinsize(handler);
            }
    
            fn signalCallback(ptr: *anyopaque) void {
                const self: *Self = @ptrCast(@alignCast(ptr));
                self.winsize_wakeup.notify() catch |err| {
                    log.warn("couldn't wake up winsize callback: {}", .{err});
                };
            }
    
            fn ttyReadCallback(
                ud: ?*Self,
                loop: *xev.Loop,
                c: *xev.Completion,
                _: xev.File,
                buf: xev.ReadBuffer,
                r: xev.ReadError!usize,
            ) xev.CallbackAction {
                const n = r catch |err| {
                    log.err("read error: {}", .{err});
                    return .disarm;
                };
                const self = ud orelse unreachable;
    
                // reset read start state
                self.read_buf_start = 0;
    
                var seq_start: usize = 0;
                parse_loop: while (seq_start < n) {
                    const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| {
                        log.err("couldn't parse input: {}", .{err});
                        return .disarm;
                    };
                    if (result.n == 0) {
                        // copy the read to the beginning. We don't use memcpy because
                        // this could be overlapping, and it's also rare
                        const initial_start = seq_start;
                        while (seq_start < n) : (seq_start += 1) {
                            self.read_buf[seq_start - initial_start] = self.read_buf[seq_start];
                        }
                        self.read_buf_start = seq_start - initial_start + 1;
                        return .rearm;
                    }
                    seq_start += n;
                    const event_inner = result.event orelse {
                        log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]});
                        continue :parse_loop;
                    };
    
                    // Capture events we want to bubble up
                    const event: ?Event = switch (event_inner) {
                        .key_press => |key| .{ .key_press = key },
                        .key_release => |key| .{ .key_release = key },
                        .mouse => |mouse| .{ .mouse = mouse },
                        .focus_in => .focus_in,
                        .focus_out => .focus_out,
                        .paste_start => .paste_start,
                        .paste_end => .paste_end,
                        .paste => |paste| .{ .paste = paste },
                        .color_report => |report| .{ .color_report = report },
                        .color_scheme => |scheme| .{ .color_scheme = scheme },
                        .winsize => |ws| .{ .winsize = ws },
    
                        // capability events which we handle below
                        .cap_kitty_keyboard,
                        .cap_kitty_graphics,
                        .cap_rgb,
                        .cap_unicode,
                        .cap_sgr_pixels,
                        .cap_color_scheme_updates,
                        .cap_da1,
                        => null, // handled below
                    };
    
                    if (event) |ev| {
                        const action = self.callback(self.ud, loop, self, ev);
                        switch (action) {
                            .disarm => return .disarm,
                            else => continue :parse_loop,
                        }
                    }
    
                    switch (event_inner) {
                        .key_press,
                        .key_release,
                        .mouse,
                        .focus_in,
                        .focus_out,
                        .paste_start,
                        .paste_end,
                        .paste,
                        .color_report,
                        .color_scheme,
                        .winsize,
                        => unreachable, // handled above
    
                        .cap_kitty_keyboard => {
                            log.info("kitty keyboard capability detected", .{});
                            self.vx.caps.kitty_keyboard = true;
                        },
                        .cap_kitty_graphics => {
                            if (!self.vx.caps.kitty_graphics) {
                                log.info("kitty graphics capability detected", .{});
                                self.vx.caps.kitty_graphics = true;
                            }
                        },
                        .cap_rgb => {
                            log.info("rgb capability detected", .{});
                            self.vx.caps.rgb = true;
                        },
                        .cap_unicode => {
                            log.info("unicode capability detected", .{});
                            self.vx.caps.unicode = .unicode;
                            self.vx.screen.width_method = .unicode;
                        },
                        .cap_sgr_pixels => {
                            log.info("pixel mouse capability detected", .{});
                            self.vx.caps.sgr_pixels = true;
                        },
                        .cap_color_scheme_updates => {
                            log.info("color_scheme_updates capability detected", .{});
                            self.vx.caps.color_scheme_updates = true;
                        },
                        .cap_da1 => {
                            self.vx.enableDetectedFeatures(self.tty.writer()) catch |err| {
                                log.err("couldn't enable features: {}", .{err});
                            };
                        },
                    }
                }
    
                self.file.read(
                    loop,
                    c,
                    .{ .slice = &self.read_buf },
                    Self,
                    self,
                    Self.ttyReadCallback,
                );
                return .disarm;
            }
    
            fn winsizeCallback(
                ud: ?*Self,
                l: *xev.Loop,
                c: *xev.Completion,
                r: xev.Async.WaitError!void,
            ) xev.CallbackAction {
                _ = r catch |err| {
                    log.err("async error: {}", .{err});
                    return .disarm;
                };
                const self = ud orelse unreachable; // no userdata
                const winsize = Tty.getWinsize(self.tty.fd) catch |err| {
                    log.err("couldn't get winsize: {}", .{err});
                    return .disarm;
                };
                const ret = self.callback(self.ud, l, self, .{ .winsize = winsize });
                if (ret == .disarm) return .disarm;
    
                self.winsize_wakeup.wait(
                    l,
                    c,
                    Self,
                    self,
                    winsizeCallback,
                );
                return .disarm;
            }
        };
    }
  10. Use zig-aio as a Vaxis event loop

    main

    This example demonstrates a LoopWithModules implementation using zig-aio and a coroutine-based scheduler.

    Key components:

    • spawn: Starts two tasks: winsize_task (to handle window resize signals) and reader_task (to handle TTY input).
    • popEvent: Called in the main application loop to retrieve events from the internal queue. Returns error.TtyCommunicationSevered if the TTY connection is lost.
    • postEvent: Used to push events (like winsize) into the queue. It notifies the aio.EventSource to wake up the scheduler so the UI can update.
    • Platform Handling: The ttyReaderTask branches between ttyReaderWindows (using vaxis.Tty.INPUT_RECORD) and ttyReaderPosix (using the vaxis.Parser on raw bytes).
    const builtin = @import("builtin");
    const std = @import("std");
    const vaxis = @import("vaxis");
    const handleEventGeneric = vaxis.loop.handleEventGeneric;
    const log = std.log.scoped(.vaxis_aio);
    
    const Yield = enum { no_state, took_event };
    
    /// zig-aio based event loop
    /// <https://github.com/Cloudef/zig-aio>
    pub fn LoopWithModules(T: type, aio: type, coro: type) type {
        return struct {
            const Event = T;
    
            winsize_task: ?coro.Task.Generic2(winsizeTask) = null,
            reader_task: ?coro.Task.Generic2(ttyReaderTask) = null,
            queue: std.BoundedArray(T, 512) = .{},
            source: aio.EventSource,
            fatal: bool = false,
    
            pub fn init() !@This() {
                return .{ .source = try aio.EventSource.init() };
            }
    
            pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
                vx.deviceStatusReport(tty.writer()) catch {};
                if (self.winsize_task) |task| task.cancel();
                if (self.reader_task) |task| task.cancel();
                self.source.deinit();
                self.* = undefined;
            }
    
            fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void {
                const Context = struct {
                    loop: *@TypeOf(self.*),
                    tty: *vaxis.Tty,
                    winsize: ?vaxis.Winsize = null,
                    fn cb(ptr: *anyopaque) void {
                        std.debug.assert(coro.current() == null);
                        const ctx: *@This() = @ptrCast(@alignCast(ptr));
                        ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return;
                        ctx.loop.source.notify();
                    }
                };
    
                // keep on stack
                var ctx: Context = .{ .loop = self, .tty = tty };
                if (builtin.target.os.tag != .windows) {
                    if (@hasField(Event, "winsize")) {
                        const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
                        try vaxis.Tty.notifyWinsize(handler);
                    }
                }
    
                while (true) {
                    try coro.io.single(aio.WaitEventSource{ .source = &self.source });
                    if (ctx.winsize) |winsize| {
                        if (!@hasField(Event, "winsize")) unreachable;
                        ctx.loop.postEvent(.{ .winsize = winsize }) catch {};
                        ctx.winsize = null;
                    }
                }
            }
    
            fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void {
                self.winsizeInner(tty) catch |err| {
                    if (err != error.Canceled) log.err("winsize: {}", .{err});
                    self.fatal = true;
                };
            }
    
            fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event {
                var state: vaxis.Tty.EventState = .{};
                while (true) {
                    var bytes_read: usize = 0;
                    var input_record: vaxis.Tty.INPUT_RECORD = undefined;
                    try coro.io.single(aio.ReadTty{
                        .tty = .{ .handle = tty.stdin },
                        .buffer = std.mem.asBytes(&input_record),
                        .out_read = &bytes_read,
                    });
    
                    if (try tty.eventFromRecord(&input_record, &state)) |ev| {
                        return ev;
                    }
                }
            }
    
            fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void {
                var cache: vaxis.GraphemeCache = .{};
                while (true) {
                    const event = try windowsReadEvent(tty);
                    try handleEventGeneric(self, vx, &cache, Event, event, null);
                }
            }
    
            fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
                // initialize a grapheme cache
                var cache: vaxis.GraphemeCache = .{};
    
                // get our initial winsize
                const winsize = try vaxis.Tty.getWinsize(tty.fd);
                if (@hasField(Event, "winsize")) {
                    try self.postEvent(.{ .winsize = winsize });
                }
    
                var parser: vaxis.Parser = .{
                    .grapheme_data = &vx.unicode.width_data.g_data,
                };
    
                const file: std.fs.File = .{ .handle = tty.fd };
                while (true) {
                    var buf: [4096]u8 = undefined;
                    var n: usize = undefined;
                    var read_start: usize = 0;
                    try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n });
                    var seq_start: usize = 0;
                    while (seq_start < n) {
                        const result = try parser.parse(buf[seq_start..n], paste_allocator);
                        if (result.n == 0) {
                            // copy the read to the beginning. We don't use memcpy because
                            // this could be overlapping, and it's also rare
                            const initial_start = seq_start;
                            while (seq_start < n) : (seq_start += 1) {
                                buf[seq_start - initial_start] = buf[seq_start];
                            }
                            read_start = seq_start - initial_start + 1;
                            continue;
                        }
                        read_start = 0;
                        seq_start += result.n;
    
                        const event = result.event orelse continue;
                        try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator);
                    }
                }
            }
    
            fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
                return switch (builtin.target.os.tag) {
                    .windows => self.ttyReaderWindows(vx, tty),
                    else => self.ttyReaderPosix(vx, tty, paste_allocator),
                } catch |err| {
                    if (err != error.Canceled) log.err("ttyReader: {}", .{err});
                    self.fatal = true;
                };
            }
    
            /// Spawns tasks to handle winsize signal and tty
            pub fn spawn(
                self: *@This(),
                scheduler: *coro.Scheduler,
                vx: *vaxis.Vaxis,
                tty: *vaxis.Tty,
                paste_allocator: ?std.mem.Allocator,
                spawn_options: coro.Scheduler.SpawnOptions,
            ) coro.Scheduler.SpawnError!void {
                if (self.reader_task) |_| unreachable; // programming error
                if (self.reader_task) |_| unreachable; // programming error
                // This is required even if app doesn't care about winsize
                // It is because it consumes the EventSource, so it can wakeup the scheduler
                // Without that custom `postEvent` wouldn't wake up the scheduler and UI wouldn't update
                self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options);
                self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options);
            }
    
            pub const PopEventError = error{TtyCommunicationSevered};
    
            /// Call this in a while loop in the main event handler until it returns null
            pub fn popEvent(self: *@This()) PopEventError!?T {
                if (self.fatal) return error.TtyCommunicationSevered;
                defer self.winsize_task.?.wakeupIf(Yield.took_event);
                defer self.reader_task.?.wakeupIf(Yield.took_event);
                return self.queue.popOrNull();
            }
    
            pub const PostEventError = error{Overflow};
    
            pub fn postEvent(self: *@This(), event: T) !void {
                if (coro.current()) |_| {
                    while (true) {
                        self.queue.insert(0, event) catch {
                            // wait for the app to take event
                            try coro.yield(Yield.took_event);
                            continue;
                        };
                        break;
                    };
                } else {
                    // queue can be full, app could handle this error by spinning the scheduler
                    try self.queue.insert(0, event);
                }
                // wakes up the scheduler, so custom events update UI
                self.source.notify();
            }
        };
    }
  11. Configure App runtime options

    main

    The Options struct allows you to control the execution parameters of the application loop.

    Fields:

    • framerate: u8 defines the target frames per second. Defaults to 60.
    pub const Options = struct {
        /// Frames per second
        framerate: u8 = 60,
    };