A delightful TUI framework for Zig, inspired by Bubble Tea and Lipgloss.
- Elm Architecture - Model-Update-View pattern for predictable state management
- Rich Styling - Comprehensive styling system with colors, borders, padding, and alignment
- Pre-built Components - TextInput, TextArea, List, Table, Viewport, Progress, Spinner, and more
- Cross-platform - Works on macOS, Linux, and Windows
- Zero Dependencies - Pure Zig with no external dependencies
Add ZigZag to your build.zig.zon:
.dependencies = .{
.zigzag = .{
.url = "https://github.com/meszmate/zigzag/archive/refs/tags/v0.1.0.tar.gz",
.hash = "...",
},
},Then in your build.zig:
const zigzag = b.dependency("zigzag", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zigzag", zigzag.module("zigzag"));const std = @import("std");
const zz = @import("zigzag");
const Model = struct {
count: i32,
pub const Msg = union(enum) {
key: zz.KeyEvent,
};
pub fn init(self: *Model, _: *zz.Context) zz.Cmd(Msg) {
self.* = .{ .count = 0 };
return .none;
}
pub fn update(self: *Model, msg: Msg, _: *zz.Context) zz.Cmd(Msg) {
switch (msg) {
.key => |k| switch (k.key) {
.char => |c| if (c == 'q') return .quit,
.up => self.count += 1,
.down => self.count -= 1,
else => {},
},
}
return .none;
}
pub fn view(self: *const Model, ctx: *const zz.Context) []const u8 {
const style = zz.Style{}.bold(true).fg(zz.Color.cyan());
const text = std.fmt.allocPrint(ctx.allocator, "Count: {d}\n\nPress q to quit", .{self.count}) catch "Error";
return style.render(ctx.allocator, text) catch text;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var program = try zz.Program(Model).init(gpa.allocator());
defer program.deinit();
try program.run();
}ZigZag uses the Elm Architecture (Model-Update-View):
- Model - Your application state
- Msg - Messages that describe state changes
- init - Initialize your model
- update - Handle messages and update state
- view - Render your model to a string
Commands let you perform side effects:
return .quit; // Quit the application
return .none; // Do nothing
return .{ .tick = ns }; // Request a tick after `ns` nanosecondsThe styling system is inspired by Lipgloss:
const style = zz.Style{}
.bold(true)
.italic(true)
.fg(zz.Color.cyan())
.bg(zz.Color.black())
.paddingAll(1)
.marginAll(2)
.borderAll(zz.Border.rounded)
.borderForeground(zz.Color.magenta())
.width(40)
.align(.center);
const output = try style.render(allocator, "Hello, World!");// Basic ANSI colors
zz.Color.red()
zz.Color.cyan()
zz.Color.brightGreen()
// 256-color palette
zz.Color.color256(123)
zz.Color.gray(15) // 0-23 grayscale
// True color (24-bit)
zz.Color.rgb(255, 128, 64)
zz.Color.hex("#FF8040")zz.Border.normal // ┌─┐
zz.Border.rounded // ╭─╮
zz.Border.double // ╔═╗
zz.Border.thick // ┏━┓
zz.Border.ascii // +-+Single-line text input with cursor and validation:
var input = zz.TextInput.init(allocator);
input.setPlaceholder("Enter name...");
input.setPrompt("> ");
input.handleKey(key_event);
const view = try input.view(allocator);Multi-line text editor:
var editor = zz.components.TextArea.init(allocator);
editor.setSize(80, 24);
editor.line_numbers = true;
editor.handleKey(key_event);Selectable list with optional filtering:
var list = zz.List(MyItem).init(allocator);
list.multi_select = true;
try list.addItem(.{ .value = item, .title = "Item 1" });
list.handleKey(key_event);Scrollable content area:
var viewport = zz.Viewport.init(allocator, 80, 24);
try viewport.setContent(long_text);
viewport.handleKey(key_event); // Supports j/k, Page Up/Down, etc.Progress bar:
var progress = zz.Progress.init();
progress.setWidth(40);
progress.setPercent(75);
const bar = try progress.view(allocator);Animated loading indicator:
var spinner = zz.Spinner.init();
spinner.update(elapsed_ns);
const view = try spinner.viewWithTitle(allocator, "Loading...");Tabular data display:
var table = zz.Table(3).init(allocator);
table.setHeaders(.{ "Name", "Age", "City" });
try table.addRow(.{ "Alice", "30", "NYC" });
try table.addRow(.{ "Bob", "25", "LA" });- Help - Display key bindings
- Paginator - Pagination controls
- Timer - Countdown/stopwatch
- FilePicker - File system navigation
Combine multiple strings:
// Horizontal (side by side)
const row = try zz.joinHorizontal(allocator, &.{ left, middle, right });
// Vertical (stacked)
const col = try zz.joinVertical(allocator, &.{ top, middle, bottom });Get text dimensions (ANSI-aware):
const w = zz.width("Hello"); // 5
const h = zz.height("Line 1\nLine 2"); // 2Position content in a bounding box:
const centered = try zz.place.place(allocator, 80, 24, .center, .middle, content);Run the examples:
zig build run-hello_world
zig build run-counter
zig build run-todo_list
zig build run-text_editor
zig build run-file_browser
zig build run-dashboard# Build the library
zig build
# Run tests
zig build test
# Build with optimizations
zig build -Doptimize=ReleaseFastzig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-macos
zig build -Dtarget=x86_64-windowsMIT License - see LICENSE file for details.
- Bubble Tea - The original Go TUI framework
- Lipgloss - Style definitions for terminal applications
- The Elm Architecture - The pattern that inspired it all