summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-01 23:45:37 +0200
committerPaul Buetow <paul@buetow.org>2026-03-01 23:45:37 +0200
commit5775246cb9c2ccfb3469addf6f5fe9a8fc198171 (patch)
tree96290d1bede538c9fd352bc3954bac1ce8ab6873 /internal/tui
parent3b4be9171b7ca13d4ff3e51d14c4e569b1a308f7 (diff)
Thread runtime config instead of global flags reads
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/dashboard/model.go12
-rw-r--r--internal/tui/dashboard/model_test.go2
-rw-r--r--internal/tui/dashboard/processes.go7
-rw-r--r--internal/tui/dashboard/processes_test.go8
-rw-r--r--internal/tui/tui.go54
-rw-r--r--internal/tui/tui_test.go2
6 files changed, 54 insertions, 31 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index c9c96c3..fc9caf6 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -39,6 +39,7 @@ type Model struct {
refreshEvery time.Duration
keys common.KeyMap
+ pidFilter int
syscallsOffset int
filesOffset int
filesDirGrouped bool
@@ -63,6 +64,7 @@ func NewModelWithConfig(engine SnapshotSource, streamSource *eventstream.RingBuf
engine: engine,
refreshEvery: time.Duration(refreshMs) * time.Millisecond,
keys: keys,
+ pidFilter: -1,
streamModel: eventstream.NewModel(streamSource),
}
}
@@ -280,6 +282,11 @@ func (m *Model) SetStreamSource(source *eventstream.RingBuffer) {
m.streamModel.SetSource(source)
}
+// SetPidFilter updates the active PID filter used by tab render hints.
+func (m *Model) SetPidFilter(pid int) {
+ m.pidFilter = pid
+}
+
// View renders the tab bar, active tab scaffold, and help bar.
func (m Model) View() string {
width, height := common.EffectiveViewport(m.width, m.height)
@@ -299,6 +306,7 @@ func (m Model) View() string {
&streamModel,
width,
activeHeight,
+ m.pidFilter,
m.syscallsOffset,
m.filesOffset,
m.filesDirGrouped,
@@ -318,7 +326,7 @@ 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, streamModel *eventstream.Model, width, height, syscallsOffset, filesOffset int, filesDirGrouped bool, filesDirOffset, processesOffset int) string {
+func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstream.Model, width, height, pidFilter, syscallsOffset, filesOffset int, filesDirGrouped bool, filesDirOffset, processesOffset int) string {
if tab == TabStream {
if streamModel == nil {
return common.PanelStyle.Render("Stream: waiting for source...")
@@ -341,7 +349,7 @@ func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstre
}
return renderFilesWithOffset(snap, width, height, filesOffset)
case TabProcesses:
- return renderProcessesWithOffset(snap, width, height, processesOffset)
+ return renderProcessesWithOffset(snap, width, height, processesOffset, pidFilter)
case TabLatency:
return renderLatencyGapsTab(snap, width, height)
default:
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index 0b269b1..87b60e3 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -386,7 +386,7 @@ func TestRenderActiveTabUsesDirectoryFilesViewWhenGrouped(t *testing.T) {
statsengine.HistogramSnapshot{},
statsengine.HistogramSnapshot{},
)
- out := renderActiveTab(TabFiles, &snap, nil, 120, 30, 0, 0, true, 0, 0)
+ out := renderActiveTab(TabFiles, &snap, nil, 120, 30, -1, 0, 0, true, 0, 0)
if !strings.Contains(out, "Directory") {
t.Fatalf("expected grouped directory files view header, got %q", out)
}
diff --git a/internal/tui/dashboard/processes.go b/internal/tui/dashboard/processes.go
index 03a38f1..281a86a 100644
--- a/internal/tui/dashboard/processes.go
+++ b/internal/tui/dashboard/processes.go
@@ -2,7 +2,6 @@ package dashboard
import (
"fmt"
- "ior/internal/flags"
"ior/internal/statsengine"
"strconv"
"strings"
@@ -11,10 +10,10 @@ import (
)
func renderProcesses(snap *statsengine.Snapshot, width, height int) string {
- return renderProcessesWithOffset(snap, width, height, 0)
+ return renderProcessesWithOffset(snap, width, height, 0, -1)
}
-func renderProcessesWithOffset(snap *statsengine.Snapshot, width, height, offset int) string {
+func renderProcessesWithOffset(snap *statsengine.Snapshot, width, height, offset, pidFilter int) string {
if snap == nil {
return "Processes: waiting for stats..."
}
@@ -44,7 +43,7 @@ func renderProcessesWithOffset(snap *statsengine.Snapshot, width, height, offset
tbl.SetCursor(cursor)
out := tbl.View() + fmt.Sprintf("\nRow %d/%d", cursor+1, len(rows))
- if flags.Get().PidFilter > 0 {
+ if pidFilter > 0 {
out += "\n" + "Note: this tab is most useful with All PIDs."
}
return out
diff --git a/internal/tui/dashboard/processes_test.go b/internal/tui/dashboard/processes_test.go
index 4db490d..24c1c1b 100644
--- a/internal/tui/dashboard/processes_test.go
+++ b/internal/tui/dashboard/processes_test.go
@@ -4,13 +4,10 @@ import (
"strings"
"testing"
- "ior/internal/flags"
"ior/internal/statsengine"
)
func TestRenderProcessesIncludesHeaders(t *testing.T) {
- flags.SetPidFilter(-1)
-
snap := statsengine.NewSnapshot(
nil, nil, nil,
nil, nil,
@@ -34,9 +31,6 @@ func TestRenderProcessesIncludesHeaders(t *testing.T) {
}
func TestRenderProcessesShowsSinglePIDNote(t *testing.T) {
- flags.SetPidFilter(77)
- t.Cleanup(func() { flags.SetPidFilter(-1) })
-
snap := statsengine.NewSnapshot(
nil, nil, nil,
nil, nil,
@@ -47,7 +41,7 @@ func TestRenderProcessesShowsSinglePIDNote(t *testing.T) {
statsengine.HistogramSnapshot{},
)
- out := renderProcesses(&snap, 100, 20)
+ out := renderProcessesWithOffset(&snap, 100, 20, 0, 77)
if !strings.Contains(out, "most useful with All PIDs") {
t.Fatalf("expected single-pid guidance note")
}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index d585a0b..10e7988 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -127,7 +127,8 @@ func Run() error {
// RunWithTraceStarter starts the TUI program with a custom trace starter.
func RunWithTraceStarter(starter TraceStarter) error {
- model := NewModel(flags.Get().PidFilter, starter)
+ cfg := flags.Get()
+ model := newModelWithRuntimeConfig(cfg.PidFilter, cfg.PidFilter, cfg.TUIExportEnable, starter)
program := tea.NewProgram(model, tea.WithAltScreen())
_, err := program.Run()
return err
@@ -153,29 +154,46 @@ type Model struct {
startTrace TraceStarter
traceStop context.CancelFunc
+
+ pidFilter int
+ exportEnabled bool
}
// NewModel creates the top-level TUI model.
func NewModel(initialPID int, startTrace TraceStarter) Model {
+ cfg := flags.Get()
+ return newModelWithRuntimeConfig(initialPID, cfg.PidFilter, cfg.TUIExportEnable, startTrace)
+}
+
+func newModelWithRuntimeConfig(initialPID, startupPidFilter int, exportEnabled bool, startTrace TraceStarter) Model {
spin := spinner.New()
spin.Spinner = spinner.MiniDot
if startTrace == nil {
startTrace = defaultTraceStarter
}
keys := Keys
- if !flags.Get().TUIExportEnable {
+ if !exportEnabled {
keys.Export = key.NewBinding()
}
+ dashboard := dashboardui.NewModelWithConfig(lateBoundDashboardSource{}, getEventStreamSource(), 1000, keys)
+ pidFilter := selectedPIDFilter(startupPidFilter)
+ if initialPID > 0 {
+ pidFilter = selectedPIDFilter(initialPID)
+ }
+ dashboard.SetPidFilter(pidFilter)
+
model := Model{
- screen: ScreenPIDPicker,
- pidPicker: pidpicker.New(),
- dashboard: dashboardui.NewModelWithConfig(lateBoundDashboardSource{}, getEventStreamSource(), 1000, keys),
- exporter: tuiexport.NewModel(),
- probeModal: probes.NewModel(getProbeManager()),
- keys: keys,
- spin: spin,
- startTrace: startTrace,
+ screen: ScreenPIDPicker,
+ pidPicker: pidpicker.New(),
+ dashboard: dashboard,
+ exporter: tuiexport.NewModel(),
+ probeModal: probes.NewModel(getProbeManager()),
+ keys: keys,
+ spin: spin,
+ startTrace: startTrace,
+ pidFilter: pidFilter,
+ exportEnabled: exportEnabled,
}
if initialPID > 0 {
@@ -216,7 +234,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.stopTrace()
return m, tea.Quit
}
- if flags.Get().TUIExportEnable && m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() {
+ if m.exportEnabled && m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() {
m.exporter = m.exporter.Open()
return m, nil
}
@@ -231,7 +249,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.reselectTID()
}
case tuiexport.RequestMsg:
- return m, runExportCmd(msg.Option, m.dashboard.LatestSnapshot())
+ return m, runExportCmd(m.exportEnabled, msg.Option, m.dashboard.LatestSnapshot())
case tuiexport.CompletedMsg:
var cmd tea.Cmd
m.exporter, cmd = m.exporter.Update(msg)
@@ -316,6 +334,8 @@ func (m Model) handlePidSelected(msg PidSelectedMsg) (tea.Model, tea.Cmd) {
m.stopTrace()
flags.SetPidFilter(pid)
flags.SetTidFilter(-1)
+ m.pidFilter = pid
+ m.dashboard.SetPidFilter(pid)
m.screen = ScreenDashboard
m.attaching = true
m.lastErr = nil
@@ -324,13 +344,15 @@ func (m Model) handlePidSelected(msg PidSelectedMsg) (tea.Model, tea.Cmd) {
func (m Model) handleTidSelected(msg TidSelectedMsg) (tea.Model, tea.Cmd) {
tid := selectedPIDFilter(msg.Tid)
- pid := flags.Get().PidFilter
+ pid := m.pidFilter
if msg.Pid > 0 {
pid = msg.Pid
}
m.stopTrace()
flags.SetPidFilter(pid)
flags.SetTidFilter(tid)
+ m.pidFilter = pid
+ m.dashboard.SetPidFilter(pid)
m.screen = ScreenDashboard
m.attaching = true
m.lastErr = nil
@@ -357,7 +379,7 @@ func (m Model) reselectPID() (tea.Model, tea.Cmd) {
}
func (m Model) reselectTID() (tea.Model, tea.Cmd) {
- pid := flags.Get().PidFilter
+ pid := m.pidFilter
m.stopTrace()
m.screen = ScreenPIDPicker
@@ -451,9 +473,9 @@ func (m Model) View() string {
}
}
-func runExportCmd(option tuiexport.Option, snap *statsengine.Snapshot) tea.Cmd {
+func runExportCmd(exportEnabled bool, option tuiexport.Option, snap *statsengine.Snapshot) tea.Cmd {
return func() tea.Msg {
- if !flags.Get().TUIExportEnable {
+ if !exportEnabled {
return tuiexport.FailedMsg{Err: errors.New("tui export is disabled by -tuiExport=false")}
}
switch option {
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index b0e1861..ed361a6 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -467,7 +467,7 @@ func TestRunExportCmdCSVWritesFile(t *testing.T) {
t.Cleanup(func() { _ = os.Chdir(prev) })
snap := &statsengine.Snapshot{TotalSyscalls: 1}
- msg := runExportCmd(tuiexport.OptionCSV, snap)()
+ msg := runExportCmd(true, tuiexport.OptionCSV, snap)()
done, ok := msg.(tuiexport.CompletedMsg)
if !ok {
t.Fatalf("expected CompletedMsg, got %T", msg)