summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph/controls.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
committerPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
commit1561987330cb898f5ff64383a9c78e7e6559f118 (patch)
tree69a823e8f98dce572566c97e6879c11c9d591bda /internal/tui/flamegraph/controls.go
parent96225fb6159212a8851043a08d781aba721b4e78 (diff)
parent110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff)
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/tui/flamegraph/controls.go')
-rw-r--r--internal/tui/flamegraph/controls.go173
1 files changed, 173 insertions, 0 deletions
diff --git a/internal/tui/flamegraph/controls.go b/internal/tui/flamegraph/controls.go
new file mode 100644
index 0000000..06e6d0d
--- /dev/null
+++ b/internal/tui/flamegraph/controls.go
@@ -0,0 +1,173 @@
+package flamegraph
+
+import (
+ "fmt"
+ "strings"
+
+ common "ior/internal/tui/common"
+
+ "charm.land/lipgloss/v2"
+)
+
+func (m *Model) togglePause() {
+ m.paused = !m.paused
+}
+
+func (m *Model) clearSnapshotState(clearSearch bool) {
+ m.zoomRoot = nil
+ m.zoomPath = ""
+ m.zoomStack = nil
+ m.selectedIdx = 0
+ m.snapshot = nil
+ m.globalTotal = 0
+ m.frames = nil
+ m.targetFrames = nil
+ m.matchIndices = make(map[int]bool)
+ m.filterVisible = make(map[int]bool)
+ m.subtreeSet = make(map[int]bool)
+ m.hasNavigableSnapshot = false
+ if clearSearch {
+ m.searchQuery = ""
+ }
+}
+
+func (m *Model) resetBaseline() {
+ if m.liveTrie != nil {
+ m.liveTrie.Reset()
+ }
+ m.clearSnapshotState(true)
+ m.statusMessage = "Baseline reset"
+}
+
+func (m *Model) cycleFieldOrder() {
+ if len(m.fieldPresets) == 0 {
+ return
+ }
+ m.fieldIndex = (m.fieldIndex + 1) % len(m.fieldPresets)
+ nextPreset := m.fieldPresets[m.fieldIndex]
+ if m.liveTrie != nil {
+ if err := m.liveTrie.Reconfigure(nextPreset); err != nil {
+ m.statusMessage = "Field order error: " + err.Error()
+ return
+ }
+ }
+ m.clearSnapshotState(false)
+ m.statusMessage = "Order: " + strings.Join(nextPreset, "/")
+}
+
+func (m *Model) toggleCountField() {
+ next := "bytes"
+ if m.countField == "bytes" {
+ next = "count"
+ }
+ if m.liveTrie != nil {
+ if err := m.liveTrie.SetCountField(next); err != nil {
+ m.statusMessage = "Metric toggle error: " + err.Error()
+ return
+ }
+ }
+ m.countField = next
+ m.clearSnapshotState(false)
+ m.statusMessage = "Metric: " + m.countFieldLabel() + " (new baseline)"
+}
+
+func (m *Model) toggleHelp() {
+ m.showHelp = !m.showHelp
+}
+
+func (m Model) toolbarLine() string {
+ state := lipgloss.NewStyle().Foreground(common.ColorPrimary).Render("[LIVE]")
+ if m.paused {
+ state = lipgloss.NewStyle().Foreground(common.ColorDanger).Bold(true).Render("[PAUSED]")
+ }
+ order := m.currentFieldPresetLabel()
+ line := fmt.Sprintf("%s | view:%s | o:order(%s) | b:metric(%s) | /:search | enter:zoom | u/esc:undo | r:reset | space/p:pause", state, compactFramePath(m.currentRootPath()), order, m.countFieldLabel())
+ if m.searchQuery != "" {
+ line += " | filter:" + m.searchQuery
+ }
+ if m.statusMessage != "" {
+ line += " | " + m.statusMessage
+ }
+ if m.lastKeyDebug != "" {
+ line += " | " + m.lastKeyDebug
+ }
+ width := m.width
+ if width <= 0 {
+ width = 80
+ }
+ return padOrTrim(line, width)
+}
+
+func (m Model) helpOverlay() string {
+ width := m.width
+ if width <= 0 {
+ width = 80
+ }
+ help := "Flame help: j/k depth h/l sibling pgup top pgdn root enter zoom u/backspace/esc undo / search n/N matches space/p pause r reset baseline o order b metric ? help"
+ return common.HelpBarStyle.Width(width).Render(padOrTrim(help, width))
+}
+
+func (m Model) selectionStatusLine() string {
+ width := m.width
+ if width <= 0 {
+ width = 80
+ }
+ mode := "LIVE"
+ if m.paused {
+ mode = "PAUSED"
+ }
+ if len(m.frames) == 0 {
+ line := fmt.Sprintf("[%s] sel:none | arrows/hjkl navigate | enter zoom | / filter", mode)
+ return common.HelpBarStyle.Width(width).Render(padOrTrim(line, width))
+ }
+ selIdx := m.selectedIdx
+ if selIdx < 0 || selIdx >= len(m.frames) {
+ selIdx = 0
+ }
+ frame := m.frames[selIdx]
+ systemShare := frame.Percent
+ if m.globalTotal > 0 {
+ systemShare = percentOfTotal(frame.Total, m.globalTotal)
+ }
+ metric := m.countFieldLabel()
+ shareLabel := fmt.Sprintf("%.2f%% of total %s", systemShare, metric)
+ if strings.TrimSpace(m.searchQuery) != "" && len(m.matchIndices) > 0 {
+ filterTotal, _ := filterCoverageTotals(m.frames, m.matchIndices, m.globalTotal)
+ if filterTotal > 0 {
+ selectedFilterTotal := filterCoverageTotalForPath(m.frames, m.matchIndices, frame.Path)
+ filterShare := percentOfTotal(selectedFilterTotal, filterTotal)
+ shareLabel = fmt.Sprintf("%.2f%% of filtered %s", filterShare, metric)
+ }
+ }
+ line := fmt.Sprintf("[%s] sel:%d/%d %s | path:%s | depth:%d | total(%s):%d | %s",
+ mode, selIdx+1, len(m.frames), frame.Name, compactFramePath(frame.Path), frame.Depth, m.countFieldLabel(), frame.Total, shareLabel)
+ if m.searchQuery != "" {
+ line += " | filter:" + m.searchQuery
+ }
+ return common.HelpBarStyle.Width(width).Render(padOrTrim(line, width))
+}
+
+func (m Model) currentFieldPresetLabel() string {
+ if len(m.fieldPresets) == 0 {
+ return "n/a"
+ }
+ idx := m.fieldIndex
+ if idx < 0 {
+ idx = 0
+ }
+ if idx >= len(m.fieldPresets) {
+ idx = len(m.fieldPresets) - 1
+ }
+ return strings.Join(m.fieldPresets[idx], "/")
+}
+
+func (m Model) countFieldLabel() string {
+ switch m.countField {
+ case "count":
+ return "events"
+ case "bytes":
+ return "bytes"
+ default:
+ return m.countField
+ }
+}