Skip to main content
ansicode
Zig

ANSI escape codes in Zig

Zig's standard library has no colour helper, but byte literals are first-class — `"\x1b[31m"` is a `*const [5:0]u8` and writes through `std.io.getStdOut().writer().print` (or `std.debug.print` for quick prototyping) without any encoding work. Since Zig 0.11 the canonical capability gate is `std.io.tty.detectConfig(std.io.getStdOut())` — it returns a tagged union (`.no_color`, `.escape_codes`, `.windows_api`) that honours `NO_COLOR`, checks `isatty(2)`, and selects the right backend on Windows pre-VT Conhost. Wrap your write sites in `switch (config) { ... }` and emit raw escapes only on `.escape_codes`. For more than ad-hoc prints: **libvaxis** (rockorager/libvaxis) is the modern full-screen TUI library — pure Zig, uses the kitty keyboard protocol when available, ships its own rendering pipeline. **mibu** (xyaman/mibu) is the small, focused ANSI helper for non-fullscreen output (`mibu.color.print(.{ .fg = .red }, "error", .{})`). **zig-spoon** is a minimal terminal-control library if you want raw-mode + cursor positioning without a full TUI framework. All three sit on the same byte forms documented here.

Recommended libraries

  • std.io.tty

    Stdlib capability detector — `std.io.tty.detectConfig(file)` returns `.no_color` / `.escape_codes` / `.windows_api`. Honours `NO_COLOR`, checks `isatty`, picks Win32 console API on legacy Conhost. The right gate to put in front of every colour write site since Zig 0.11.

  • libvaxis

    Modern full-screen TUI library — pure Zig, no C deps. Supports kitty keyboard protocol, mouse, paste, OSC 8 hyperlinks, sixel images on capable terminals. Drives the `zit` git TUI, `vaxe` editor, and several internal-tool TUIs. Tracks Zig master closely.

  • mibu

    Small ANSI helper — colour, cursor, screen, raw-mode termios wrapped in a friendly Zig API. `mibu.color.print(.{ .fg = .red }, "error", .{})`, `mibu.cursor.goto(stdout, x, y)`. Pairs well with std.io.tty.detectConfig — mibu emits raw escapes, you gate them at the call site.

  • zig-spoon

    Minimal terminal-control library — raw-mode toggle, cursor positioning, key parsing without a full TUI framework. Use when you need raw keyboard input + a few escape writes (text editors, REPLs, in-line pickers) but not a Window / Panel hierarchy.

Idiomatic patterns

Direct byte literals through std.io.getStdOut().writer()
const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    // \x1b is the standard hex escape — Zig string literals are []const u8,
    // bytes pass through untouched. No \e shortcut, no encoding work.
    try stdout.print("\x1b[1;31merror:\x1b[0m permission denied\n", .{});
    try stdout.print("\x1b[1;32mok:\x1b[0m all 142 tests passed\n", .{});
}
Capability gate with std.io.tty.detectConfig
const std = @import("std");

pub fn main() !void {
    const stdout_file = std.io.getStdOut();
    const stdout = stdout_file.writer();
    const config = std.io.tty.detectConfig(stdout_file);

    // config is a tagged union: .no_color, .escape_codes, .windows_api.
    // setColor() routes to the right backend automatically.
    try config.setColor(stdout, .bold);
    try config.setColor(stdout, .red);
    try stdout.writeAll("error:");
    try config.setColor(stdout, .reset);
    try stdout.writeAll(" permission denied\n");
}
Coloured output with mibu
// build.zig.zon: .mibu = .{ .url = "https://github.com/xyaman/mibu/...", ... }
const std = @import("std");
const mibu = @import("mibu");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try mibu.color.print(stdout, .{ .fg = .red, .style = .bold }, "error:", .{});
    try stdout.writeAll(" permission denied\n");

    // Truecolor:
    try mibu.color.print(stdout, .{
        .fg = .{ .rgb = .{ 203, 166, 247 } },
    }, "lavender truecolor\n", .{});
}
Full-screen TUI scaffold with libvaxis
// build.zig.zon: .vaxis = .{ .url = "https://github.com/rockorager/libvaxis/...", ... }
const std = @import("std");
const vaxis = @import("vaxis");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    var vx = try vaxis.init(alloc, .{});
    defer vx.deinit(alloc, std.io.tty.tty());

    var tty = try vaxis.Tty.init();
    defer tty.deinit();

    try vx.enterAltScreen(tty.anyWriter());
    defer vx.exitAltScreen(tty.anyWriter()) catch {};

    var loop: vaxis.Loop(vaxis.Event) = .{ .tty = &tty, .vaxis = &vx };
    try loop.init();
    try loop.start();
    defer loop.stop();

    while (true) {
        const event = loop.nextEvent();
        switch (event) {
            .key_press => |k| if (k.matches('q', .{})) break,
            .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
            else => {},
        }
        const win = vx.window();
        win.clear();
        _ = try win.printSegment(.{ .text = "press q to quit", .style = .{ .fg = .{ .index = 2 } } }, .{});
        try vx.render(tty.anyWriter());
    }
}

Related sequences

Other languages