summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph
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/flamegraph
parentb48fb545191be25e9795e79336c45c439466986c (diff)
Make flame tab default and fix flame hotkey routing
Diffstat (limited to 'internal/tui/flamegraph')
-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
3 files changed, 78 insertions, 0 deletions
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") {