A Terminal User Interface (TUI) library for Zig, inspired by Ratatui. Build beautiful, interactive terminal applications with a simple, composable API.
The first full-featured cross-platform TUI framework for Zig - works seamlessly on Windows and Linux.
System Monitor Dashboard Example - Real-time CPU, memory, disk usage with interactive process list and CPU history sparkline
- Cross-platform support (Windows and Linux)
- Cell-based rendering with diff algorithm for efficient updates
- Constraint-based layouts
- Composable widgets (Block, Paragraph, List, Gauge, Table)
- Keyboard and mouse event handling
- ANSI color and text styling support
- Explicit memory management (no hidden allocations)
- Zig 0.15.0 or later
- Windows 10+ or Linux with a terminal that supports ANSI escape sequences
Add ZigTUI as a submodule to your project:
git submodule add https://github.com/yourusername/zigtui.git libs/zigtuiThen in your build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Import ZigTUI module
const zigtui_module = b.addModule("zigtui", .{
.root_source_file = b.path("libs/zigtui/src/lib.zig"),
});
// Your executable
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zigtui", .module = zigtui_module },
},
}),
});
b.installArtifact(exe);
}Copy the src/ folder into your project and import directly:
const tui = @import("path/to/zigtui/src/lib.zig");Here is a minimal example that displays a box and exits when you press 'q':
const std = @import("std");
const tui = @import("zigtui");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Initialize backend (platform-specific)
var backend = if (@import("builtin").os.tag == .windows)
try tui.backend.WindowsBackend.init(allocator)
else
try tui.backend.AnsiBackend.init(allocator);
defer backend.deinit();
// Initialize terminal
var terminal = try tui.terminal.Terminal.init(allocator, backend.interface());
defer terminal.deinit();
// Hide cursor
try terminal.hideCursor();
// Main loop
var running = true;
while (running) {
// Poll for events (100ms timeout)
const event = try backend.interface().pollEvent(100);
// Handle input
switch (event) {
.key => |key| {
switch (key.code) {
.char => |c| {
if (c == 'q') running = false;
},
.esc => running = false,
else => {},
}
},
else => {},
}
// Draw UI
try terminal.draw({}, struct {
fn render(_: void, buf: *tui.render.Buffer) !void {
const area = buf.getArea();
const block = tui.widgets.Block{
.title = "Hello ZigTUI - Press 'q' to quit",
.borders = tui.widgets.Borders.all(),
.border_style = tui.style.Style{ .fg = .cyan },
};
block.render(area, buf);
}
}.render);
}
try terminal.showCursor();
}The backend handles platform-specific terminal operations. Use the appropriate backend for your platform:
// Windows
var backend = try tui.backend.WindowsBackend.init(allocator);
// Linux/macOS
var backend = try tui.backend.AnsiBackend.init(allocator);
// Platform detection
var backend = if (@import("builtin").os.tag == .windows)
try tui.backend.WindowsBackend.init(allocator)
else
try tui.backend.AnsiBackend.init(allocator);The terminal manages the screen buffer and rendering:
var terminal = try tui.terminal.Terminal.init(allocator, backend.interface());
defer terminal.deinit();
// Draw a frame
try terminal.draw(context, renderFunction);
// Cursor control
try terminal.hideCursor();
try terminal.showCursor();
try terminal.setCursor(x, y);
// Get terminal size
const size = try terminal.getSize();Poll for keyboard, mouse, and resize events:
const event = try backend.interface().pollEvent(timeout_ms);
switch (event) {
.key => |key| {
// key.code: KeyCode (.char, .enter, .esc, .up, .down, .left, .right, etc.)
// key.modifiers: KeyModifiers (.ctrl, .alt, .shift)
},
.resize => |size| {
// size.width, size.height
},
.focus_gained => {},
.focus_lost => {},
.none => {},
else => {},
}A container with optional border and title:
const block = tui.widgets.Block{
.title = "My Title",
.borders = tui.widgets.Borders.all(), // or .none(), .TOP, .BOTTOM, etc.
.style = tui.style.Style{ .fg = .white, .bg = .black },
.border_style = tui.style.Style{ .fg = .cyan },
.title_style = tui.style.Style{ .fg = .yellow },
};
block.render(area, buf);Display text with optional wrapping:
const paragraph = tui.widgets.Paragraph{
.text = "Hello, world!",
.style = tui.style.Style{ .fg = .white },
.wrap = true,
};
paragraph.render(area, buf);A scrollable list of items:
const items = [_]tui.widgets.ListItem{
.{ .content = "Item 1" },
.{ .content = "Item 2" },
.{ .content = "Item 3" },
};
const list = tui.widgets.List{
.items = &items,
.selected = 0,
.highlight_style = tui.style.Style{ .bg = .blue },
};
list.render(area, buf);A progress bar:
const gauge = tui.widgets.Gauge{
.ratio = 0.75, // 0.0 to 1.0
.label = "75%",
.gauge_style = tui.style.Style{ .fg = .green },
};
gauge.render(area, buf);Display tabular data:
const table = tui.widgets.Table{
.header = &[_]tui.widgets.Column{
.{ .title = "Name", .width = 20 },
.{ .title = "Value", .width = 10 },
},
.rows = &rows,
};
table.render(area, buf);Apply colors and text modifiers:
const style = tui.style.Style{
.fg = .red, // Foreground color
.bg = .black, // Background color
.modifier = tui.style.Modifier{
.bold = true,
.italic = true,
.underlined = true,
},
};Available colors:
- Basic:
.black,.red,.green,.yellow,.blue,.magenta,.cyan,.white,.gray - Light variants:
.light_red,.light_green,.light_yellow,.light_blue,.light_magenta,.light_cyan - RGB:
.{ .rgb = .{ .r = 255, .g = 128, .b = 0 } } - Indexed (256 colors):
.{ .indexed = 42 } - Reset to default:
.reset
Direct buffer manipulation:
// Set a character at position
buf.setChar(x, y, 'X', style);
// Set a string at position
buf.setString(x, y, "Hello", style);
// Fill an area
buf.fillArea(rect, ' ', style);
// Get a cell
if (buf.get(x, y)) |cell| {
cell.char = 'A';
cell.setStyle(style);
}Use the Rect structure for positioning:
const area = tui.render.Rect{
.x = 0,
.y = 0,
.width = 40,
.height = 10,
};
// Get inner area with margin
const inner = area.inner(1);
// Split horizontally
const split = area.splitHorizontal(20);
// split.left, split.right
// Split vertically
const vsplit = area.splitVertical(5);
// vsplit.top, vsplit.bottomBuild and run the dashboard example:
# Build examples
zig build examples
# Run the system monitor dashboard
zig build run-dashboard- Requires Windows 10 or later for Virtual Terminal support
- Uses native Windows Console API for input handling
- ANSI escape sequences are enabled automatically
- Works with any terminal that supports ANSI escape sequences
- Uses POSIX termios for raw mode
- Tested on common terminals (xterm, gnome-terminal, alacritty, kitty)
zigtui/
├── src/
│ ├── lib.zig # Main library entry point
│ ├── backend/
│ │ ├── mod.zig # Backend interface
│ │ ├── windows.zig # Windows console backend
│ │ └── ansi.zig # ANSI/POSIX backend
│ ├── terminal/
│ │ └── mod.zig # Terminal management
│ ├── render/
│ │ └── mod.zig # Buffer and rendering
│ ├── widgets/
│ │ ├── mod.zig # Widget definitions
│ │ ├── gauge.zig # Gauge widget
│ │ ├── list.zig # List widget
│ │ └── table.zig # Table widget
│ ├── style/
│ │ └── mod.zig # Colors and styles
│ ├── layout/
│ │ └── mod.zig # Layout system
│ └── events/
│ └── mod.zig # Event handling
├── examples/
│ └── dashboard.zig # System monitor dashboard
├── build.zig # Build configuration
└── README.md
MIT License - See LICENSE file for details.
Contributions are welcome. Please open an issue or submit a pull request.