fern – a TUI framework for Zig 0.16.0
I’ve been building a terminal UI framework in Zig. Just pushed it public.
It’s beta. The core works. Sharing it here because I want feedback more
than I want stars.
The model is Elm Architecture: init, update, view. Every frame is a pure
function of state. Every side effect is a value you return to the runtime.
No magic, no hidden threads you didn’t ask for – well, four threads, but
you asked for them when you called run().
Under the hood it’s six libraries:
fern_ansi– parses terminal input. std only, zero deps.fern_style– chainable style builder, inherit() semantics.fern_zone– mouse hit-testing via CSI coordinate markers.fern_anim– damped spring + projectile physics.fern_widget– spinner, progress, viewport, textinput, list, timer,
stopwatch, paginator.fern_app– the runtime. raw mode, 60fps event loop, SIGWINCH,
four worker threads, diff renderer.
Dep graph is a strict DAG. Nothing reaches up the stack:
fern_ansi → nothing
fern_anim → nothing
fern_style → fern_ansi
fern_zone → fern_ansi
fern_app → fern_ansi
fern_widget → fern_style, fern_anim, fern_zone, fern_app
Enforced, not aspirational.
The part I’m least sure about:
fern_zone does mouse hit-testing by embedding invisible coordinate markers
directly into the render output as private-use CSI escape sequences
(ESC [ N z). Terminals ignore them. fern intercepts them during the render
pass to build a spatial map – which widget owned which cell this frame.
Mouse event comes in, zone lookup, done. No coordinate math on the user’s
end.
It works. I have not seen anyone else do it this way. I don’t know if
that’s because it’s a reasonable idea no one got to yet, or because
there’s an obvious reason not to that I’m missing. If you know, tell me.
The other thing I’m not sure about:
fern_anim. Spring is a proper damped harmonic oscillator – a port of
Ryan Juckett’s 2008 implementation. Three damping regimes, precomputed
transition matrix, no lerp. The progress bar uses ang_freq=18, damping=1.0
(critically damped). Whether this is overkill for a terminal UI is a
legitimate question. It looks noticeably better than a lerp.
Three things you can run right now:
zig build example-spinner # 60fps, press q
zig build example-progress # spring-animated fill, RGB gradient
zig build example-list # paginated list, j/k/arrows/enter
All single files in examples/. Worth reading before trusting.
What’s missing:
fern_chart is stubbed – braille canvas, bar/line/heatmap are next.
Alt-screen support is not wired yet.
fern_zone and fern_app are not connected – zone tracking compiles and
works standalone but the runtime doesn’t call it yet. That’s the next
thing I’m fixing.
I’m listing these because I’d rather you know than find out.
Solo project. Issues and PRs open.
The bar for a PR: compiles on 0.16.0, tests pass, dep table stays intact.
AI / LLM usage disclosure
I used Claude to help draft and iterate this post.
No code was generated by AI. All architecture, implementation, and design
decisions are my own.