Library / SDK
rockorager/libvaxis avatar
rockorager/libvaxis

libvaxis: a Zig TUI library that queries the terminal instead of reading terminfo

a modern tui library written in zig

2,029 stars159 forksZigMIT

At a glance

What is it?
libvaxis gives Zig programs full control of every screen cell plus a vxfw widget runtime above it, and it works out which terminal features exist by querying the terminal rather than reading terminfo. The cost of that choice shows in the details: a toolchain pin with no compatibility table, and features with no documented fallback.
Who is it for?
libvaxis is worth adopting when you are writing a full screen terminal program in Zig and want negotiated terminal features instead of a capability database, and the vxfw layer removes the event loop, focus and mouse plumbing you would otherwise write yourself. Skip it for a small status line, where the raw cell API is all you need, or for terminals behind a multiplexer that swallows replies, because nothing in the project describes a fallback for an unanswered query.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Zig, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Capabilities are negotiated at startup, not read from terminfo

Libvaxis does not use terminfo. Support for vt features is detected through terminal queries, so what a program can draw is decided by the emulator on the other end of the pipe rather than by a database shipped with the operating system.

That decision is why the feature list is written as named terminal protocols instead of abstract capability names. Hyperlinks arrive through OSC 8, the system clipboard through OSC 52, mouse cursor shapes through OSC 22, and notifications through OSC 9 and OSC 777. Frame pacing uses Mode 2026, Synchronized Output, which asks the terminal to hold the screen until a frame is complete rather than show it tearing line by line. Unicode Core is Mode 2027, colour mode updates are Mode 2031, and In-Band Resize Reports are Mode 2048. Keyboard input goes through the Kitty Keyboard Protocol, curved underlines through the kitty underlines specification, and images through the kitty graphics protocol.

Two entries are narrower than their names. Explicit Width is marked as width modifiers only, and Fancy underlines are qualified as undercurl and similar effects rather than a general styling system. Every item depends on an answer from the terminal, and no fallback is described for a terminal that stays quiet.

Two APIs in one library: raw cells and the vxfw runtime

The library ships two layers, and the split is not cosmetic. The low level API exposes the screen cell by cell and leaves the event loop to you, which suits anything from a full screen editor down to a one line status display where a framework would be more machinery than the program needs.

Above it sits `vxfw`, the Vaxis framework, whose API the project calls Flutter-like. Its runtime owns the event loop, focus management and mouse handling, so an application supplies widgets instead of a draw loop, and the counter example is written that way. This is the layer the project points ordinary TUI applications at.

Focus in `vxfw` is not a boolean sitting on a widget. Key events travel up the tree to whatever holds focus and then bubble back down to the root, and a handler stops the capture or bubble phase by telling the runtime the event was handled. One consequence is easy to miss in the sample: focus is requested for the button, yet the Ctrl-C handler that sets the quit flag lives on the root model. It only ever fires because unhandled keys fall back down the tree after passing through the button.

Wiring the dependency in with zig fetch and build.zig

Fetching the package takes one command, and it has to be the one the project prints:

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

The dependency is then declared in `build.zig` against your target and optimisation settings, and the resulting module is imported into the executable root module under the name vaxis:

zig
    const vaxis = b.dependency("vaxis", .{
        .target = target,
        .optimize = optimize,
    });

    exe.root_module.addImport("vaxis", vaxis.module("vaxis"));

Source files reach the library through a plain vaxis import. If your editor runs the Zig language server, the same dependency has to be created as its own module instead, which is the second form the project prints:

zig
    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

That snippet ends on a comment promising to install the executable below, with no install call after it, so take the module creation and finish the install step against your own build file.

The project is MIT licensed, and the last push was on 2026-10-03. On the toolchain side, Vaxis uses zig `0.16.0`, and no other Zig version is named in the printed pages.

One uucode table instead of two copies in the binary

By default libvaxis pulls in `uucode`, the Unicode character database, built with a fixed set of fields the library needs. If your program already depends on `uucode`, you get two builds of the same tables. Passing `.external_uucode = true` replaces the bundled one: declare `uucode` yourself with the fields you need, then wire your module into the vaxis module.

zig
    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"));

The field list becomes yours to fill, and the failure mode is loud rather than silent: the compiler reports which fields are missing, so a mismatch stops the build instead of producing an empty column. Two details are easy to skim past. Your list has to be a superset rather than an equal set, because libvaxis keeps its own requirements even when you touch only part of the library. And the same module object goes to both the executable and vaxis, so the one table the project wants you to share is compiled once and reached from both sides.

The Button widget owns press, hover and a cancelled click

The counter example keeps its state in a plain struct and hands the framework three callbacks: one that produces a widget, one that handles events, one that draws. The button is a field on that struct rather than something allocated per frame, because a stateful widget has to live between frames.

zig
const Model = struct {
    count: u32 = 0,
    button: vxfw.Button,

The widget function is a short adapter. It stores a pointer to your state as userdata and names the two type erased functions the runtime will call:

zig
pub fn widget(self: *Model) vxfw.Widget {
    return .{
        .userdata = self,
        .eventHandler = Model.typeErasedEventHandler,
        .drawFn = Model.typeErasedDrawFn,
    };
}

The interesting part is the event handler. The root widget is sent an init event first, and the sample answers it by requesting focus for the button, so keyboard input starts inside the button rather than at the root. The same handler asks for focus again on focus_in, which means a focus change loops back to the button instead of letting focus escape elsewhere. Ctrl-C is matched with the modifier attached and sets the quit flag on the context.

Hover styling, the cursor shape change on hover and click, and cancelling a press when the mouse button comes up away from the widget are described as already baked into the base `Button` widget, so none of that logic appears in the sample's own code.

Frames are drawn only after a handler sets the redraw flag

The draw callback has a long signature because it returns a surface for the runtime to composite:

zig
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {

The scheduling rule is the part worth understanding. The runtime calls this function on a regular interval, but only after some event handler has marked the redraw flag in the event context. Mouse motion that changes nothing, or a key press nobody handles, will not trigger a redraw on its own. An application that mutates state and forgets the flag gets a frozen screen rather than a busy loop, which is a failure you notice quickly.

The flag sits beside a second field, ctx.quit, which is how the counter leaves, and both are properties of the context passed into the handler rather than global calls. Getting the first one right is the one piece of redraw bookkeeping a `vxfw` application owns.

One gap here: the printed example stops partway through the draw function, inside a comment, so the surface that goes back to the runtime is never shown. The full example in the repository is where the button state reaches the screen.

The toolchain pin sits ahead of the newest tag

Vaxis uses zig `0.16.0`. That single line is the whole of the toolchain documentation, with no compatibility table, no minimum version list and no note about which Zig versions the library is exercised against. The newest tag on record is v0.5.1 from 2024-11-12, which sits behind both that Zig version and the 2026-10-03 push.

Read together, the two facts mean the printed pages describe the head of the main branch rather than the artifact behind that tag. Pin the dependency to the tag and you are pinning a build that predates the Zig version the project names, and the two will not agree. The repository root carries `build.zig.zon`, where a Zig package records its own minimum toolchain, so that file is the place to confirm the real constraint before you pick a version to depend on.

Examples cover lists, tables and text input, and no numbers

The top level of the repository is small: `build.zig`, `build.zig.zon`, include, src, examples, bench, flake.nix, flake.lock, USAGE.md, typos.toml, an .agents directory and a vaxis.gif. The examples directory is where the breadth shows, with cli, counter, fuzzy, image, list_view, scroll, split_view, table, text_input, text_view, view, vt and vaxis entries, two PNG files, and a c subdirectory whose contents the README does not show. A C directory sitting next to Zig sources says something about intended reach that the printed pages do not spell out.

Two other entries invite questions left open. A bench directory exists at the top level and no figure from it appears anywhere in the README, so there is no published number to weigh the library against. USAGE.md is listed as a separate file and the README does not reproduce it, while the API reference lives at the documentation site on rockorager.github.io rather than in the tree.

That shapes the evaluation. Judge the library on the build integration and the event model you can read, and treat rendering performance as undocumented until you measure it on your own terminal, since the README prints no timing, no frame budget and no comparison with a terminfo based stack.

Editorial conclusion

libvaxis is worth adopting when you are writing a full screen terminal program in Zig and want negotiated terminal features instead of a capability database, and the vxfw layer removes the event loop, focus and mouse plumbing you would otherwise write yourself. Skip it for a small status line, where the raw cell API is all you need, or for terminals behind a multiplexer that swallows replies, because nothing in the project describes a fallback for an unanswered query. Before you commit, open build.zig.zon for the minimum Zig version, then build the counter example and see how it behaves on your terminal.

Frequently asked questions

How do I add libvaxis to a Zig project?

Fetch it with zig fetch --save pointing at the git+https URL for rockorager/libvaxis, then declare the dependency in build.zig and add the vaxis module to your executable root module under the name vaxis. There is no separate install step and no system package to put in place.

Does libvaxis need terminfo installed on the system?

No. Libvaxis does not use terminfo, and support for vt features is detected through terminal queries sent to the terminal instead.

Which Zig version does libvaxis require?

Vaxis uses zig 0.16.0. No other toolchain versions are listed in the project pages, so build.zig.zon at the repository root is where to confirm the minimum a package declares.

What is vxfw and when should I reach for it?

vxfw is the higher level Vaxis framework, an API the project describes as Flutter-like, whose runtime handles the event loop, focus management and mouse handling. It suits full screen TUI applications, while the low level API suits programs that want to supply their own event loop and control each cell.

How do I stop libvaxis compiling a second copy of uucode?

Pass .external_uucode = true to the vaxis dependency, declare uucode yourself with the fields your application needs, and wire your module into the vaxis module. If a field libvaxis needs is missing, the compiler reports it at build time.

How do I run the bundled counter example?

Build it with the example target and the counter define: zig build example -Dexample=counter. The button changes style on hover and click, cancels a press when the mouse comes up away from it, and Ctrl-C quits through the quit flag on the event context.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. rockorager/libvaxis on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rockorager-libvaxis.svg)](https://hysenlabs.com/projects/rockorager-libvaxis)

Was this useful?