summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-05 19:42:22 +0200
committerPaul Buetow <paul@buetow.org>2026-03-05 19:42:22 +0200
commit1318a3a927ea3bae77a7560e149070583f215982 (patch)
treefde86b0250643da119ca75e051028e3e92466d32 /internal/tui
parent47c53c0d9f06451972fa32d6d74ebe572757c639 (diff)
feat(tui): pause dashboard refresh on terminal blur
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/dashboard/model.go16
-rw-r--r--internal/tui/tui.go14
-rw-r--r--internal/tui/tui_test.go34
3 files changed, 64 insertions, 0 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index a4cd4a5..f097da7 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -48,6 +48,7 @@ type Model struct {
streamModel eventstream.Model
showHelp bool
isDark bool
+ focused bool
}
// NewModel creates a dashboard model with default refresh cadence.
@@ -68,6 +69,7 @@ func NewModelWithConfig(engine SnapshotSource, streamSource *eventstream.RingBuf
pidFilter: -1,
streamModel: eventstream.NewModel(streamSource),
isDark: true,
+ focused: true,
}
m.SetDarkMode(true)
return m
@@ -88,6 +90,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.streamModel.SetViewport(streamWidth, streamHeight)
return m, nil
case refreshTickMsg:
+ if !m.focused {
+ return m, nil
+ }
snap := m.snapshot()
return m, tea.Batch(
tickCmd(m.refreshEvery),
@@ -292,6 +297,17 @@ func (m *Model) SetDarkMode(isDark bool) {
m.streamModel.SetDarkMode(isDark)
}
+// SetFocused controls whether periodic refresh ticks are processed.
+func (m *Model) SetFocused(focused bool) {
+ m.focused = focused
+}
+
+// SnapshotCmd returns a command that fetches and emits a fresh dashboard snapshot.
+func (m Model) SnapshotCmd() tea.Cmd {
+ snap := m.snapshot()
+ return func() tea.Msg { return messages.StatsTickMsg{Snap: snap} }
+}
+
// SetPidFilter updates the active PID filter used by tab render hints.
func (m *Model) SetPidFilter(pid int) {
m.pidFilter = pid
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index a12554a..f1de224 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -174,6 +174,7 @@ type Model struct {
pidFilter int
exportEnabled bool
isDark bool
+ focused bool
}
// NewModel creates the top-level TUI model.
@@ -218,6 +219,7 @@ func newModelWithRuntimeConfig(initialPID, startupPidFilter int, exportEnabled b
pidFilter: pidFilter,
exportEnabled: exportEnabled,
isDark: true,
+ focused: true,
}
if initialPID > 0 {
@@ -255,6 +257,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.BackgroundColorMsg:
m.applyTheme(msg.IsDark())
return m, nil
+ case tea.FocusMsg:
+ m.focused = true
+ m.dashboard.SetFocused(true)
+ if m.screen == ScreenDashboard && !m.attaching {
+ return m, tea.Batch(m.dashboard.Init(), m.dashboard.SnapshotCmd())
+ }
+ return m, nil
+ case tea.BlurMsg:
+ m.focused = false
+ m.dashboard.SetFocused(false)
+ return m, nil
case tea.KeyPressMsg:
if key.Matches(msg, m.keys.Quit) {
m.quitting = true
@@ -682,5 +695,6 @@ func placeToViewport(width, height int, content string) string {
func altScreenView(content string) tea.View {
view := tea.NewView(content)
view.AltScreen = true
+ view.ReportFocus = true
return view
}
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index 5c1ea5f..e705f41 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -5,6 +5,7 @@ import (
"errors"
"ior/internal/probemanager"
"ior/internal/statsengine"
+ dashboardui "ior/internal/tui/dashboard"
"ior/internal/tui/eventstream"
tuiexport "ior/internal/tui/export"
"ior/internal/tui/messages"
@@ -606,3 +607,36 @@ func TestProbeModalViewDoesNotStackDashboardContent(t *testing.T) {
t.Fatalf("expected probe modal to render as standalone view, got stacked dashboard content")
}
}
+
+func TestBlurPausesDashboardRefreshAndFocusResumesIt(t *testing.T) {
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+ m.dashboard = dashboardui.NewModelWithConfig(nil, nil, 1, m.keys)
+ m.focused = true
+
+ next, _ := m.Update(tea.BlurMsg{})
+ m = next.(Model)
+ if m.focused {
+ t.Fatalf("expected focused=false after blur")
+ }
+
+ tickMsg := m.dashboard.Init()()
+ next, tickCmd := m.Update(tickMsg)
+ m = next.(Model)
+ if tickCmd != nil {
+ t.Fatalf("expected no follow-up tick command while blurred")
+ }
+
+ next, focusCmd := m.Update(tea.FocusMsg{})
+ m = next.(Model)
+ if !m.focused {
+ t.Fatalf("expected focused=true after focus")
+ }
+ if focusCmd == nil {
+ t.Fatalf("expected focus to resume refresh with a command batch")
+ }
+ if _, ok := focusCmd().(tea.BatchMsg); !ok {
+ t.Fatalf("expected focus command to be a batch")
+ }
+}