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>
This commit is contained in:
parent
5556ee1370
commit
b9223dec85
5 changed files with 736 additions and 13 deletions
57
build.zig
57
build.zig
|
|
@ -61,4 +61,61 @@ pub fn build(b: *std.Build) void {
|
|||
run_events_demo.step.dependOn(b.getInstallStep());
|
||||
const events_demo_step = b.step("events-demo", "Run events demo");
|
||||
events_demo_step.dependOn(&run_events_demo.step);
|
||||
|
||||
// Ejemplo: list_demo
|
||||
const list_demo_exe = b.addExecutable(.{
|
||||
.name = "list-demo",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("examples/list_demo.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "zcatui", .module = zcatui_mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
b.installArtifact(list_demo_exe);
|
||||
|
||||
const run_list_demo = b.addRunArtifact(list_demo_exe);
|
||||
run_list_demo.step.dependOn(b.getInstallStep());
|
||||
const list_demo_step = b.step("list-demo", "Run list demo");
|
||||
list_demo_step.dependOn(&run_list_demo.step);
|
||||
|
||||
// Ejemplo: table_demo
|
||||
const table_demo_exe = b.addExecutable(.{
|
||||
.name = "table-demo",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("examples/table_demo.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "zcatui", .module = zcatui_mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
b.installArtifact(table_demo_exe);
|
||||
|
||||
const run_table_demo = b.addRunArtifact(table_demo_exe);
|
||||
run_table_demo.step.dependOn(b.getInstallStep());
|
||||
const table_demo_step = b.step("table-demo", "Run table demo");
|
||||
table_demo_step.dependOn(&run_table_demo.step);
|
||||
|
||||
// Ejemplo: dashboard
|
||||
const dashboard_exe = b.addExecutable(.{
|
||||
.name = "dashboard",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("examples/dashboard.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "zcatui", .module = zcatui_mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
b.installArtifact(dashboard_exe);
|
||||
|
||||
const run_dashboard = b.addRunArtifact(dashboard_exe);
|
||||
run_dashboard.step.dependOn(b.getInstallStep());
|
||||
const dashboard_step = b.step("dashboard", "Run dashboard demo");
|
||||
dashboard_step.dependOn(&run_dashboard.step);
|
||||
}
|
||||
|
|
|
|||
253
examples/dashboard.zig
Normal file
253
examples/dashboard.zig
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
//! Dashboard demo for zcatui.
|
||||
//!
|
||||
//! Demonstrates multiple widgets working together:
|
||||
//! - Gauges showing system metrics
|
||||
//! - Sparkline for real-time data
|
||||
//! - List for logs/events
|
||||
//! - Tabs for navigation
|
||||
//!
|
||||
//! Run with: zig build dashboard
|
||||
|
||||
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 Gauge = zcatui.widgets.Gauge;
|
||||
const Sparkline = zcatui.widgets.Sparkline;
|
||||
const List = zcatui.widgets.List;
|
||||
const ListItem = zcatui.widgets.ListItem;
|
||||
const Tabs = zcatui.widgets.Tabs;
|
||||
const Paragraph = zcatui.widgets.Paragraph;
|
||||
const Line = zcatui.Line;
|
||||
|
||||
const AppState = struct {
|
||||
running: bool = true,
|
||||
tick: u64 = 0,
|
||||
tab_index: usize = 0,
|
||||
cpu_history: [64]u64 = [_]u64{0} ** 64,
|
||||
mem_history: [64]u64 = [_]u64{0} ** 64,
|
||||
history_idx: usize = 0,
|
||||
cpu: u64 = 0,
|
||||
mem: u64 = 0,
|
||||
|
||||
fn update(self: *AppState) void {
|
||||
self.tick += 1;
|
||||
|
||||
// Simulate changing metrics
|
||||
const seed = @as(u32, @truncate(self.tick));
|
||||
self.cpu = 30 + (seed * 7) % 50;
|
||||
self.mem = 40 + (seed * 13) % 40;
|
||||
|
||||
// Update history
|
||||
self.cpu_history[self.history_idx] = self.cpu;
|
||||
self.mem_history[self.history_idx] = self.mem;
|
||||
self.history_idx = (self.history_idx + 1) % 64;
|
||||
}
|
||||
|
||||
fn nextTab(self: *AppState) void {
|
||||
self.tab_index = (self.tab_index + 1) % 3;
|
||||
}
|
||||
|
||||
fn prevTab(self: *AppState) void {
|
||||
if (self.tab_index == 0) {
|
||||
self.tab_index = 2;
|
||||
} else {
|
||||
self.tab_index -= 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{};
|
||||
|
||||
while (state.running) {
|
||||
state.update();
|
||||
try term.drawWithContext(&state, render);
|
||||
|
||||
if (try term.pollEvent(50)) |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,
|
||||
'1' => state.tab_index = 0,
|
||||
'2' => state.tab_index = 1,
|
||||
'3' => state.tab_index = 2,
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
.tab => state.nextTab(),
|
||||
.backtab => state.prevTab(),
|
||||
.right => state.nextTab(),
|
||||
.left => state.prevTab(),
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
|
||||
// Main layout: tabs at top, content below
|
||||
const main_chunks = Layout.vertical(&.{
|
||||
Constraint.length(3),
|
||||
Constraint.min(0),
|
||||
}).split(area);
|
||||
|
||||
// Render tabs
|
||||
renderTabs(state, main_chunks.get(0), buf);
|
||||
|
||||
// Render content based on selected tab
|
||||
switch (state.tab_index) {
|
||||
0 => renderOverview(state, main_chunks.get(1), buf),
|
||||
1 => renderMetrics(state, main_chunks.get(1), buf),
|
||||
2 => renderHelp(main_chunks.get(1), buf),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn renderTabs(state: *AppState, area: Rect, buf: *Buffer) void {
|
||||
const titles = [_]Line{
|
||||
Line.raw("Overview"),
|
||||
Line.raw("Metrics"),
|
||||
Line.raw("Help"),
|
||||
};
|
||||
const tabs = Tabs.init(&titles)
|
||||
.setBlock(Block.init()
|
||||
.title(" Dashboard ")
|
||||
.setBorders(Borders.all)
|
||||
.style(Style.default.fg(Color.cyan)))
|
||||
.select(state.tab_index)
|
||||
.highlightStyle(Style.default.fg(Color.yellow).bold());
|
||||
tabs.render(area, buf);
|
||||
}
|
||||
|
||||
fn renderOverview(state: *AppState, area: Rect, buf: *Buffer) void {
|
||||
// Split into gauges and sparklines
|
||||
const chunks = Layout.vertical(&.{
|
||||
Constraint.length(5),
|
||||
Constraint.length(5),
|
||||
Constraint.min(0),
|
||||
}).split(area);
|
||||
|
||||
// CPU Gauge
|
||||
const cpu_gauge = Gauge.init()
|
||||
.setBlock(Block.init().title(" CPU ").setBorders(Borders.all))
|
||||
.percent(@intCast(state.cpu))
|
||||
.gaugeStyle(Style.default.fg(Color.green));
|
||||
cpu_gauge.render(chunks.get(0), buf);
|
||||
|
||||
// Memory Gauge
|
||||
const mem_gauge = Gauge.init()
|
||||
.setBlock(Block.init().title(" Memory ").setBorders(Borders.all))
|
||||
.percent(@intCast(state.mem))
|
||||
.gaugeStyle(Style.default.fg(Color.blue));
|
||||
mem_gauge.render(chunks.get(1), buf);
|
||||
|
||||
// Recent events
|
||||
renderEvents(state, chunks.get(2), buf);
|
||||
}
|
||||
|
||||
fn renderMetrics(state: *AppState, area: Rect, buf: *Buffer) void {
|
||||
// Split for two sparklines
|
||||
const chunks = Layout.vertical(&.{
|
||||
Constraint.percentage(50),
|
||||
Constraint.percentage(50),
|
||||
}).split(area);
|
||||
|
||||
// CPU Sparkline
|
||||
const cpu_spark = Sparkline.init()
|
||||
.setBlock(Block.init().title(" CPU History ").setBorders(Borders.all))
|
||||
.setData(&state.cpu_history)
|
||||
.setMax(100)
|
||||
.setStyle(Style.default.fg(Color.green));
|
||||
cpu_spark.render(chunks.get(0), buf);
|
||||
|
||||
// Memory Sparkline
|
||||
const mem_spark = Sparkline.init()
|
||||
.setBlock(Block.init().title(" Memory History ").setBorders(Borders.all))
|
||||
.setData(&state.mem_history)
|
||||
.setMax(100)
|
||||
.setStyle(Style.default.fg(Color.blue));
|
||||
mem_spark.render(chunks.get(1), buf);
|
||||
}
|
||||
|
||||
fn renderEvents(state: *AppState, area: Rect, buf: *Buffer) void {
|
||||
// Generate some fake events based on tick
|
||||
var event_strs: [5][64]u8 = undefined;
|
||||
var event_items: [5]ListItem = undefined;
|
||||
|
||||
for (0..5) |i| {
|
||||
const tick_val = state.tick -| (4 - i);
|
||||
const written = std.fmt.bufPrint(&event_strs[i], "[{d:0>4}] System event {d}", .{ tick_val, i + 1 }) catch "???";
|
||||
event_items[i] = ListItem.raw(written);
|
||||
}
|
||||
|
||||
const list = List.init(&event_items)
|
||||
.setBlock(Block.init()
|
||||
.title(" Recent Events ")
|
||||
.setBorders(Borders.all)
|
||||
.style(Style.default.fg(Color.yellow)));
|
||||
list.render(area, buf);
|
||||
}
|
||||
|
||||
fn renderHelp(area: Rect, buf: *Buffer) void {
|
||||
const help_block = Block.init()
|
||||
.title(" Help ")
|
||||
.setBorders(Borders.all)
|
||||
.style(Style.default.fg(Color.magenta));
|
||||
help_block.render(area, buf);
|
||||
|
||||
const inner = help_block.inner(area);
|
||||
var y = inner.top();
|
||||
|
||||
const lines = [_][]const u8{
|
||||
"Dashboard Demo - zcatui",
|
||||
"",
|
||||
"Navigation:",
|
||||
" Tab/Shift+Tab - Switch tabs",
|
||||
" Left/Right - Switch tabs",
|
||||
" 1/2/3 - Jump to tab",
|
||||
"",
|
||||
"General:",
|
||||
" q/ESC - Quit",
|
||||
"",
|
||||
"This demo shows multiple widgets:",
|
||||
" - Tabs for navigation",
|
||||
" - Gauges for percentages",
|
||||
" - Sparklines for time series",
|
||||
" - Lists for events/logs",
|
||||
};
|
||||
|
||||
for (lines) |line| {
|
||||
if (y < inner.bottom()) {
|
||||
_ = buf.setString(inner.left(), y, line, Style.default);
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
examples/list_demo.zig
Normal file
188
examples/list_demo.zig
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
//! 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);
|
||||
}
|
||||
210
examples/table_demo.zig
Normal file
210
examples/table_demo.zig
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
//! 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));
|
||||
}
|
||||
|
|
@ -28,6 +28,16 @@ const Constraint = layout_mod.Constraint;
|
|||
const list_mod = @import("list.zig");
|
||||
const HighlightSpacing = list_mod.HighlightSpacing;
|
||||
|
||||
// ============================================================================
|
||||
// ColumnPos
|
||||
// ============================================================================
|
||||
|
||||
/// Column position information used during rendering.
|
||||
const ColumnPos = struct {
|
||||
x: u16,
|
||||
width: u16,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Cell
|
||||
// ============================================================================
|
||||
|
|
@ -37,7 +47,7 @@ const HighlightSpacing = list_mod.HighlightSpacing;
|
|||
/// You can style the cell and its content independently.
|
||||
pub const Cell = struct {
|
||||
/// The text content of the cell.
|
||||
content: Text = Text.default,
|
||||
content: Text = Text.empty,
|
||||
/// Style applied to the cell area.
|
||||
style: Style = Style.default,
|
||||
|
||||
|
|
@ -423,7 +433,12 @@ pub const Table = struct {
|
|||
/// Returns the selection column width.
|
||||
fn selectionWidth(self: Table, state: *TableState) u16 {
|
||||
const has_selection = state.selected != null;
|
||||
if (self.highlight_spacing.shouldAdd(has_selection)) {
|
||||
const should_show = switch (self.highlight_spacing) {
|
||||
.always => true,
|
||||
.when_selected => has_selection,
|
||||
.never => false,
|
||||
};
|
||||
if (should_show) {
|
||||
return @intCast(text_mod.unicodeWidth(self.highlight_symbol));
|
||||
}
|
||||
return 0;
|
||||
|
|
@ -470,7 +485,7 @@ pub const Table = struct {
|
|||
const sel_width = self.selectionWidth(state);
|
||||
|
||||
// Calculate column widths
|
||||
var column_positions: [64]struct { x: u16, width: u16 } = undefined;
|
||||
var column_positions: [64]ColumnPos = undefined;
|
||||
const positions = self.calculateColumnPositions(table_area.width, sel_width, col_count, &column_positions);
|
||||
|
||||
// Calculate layout areas
|
||||
|
|
@ -519,8 +534,8 @@ pub const Table = struct {
|
|||
max_width: u16,
|
||||
selection_width: u16,
|
||||
col_count: usize,
|
||||
out: []struct { x: u16, width: u16 },
|
||||
) []struct { x: u16, width: u16 } {
|
||||
out: []ColumnPos,
|
||||
) []ColumnPos {
|
||||
if (col_count == 0) return out[0..0];
|
||||
|
||||
const actual_count = @min(col_count, out.len);
|
||||
|
|
@ -543,13 +558,13 @@ pub const Table = struct {
|
|||
const space_for_cols = available_width -| total_spacing;
|
||||
|
||||
for (out[0..actual_count], 0..) |*pos, i| {
|
||||
const constraint = if (i < self.widths.len) self.widths[i] else Constraint{ .min = 0 };
|
||||
const constraint = if (i < self.widths.len) self.widths[i] else Constraint.min(0);
|
||||
const width: u16 = switch (constraint) {
|
||||
.length => |l| @min(l, space_for_cols),
|
||||
.percentage => |p| @intCast((space_for_cols * p) / 100),
|
||||
.min => |m| m,
|
||||
.max => |m| @min(m, space_for_cols),
|
||||
.ratio => |r| @intCast(@as(u32, space_for_cols) * r.num / @max(1, r.den)),
|
||||
.len => |l| @min(l, space_for_cols),
|
||||
.pct => |p| @intCast((space_for_cols * p) / 100),
|
||||
.min_size => |m| m,
|
||||
.max_size => |m| @min(m, space_for_cols),
|
||||
.rat => |r| @intCast(@as(u32, space_for_cols) * r.num / @max(1, r.den)),
|
||||
};
|
||||
pos.x = x;
|
||||
pos.width = width;
|
||||
|
|
@ -565,7 +580,7 @@ pub const Table = struct {
|
|||
row: Row,
|
||||
area: Rect,
|
||||
buf: *Buffer,
|
||||
positions: []struct { x: u16, width: u16 },
|
||||
positions: []ColumnPos,
|
||||
) void {
|
||||
_ = self;
|
||||
for (positions, 0..) |pos, i| {
|
||||
|
|
@ -587,7 +602,7 @@ pub const Table = struct {
|
|||
buf: *Buffer,
|
||||
state: *TableState,
|
||||
selection_width: u16,
|
||||
positions: []struct { x: u16, width: u16 },
|
||||
positions: []ColumnPos,
|
||||
) void {
|
||||
if (self.rows.len == 0) return;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue