Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A delightful TUI framework for Zig, inspired by [Bubble Tea](https://github.com/

- **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
- **18 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, Notification/Toast, Confirm dialog, Modal/Popup, Tooltip, Help, Paginator, Timer, FilePicker
- **19 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, 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
Expand Down Expand Up @@ -463,6 +463,38 @@ if (tip.isVisible()) {

Presets: `Tooltip.init(text)`, `Tooltip.titled(title, text)`, `Tooltip.help(text)`, `Tooltip.shortcut(label, key)`. Supports `border_bg`, `arrow_bg`, `content_bg`, and `inherit_bg` for full background control.

### TabGroup

Multi-screen tab navigation with fully customizable keymaps, styles, and optional per-tab route callbacks:

```zig
var tabs = zz.TabGroup.init(allocator);
defer tabs.deinit();

tabs.show_numbers = true;
tabs.max_width = 60; // overflow-aware tab strip
tabs.overflow_mode = .scroll; // .none, .clip, .scroll
tabs.activate_on_focus = true; // set false for manual activation

_ = try tabs.addTab(.{ .id = "home", .title = "Home" });
_ = try tabs.addTab(.{ .id = "logs", .title = "Logs", .enabled = false });
_ = try tabs.addTab(.{ .id = "settings", .title = "Settings" });

// In update:
const result = tabs.handleKey(key_event); // Left/Right, Home/End, 1..9 by default
_ = result.change; // optional active-tab change info

// Optional: route unconsumed keys to active tab callback
const routed = tabs.handleKeyAndRoute(key_event).routed;
_ = routed;

// In view:
const strip = try tabs.view(allocator);
const with_content = try tabs.viewWithContent(allocator, "No active tab");
```

Per-tab route callback hooks: `render_fn`, `key_fn`, `on_enter_fn`, `on_leave_fn`.

### More Components

- **Help** - Display key bindings with responsive truncation
Expand Down Expand Up @@ -897,6 +929,7 @@ zig build run-file_browser
zig build run-dashboard
zig build run-showcase # Multi-tab demo of all features
zig build run-focus_form # Focus management with Tab cycling
zig build run-tabs # TabGroup multi-screen routing
```

## Building
Expand Down
2 changes: 2 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub fn build(b: *std.Build) void {
"focus_form",
"modal",
"tooltip",
"tabs",
};

for (examples) |example_name| {
Expand Down Expand Up @@ -62,6 +63,7 @@ pub fn build(b: *std.Build) void {
"tests/focus_tests.zig",
"tests/modal_tests.zig",
"tests/tooltip_tests.zig",
"tests/tab_group_tests.zig",
};

const test_step = b.step("test", "Run unit tests");
Expand Down
134 changes: 134 additions & 0 deletions examples/tabs.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! TabGroup example with multi-screen routing.

const std = @import("std");
const zz = @import("zigzag");

const ScreenA = struct {
visits: usize = 0,

fn onEnter(ctx: *anyopaque) void {
const self: *ScreenA = @ptrCast(@alignCast(ctx));
self.visits += 1;
}

fn onKey(_: *anyopaque, _: zz.KeyEvent) bool {
return false;
}

fn render(ctx: *anyopaque, allocator: std.mem.Allocator) ![]const u8 {
const self: *ScreenA = @ptrCast(@alignCast(ctx));
return std.fmt.allocPrint(
allocator,
"Home Screen\n\nVisits: {d}\n\nUse Left/Right or 1..9 to switch tabs.",
.{self.visits},
);
}
};

const ScreenB = struct {
count: i32 = 0,

fn onKey(ctx: *anyopaque, key: zz.KeyEvent) bool {
const self: *ScreenB = @ptrCast(@alignCast(ctx));
if (key.key == .char) {
switch (key.key.char) {
'+' => {
self.count += 1;
return true;
},
'-' => {
self.count -= 1;
return true;
},
else => {},
}
}
return false;
}

fn render(ctx: *anyopaque, allocator: std.mem.Allocator) ![]const u8 {
const self: *ScreenB = @ptrCast(@alignCast(ctx));
return std.fmt.allocPrint(
allocator,
"Counter Screen\n\nCount: {d}\n\nPress + / - while this tab is active.",
.{self.count},
);
}
};

const Model = struct {
tabs: zz.TabGroup,
home: ScreenA,
counter: ScreenB,

pub const Msg = union(enum) {
key: zz.KeyEvent,
};

pub fn init(self: *Model, ctx: *zz.Context) zz.Cmd(Msg) {
self.tabs = zz.TabGroup.init(ctx.persistent_allocator);
self.tabs.show_numbers = true;
self.tabs.max_width = 60;

self.home = .{};
self.counter = .{};

_ = self.tabs.addTab(.{
.id = "home",
.title = "Home",
.route = .{
.ctx = &self.home,
.render_fn = ScreenA.render,
.key_fn = ScreenA.onKey,
.on_enter_fn = ScreenA.onEnter,
},
}) catch {};

_ = self.tabs.addTab(.{
.id = "counter",
.title = "Counter",
.route = .{
.ctx = &self.counter,
.render_fn = ScreenB.render,
.key_fn = ScreenB.onKey,
},
}) catch {};

return .none;
}

pub fn deinit(self: *Model) void {
self.tabs.deinit();
}

pub fn update(self: *Model, msg: Msg, _: *zz.Context) zz.Cmd(Msg) {
switch (msg) {
.key => |k| {
if (k.key == .char and k.key.char == 'q') return .quit;
_ = self.tabs.handleKeyAndRoute(k);
},
}
return .none;
}

pub fn view(self: *const Model, ctx: *const zz.Context) []const u8 {
const body = self.tabs.viewWithContent(ctx.allocator, "No active route") catch "render error";

var hint_style = zz.Style{};
hint_style = hint_style.fg(zz.Color.gray(12));
hint_style = hint_style.inline_style(true);
const help = hint_style.render(ctx.allocator, "q: quit | ←/→: switch | 1..9: jump | +/-: counter actions") catch "";

return std.fmt.allocPrint(ctx.allocator, "{s}\n\n{s}", .{ body, help }) catch body;
}
};

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