From f903279e8a872cd7c417f2f57bf306bfb3f3cb87 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 8 Mar 2026 11:26:11 +0200 Subject: dashboard: clamp icicle selection by rendered tile count --- internal/tui/dashboard/icicle.go | 31 +++++++++++++++++++++++++++++++ internal/tui/dashboard/model.go | 12 ++++++++++-- internal/tui/dashboard/model_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/internal/tui/dashboard/icicle.go b/internal/tui/dashboard/icicle.go index 3761fc8..92c4834 100644 --- a/internal/tui/dashboard/icicle.go +++ b/internal/tui/dashboard/icicle.go @@ -79,6 +79,37 @@ func renderFilesIcicle(snap *statsengine.Snapshot, width, height int, metric bub return strings.Join(lines, "\n") } +func filesIcicleTileCount(snap *statsengine.Snapshot, width, height int, metric bubbleMetric) int { + if snap == nil { + return 0 + } + if width <= 0 { + width = 80 + } + if height <= 0 { + height = 18 + } + + dirs := aggregateFilesByDir(snap.Files()) + if len(dirs) == 0 { + return 0 + } + root := buildIcicleTree(dirs) + children := sortedIcicleChildren(root, metric) + if len(children) == 0 { + return 0 + } + + chartHeight := height - 2 + if chartHeight < 4 { + chartHeight = 4 + } + + tiles := make([]icicleTile, 0, 64) + layoutIcicle(children, 0, width, 0, chartHeight, 0, metric, &tiles) + return len(tiles) +} + func buildIcicleTree(dirs []DirSnapshot) *icicleNode { root := &icicleNode{ name: "/", diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go index e496776..a7d415d 100644 --- a/internal/tui/dashboard/model.go +++ b/internal/tui/dashboard/model.go @@ -219,7 +219,7 @@ func (m Model) handleStatsTick(msg messages.StatsTickMsg) (tea.Model, tea.Cmd) { m.syscallsOffset = clampOffset(m.syscallsOffset, m.maxSyscallsRows()) m.syscallsTreemapSelection = clampOffset(m.syscallsTreemapSelection, m.maxSyscallsRows()) m.filesOffset = clampOffset(m.filesOffset, m.maxFilesRows()) - m.filesDirOffset = clampOffset(m.filesDirOffset, m.maxFilesDirRows()) + m.filesDirOffset = clampOffset(m.filesDirOffset, m.maxFilesDirRowsForMode()) m.processesOffset = clampOffset(m.processesOffset, m.maxProcessesRows()) m.streamModel.Refresh() if m.refreshBubbleData() { @@ -407,7 +407,7 @@ func (m *Model) handleScrollKey(msg tea.KeyPressMsg) (bool, tea.Cmd) { return scrollOffset(keyStr, &m.syscallsOffset, m.maxSyscallsRows()), nil case TabFiles: if m.filesDirGrouped { - return scrollOffset(keyStr, &m.filesDirOffset, m.maxFilesDirRows()), nil + return scrollOffset(keyStr, &m.filesDirOffset, m.maxFilesDirRowsForMode()), nil } return scrollOffset(keyStr, &m.filesOffset, m.maxFilesRows()), nil case TabProcesses: @@ -470,6 +470,14 @@ func (m Model) maxFilesDirRows() int { return len(aggregateFilesByDir(m.latest.Files())) } +func (m Model) maxFilesDirRowsForMode() int { + if m.filesVizMode != tabVizModeIcicle { + return m.maxFilesDirRows() + } + width, height := flameViewport(m.width, m.height, m.showHelp) + return filesIcicleTileCount(m.latest, width, height, m.filesChart.Metric()) +} + func (m Model) maxProcessesRows() int { if m.latest == nil { return 0 diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go index 5a3be89..f01a701 100644 --- a/internal/tui/dashboard/model_test.go +++ b/internal/tui/dashboard/model_test.go @@ -492,6 +492,33 @@ func TestTreemapModeUsesJKForSelection(t *testing.T) { } } +func TestFilesIcicleModeSelectionUsesIcicleTileCount(t *testing.T) { + snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{ + {Path: "/a/b/c/file1", Accesses: 9}, + {Path: "/a/d/e/file2", Accesses: 7}, + }, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{}) + m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap()) + m.activeTab = TabFiles + m.latest = &snap + m.filesDirGrouped = true + m.filesVizMode = tabVizModeIcicle + m.width = 120 + m.height = 28 + + expectedMax := m.maxFilesDirRowsForMode() + if expectedMax <= m.maxFilesDirRows() { + t.Fatalf("expected icicle tile count to exceed grouped dir count: tiles=%d dirs=%d", expectedMax, m.maxFilesDirRows()) + } + + for i := 0; i < expectedMax+4; i++ { + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'j'}[0], Text: string([]rune{'j'})}) + m = next.(Model) + } + if m.filesDirOffset != expectedMax-1 { + t.Fatalf("expected icicle selection clamped by tile count to %d, got %d", expectedMax-1, m.filesDirOffset) + } +} + func TestTreemapModeRendersTreemapHeader(t *testing.T) { snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{ {Name: "read", Count: 9, Bytes: 512}, -- cgit v1.2.3