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