summaryrefslogtreecommitdiff
path: root/internal/tui/dashboard/model.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-24 08:32:39 +0200
committerPaul Buetow <paul@buetow.org>2026-02-24 08:32:39 +0200
commit9c400937d9e32f3ce85c668d9ca52c351f8b5d13 (patch)
tree90170eedda64c59a063c0010d1452f6b3e5d71f5 /internal/tui/dashboard/model.go
parentba7af922d289a9d0fff1c4ef33764b1852c774f6 (diff)
tui: add dashboard tab framework model
Diffstat (limited to 'internal/tui/dashboard/model.go')
-rw-r--r--internal/tui/dashboard/model.go151
1 files changed, 151 insertions, 0 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
new file mode 100644
index 0000000..3c4d9b8
--- /dev/null
+++ b/internal/tui/dashboard/model.go
@@ -0,0 +1,151 @@
+package dashboard
+
+import (
+ "fmt"
+ "ior/internal/statsengine"
+ "ior/internal/tui"
+ "ior/internal/tui/messages"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+const defaultRefreshMs = 1000
+
+// SnapshotSource is the dashboard data source.
+type SnapshotSource interface {
+ Snapshot() *statsengine.Snapshot
+}
+
+type refreshTickMsg struct{}
+
+// Model is the dashboard tab framework model.
+type Model struct {
+ activeTab Tab
+
+ engine SnapshotSource
+ latest *statsengine.Snapshot
+
+ width int
+ height int
+
+ refreshEvery time.Duration
+ keys tui.KeyMap
+}
+
+// NewModel creates a dashboard model with default refresh cadence.
+func NewModel(engine SnapshotSource) Model {
+ return NewModelWithConfig(engine, defaultRefreshMs, tui.Keys)
+}
+
+// NewModelWithConfig creates a dashboard model with explicit refresh and keys.
+func NewModelWithConfig(engine SnapshotSource, refreshMs int, keys tui.KeyMap) Model {
+ if refreshMs <= 0 {
+ refreshMs = defaultRefreshMs
+ }
+ return Model{
+ activeTab: TabOverview,
+ engine: engine,
+ refreshEvery: time.Duration(refreshMs) * time.Millisecond,
+ keys: keys,
+ }
+}
+
+// Init starts periodic refresh ticks.
+func (m Model) Init() tea.Cmd {
+ return tickCmd(m.refreshEvery)
+}
+
+// Update handles ticks, snapshots, tab changes, and resize events.
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ return m, nil
+ case refreshTickMsg:
+ snap := m.snapshot()
+ return m, tea.Batch(
+ tickCmd(m.refreshEvery),
+ func() tea.Msg { return messages.StatsTickMsg{Snap: snap} },
+ )
+ case messages.StatsTickMsg:
+ m.latest = msg.Snap
+ return m, nil
+ case tea.KeyMsg:
+ return m.handleKey(msg)
+ }
+ return m, nil
+}
+
+func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+ switch {
+ case key.Matches(msg, m.keys.Tab):
+ m.activeTab = nextTab(m.activeTab)
+ case key.Matches(msg, m.keys.ShiftTab):
+ m.activeTab = prevTab(m.activeTab)
+ case key.Matches(msg, m.keys.One):
+ m.activeTab = TabOverview
+ case key.Matches(msg, m.keys.Two):
+ m.activeTab = TabSyscalls
+ case key.Matches(msg, m.keys.Three):
+ m.activeTab = TabFiles
+ case key.Matches(msg, m.keys.Four):
+ m.activeTab = TabProcesses
+ case key.Matches(msg, m.keys.Five):
+ m.activeTab = TabLatency
+ case key.Matches(msg, m.keys.Six):
+ m.activeTab = TabGaps
+ }
+ return m, nil
+}
+
+func (m Model) snapshot() *statsengine.Snapshot {
+ if m.engine == nil {
+ return nil
+ }
+ return m.engine.Snapshot()
+}
+
+// View renders the tab bar, active tab scaffold, and help bar.
+func (m Model) View() string {
+ var b strings.Builder
+ b.WriteString(renderTabBar(m.activeTab, m.width))
+ b.WriteString("\n")
+ b.WriteString(renderActiveTab(m.activeTab, m.latest, m.width, m.height))
+ b.WriteString("\n")
+ b.WriteString(renderHelpBar(m.keys))
+ return tui.ScreenStyle.Render(b.String())
+}
+
+func tickCmd(d time.Duration) tea.Cmd {
+ return tea.Tick(d, func(time.Time) tea.Msg { return refreshTickMsg{} })
+}
+
+func renderActiveTab(tab Tab, snap *statsengine.Snapshot, width, height int) string {
+ _ = width
+ _ = height
+
+ if snap == nil {
+ return tui.PanelStyle.Render(tab.String() + ": waiting for stats...")
+ }
+
+ switch tab {
+ case TabOverview:
+ return tui.PanelStyle.Render(fmt.Sprintf("Overview: %d syscalls", snap.TotalSyscalls))
+ case TabSyscalls:
+ return tui.PanelStyle.Render(fmt.Sprintf("Syscalls: %d rows", len(snap.Syscalls())))
+ case TabFiles:
+ return tui.PanelStyle.Render(fmt.Sprintf("Files: %d rows", len(snap.Files())))
+ case TabProcesses:
+ return tui.PanelStyle.Render(fmt.Sprintf("Processes: %d rows", len(snap.Processes())))
+ case TabLatency:
+ return tui.PanelStyle.Render("Latency histogram")
+ case TabGaps:
+ return tui.PanelStyle.Render("Gap histogram")
+ default:
+ return tui.PanelStyle.Render("Unknown tab")
+ }
+}