SkillAgentSearch skills...

Zigzag

A delightful TUI framework for Zig

Install / Use

/learn @meszmate/Zigzag
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

ZigZag

A delightful TUI framework for Zig, inspired by Bubble Tea and Lipgloss.

Demo

Features

  • Elm Architecture - Model-Update-View pattern for predictable state management
  • Rich Styling - Comprehensive styling system with colors, borders, padding, margin backgrounds, per-side border colors, tab width control, style ranges, full style inheritance, text transforms, whitespace formatting controls, and unset methods
  • 22 Pre-built Components - TextInput (with autocomplete/word movement), TextArea, List (fuzzy filtering), Table (interactive with row selection), Viewport, Progress (color gradients), Spinner, Tree, StyledList, Sparkline, Chart (linear, stepped, smoothed, area, scatter), BarChart, Canvas, Notification/Toast, Confirm dialog, Modal/Popup, Tooltip, Help, Paginator, Timer, FilePicker, TabGroup (multi-view routing)
  • Focus Management - FocusGroup with Tab/Shift+Tab cycling, comptime focusable protocol, FocusStyle for visual focus ring indicators
  • Keybinding Management - Structured KeyBinding/KeyMap with matching, display formatting, and Help component integration
  • Color System - ANSI 16, 256, and TrueColor with adaptive colors, color profile detection, and dark background detection
  • Command System - Quit, tick, repeating tick (every), batch, sequence, suspend/resume, runtime terminal control (mouse, cursor, alt screen, title), print above program, comprehensive image rendering
  • Image Rendering - Kitty/iTerm2/Sixel with in-memory data, file paths, image caching (transmit once, display many), z-index layering, unicode placeholders for text reflow, protocol override, and file validation
  • Custom I/O - Pipe-friendly with configurable input/output streams for testing and automation
  • Kitty Keyboard Protocol - Modern keyboard handling with key release events and unambiguous key identification
  • Bracketed Paste - Paste events delivered as a single message instead of individual keystrokes
  • Debug Logging - File-based timestamped logging since stdout is owned by the renderer
  • Message Filtering - Intercept and transform messages before they reach your model
  • ANSI Compression - Reduce output overhead with diff-based style state tracking and redundant sequence elimination
  • Layout - Horizontal/vertical joining, ANSI-aware measurement, 2D placement, float-based positioning, horizontal/vertical single-axis placement, overlay compositing
  • Cross-platform - Works on macOS, Linux, and Windows
  • Zero Dependencies - Pure Zig with no external dependencies

Installation

Add ZigZag to your build.zig.zon:

.dependencies = .{
    .zigzag = .{
        .url = "https://github.com/meszmate/zigzag/archive/refs/heads/main.tar.gz",
        .hash = "...",
    },
},
// To pin a specific version instead:
// .url = "https://github.com/meszmate/zigzag/archive/refs/tags/v0.1.0.tar.gz",

Then in your build.zig:

const zigzag = b.dependency("zigzag", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zigzag", zigzag.module("zigzag"));

Quick Start

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();
}

Core Concepts

The Elm Architecture

ZigZag uses the Elm Architecture (Model-Update-View):

  1. Model - Your application state
  2. Msg - Messages that describe state changes
  3. init - Initialize your model
  4. update - Handle messages and update state
  5. view - Render your model to a string

Commands

Commands let you perform side effects:

return .quit;                          // Quit the application
return .none;                          // Do nothing
return .{ .tick = ns };                // Request a tick after `ns` nanoseconds
return Cmd(Msg).everyMs(16);           // Repeating tick every 16ms (~60fps)
return Cmd(Msg).tickMs(1000);          // One-shot tick after 1 second
return .suspend_process;               // Suspend (like Ctrl+Z)
return .enable_mouse;                  // Enable mouse tracking at runtime
return .disable_mouse;                 // Disable mouse tracking
return .show_cursor;                   // Show terminal cursor
return .hide_cursor;                   // Hide terminal cursor
return .{ .set_title = "My App" };     // Set terminal window title
return .{ .println = "Log message" };  // Print above the program output
return .{ .image_file = .{             // Draw image via Kitty/iTerm2/Sixel when available
    .path = "assets/cat.png",
    .width_cells = 40,
    .height_cells = 20,
    .placement = .center,              // .cursor, .top_left, .top_center, .center
    .row_offset = -6,                  // Negative = higher, positive = lower
    .col_offset = 0,                   // Negative = left, positive = right
    // .row = 2, .col = 10,            // Optional absolute position override
    .move_cursor = false,              // Helpful for iTerm2 placement
    .protocol = .auto,                 // .auto, .kitty, .iterm2, .sixel
    .z_index = -1,                     // Kitty: render behind text
    .unicode_placeholder = false,       // Kitty: participate in text reflow
} };
return .{ .image_data = .{            // Draw in-memory image data
    .data = png_bytes,                 // Raw RGB, RGBA, or PNG bytes
    .format = .png,                    // .rgb, .rgba, .png
    .pixel_width = 100,                // Required for RGB/RGBA
    .pixel_height = 100,
    .width_cells = 20,
    .height_cells = 10,
    .placement = .center,
} };
return .{ .cache_image = .{           // Upload to Kitty cache (transmit once)
    .source = .{ .file = "assets/logo.png" },
    .image_id = 1,
} };
return .{ .place_cached_image = .{    // Display cached image (no re-upload)
    .image_id = 1,
    .placement = .center,
    .width_cells = 20,
    .height_cells = 10,
} };
return .{ .delete_image = .{ .by_id = 1 } };  // Free cached image
return .{ .delete_image = .all };              // Free all cached images

Styling

The 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)
    .marginBackground(zz.Color.gray(3))
    .borderAll(zz.Border.rounded)
    .borderForeground(zz.Color.magenta())
    .borderTopForeground(zz.Color.cyan())    // Per-side border colors
    .borderBottomForeground(zz.Color.green())
    .tabWidth(4)
    .width(40)
    .alignH(.center);

const output = try style.render(allocator, "Hello, World!");
// render() does not append an implicit trailing '\n'

// Text transforms
const upper_style = (zz.Style{}).transform(zz.transforms.uppercase);
const shouting = try upper_style.render(allocator, "hello"); // "HELLO"

// Inline mode is useful when embedding block-styled output in a single line
const inline = (zz.Style{}).fg(zz.Color.cyan()).inline_style(true);

// Whitespace formatting controls
const ws_style = (zz.Style{})
    .underline(true)
    .setUnderlineSpaces(true)      // Underline extends through spaces
    .setColorWhitespace(false);     // Don't apply bg color to whitespace

// Unset individual properties
const derived = style.unsetBold().unsetPadding().unsetBorder();

// Style inheritance (unset values inherit from parent)
const child = (zz.Style{}).fg(zz.Color.red()).inherit(style);

// Style ranges - apply different styles to byte ranges
const ranges = &[_]zz.StyleRange{
    .{ .start = 0, .end = 5, .s = (zz.Style{}).bold(true) },
};
const ranged = try zz.renderWithRanges(allocator, "Hello World", ranges);

// Highlight specific positions (for fuzzy match results)
const highlighted = try zz.renderWithHighlights(allocator, "hello", &.{0, 2}, highlight_style, base_style);

Colors

// 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.fromRgb(255, 128, 64)
zz.Color.hex("#FF8040")

// Adaptive colors (change based on terminal capabilities)
const adaptive = zz.AdaptiveColor{
    .true_color = zz.Color.hex("#FF8040"),
    .color_256 = zz.Color.color256(208),
    .ansi = zz.Color.red(),
};
const resolved = adaptive.resolve(ctx.true_color, ctx.color_256);

// Color profile detection (automatic via context)
// ctx.color_profile: .ascii, .ansi, .ansi256, .true_color
// ctx.is_dark_background: bool

// Color interpolation (for gradients)
const mid = zz.interpolateColor(zz.Color.red(), zz.Color.green(), 0.5);

Borders

zz.Border.normal           // ┌─┐
zz.Border.rounded          // ╭─╮
zz.Border.double           // ╔═╗
zz.Border.thick            // ┏━┓
zz.Border.ascii            
View on GitHub
GitHub Stars234
CategoryDevelopment
Updated6h ago
Forks5

Languages

Zig

Security Score

100/100

Audited on Mar 27, 2026

No findings