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>
253 lines
7.3 KiB
Zig
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;
|
|
}
|
|
}
|
|
}
|