diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-18 09:13:28 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-18 09:13:28 +0200 |
| commit | 3f85aa438bffaad287a450898c44942634944c22 (patch) | |
| tree | 1e5ec6303d45d5003cf548bb1df58b34038b7113 /internal/tui | |
| parent | 1731c3723ced92a5dc8e54fb0caf4e33b2c7ba70 (diff) | |
refactor: pass flags.Config explicitly, remove flags.Get() from library code (task 429)
flags.Get() (global mutable singleton) was called inside library packages,
coupling them to global state and making tests fragile (DIP violation).
- internal.Run() now takes an explicit flags.Config; main.go calls
flags.Get() once at the CLI boundary and passes it in.
- tui.Run(), RunWithTraceStarter(), RunTestFlamesWithTraceStarter() removed;
callers already used the WithConfig variants directly.
- tui.NewModel() preserved for test ergonomics but now uses flags.NewFlags()
(pure defaults) instead of flags.Get() (global state).
- Tests updated to use NewModelWithConfig() with explicit config structs
instead of flags.Set*() + NewModel(), eliminating all test-level
global-state mutation.
flags.Get() is now called only in cmd/ior/main.go, the correct boundary.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'internal/tui')
| -rw-r--r-- | internal/tui/tui.go | 93 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 41 |
2 files changed, 28 insertions, 106 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go index ef94748..432fc14 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log" - "os" "strconv" "strings" "sync" @@ -231,16 +230,6 @@ func TraceFiltersFromContext(ctx context.Context) (globalfilter.Filter, bool) { return filters.filter.Clone(), true } -// Run starts the TUI program in alternate screen mode. -func Run() error { - return RunWithTraceStarter(defaultTraceStarter) -} - -// RunWithTraceStarter starts the TUI program with a custom trace starter. -func RunWithTraceStarter(starter TraceStarter) error { - return RunWithTraceStarterConfig(flags.Get(), starter) -} - // RunWithTraceStarterConfig starts the TUI with explicit runtime flags. func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { model := newModelWithRuntimeConfig(cfg.PidFilter, filterFromConfig(cfg), cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, starter) @@ -249,12 +238,6 @@ func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { return err } -// RunTestFlamesWithTraceStarter starts the TUI directly on dashboard/flame view -// with a synthetic static flamegraph source. -func RunTestFlamesWithTraceStarter(starter TraceStarter) error { - return RunTestFlamesWithTraceStarterConfig(flags.Get(), starter) -} - // RunTestFlamesWithTraceStarterConfig starts test-flames mode with explicit runtime flags. func RunTestFlamesWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { model := newModelWithRuntimeConfig(1, filterFromConfig(cfg), 1, -1, cfg.TUIExportEnable, starter) @@ -317,9 +300,10 @@ type pickerReturnState struct { tidFilter int } -// NewModel creates the top-level TUI model. +// NewModel creates the top-level TUI model with default runtime flags. +// Prefer NewModelWithConfig to pass parsed CLI config explicitly. func NewModel(initialPID int, startTrace TraceStarter) Model { - return NewModelWithConfig(flags.Get(), initialPID, startTrace) + return NewModelWithConfig(flags.NewFlags(), initialPID, startTrace) } // NewModelWithConfig creates the top-level TUI model with explicit runtime flags. @@ -500,17 +484,6 @@ func (m Model) canHandleDashboardShortcut(msg tea.KeyPressMsg) bool { !m.dashboard.BlocksGlobalShortcuts(msg) } -func (m Model) canQuitFromMainDashboard(msg tea.KeyPressMsg) bool { - return m.screen == ScreenDashboard && - !m.attaching && - m.lastErr == nil && - !m.filterModal.Visible() && - !m.exporter.Visible() && - !m.recordModal.Visible() && - !m.probeModal.Visible() && - !m.dashboard.BlocksGlobalShortcuts(msg) -} - func (m Model) shouldCancelPickerToDashboard(msg tea.KeyPressMsg) bool { return m.screen == ScreenPIDPicker && m.pickerReturn != nil && @@ -537,7 +510,7 @@ func (m Model) handleGlobalKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd, bo return next, cmd, true } if key.Matches(msg, m.keys.Quit) { - if m.canQuitFromMainDashboard(msg) { + if m.canHandleDashboardShortcut(msg) { if err := m.stopRecording(); err != nil { m.lastErr = err return m, nil, true @@ -848,31 +821,9 @@ func defaultTraceStarter(context.Context) error { return nil } +// filterFromConfig delegates to the canonical Config.TraceFilter method. func filterFromConfig(cfg flags.Config) globalfilter.Filter { - filter := cfg.GlobalFilter.Clone() - if filter.IsActive() { - return filter - } - if cfg.CommFilter != "" { - filter.Comm = &globalfilter.StringFilter{Pattern: cfg.CommFilter} - } - if cfg.PathFilter != "" { - filter.File = &globalfilter.StringFilter{Pattern: cfg.PathFilter} - } - if cfg.PidFilter > 0 { - filter.PID = eqNumericFilter(cfg.PidFilter) - } - if cfg.TidFilter > 0 { - filter.TID = eqNumericFilter(cfg.TidFilter) - } - return filter -} - -func eqNumericFilter(value int) *globalfilter.NumericFilter { - if value <= 0 { - return nil - } - return &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(value)} + return cfg.TraceFilter() } func (m *Model) setProcessFilters(pid, tid int) { @@ -887,10 +838,12 @@ func (m *Model) setProcessFilters(pid, tid int) { func (m *Model) setGlobalFilter(filter globalfilter.Filter) { m.globalFilter = filter.Clone() - pid, _ := eqNumericFilterValue(m.globalFilter.PID) - tid, _ := eqNumericFilterValue(m.globalFilter.TID) - m.pidFilter = pid - m.tidFilter = tid + // EqValue returns (0, false) when no equality filter is set; + // selectedPIDFilter maps non-positive values to -1 ("no filter"). + pid, _ := m.globalFilter.PID.EqValue() + tid, _ := m.globalFilter.TID.EqValue() + m.pidFilter = selectedPIDFilter(pid) + m.tidFilter = selectedPIDFilter(tid) m.syncDashboardFilterState() } @@ -903,8 +856,8 @@ func (m *Model) syncDashboardFilterState() { func applyProcessFilters(filter globalfilter.Filter, pid, tid int) globalfilter.Filter { out := filter.Clone() - out.PID = eqNumericFilter(pid) - out.TID = eqNumericFilter(tid) + out.PID = globalfilter.NewEqFilter(int64(pid)) + out.TID = globalfilter.NewEqFilter(int64(tid)) return out } @@ -950,13 +903,6 @@ func (m Model) undoGlobalFilter() (tea.Model, tea.Cmd) { return m, tea.Batch(m.spin.Tick, m.beginTraceCmd()) } -func eqNumericFilterValue(filter *globalfilter.NumericFilter) (int, bool) { - if filter == nil || filter.Op != globalfilter.OpEq || filter.Value <= 0 { - return -1, false - } - return int(filter.Value), true -} - func globalFilterActionLabel(prev, next globalfilter.Filter, action string) string { if strings.TrimSpace(action) != "" { return action @@ -1196,16 +1142,9 @@ func defaultParquetRecordingFilename() string { return fmt.Sprintf("ior-recording-%s.parquet", time.Now().Format("20060102-150405")) } +// tuiParquetMetadata delegates to the canonical parquet.NewFileMetadata. func tuiParquetMetadata() parquet.FileMetadata { - meta := parquet.FileMetadata{ - StartedAtUnixNano: uint64(time.Now().UnixNano()), - Mode: "tui", - IORVersion: flags.Version, - } - if hostname, err := os.Hostname(); err == nil { - meta.Hostname = hostname - } - return meta + return parquet.NewFileMetadata("tui") } func shortenRecordingPath(path string) string { diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 6ce16e6..7f43961 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -88,8 +88,6 @@ func TestRuntimeBindingsContextRoundTrip(t *testing.T) { } func TestPidSelectedTransitionsToDashboardAndSetsPIDFilter(t *testing.T) { - flags.SetPidFilter(-1) - flags.SetTidFilter(99) m := NewModel(-1, func(context.Context) error { return nil }) next, cmd := m.Update(PidSelectedMsg{Pid: 42}) @@ -113,7 +111,6 @@ func TestPidSelectedTransitionsToDashboardAndSetsPIDFilter(t *testing.T) { } func TestInitialPIDSkipsPickerAndStartsTracing(t *testing.T) { - flags.SetPidFilter(-1) m := NewModel(7, func(context.Context) error { return nil }) if m.screen != ScreenDashboard { @@ -127,7 +124,6 @@ func TestInitialPIDSkipsPickerAndStartsTracing(t *testing.T) { } func TestPidSelectedAllSetsNoFilter(t *testing.T) { - flags.SetPidFilter(999) m := NewModel(-1, func(context.Context) error { return nil }) next, _ := m.Update(PidSelectedMsg{Pid: 0}) @@ -696,9 +692,6 @@ func TestTracingStartedAppliesViewportWhenModelSizeIsUnset(t *testing.T) { } func TestExportKeyOpensModalOnDashboard(t *testing.T) { - flags.SetTUIExportEnable(true) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) - m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false @@ -1118,8 +1111,6 @@ func TestPidSelectedClearsPersistentStreamBuffer(t *testing.T) { } func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) { - flags.SetPidFilter(-1) - flags.SetTidFilter(-1) m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false @@ -1146,9 +1137,9 @@ func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) { } func TestSelectTIDKeyReturnsToPickerWhenSinglePIDSelected(t *testing.T) { - flags.SetPidFilter(1234) - flags.SetTidFilter(-1) - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.PidFilter = 1234 + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false m.width = 120 @@ -1174,9 +1165,9 @@ func TestSelectTIDKeyReturnsToPickerWhenSinglePIDSelected(t *testing.T) { } func TestTidSelectedTransitionsToDashboardAndSetsTIDFilter(t *testing.T) { - flags.SetPidFilter(2222) - flags.SetTidFilter(-1) - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.PidFilter = 2222 + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) next, cmd := m.Update(TidSelectedMsg{Pid: 0, Tid: 3333}) if cmd == nil { @@ -1198,8 +1189,6 @@ func TestTidSelectedTransitionsToDashboardAndSetsTIDFilter(t *testing.T) { } func TestTidSelectedFromAllPIDModeSetsOwningPID(t *testing.T) { - flags.SetPidFilter(-1) - flags.SetTidFilter(-1) m := NewModel(-1, func(context.Context) error { return nil }) next, cmd := m.Update(TidSelectedMsg{Pid: 4444, Tid: 5555}) @@ -1219,10 +1208,9 @@ func TestTidSelectedFromAllPIDModeSetsOwningPID(t *testing.T) { } func TestExportKeyIgnoredWhenExportDisabled(t *testing.T) { - flags.SetTUIExportEnable(false) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) - - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.TUIExportEnable = false + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false @@ -1234,8 +1222,6 @@ func TestExportKeyIgnoredWhenExportDisabled(t *testing.T) { } func TestStreamFilterModalConsumesEKeyInsteadOfOpeningExport(t *testing.T) { - flags.SetTUIExportEnable(true) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard @@ -1925,8 +1911,6 @@ func advanceFlameSelection(t *testing.T, m *Model) string { } func TestQuestionMarkDoesNotBreakExportModalInput(t *testing.T) { - flags.SetTUIExportEnable(true) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard @@ -1951,10 +1935,9 @@ func TestQuestionMarkDoesNotBreakExportModalInput(t *testing.T) { } func TestStatusBarHidesExportBindingWhenExportDisabled(t *testing.T) { - flags.SetTUIExportEnable(false) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) - - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.TUIExportEnable = false + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.width = 100 m.height = 30 |
