summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-05 23:47:19 +0200
committerPaul Buetow <paul@buetow.org>2026-03-05 23:47:19 +0200
commit77310af6f292004fbdd11eaa0bcfeaff812a365d (patch)
tree02c0c242759efa8a9fa2dfc970515bcc6b77bc1a /internal/tui
parentb48fb545191be25e9795e79336c45c439466986c (diff)
Make flame tab default and fix flame hotkey routing
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/common/keys.go14
-rw-r--r--internal/tui/common/keys_test.go20
-rw-r--r--internal/tui/dashboard/model.go44
-rw-r--r--internal/tui/dashboard/model_test.go87
-rw-r--r--internal/tui/dashboard/tabs.go2
-rw-r--r--internal/tui/flamegraph/model.go28
-rw-r--r--internal/tui/flamegraph/renderer.go26
-rw-r--r--internal/tui/flamegraph/renderer_test.go24
-rw-r--r--internal/tui/tui.go8
-rw-r--r--internal/tui/tui_test.go58
10 files changed, 252 insertions, 59 deletions
diff --git a/internal/tui/common/keys.go b/internal/tui/common/keys.go
index 6b0ae27..ab9865d 100644
--- a/internal/tui/common/keys.go
+++ b/internal/tui/common/keys.go
@@ -38,13 +38,13 @@ func DefaultKeyMap() KeyMap {
return KeyMap{
Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next tab")),
ShiftTab: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "prev tab")),
- One: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "overview")),
- Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "syscalls")),
- Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "files")),
- Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "processes")),
- Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "lat+gaps")),
- Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "stream")),
- Seven: key.NewBinding(key.WithKeys("7"), key.WithHelp("7", "flame")),
+ One: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "flame")),
+ Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "overview")),
+ Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "syscalls")),
+ Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "files")),
+ Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "processes")),
+ Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "lat+gaps")),
+ Seven: key.NewBinding(key.WithKeys("7"), key.WithHelp("7", "stream")),
DirGroup: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "dir group")),
SelectPID: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "select pid")),
SelectTID: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "select tid")),
diff --git a/internal/tui/common/keys_test.go b/internal/tui/common/keys_test.go
index e043f9e..4284faf 100644
--- a/internal/tui/common/keys_test.go
+++ b/internal/tui/common/keys_test.go
@@ -24,8 +24,8 @@ func TestDefaultKeyMapIncludesDirGroupBinding(t *testing.T) {
t.Fatalf("unexpected select tid binding help: key=%q desc=%q", selectTIDHelp.Key, selectTIDHelp.Desc)
}
- flameHelp := keys.Seven.Help()
- if flameHelp.Key != "7" || flameHelp.Desc != "flame" {
+ flameHelp := keys.One.Help()
+ if flameHelp.Key != "1" || flameHelp.Desc != "flame" {
t.Fatalf("unexpected flame binding help: key=%q desc=%q", flameHelp.Key, flameHelp.Desc)
}
}
@@ -38,7 +38,7 @@ func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) {
}
found := false
- foundSeven := false
+ foundOne := false
for _, binding := range groups[1] {
help := binding.Help()
if help.Key == "d" && help.Desc == "dir group" {
@@ -52,12 +52,12 @@ func TestDashboardFullHelpIncludesDirGroupBinding(t *testing.T) {
for _, binding := range groups[0] {
help := binding.Help()
- if help.Key == "7" && help.Desc == "flame" {
- foundSeven = true
+ if help.Key == "1" && help.Desc == "flame" {
+ foundOne = true
break
}
}
- if !foundSeven {
+ if !foundOne {
t.Fatalf("expected flame tab binding in dashboard full help tabs")
}
@@ -103,7 +103,7 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) {
short := keys.DashboardStatusHelp()
found := false
foundSelectTID := false
- foundSeven := false
+ foundOne := false
for _, binding := range short {
help := binding.Help()
if help.Key == "o" && help.Desc == "probes" {
@@ -112,8 +112,8 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) {
if help.Key == "t" && help.Desc == "select tid" {
foundSelectTID = true
}
- if help.Key == "7" && help.Desc == "flame" {
- foundSeven = true
+ if help.Key == "1" && help.Desc == "flame" {
+ foundOne = true
}
}
if !found {
@@ -122,7 +122,7 @@ func TestDashboardStatusHelpIncludesProbesBinding(t *testing.T) {
if !foundSelectTID {
t.Fatalf("expected select tid binding in dashboard short help")
}
- if !foundSeven {
+ if !foundOne {
t.Fatalf("expected flame tab binding in dashboard short help")
}
}
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 {
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index 5f5a83c..5d101c2 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -208,6 +208,34 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
+// ConsumesKey reports whether the flamegraph should handle a key press before
+// dashboard- or app-level shortcuts.
+func (m Model) ConsumesKey(msg tea.KeyPressMsg) bool {
+ if m.searchActive {
+ return true
+ }
+ switch {
+ case msg.String() == "/",
+ msg.String() == "n",
+ msg.String() == "N",
+ msg.String() == "p",
+ msg.String() == "r",
+ msg.String() == "o",
+ msg.String() == "?":
+ return true
+ case key.Matches(msg, m.keys.ZoomIn),
+ key.Matches(msg, m.keys.ZoomUndo),
+ key.Matches(msg, m.keys.ZoomReset),
+ key.Matches(msg, m.keys.MoveShallower),
+ key.Matches(msg, m.keys.MoveDeeper),
+ key.Matches(msg, m.keys.PrevSibling),
+ key.Matches(msg, m.keys.NextSibling):
+ return true
+ default:
+ return false
+ }
+}
+
// View renders the flamegraph viewport.
func (m Model) View() tea.View {
content := RenderTerminalView(m.frames, m.width, m.height, m.selectedIdx, m.subtreeSet, m.matchIndices, m.isDark, m.searchActive, m.searchQuery)
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
index ad74173..9f31023 100644
--- a/internal/tui/flamegraph/renderer.go
+++ b/internal/tui/flamegraph/renderer.go
@@ -102,6 +102,10 @@ func frameName(name string, depth int) string {
}
func terminalFrameColor(name string) color.Color {
+ if semantic, ok := semanticFrameColor(name); ok {
+ return semantic
+ }
+
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(name))
h := hasher.Sum32()
@@ -113,6 +117,28 @@ func terminalFrameColor(name string) color.Color {
}
}
+func semanticFrameColor(name string) (color.Color, bool) {
+ label := strings.ToLower(strings.TrimSpace(name))
+ switch {
+ case label == "":
+ return nil, false
+ case strings.Contains(label, "read"), strings.Contains(label, "pread"):
+ return color.RGBA{R: 78, G: 132, B: 201, A: 255}, true // read I/O: blue
+ case strings.Contains(label, "write"), strings.Contains(label, "pwrite"):
+ return color.RGBA{R: 222, G: 122, B: 58, A: 255}, true // write I/O: orange
+ case strings.Contains(label, "open"), strings.Contains(label, "close"), strings.Contains(label, "stat"), strings.Contains(label, "rename"), strings.Contains(label, "link"):
+ return color.RGBA{R: 196, G: 168, B: 72, A: 255}, true // metadata I/O: amber
+ case strings.HasPrefix(label, "/"), strings.Contains(label, "path:"), strings.Contains(label, "/"):
+ return color.RGBA{R: 88, G: 156, B: 84, A: 255}, true // file paths: green
+ case strings.Contains(label, "pid"), strings.Contains(label, "tid"):
+ return color.RGBA{R: 67, G: 151, B: 149, A: 255}, true // process/thread dimensions: teal
+ case strings.HasPrefix(label, "sys_"):
+ return color.RGBA{R: 191, G: 99, B: 74, A: 255}, true // other syscall buckets: rust
+ default:
+ return nil, false
+ }
+}
+
// RenderTerminalView renders a terminal flamegraph viewport from laid out frames.
func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtreeSet, matchSet map[int]bool, isDark, searchActive bool, searchQuery string) string {
if width < minFlameWidth {
diff --git a/internal/tui/flamegraph/renderer_test.go b/internal/tui/flamegraph/renderer_test.go
index ca837fe..eb111b8 100644
--- a/internal/tui/flamegraph/renderer_test.go
+++ b/internal/tui/flamegraph/renderer_test.go
@@ -1,6 +1,7 @@
package flamegraph
import (
+ "image/color"
"strings"
"testing"
)
@@ -108,6 +109,29 @@ func TestBuildTerminalLayoutUsesPathSeparatorAndColor(t *testing.T) {
}
}
+func TestTerminalFrameColorSemanticPalette(t *testing.T) {
+ tests := []struct {
+ name string
+ label string
+ want color.RGBA
+ }{
+ {name: "read", label: "sys_enter_read", want: color.RGBA{R: 78, G: 132, B: 201, A: 255}},
+ {name: "write", label: "sys_enter_write", want: color.RGBA{R: 222, G: 122, B: 58, A: 255}},
+ {name: "metadata", label: "sys_enter_openat", want: color.RGBA{R: 196, G: 168, B: 72, A: 255}},
+ {name: "path", label: "/var/log/app.log", want: color.RGBA{R: 88, G: 156, B: 84, A: 255}},
+ {name: "pid", label: "pid=1234", want: color.RGBA{R: 67, G: 151, B: 149, A: 255}},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got := terminalFrameColor(tc.label)
+ if got != tc.want {
+ t.Fatalf("unexpected semantic color for %q: got=%v want=%v", tc.label, got, tc.want)
+ }
+ })
+ }
+}
+
func TestRenderTerminalViewShowsNarrowMessage(t *testing.T) {
out := RenderTerminalView(nil, 50, 10, 0, nil, nil, true, false, "")
if !strings.Contains(out, "terminal too narrow") {
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index ab719fb..328202e 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -300,18 +300,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.stopTrace()
return m, tea.Quit
}
- 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() {
+ 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(msg) {
m.exporter = m.exporter.Open()
return m, nil
}
- if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Probes) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() {
+ if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Probes) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) {
m.probeModal = probes.NewModel(m.runtime.currentProbeManager()).SetDarkMode(m.isDark).Open()
return m, nil
}
- if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectPID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() {
+ if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectPID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) {
return m.reselectPID()
}
- if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectTID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() {
+ if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectTID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts(msg) {
return m.reselectTID()
}
case tuiexport.RequestMsg:
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index c801b24..876fe8f 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -270,7 +270,7 @@ func TestTracingStartedRebindsEventStreamSource(t *testing.T) {
next, _ = m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
m = next.(Model)
- next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'6'}[0], Text: string([]rune{'6'})})
+ next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})})
m = next.(Model)
next, _ = m.Update(messages.StatsTickMsg{})
m = next.(Model)
@@ -295,6 +295,37 @@ func TestExportKeyOpensModalOnDashboard(t *testing.T) {
}
}
+func TestFlamePauseKeyDoesNotTriggerPIDReselect(t *testing.T) {
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+ m.width = 120
+ m.height = 30
+
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'p'}[0], Text: string([]rune{'p'})})
+ updated := next.(Model)
+ if updated.screen != ScreenDashboard {
+ t.Fatalf("expected flame pause key to keep dashboard screen, got %v", updated.screen)
+ }
+ if !strings.Contains(updated.View().Content, "[PAUSED]") {
+ t.Fatalf("expected flame pause key to toggle flame paused state")
+ }
+}
+
+func TestFlameOrderKeyDoesNotOpenProbeModal(t *testing.T) {
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+ m.width = 120
+ m.height = 30
+
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'o'}[0], Text: string([]rune{'o'})})
+ updated := next.(Model)
+ if updated.probeModal.Visible() {
+ t.Fatalf("expected flame order key to stay in flame tab, not open probes modal")
+ }
+}
+
func TestSelectPIDKeyReturnsToFreshPickerAndStopsTrace(t *testing.T) {
m := NewModel(-1, func(context.Context) error { return nil })
m.screen = ScreenDashboard
@@ -304,6 +335,9 @@ func TestSelectPIDKeyReturnsToFreshPickerAndStopsTrace(t *testing.T) {
stopped := false
m.traceStop = func() { stopped = true }
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})})
+ m = next.(Model)
+
next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'p'}[0], Text: string([]rune{'p'})})
updated := next.(Model)
@@ -336,6 +370,9 @@ func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) {
stopped := false
m.traceStop = func() { stopped = true }
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})})
+ m = next.(Model)
+
next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'t'}[0], Text: string([]rune{'t'})})
updated := next.(Model)
if !stopped {
@@ -361,6 +398,9 @@ func TestSelectTIDKeyReturnsToPickerWhenSinglePIDSelected(t *testing.T) {
stopped := false
m.traceStop = func() { stopped = true }
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})})
+ m = next.(Model)
+
next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'t'}[0], Text: string([]rune{'t'})})
updated := next.(Model)
if !stopped {
@@ -444,7 +484,7 @@ func TestStreamFilterModalConsumesEKeyInsteadOfOpeningExport(t *testing.T) {
m.width = 120
m.height = 30
- next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'6'}[0], Text: string([]rune{'6'})})
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})})
m = next.(Model)
next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: string([]rune{'f'})})
m = next.(Model)
@@ -586,22 +626,22 @@ func TestDashboardTabKeysChangeActiveView(t *testing.T) {
m.height = 30
out := m.View().Content
- if !strings.Contains(out, "Overview: waiting for stats") {
- t.Fatalf("expected overview waiting view by default")
+ if !strings.Contains(out, "Flame: waiting for data") {
+ t.Fatalf("expected flame waiting view by default")
}
next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})})
updated := next.(Model)
out = updated.View().Content
- if !strings.Contains(out, "Syscalls: waiting for stats") {
- t.Fatalf("expected syscalls waiting view after pressing 2")
+ if !strings.Contains(out, "Overview: waiting for stats") {
+ t.Fatalf("expected overview waiting view after pressing 2")
}
next, _ = updated.Update(tea.KeyPressMsg{Code: tea.KeyTab})
updated = next.(Model)
out = updated.View().Content
- if !strings.Contains(out, "Files: waiting for stats") {
- t.Fatalf("expected files waiting view after tab")
+ if !strings.Contains(out, "Syscalls: waiting for stats") {
+ t.Fatalf("expected syscalls waiting view after tab")
}
}
@@ -619,7 +659,7 @@ func TestProbeModalViewDoesNotStackDashboardContent(t *testing.T) {
if !strings.Contains(out, "Probes (") {
t.Fatalf("expected probe modal content, got %q", out)
}
- if strings.Contains(out, "Overview: waiting for stats") {
+ if strings.Contains(out, "Flame: waiting for data") || strings.Contains(out, "Overview: waiting for stats") {
t.Fatalf("expected probe modal to render as standalone view, got stacked dashboard content")
}
}