New examples: - list_demo.zig: Interactive list with j/k navigation - table_demo.zig: Table with row selection and status colors - dashboard.zig: Multi-widget demo (Tabs, Gauges, Sparklines, List) Also includes: - build.zig: Added build targets for all new examples - table.zig: Fixed Cell.content default, HighlightSpacing check, ColumnPos type Run with: zig build list-demo / table-demo / dashboard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
188 lines
5.3 KiB
Zig
188 lines
5.3 KiB
Zig
//! Interactive list demo for zcatui.
|
|
//!
|
|
//! Demonstrates a navigable list with keyboard controls.
|
|
//! - Up/Down or j/k: Navigate items
|
|
//! - Enter: Select item
|
|
//! - q/ESC: Quit
|
|
//!
|
|
//! Run with: zig build list-demo
|
|
|
|
const std = @import("std");
|
|
const zcatui = @import("zcatui");
|
|
|
|
const Terminal = zcatui.Terminal;
|
|
const Buffer = zcatui.Buffer;
|
|
const Rect = zcatui.Rect;
|
|
const Style = zcatui.Style;
|
|
const Color = zcatui.Color;
|
|
const Event = zcatui.Event;
|
|
const KeyCode = zcatui.KeyCode;
|
|
const Layout = zcatui.Layout;
|
|
const Constraint = zcatui.Constraint;
|
|
const Block = zcatui.widgets.Block;
|
|
const Borders = zcatui.widgets.Borders;
|
|
const List = zcatui.widgets.List;
|
|
const ListItem = zcatui.widgets.ListItem;
|
|
const ListState = zcatui.widgets.ListState;
|
|
|
|
const items = [_][]const u8{
|
|
"Item 1 - First item in the list",
|
|
"Item 2 - Second item",
|
|
"Item 3 - Third item",
|
|
"Item 4 - Fourth item",
|
|
"Item 5 - Fifth item",
|
|
"Item 6 - Sixth item",
|
|
"Item 7 - Seventh item",
|
|
"Item 8 - Eighth item",
|
|
"Item 9 - Ninth item",
|
|
"Item 10 - Tenth item",
|
|
};
|
|
|
|
const AppState = struct {
|
|
list_state: ListState,
|
|
selected_item: ?usize = null,
|
|
running: bool = true,
|
|
|
|
fn init() AppState {
|
|
return .{
|
|
.list_state = ListState.default,
|
|
};
|
|
}
|
|
|
|
fn nextItem(self: *AppState) void {
|
|
const current = self.list_state.selected orelse 0;
|
|
if (current < items.len - 1) {
|
|
self.list_state.selected = current + 1;
|
|
}
|
|
}
|
|
|
|
fn prevItem(self: *AppState) void {
|
|
const current = self.list_state.selected orelse 0;
|
|
if (current > 0) {
|
|
self.list_state.selected = current - 1;
|
|
}
|
|
}
|
|
|
|
fn selectCurrent(self: *AppState) void {
|
|
self.selected_item = self.list_state.selected;
|
|
}
|
|
};
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
var term = try Terminal.init(allocator);
|
|
defer term.deinit();
|
|
|
|
var state = AppState.init();
|
|
state.list_state.selected = 0; // Start with first item selected
|
|
|
|
while (state.running) {
|
|
try term.drawWithContext(&state, render);
|
|
|
|
if (try term.pollEvent(100)) |event| {
|
|
handleEvent(&state, event);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handleEvent(state: *AppState, event: Event) void {
|
|
switch (event) {
|
|
.key => |key| {
|
|
switch (key.code) {
|
|
.esc => state.running = false,
|
|
.char => |c| {
|
|
switch (c) {
|
|
'q', 'Q' => state.running = false,
|
|
'j' => state.nextItem(),
|
|
'k' => state.prevItem(),
|
|
else => {},
|
|
}
|
|
},
|
|
.down => state.nextItem(),
|
|
.up => state.prevItem(),
|
|
.enter => state.selectCurrent(),
|
|
else => {},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
// Layout: list on left, info on right
|
|
const chunks = Layout.horizontal(&.{
|
|
Constraint.percentage(60),
|
|
Constraint.percentage(40),
|
|
}).split(area);
|
|
|
|
// Render list
|
|
renderList(state, chunks.get(0), buf);
|
|
|
|
// Render info panel
|
|
renderInfo(state, chunks.get(1), buf);
|
|
}
|
|
|
|
fn renderList(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
// Create list items
|
|
var list_items: [items.len]ListItem = undefined;
|
|
for (items, 0..) |item, i| {
|
|
list_items[i] = ListItem.raw(item);
|
|
}
|
|
|
|
const list = List.init(&list_items)
|
|
.setBlock(Block.init()
|
|
.title(" Items (j/k or arrows) ")
|
|
.setBorders(Borders.all)
|
|
.style(Style.default.fg(Color.cyan)))
|
|
.highlightStyle(Style.default.fg(Color.black).bg(Color.cyan))
|
|
.highlightSymbol("> ");
|
|
|
|
list.renderStateful(area, buf, &state.list_state);
|
|
}
|
|
|
|
fn renderInfo(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
const info_block = Block.init()
|
|
.title(" Info ")
|
|
.setBorders(Borders.all)
|
|
.style(Style.default.fg(Color.yellow));
|
|
info_block.render(area, buf);
|
|
|
|
const inner = info_block.inner(area);
|
|
var y = inner.top();
|
|
|
|
// Selected index
|
|
var idx_buf: [32]u8 = undefined;
|
|
const idx_str = if (state.list_state.selected) |sel|
|
|
std.fmt.bufPrint(&idx_buf, "Index: {d}", .{sel}) catch "?"
|
|
else
|
|
"Index: none";
|
|
_ = buf.setString(inner.left(), y, idx_str, Style.default);
|
|
y += 2;
|
|
|
|
// Last selected item
|
|
_ = buf.setString(inner.left(), y, "Last selected:", Style.default.bold());
|
|
y += 1;
|
|
|
|
if (state.selected_item) |sel| {
|
|
if (sel < items.len) {
|
|
_ = buf.setString(inner.left(), y, items[sel], Style.default.fg(Color.green));
|
|
}
|
|
} else {
|
|
_ = buf.setString(inner.left(), y, "(press Enter)", Style.default.fg(Color.white));
|
|
}
|
|
y += 3;
|
|
|
|
// Help
|
|
_ = buf.setString(inner.left(), y, "Controls:", Style.default.bold());
|
|
y += 1;
|
|
_ = buf.setString(inner.left(), y, "Up/k - Previous", Style.default);
|
|
y += 1;
|
|
_ = buf.setString(inner.left(), y, "Down/j - Next", Style.default);
|
|
y += 1;
|
|
_ = buf.setString(inner.left(), y, "Enter - Select", Style.default);
|
|
y += 1;
|
|
_ = buf.setString(inner.left(), y, "q/ESC - Quit", Style.default);
|
|
}
|