diff options
Diffstat (limited to 'internal/tui/dashboard')
| -rw-r--r-- | internal/tui/dashboard/model.go | 44 | ||||
| -rw-r--r-- | internal/tui/dashboard/model_test.go | 87 | ||||
| -rw-r--r-- | internal/tui/dashboard/tabs.go | 2 |
3 files changed, 104 insertions, 29 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go index 7807b31..24b6c8e 100644 --- a/internal/tui/dashboard/model.go +++ b/internal/tui/dashboard/model.go @@ -68,7 +68,7 @@ func NewModelWithConfig(engine SnapshotSource, streamSource *eventstream.RingBuf refreshMs = defaultRefreshMs } m := Model{ - activeTab: TabOverview, + activeTab: TabFlame, engine: engine, refreshEvery: time.Duration(refreshMs) * time.Millisecond, keys: keys, @@ -157,6 +157,11 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.showHelp = !m.showHelp return m, nil } + if m.activeTab == TabFlame && m.flamegraphModel.ConsumesKey(msg) { + next, flameCmd := m.flamegraphModel.Update(msg) + m.flamegraphModel = next.(flamegraphtui.Model) + return m, flameCmd + } handled, scrollCmd := m.handleScrollKey(msg) if scrollCmd != nil { cmd = scrollCmd @@ -167,32 +172,32 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if !handled { switch { + case key.Matches(msg, m.keys.One): + m.activeTab = TabFlame + handled = true 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 + m.activeTab = TabOverview handled = true case key.Matches(msg, m.keys.Three): - m.activeTab = TabFiles + m.activeTab = TabSyscalls handled = true case key.Matches(msg, m.keys.Four): - m.activeTab = TabProcesses + m.activeTab = TabFiles handled = true case key.Matches(msg, m.keys.Five): - m.activeTab = TabLatency + m.activeTab = TabProcesses handled = true case key.Matches(msg, m.keys.Six): - m.activeTab = TabStream + m.activeTab = TabLatency handled = true case key.Matches(msg, m.keys.Seven): - m.activeTab = TabFlame + m.activeTab = TabStream handled = true case key.Matches(msg, m.keys.Refresh): snap := m.snapshot() @@ -206,6 +211,11 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } } if !handled { + if m.activeTab == TabFlame { + next, flameCmd := m.flamegraphModel.Update(msg) + m.flamegraphModel = next.(flamegraphtui.Model) + return m, flameCmd + } return m, nil } batch := make([]tea.Cmd, 0, 3) @@ -317,10 +327,16 @@ func (m Model) LatestSnapshot() *statsengine.Snapshot { return m.latest } -// BlocksGlobalShortcuts reports whether modal UI in the active tab should -// suppress top-level shortcuts (for example global export key handling). -func (m Model) BlocksGlobalShortcuts() bool { - return m.activeTab == TabStream && (m.streamModel.FilterModalVisible() || m.streamModel.ExportModalVisible() || m.streamModel.SearchModalVisible()) +// BlocksGlobalShortcuts reports whether the active tab should suppress a +// top-level shortcut for the given key press. +func (m Model) BlocksGlobalShortcuts(msg tea.KeyPressMsg) bool { + if m.activeTab == TabStream { + return m.streamModel.FilterModalVisible() || m.streamModel.ExportModalVisible() || m.streamModel.SearchModalVisible() + } + if m.activeTab == TabFlame { + return m.flamegraphModel.ConsumesKey(msg) + } + return false } // SetStreamSource updates the live stream source used by the stream tab. diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go index 46f4944..6d35d5a 100644 --- a/internal/tui/dashboard/model_test.go +++ b/internal/tui/dashboard/model_test.go @@ -31,37 +31,38 @@ func TestKeySwitchingChangesActiveTab(t *testing.T) { next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) model := next.(Model) - if model.activeTab != TabSyscalls { - t.Fatalf("expected syscalls tab, got %v", model.activeTab) + if model.activeTab != TabOverview { + t.Fatalf("expected overview tab on key 2, got %v", model.activeTab) } next, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyTab}) model = next.(Model) - if model.activeTab != TabFiles { - t.Fatalf("expected next tab to be files, got %v", model.activeTab) + if model.activeTab != TabSyscalls { + t.Fatalf("expected next tab to be syscalls, got %v", model.activeTab) } next, _ = model.Update(tea.KeyPressMsg{Code: tea.KeyTab, Mod: tea.ModShift}) model = next.(Model) - if model.activeTab != TabSyscalls { - t.Fatalf("expected previous tab to be syscalls, got %v", model.activeTab) + if model.activeTab != TabOverview { + t.Fatalf("expected previous tab to be overview, got %v", model.activeTab) } next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})}) model = next.(Model) - if model.activeTab != TabFlame { - t.Fatalf("expected flame tab on key 7, got %v", model.activeTab) + if model.activeTab != TabStream { + t.Fatalf("expected stream tab on key 7, got %v", model.activeTab) } - next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'6'}[0], Text: string([]rune{'6'})}) + next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'1'}[0], Text: string([]rune{'1'})}) model = next.(Model) - if model.activeTab != TabStream { - t.Fatalf("expected stream tab on key 6, got %v", model.activeTab) + if model.activeTab != TabFlame { + t.Fatalf("expected flame tab on key 1, got %v", model.activeTab) } } func TestArrowAndViKeysDoNotCycleTabs(t *testing.T) { m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabOverview next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyRight}) model := next.(Model) @@ -303,6 +304,7 @@ func TestRefreshKeyEmitsRefreshTick(t *testing.T) { snap := &statsengine.Snapshot{TotalSyscalls: 13} engine := &fakeSnapshotSource{snap: snap} m := NewModelWithConfig(engine, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabOverview next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})}) _ = next if cmd == nil { @@ -318,6 +320,63 @@ func TestRefreshKeyEmitsRefreshTick(t *testing.T) { } } +func TestFlameTabReceivesSlashKey(t *testing.T) { + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFlame + m.width = 120 + m.height = 30 + + next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'/'}[0], Text: string([]rune{'/'})}) + model := next.(Model) + if cmd != nil { + t.Fatalf("did not expect global command for flame search key") + } + if !strings.Contains(model.View().Content, "0/0 matches") { + t.Fatalf("expected flame search footer after pressing /") + } +} + +func TestFlameTabReceivesResetAndPauseKeys(t *testing.T) { + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFlame + m.width = 120 + m.height = 30 + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'p'}[0], Text: string([]rune{'p'})}) + model := next.(Model) + if !strings.Contains(model.View().Content, "[PAUSED]") { + t.Fatalf("expected flame pause key to toggle paused state") + } + + next, cmd := model.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})}) + model = next.(Model) + if cmd != nil { + t.Fatalf("expected flame reset key to be handled by flame tab without global refresh command") + } + if model.activeTab != TabFlame { + t.Fatalf("expected flame tab to stay active after reset key") + } +} + +func TestFlameSearchConsumesNumericTabKeys(t *testing.T) { + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFlame + m.width = 120 + m.height = 30 + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'/'}[0], Text: string([]rune{'/'})}) + model := next.(Model) + if model.activeTab != TabFlame { + t.Fatalf("expected flame tab to stay active after opening search") + } + + next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})}) + model = next.(Model) + if model.activeTab != TabFlame { + t.Fatalf("expected numeric key while searching to stay in flame tab") + } +} + func TestRefreshTickEmitsStatsTickMsg(t *testing.T) { snap := &statsengine.Snapshot{TotalSyscalls: 9} engine := &fakeSnapshotSource{snap: snap} @@ -386,8 +445,8 @@ func TestStatsTickClampsGroupedFilesOffset(t *testing.T) { func TestViewRendersTabBarAndHelp(t *testing.T) { m := NewModelWithConfig(nil, nil, 1000, common.DefaultKeyMap()) out := m.View().Content - if !strings.Contains(out, "Overview") { - t.Fatalf("expected overview label in view") + if !strings.Contains(out, "Flame") { + t.Fatalf("expected flame tab label in view") } if !strings.Contains(out, "press H for help") { t.Fatalf("expected help hint text in view") @@ -437,7 +496,7 @@ func TestStreamTabViewKeepsTabAndHelpChromeVisible(t *testing.T) { m.streamModel.Refresh() out := m.View().Content - if !strings.Contains(out, "1:Overview") { + if !strings.Contains(out, "1:Flame") { t.Fatalf("expected tab bar to remain visible in stream view") } if !strings.Contains(out, "press H for help") { diff --git a/internal/tui/dashboard/tabs.go b/internal/tui/dashboard/tabs.go index 731e21f..85ce319 100644 --- a/internal/tui/dashboard/tabs.go +++ b/internal/tui/dashboard/tabs.go @@ -30,13 +30,13 @@ const ( ) var allTabs = []Tab{ + TabFlame, TabOverview, TabSyscalls, TabFiles, TabProcesses, TabLatency, TabStream, - TabFlame, } func (t Tab) String() string { |
