summaryrefslogtreecommitdiff
path: root/internal/tui/dashboard/model.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-25 08:43:15 +0200
committerPaul Buetow <paul@buetow.org>2026-02-25 08:43:15 +0200
commitd423225771a10ebae87d22c69fe88e5b65a3d378 (patch)
tree9f701073be1e53ff06d89eb7c55f5b58b8aba1d3 /internal/tui/dashboard/model.go
parent1a6e71ac31353167ec4c614d45e8e06de411a8f9 (diff)
Integrate Stream tab into dashboard and TUI
Diffstat (limited to 'internal/tui/dashboard/model.go')
-rw-r--r--internal/tui/dashboard/model.go105
1 files changed, 75 insertions, 30 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index c485dba..5500369 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -3,6 +3,7 @@ package dashboard
import (
"ior/internal/statsengine"
common "ior/internal/tui/common"
+ "ior/internal/tui/eventstream"
"ior/internal/tui/messages"
"strings"
"time"
@@ -12,6 +13,7 @@ import (
)
const defaultRefreshMs = 1000
+const streamRefreshMs = 200
// SnapshotSource is the dashboard data source.
type SnapshotSource interface {
@@ -19,6 +21,7 @@ type SnapshotSource interface {
}
type refreshTickMsg struct{}
+type streamTickMsg struct{}
// Model is the dashboard tab framework model.
type Model struct {
@@ -35,15 +38,16 @@ type Model struct {
syscallsOffset int
filesOffset int
processesOffset int
+ streamModel eventstream.Model
}
// NewModel creates a dashboard model with default refresh cadence.
-func NewModel(engine SnapshotSource) Model {
- return NewModelWithConfig(engine, defaultRefreshMs, common.Keys)
+func NewModel(engine SnapshotSource, streamSource *eventstream.RingBuffer) Model {
+ return NewModelWithConfig(engine, streamSource, defaultRefreshMs, common.Keys)
}
// NewModelWithConfig creates a dashboard model with explicit refresh and keys.
-func NewModelWithConfig(engine SnapshotSource, refreshMs int, keys common.KeyMap) Model {
+func NewModelWithConfig(engine SnapshotSource, streamSource *eventstream.RingBuffer, refreshMs int, keys common.KeyMap) Model {
if refreshMs <= 0 {
refreshMs = defaultRefreshMs
}
@@ -52,6 +56,7 @@ func NewModelWithConfig(engine SnapshotSource, refreshMs int, keys common.KeyMap
engine: engine,
refreshEvery: time.Duration(refreshMs) * time.Millisecond,
keys: keys,
+ streamModel: eventstream.NewModel(streamSource),
}
}
@@ -73,11 +78,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
tickCmd(m.refreshEvery),
func() tea.Msg { return messages.StatsTickMsg{Snap: snap} },
)
+ case streamTickMsg:
+ if m.activeTab != TabStream {
+ return m, nil
+ }
+ m.streamModel.Refresh()
+ return m, streamTickCmd()
case messages.StatsTickMsg:
m.latest = msg.Snap
m.syscallsOffset = clampOffset(m.syscallsOffset, m.maxSyscallsRows())
m.filesOffset = clampOffset(m.filesOffset, m.maxFilesRows())
m.processesOffset = clampOffset(m.processesOffset, m.maxProcessesRows())
+ m.streamModel.Refresh()
return m, nil
case tea.KeyMsg:
return m.handleKey(msg)
@@ -86,36 +98,56 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+ prevActiveTab := m.activeTab
+ var cmd tea.Cmd
keyStr := msg.String()
- if m.handleArrowTabKey(keyStr) {
- return m, nil
+ handled := m.handleArrowTabKey(keyStr) || m.handleScrollKey(keyStr)
+
+ if !handled {
+ switch {
+ case key.Matches(msg, m.keys.Tab):
+ m.activeTab = nextTab(m.activeTab)
+ handled = true
+ case key.Matches(msg, m.keys.ShiftTab):
+ m.activeTab = prevTab(m.activeTab)
+ handled = true
+ case key.Matches(msg, m.keys.One):
+ m.activeTab = TabOverview
+ handled = true
+ case key.Matches(msg, m.keys.Two):
+ m.activeTab = TabSyscalls
+ handled = true
+ case key.Matches(msg, m.keys.Three):
+ m.activeTab = TabFiles
+ handled = true
+ case key.Matches(msg, m.keys.Four):
+ m.activeTab = TabProcesses
+ handled = true
+ case key.Matches(msg, m.keys.Five):
+ m.activeTab = TabLatency
+ handled = true
+ case key.Matches(msg, m.keys.Six):
+ m.activeTab = TabGaps
+ handled = true
+ case key.Matches(msg, m.keys.Seven):
+ m.activeTab = TabStream
+ handled = true
+ case key.Matches(msg, m.keys.Refresh):
+ snap := m.snapshot()
+ cmd = func() tea.Msg { return messages.StatsTickMsg{Snap: snap} }
+ handled = true
+ }
}
- if m.handleScrollKey(keyStr) {
+ if !handled {
return m, nil
}
-
- 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
- case key.Matches(msg, m.keys.Refresh):
- snap := m.snapshot()
- return m, func() tea.Msg { return messages.StatsTickMsg{Snap: snap} }
+ if prevActiveTab != TabStream && m.activeTab == TabStream {
+ if cmd == nil {
+ return m, streamTickCmd()
+ }
+ return m, tea.Batch(cmd, streamTickCmd())
}
- return m, nil
+ return m, cmd
}
func (m *Model) handleArrowTabKey(keyStr string) bool {
@@ -139,6 +171,8 @@ func (m *Model) handleScrollKey(keyStr string) bool {
return scrollOffset(keyStr, &m.filesOffset, m.maxFilesRows())
case TabProcesses:
return scrollOffset(keyStr, &m.processesOffset, m.maxProcessesRows())
+ case TabStream:
+ return m.streamModel.HandleKey(keyStr)
default:
return false
}
@@ -199,7 +233,7 @@ 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, m.syscallsOffset, m.filesOffset, m.processesOffset))
+ b.WriteString(renderActiveTab(m.activeTab, m.latest, &m.streamModel, m.width, m.height, m.syscallsOffset, m.filesOffset, m.processesOffset))
b.WriteString("\n")
b.WriteString(common.HighlightStyle.Render("Press ? for help"))
b.WriteString("\n")
@@ -211,7 +245,14 @@ 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, syscallsOffset, filesOffset, processesOffset int) string {
+func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstream.Model, width, height, syscallsOffset, filesOffset, processesOffset int) string {
+ if tab == TabStream {
+ if streamModel == nil {
+ return common.PanelStyle.Render("Stream: waiting for source...")
+ }
+ return streamModel.View(width, height)
+ }
+
if snap == nil {
return common.PanelStyle.Render(tab.String() + ": waiting for stats...")
}
@@ -233,3 +274,7 @@ func renderActiveTab(tab Tab, snap *statsengine.Snapshot, width, height, syscall
return common.PanelStyle.Render("Unknown tab")
}
}
+
+func streamTickCmd() tea.Cmd {
+ return tea.Tick(streamRefreshMs*time.Millisecond, func(time.Time) tea.Msg { return streamTickMsg{} })
+}