diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-12 00:00:14 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-12 00:00:14 +0300 |
| commit | 7226b87d8cd8f74e2771894dda7185406b265f80 (patch) | |
| tree | 366888ba72399a2033705e3be3dd9b78cdd567d3 /internal/tui/flamegraph/model.go | |
| parent | 933be1ba2dbb7f6397a4112969bc85a4eac9d155 (diff) | |
refactor flamegraph model.go: split Update and frameIndexAt below 50-line limit
Extract handleSearchInput and handleKeyNavigation from Update (111 lines → 33),
then split handleKeyNavigation into handleModeKey and handleMovementKey.
Extract frameCoordToTargetRow and findFrameAtRow from frameIndexAt (66 lines → 32).
All new helpers are ≤ 29 lines; behavior is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/flamegraph/model.go')
| -rw-r--r-- | internal/tui/flamegraph/model.go | 205 |
1 files changed, 120 insertions, 85 deletions
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go index 6930170..7fbed6c 100644 --- a/internal/tui/flamegraph/model.go +++ b/internal/tui/flamegraph/model.go @@ -259,91 +259,109 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // the view and defer / unanimate accordingly. m.lastKeyAt = time.Now() if m.searchActive { - handled := false - switch msg.String() { - case "esc": - handled = true - m.clearSearch() - m.recordKeyDebug(msg, handled, false) - return m, nil - case "enter": - handled = true - m.applySearchQuery(m.searchInput.Value()) - m.searchActive = false - m.searchInput.Blur() - m.recordKeyDebug(msg, handled, false) - return m, nil - } - var cmd tea.Cmd - m.searchInput, cmd = m.searchInput.Update(msg) - _ = cmd - m.recordKeyDebug(msg, true, false) - return m, nil + return m.handleSearchInput(msg) } + return m.handleKeyNavigation(msg) + } + return m, nil +} - prev := m.selectedIdx - handled := false - switch { - case isSearchOpenKey(msg): - handled = true - m.openSearch() - case isNextMatchKey(msg): - handled = true - m.jumpMatch(1) - case isPrevMatchKey(msg): - handled = true - m.jumpMatch(-1) - case isPauseKey(msg): - handled = true - m.togglePause() - case isResetBaselineKey(msg): - handled = true - m.resetBaseline() - case isCycleOrderKey(msg): - handled = true - m.cycleFieldOrder() - case isCycleMetricKey(msg): - handled = true - m.toggleCountField() - case isHelpToggleKey(msg): - handled = true - m.toggleHelp() - case isZoomInKey(msg, m.keys): - handled = true - m.zoomIn() - case isZoomUndoKey(msg, m.keys): - handled = true - m.zoomUndo() - case isZoomResetKey(msg, m.keys): - handled = true - m.zoomReset() - case isMoveShallowerKey(msg, m.keys): - handled = true - m.moveVerticalWithFallback(-1, 1, -1) - case isMoveDeeperKey(msg, m.keys): - handled = true - m.moveVerticalWithFallback(1, -1, 1) - case isPrevSiblingKey(msg, m.keys): - handled = true - m.moveSibling(-1) - case isNextSiblingKey(msg, m.keys): - handled = true - m.moveSibling(1) - case isJumpTopKey(msg, m.keys): - handled = true - m.jumpToTop() - case isJumpRootKey(msg, m.keys): - handled = true - m.jumpToRoot() - } - if m.selectedIdx != prev { - m.subtreeSet = subtreeSetUsingAncestry(m.frames, m.selectedIdx, m.ancestry, m.subtreeSet) - } - m.recordKeyDebug(msg, handled, m.selectedIdx != prev) +// handleSearchInput processes key events while search mode is active. +// Handles escape (cancel search), enter (apply query), and delegates +// all other keys to the text input component for in-place editing. +func (m Model) handleSearchInput(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + handled := false + switch msg.String() { + case "esc": + handled = true + m.clearSearch() + m.recordKeyDebug(msg, handled, false) + return m, nil + case "enter": + handled = true + m.applySearchQuery(m.searchInput.Value()) + m.searchActive = false + m.searchInput.Blur() + m.recordKeyDebug(msg, handled, false) + return m, nil } + var cmd tea.Cmd + m.searchInput, cmd = m.searchInput.Update(msg) + _ = cmd + m.recordKeyDebug(msg, true, false) return m, nil } +// handleKeyNavigation processes navigation key events when search is not active. +// Delegates mode-toggle and zoom actions to handleModeKey, movement actions to +// handleMovementKey, then updates the subtree highlight when selection changes. +func (m Model) handleKeyNavigation(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + prev := m.selectedIdx + handled := m.handleModeKey(msg) + if !handled { + handled = m.handleMovementKey(msg) + } + if m.selectedIdx != prev { + m.subtreeSet = subtreeSetUsingAncestry(m.frames, m.selectedIdx, m.ancestry, m.subtreeSet) + } + m.recordKeyDebug(msg, handled, m.selectedIdx != prev) + return m, nil +} + +// handleModeKey dispatches search, pause, zoom, and view-toggle key actions. +// Returns true when a key was handled so handleKeyNavigation can skip movement. +func (m *Model) handleModeKey(msg tea.KeyPressMsg) bool { + switch { + case isSearchOpenKey(msg): + m.openSearch() + case isNextMatchKey(msg): + m.jumpMatch(1) + case isPrevMatchKey(msg): + m.jumpMatch(-1) + case isPauseKey(msg): + m.togglePause() + case isResetBaselineKey(msg): + m.resetBaseline() + case isCycleOrderKey(msg): + m.cycleFieldOrder() + case isCycleMetricKey(msg): + m.toggleCountField() + case isHelpToggleKey(msg): + m.toggleHelp() + case isZoomInKey(msg, m.keys): + m.zoomIn() + case isZoomUndoKey(msg, m.keys): + m.zoomUndo() + case isZoomResetKey(msg, m.keys): + m.zoomReset() + default: + return false + } + return true +} + +// handleMovementKey dispatches directional and jump key actions. +// Returns true when a key was handled, false otherwise. +func (m *Model) handleMovementKey(msg tea.KeyPressMsg) bool { + switch { + case isMoveShallowerKey(msg, m.keys): + m.moveVerticalWithFallback(-1, 1, -1) + case isMoveDeeperKey(msg, m.keys): + m.moveVerticalWithFallback(1, -1, 1) + case isPrevSiblingKey(msg, m.keys): + m.moveSibling(-1) + case isNextSiblingKey(msg, m.keys): + m.moveSibling(1) + case isJumpTopKey(msg, m.keys): + m.jumpToTop() + case isJumpRootKey(msg, m.keys): + m.jumpToRoot() + default: + return false + } + return true +} + // handleSnapshotReady applies the result of a background snapshot+layout job. // Discards the result if viewport or zoom changed while the job was in flight // (the next tick will dispatch a fresh refresh), or if the user paused after a @@ -1362,6 +1380,9 @@ func buildZoomStack(path string) []zoomState { return stack } +// frameIndexAt returns the index of the frame rendered at terminal coordinates +// (x, y), or -1 if no frame occupies that cell. Delegates boundary validation +// to frameCoordToTargetRow and frame scanning to findFrameAtRow. func (m Model) frameIndexAt(x, y int) int { if len(m.frames) == 0 || m.width <= 0 || m.height <= 0 { return -1 @@ -1387,8 +1408,18 @@ func (m Model) frameIndexAt(x, y int) int { if y < 1 || y > availableRows { return -1 } - dataRow := y - 1 + targetRow := m.frameCoordToTargetRow(y-1, availableRows) + if targetRow < 0 { + return -1 + } + return findFrameAtRow(m.frames, targetRow, x, m.width) +} + +// frameCoordToTargetRow converts a data-area row offset (0-based, after +// stripping the toolbar row) into the logical frame row index. Returns -1 when +// the coordinate falls in the top padding area above the first visible row. +func (m Model) frameCoordToTargetRow(dataRow, availableRows int) int { maxRow := maxFrameRowForSet(m.frames, nil) barHeight := computeBarHeight(availableRows, maxRow+1, maxBarVisualHeight) visibleDepthRows := availableRows / barHeight @@ -1407,17 +1438,21 @@ func (m Model) frameIndexAt(x, y int) int { if dataRow < padTop { return -1 } - depthFromTop := (dataRow - padTop) / barHeight - targetRow := maxRow - depthFromTop + return maxRow - depthFromTop +} +// findFrameAtRow scans frames for the narrowest one that occupies logical row +// targetRow and contains pixel column x within [0, width). Returning the +// narrowest frame resolves overlap between wide parent and narrow child bars. +func findFrameAtRow(frames []tuiFrame, targetRow, x, width int) int { best := -1 bestWidth := int(^uint(0) >> 1) - for idx, frame := range m.frames { - if frame.Row != targetRow || frame.Col >= m.width { + for idx, frame := range frames { + if frame.Row != targetRow || frame.Col >= width { continue } - right := min(m.width, frame.Col+frame.Width) + right := min(width, frame.Col+frame.Width) if x < frame.Col || x >= right { continue } |
