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