summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-08 11:26:11 +0200
committerPaul Buetow <paul@buetow.org>2026-03-08 11:26:11 +0200
commitf903279e8a872cd7c417f2f57bf306bfb3f3cb87 (patch)
tree03f68465d68cee8919f45f90c6cbc2ebde2a9b17
parent9cbf9ec8e9eac92431b9a742c1b625888cb69dfa (diff)
dashboard: clamp icicle selection by rendered tile count
-rw-r--r--internal/tui/dashboard/icicle.go31
-rw-r--r--internal/tui/dashboard/model.go12
-rw-r--r--internal/tui/dashboard/model_test.go27
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},