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>
210 lines
6.5 KiB
Zig
210 lines
6.5 KiB
Zig
//! Interactive table demo for zcatui.
|
|
//!
|
|
//! Demonstrates a table with row selection and scrolling.
|
|
//! - Up/Down or j/k: Navigate rows
|
|
//! - Left/Right or h/l: Change column widths
|
|
//! - q/ESC: Quit
|
|
//!
|
|
//! Run with: zig build table-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 Table = zcatui.widgets.Table;
|
|
const TableRow = zcatui.widgets.TableRow;
|
|
const TableCell = zcatui.widgets.TableCell;
|
|
const TableState = zcatui.widgets.TableState;
|
|
|
|
const data = [_][4][]const u8{
|
|
.{ "1", "Alice", "alice@example.com", "Active" },
|
|
.{ "2", "Bob", "bob@example.com", "Active" },
|
|
.{ "3", "Charlie", "charlie@example.com", "Inactive" },
|
|
.{ "4", "Diana", "diana@example.com", "Active" },
|
|
.{ "5", "Eve", "eve@example.com", "Pending" },
|
|
.{ "6", "Frank", "frank@example.com", "Active" },
|
|
.{ "7", "Grace", "grace@example.com", "Inactive" },
|
|
.{ "8", "Henry", "henry@example.com", "Active" },
|
|
.{ "9", "Ivy", "ivy@example.com", "Pending" },
|
|
.{ "10", "Jack", "jack@example.com", "Active" },
|
|
.{ "11", "Kate", "kate@example.com", "Active" },
|
|
.{ "12", "Leo", "leo@example.com", "Inactive" },
|
|
};
|
|
|
|
const AppState = struct {
|
|
table_state: TableState,
|
|
running: bool = true,
|
|
|
|
fn init() AppState {
|
|
var state = AppState{
|
|
.table_state = TableState.init(),
|
|
};
|
|
state.table_state.selected = 0;
|
|
return state;
|
|
}
|
|
|
|
fn nextRow(self: *AppState) void {
|
|
const current = self.table_state.selected orelse 0;
|
|
if (current < data.len - 1) {
|
|
self.table_state.selected = current + 1;
|
|
}
|
|
}
|
|
|
|
fn prevRow(self: *AppState) void {
|
|
const current = self.table_state.selected orelse 0;
|
|
if (current > 0) {
|
|
self.table_state.selected = current - 1;
|
|
}
|
|
}
|
|
|
|
fn firstRow(self: *AppState) void {
|
|
self.table_state.selected = 0;
|
|
}
|
|
|
|
fn lastRow(self: *AppState) void {
|
|
self.table_state.selected = data.len - 1;
|
|
}
|
|
};
|
|
|
|
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();
|
|
|
|
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.nextRow(),
|
|
'k' => state.prevRow(),
|
|
'g' => state.firstRow(),
|
|
'G' => state.lastRow(),
|
|
else => {},
|
|
}
|
|
},
|
|
.down => state.nextRow(),
|
|
.up => state.prevRow(),
|
|
.home => state.firstRow(),
|
|
.end => state.lastRow(),
|
|
else => {},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
// Layout: table takes most space, status bar at bottom
|
|
const chunks = Layout.vertical(&.{
|
|
Constraint.min(0),
|
|
Constraint.length(3),
|
|
}).split(area);
|
|
|
|
// Render table
|
|
renderTable(state, chunks.get(0), buf);
|
|
|
|
// Render status bar
|
|
renderStatus(state, chunks.get(1), buf);
|
|
}
|
|
|
|
fn renderTable(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
// Create header
|
|
var header_cells: [4]TableCell = undefined;
|
|
const headers = [_][]const u8{ "ID", "Name", "Email", "Status" };
|
|
for (headers, 0..) |h, i| {
|
|
header_cells[i] = TableCell.fromString(h);
|
|
}
|
|
const header = TableRow.init(&header_cells)
|
|
.setStyle(Style.default.fg(Color.yellow).bold());
|
|
|
|
// Create rows
|
|
var rows: [data.len]TableRow = undefined;
|
|
for (data, 0..) |row_data, i| {
|
|
var cells: [4]TableCell = undefined;
|
|
for (row_data, 0..) |cell_data, j| {
|
|
// Color status column
|
|
const cell_style = if (j == 3) blk: {
|
|
if (std.mem.eql(u8, cell_data, "Active")) {
|
|
break :blk Style.default.fg(Color.green);
|
|
} else if (std.mem.eql(u8, cell_data, "Inactive")) {
|
|
break :blk Style.default.fg(Color.red);
|
|
} else {
|
|
break :blk Style.default.fg(Color.yellow);
|
|
}
|
|
} else Style.default;
|
|
cells[j] = TableCell.fromString(cell_data).setStyle(cell_style);
|
|
}
|
|
rows[i] = TableRow.init(&cells);
|
|
}
|
|
|
|
// Column widths
|
|
const widths = [_]Constraint{
|
|
Constraint.length(4),
|
|
Constraint.length(12),
|
|
Constraint.min(20),
|
|
Constraint.length(10),
|
|
};
|
|
|
|
const table = Table.init()
|
|
.setRows(&rows)
|
|
.setWidths(&widths)
|
|
.setHeader(header)
|
|
.setBlock(Block.init()
|
|
.title(" Users Table ")
|
|
.setBorders(Borders.all)
|
|
.style(Style.default.fg(Color.cyan)))
|
|
.rowHighlightStyle(Style.default.fg(Color.black).bg(Color.cyan))
|
|
.highlightSymbol("> ");
|
|
|
|
table.renderStateful(area, buf, &state.table_state);
|
|
}
|
|
|
|
fn renderStatus(state: *AppState, area: Rect, buf: *Buffer) void {
|
|
const status_block = Block.init()
|
|
.setBorders(Borders.all)
|
|
.style(Style.default.fg(Color.blue));
|
|
status_block.render(area, buf);
|
|
|
|
const inner = status_block.inner(area);
|
|
|
|
// Row info
|
|
var row_buf: [64]u8 = undefined;
|
|
const row_str = if (state.table_state.selected) |sel|
|
|
std.fmt.bufPrint(&row_buf, "Row {d}/{d}", .{ sel + 1, data.len }) catch "?"
|
|
else
|
|
"No selection";
|
|
_ = buf.setString(inner.left(), inner.top(), row_str, Style.default);
|
|
|
|
// Help text
|
|
const help = "j/k:Navigate | g/G:First/Last | q:Quit";
|
|
const help_x = if (inner.width > help.len) inner.right() - @as(u16, @intCast(help.len)) else inner.left();
|
|
_ = buf.setString(help_x, inner.top(), help, Style.default.fg(Color.white));
|
|
}
|