diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
| commit | 1561987330cb898f5ff64383a9c78e7e6559f118 (patch) | |
| tree | 69a823e8f98dce572566c97e6879c11c9d591bda /internal/tui/flamegraph/controls.go | |
| parent | 96225fb6159212a8851043a08d781aba721b4e78 (diff) | |
| parent | 110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff) | |
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/tui/flamegraph/controls.go')
| -rw-r--r-- | internal/tui/flamegraph/controls.go | 173 |
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 + } +} |
