zcatui/examples/table_demo.zig
reugenio b9223dec85 Add 3 new interactive demo examples
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>
2025-12-08 13:22:42 +01:00

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