summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-24 08:45:38 +0200
committerPaul Buetow <paul@buetow.org>2026-02-24 08:45:38 +0200
commite5116514c33b2fce6ce2fc7904d6720c5236221f (patch)
treee5bc8c9ba37836305ec1d095573535fb8344cbe7 /internal/tui
parentb01e24374398eb3d343e9472f3262668039db56c (diff)
tui: wire eventloop stats engine into dashboard snapshots
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/tui.go62
-rw-r--r--internal/tui/tui_test.go25
2 files changed, 83 insertions, 4 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 49d2365..b54875a 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -5,7 +5,10 @@ import (
"errors"
"fmt"
"ior/internal/flags"
+ "ior/internal/statsengine"
"ior/internal/tui/pidpicker"
+ "sync"
+ "time"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
@@ -26,6 +29,30 @@ const (
// Long-lived tracing work should continue in background goroutines.
type TraceStarter func(context.Context) error
+type snapshotSource interface {
+ Snapshot() *statsengine.Snapshot
+}
+
+type dashboardTickMsg struct{}
+
+var dashboardSourceState struct {
+ mu sync.RWMutex
+ source snapshotSource
+}
+
+// SetDashboardSnapshotSource sets the snapshot source used by dashboard mode.
+func SetDashboardSnapshotSource(source snapshotSource) {
+ dashboardSourceState.mu.Lock()
+ dashboardSourceState.source = source
+ dashboardSourceState.mu.Unlock()
+}
+
+func getDashboardSnapshotSource() snapshotSource {
+ dashboardSourceState.mu.RLock()
+ defer dashboardSourceState.mu.RUnlock()
+ return dashboardSourceState.source
+}
+
// Run starts the TUI program in alternate screen mode.
func Run() error {
return RunWithTraceStarter(defaultTraceStarter)
@@ -70,7 +97,7 @@ func NewModel(initialPID int, startTrace TraceStarter) Model {
model := Model{
screen: ScreenPIDPicker,
pidPicker: pidpicker.New(),
- dashboard: newDashboardModel(),
+ dashboard: newDashboardModel(getDashboardSnapshotSource()),
keys: Keys,
spin: spin,
startTrace: startTrace,
@@ -111,11 +138,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.handlePidSelected(msg)
case TracingStartedMsg:
m.attaching = false
- return m, nil
+ return m, dashboardTickCmd()
case TracingErrorMsg:
m.attaching = false
m.lastErr = msg.Err
return m, nil
+ case dashboardTickMsg:
+ m.dashboard.refresh()
+ return m, dashboardTickCmd()
}
if m.attaching {
@@ -216,10 +246,15 @@ func (m Model) View() string {
type dashboardModel struct {
selectedPID int
+ source snapshotSource
+ latest *statsengine.Snapshot
}
-func newDashboardModel() dashboardModel {
- return dashboardModel{selectedPID: -1}
+func newDashboardModel(source snapshotSource) dashboardModel {
+ return dashboardModel{
+ selectedPID: -1,
+ source: source,
+ }
}
func (d dashboardModel) Init() tea.Cmd {
@@ -231,8 +266,27 @@ func (d dashboardModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
}
func (d dashboardModel) View() string {
+ if d.latest != nil {
+ return PanelStyle.Render(
+ fmt.Sprintf("Dashboard (%d syscalls, %.1f/s)", d.latest.TotalSyscalls, d.latest.SyscallRatePerSec),
+ )
+ }
if d.selectedPID > 0 {
return PanelStyle.Render(fmt.Sprintf("Dashboard (PID %d)", d.selectedPID))
}
return PanelStyle.Render("Dashboard (All PIDs)")
}
+
+func (d *dashboardModel) refresh() {
+ if source := getDashboardSnapshotSource(); source != nil {
+ d.source = source
+ }
+ if d.source == nil {
+ return
+ }
+ d.latest = d.source.Snapshot()
+}
+
+func dashboardTickCmd() tea.Cmd {
+ return tea.Tick(time.Second, func(time.Time) tea.Msg { return dashboardTickMsg{} })
+}
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index 3801813..40ea67b 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -3,6 +3,7 @@ package tui
import (
"context"
"errors"
+ "ior/internal/statsengine"
"strings"
"testing"
"time"
@@ -164,3 +165,27 @@ func TestQuitInvokesTraceStop(t *testing.T) {
t.Fatalf("expected stopTrace to be invoked on quit")
}
}
+
+type fakeDashboardSource struct {
+ snap *statsengine.Snapshot
+}
+
+func (f fakeDashboardSource) Snapshot() *statsengine.Snapshot {
+ return f.snap
+}
+
+func TestDashboardRefreshPicksLateBoundSource(t *testing.T) {
+ orig := getDashboardSnapshotSource()
+ defer SetDashboardSnapshotSource(orig)
+
+ SetDashboardSnapshotSource(nil)
+ d := newDashboardModel(nil)
+
+ want := &statsengine.Snapshot{TotalSyscalls: 77}
+ SetDashboardSnapshotSource(fakeDashboardSource{snap: want})
+
+ d.refresh()
+ if d.latest != want {
+ t.Fatalf("expected dashboard refresh to bind and use latest global source")
+ }
+}