Why I ported my TUI tools from C to Zig

Comptime, build ergonomics, and no hidden allocations. The migration that was actually worth it.

2 min read

I spent three years writing TUI tools in C. I’m not going back. Zig gives me everything C does — direct memory control, no runtime, single binary output — plus a build system that doesn’t make me want to quit programming.

what I was doing in C

My TUI toolkit was about 4k lines of C spread across a handful of tools: a hex viewer, a log tailer, and a process inspector. They shared a small rendering layer I called termbox.c — basically a manual implementation of the parts of ncurses I actually used.

The pain points were all in the build system and the allocator story. make is fine until you have cross-compilation targets, and tracking allocations manually across tools that share a library got old fast.

what Zig gives me instead

Comptime replaces most of my C preprocessor usage. Instead of #define COLS 80 and a thousand #ifdef PLATFORM_LINUX guards, I write:

const platform = @import("builtin").os.tag;
const default_cols: u16 = if (platform == .windows) 120 else 80;

The allocator pattern means I can write a function once and the caller decides whether it heap-allocates, arena-allocates, or uses a fixed buffer:

pub fn renderLine(buf: []u8, allocator: std.mem.Allocator, row: Row) ![]u8 {
    var out = std.ArrayList(u8).init(allocator);
    // ...
    return out.toOwnedSlice();
}

zig build is just a Zig program. Cross-compiling to aarch64-linux-musl is one flag.

what I gave up

Error messages. Zig’s are improving but a complex comptime failure still produces output that requires two cups of coffee and a quiet room to decode. C’s errors are worse on average but the ceiling is lower.

The migration took about two weekends. The resulting tools are marginally slower to compile and significantly easier to maintain. Good trade.