zcatui/examples/dashboard.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

253 lines
7.3 KiB
Zig

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