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, Linux, macOS)
- 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
- Kitty Graphics Protocol for image display
- Unicode block fallback for terminals without graphics 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
Fetch the zigTUI module:
zig fetch --save git+https://github.com/adxdits/zigtui.gitThen 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 = b.dependency("zigtui", .{
.target = target,
.optimize = optimize,
});
// 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("zigtui") },
},
}),
});
b.installArtifact(exe);
}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. The library automatically selects the correct backend for your platform:
// Automatic platform detection (recommended)
var backend = try tui.backend.init(allocator);
defer backend.deinit();You can also use the NativeBackend type alias if you need the type explicitly:
var backend: tui.backend.NativeBackend = try tui.backend.init(allocator);Or select a specific backend manually if needed:
// Windows only
var backend = try tui.backend.WindowsBackend.init(allocator);
// Linux/macOS only
var backend = 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 examples:
# Build all examples
zig build examples
# Run the system monitor dashboard
zig build run-dashboard
# Run the Kitty Graphics demo
zig build run-kittyZigTUI includes support for displaying images in the terminal using the Kitty Graphics Protocol.
The Kitty Graphics Protocol allows terminals to display true images (PNG, BMP, raw pixel data) directly in the terminal. When the terminal does not support this protocol, ZigTUI automatically falls back to rendering images using Unicode block characters.
const tui = @import("zigtui");
// Initialize graphics with auto-detection
var gfx = tui.Graphics.init(allocator);
defer gfx.deinit();
// Detect terminal capabilities
const mode = gfx.detect();
// Load a BMP image
var bmp = try tui.graphics.bmp.loadFile(allocator, "image.bmp");
const image = tui.Image{
.data = bmp.data,
.width = bmp.width,
.height = bmp.height,
.format = .rgba,
.stride = 4,
};
// Display based on terminal support
if (gfx.supportsImages()) {
// Terminal supports Kitty Graphics - send escape sequence
if (try gfx.drawImage(image, .{ .x = 0, .y = 0 })) |seq| {
try backend.write(seq);
}
} else {
// Fallback - render using Unicode half-blocks
gfx.renderImageToBuffer(image, buffer, area);
}| Format | Kitty Protocol | Fallback Mode |
|---|---|---|
| BMP (24/32-bit) | Yes | Yes (built-in decoder) |
| PNG | Yes | No (requires external decoder) |
| Raw RGBA | Yes | Yes |
- Requires Windows 10 or later for Virtual Terminal support
- Uses native Windows Console API for input handling
- ANSI escape sequences are enabled automatically
- Windows Terminal does not support Kitty Graphics Protocol
- For image display on Windows, use WezTerm or the Unicode block fallback
- Works with any terminal that supports ANSI escape sequences
- Uses POSIX termios for raw mode
- Kitty Graphics supported in: Kitty, WezTerm, Konsole (partial)
- Tested on common terminals (xterm, gnome-terminal, alacritty, kitty)
- Works with terminals that support ANSI escape sequences
- Uses POSIX termios for raw mode
- Kitty Graphics supported in: Kitty, WezTerm
- Terminal.app and iTerm2 do not support Kitty Graphics (use fallback)
| Terminal | Platform | Kitty Graphics |
|---|---|---|
| Kitty | Linux, macOS | Full support |
| WezTerm | Windows, Linux, macOS | Full support |
| Konsole | Linux | Partial support |
| foot | Linux (Wayland) | Full support |
| Windows Terminal | Windows | Not supported (use fallback) |
| iTerm2 | macOS | Not supported (use fallback) |
| Terminal.app | macOS | Not supported (use fallback) |
MIT License - See LICENSE file for details.
Contributions are welcome. Please open an issue or submit a pull request.