Native Zig Libraries: Vaxis TUI in Koru

· 9 min read

It Works

Today we ran the first Koru program that uses an external Zig library. A TUI app using vaxis, compiled from Koru source, that clears the screen and exits on keypress.

This is what the user code looks like:

~import "$vaxis"

~vaxis:run("Hello Vaxis")
| key k |> _
| quit |> _

That’s it. Four lines. The vaxis library handles:

  • Terminal initialization
  • Event loop setup
  • Input handling
  • Cleanup on exit

How Libraries Declare Dependencies

The magic is in ~std.build:requires. Inside the vaxis library’s index.kz:

~std.build:requires {
    const vaxis_dep = b.dependency("vaxis", .{
        .target = target,
        .optimize = optimize,
    });
    exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
}

This is raw Zig build code. When you ~import "$vaxis", the compiler:

  1. Parses the library’s Koru source
  2. Extracts all ~std.build:requires blocks
  3. Merges them into the output build.zig
  4. Compiles with full dependency resolution

The user never sees build configuration. The library author writes it once.

C Libraries Too

Since this is Zig build code, C interop works naturally:

// In a hypothetical sqlite library
~std.build:requires {
    exe.linkSystemLibrary("sqlite3");
    exe.linkLibC();
}

Or for vendored C:

~std.build:requires {
    exe.addCSourceFile(.{
        .file = b.path("sqlite3.c"),
        .flags = &.{}
    });
    exe.linkLibC();
}

Koru inherits Zig’s superpower: the entire C ecosystem as a first-class citizen.

Package Distribution via NPM

Here’s the thing: nothing stops us from hosting Koru packages on npm. A vaxis wrapper library could live at @koru/vaxis:

~std.package:requires.npm {
    "@koru/vaxis": "^1.0.0"
}

The compiler collects these declarations and generates package.json. Libraries declare their own npm dependencies:

~std.package:requires.npm {
    "@koru/graphics": "^1.0.0",
    "lodash": "^4.17.21"
}

This isn’t about JavaScript interop - it’s about distribution. NPM becomes a delivery mechanism for Koru packages, just like it is for any other ecosystem that wants global reach.

Same pattern for other package managers too: requires.cargo for Rust crates, requires.pip for Python, requires.go for Go modules. The library declares what it needs, the compiler handles installation.

Two Build Targets

We actually have two requires events:

  • ~std.build:requires - Dependencies for the OUTPUT binary (what users run)
  • ~std.compiler:requires - Dependencies for the BACKEND (compiler validation)

Both are needed for libraries like vaxis. The compiler needs vaxis to validate the generated Zig code. The output binary needs vaxis to actually run.

// For backend compilation (validates generated code)
~std.compiler:requires {
    const vaxis_dep = b.dependency("vaxis", .{ ... });
    exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
}

// For output binary (the final executable)
~std.build:requires {
    const vaxis_dep = b.dependency("vaxis", .{ ... });
    exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
}

Semantic Space Lifting

The deeper principle here is what we call semantic space lifting. The library lifts its build complexity out of user space.

User sees: ~vaxis:run("title")

Library handles:

  • TTY initialization
  • Raw mode setup
  • Event loop architecture
  • Signal handling
  • Cleanup sequences

The user operates in a higher semantic space. They think in terms of “run a TUI app” not “initialize termios structures and poll for input events.”

What This Enables

With self-describing libraries, Koru programs can tap into two native ecosystems:

Zig packages:

  • TUI frameworks (vaxis, zbox)
  • HTTP servers (zap, http.zig)
  • Native async I/O

C libraries:

  • Databases (sqlite, postgres)
  • Graphics (raylib, SDL2)
  • Cryptography (openssl, libsodium)

And distribution through any package manager - npm, cargo, pip, go modules. The library declares what it needs, the compiler handles the rest.

Cross-Compilation

Zig’s killer feature: cross-compile from anywhere to anywhere. A library can declare multi-platform builds:

~std.build:requires {
    const targets = [_]std.Target.Query{
        .{ .cpu_arch = .x86_64, .os_tag = .windows },
        .{ .cpu_arch = .x86_64, .os_tag = .macos },
        .{ .cpu_arch = .aarch64, .os_tag = .macos }, // Apple Silicon
        .{ .cpu_arch = .x86_64, .os_tag = .linux },
    };

    for (targets) |t| {
        const cross_exe = b.addExecutable(.{
            .name = b.fmt("app-{s}-{s}", .{
                @tagName(t.cpu_arch.?),
                @tagName(t.os_tag.?),
            }),
            .root_source_file = b.path("output_emitted.zig"),
            .target = b.resolveTargetQuery(t),
        });
        b.installArtifact(cross_exe);
    }
}

One zig build on your Mac → Windows, Linux, and both Mac architectures. No Docker, no CI matrix, no cross-compilation toolchains to install.

Post-Modern: No “KoruPkg”

Here’s what we’re not doing: inventing korupkg.

Koru is post-modern. We use what exists:

  • Distribution? npm, cargo, pip - pick your ecosystem
  • Native builds? Zig’s build system
  • C libraries? Zig’s C interop
  • Cross-compilation? Zig handles it

Every language wants to build its own package manager, its own build system, its own ecosystem. We don’t. The world has npm with millions of packages. It has cargo with excellent dependency resolution. It has Zig with the best cross-compilation story in existence.

Koru is a language, not an empire. We plug into what works.

~std.build:requires isn’t a package manager - it’s a bridge to every package manager. ~std.package:requires.npm doesn’t compete with npm - it uses npm.

This is the post-modern approach: composition over invention.

The Code

The vaxis example lives in examples/vaxis/ in the Koru repo. The key files:

  • index.kz - Library wrapper with ~std.build:requires
  • hello.kz - User code (4 lines)
  • build.zon - Declares vaxis as a Zig dependency

Run it yourself:

cd examples/vaxis
../../zig-out/bin/koruc hello.kz --emit-zig
./zig-out/bin/output

Screen clears. Press any key. Program exits. Native Zig TUI from Koru source.

Next Steps

We’re eyeing sqlite next. Simpler API surface, demonstrates C library linking, and opens up data persistence patterns.

The build system is ready. Now we explore what we can build with it.