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
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", .{});
}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");
}// 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", .{});
}// 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());
}
}