diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-08 08:59:02 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-08 08:59:02 +0200 |
| commit | 77b993ded6e8cfa15e053a09ef79581afd0b7e4b (patch) | |
| tree | d72b0a66152bfb2fd9404f47101fe7159ada33fb | |
| parent | 9950c77981ce06be34e877a6729abb23a36789c6 (diff) | |
dashboard: wire files icicle mode and root path labels
| -rw-r--r-- | internal/tui/dashboard/bubbles.go | 11 | ||||
| -rw-r--r-- | internal/tui/dashboard/icicle.go | 4 | ||||
| -rw-r--r-- | internal/tui/dashboard/model.go | 5 | ||||
| -rw-r--r-- | internal/tui/dashboard/model_test.go | 27 | ||||
| -rw-r--r-- | internal/tui/dashboard/pathlabel.go | 17 | ||||
| -rw-r--r-- | internal/tui/dashboard/pathlabel_test.go | 81 | ||||
| -rw-r--r-- | internal/tui/dashboard/treemap.go | 12 |
7 files changed, 141 insertions, 16 deletions
diff --git a/internal/tui/dashboard/bubbles.go b/internal/tui/dashboard/bubbles.go index 0ec91a9..e6e2909 100644 --- a/internal/tui/dashboard/bubbles.go +++ b/internal/tui/dashboard/bubbles.go @@ -5,7 +5,6 @@ import ( "hash/fnv" "image/color" "math" - "path/filepath" "sort" "strings" "unicode/utf8" @@ -587,6 +586,7 @@ func renderBubbleRow(cells []bubbleCell, palette []color.Color) string { } var b strings.Builder styleCache := make(map[string]lipgloss.Style, 8) + selectedColor := lipgloss.Color("129") for _, cell := range cells { if cell.colorSlot < 0 { if cell.bold { @@ -605,6 +605,9 @@ func renderBubbleRow(cells []bubbleCell, palette []color.Color) string { if !ok { style = lipgloss.NewStyle().Foreground(palette[slot]) if cell.bold { + style = style.Foreground(selectedColor) + } + if cell.bold { style = style.Bold(true) } styleCache[key] = style @@ -795,15 +798,11 @@ func filesDirBubbleData(snap *statsengine.Snapshot) []bubbleDatum { dirs := aggregateFilesByDir(snap.Files()) data := make([]bubbleDatum, 0, len(dirs)) for _, dir := range dirs { - label := filepath.Base(dir.Dir) - if label == "." || label == "/" || label == "" { - label = dir.Dir - } totalBytes := dir.BytesRead + dir.BytesWritten detail := fmt.Sprintf("dir %s, files %d, read %s, write %s", dir.Dir, dir.FileCount, formatBytes(float64(dir.BytesRead)), formatBytes(float64(dir.BytesWritten))) data = append(data, bubbleDatum{ ID: dir.Dir, - Label: label, + Label: rootPathLabelFromFSPath(dir.Dir), Count: dir.Accesses, Bytes: totalBytes, Detail: detail, diff --git a/internal/tui/dashboard/icicle.go b/internal/tui/dashboard/icicle.go index d868ae3..3761fc8 100644 --- a/internal/tui/dashboard/icicle.go +++ b/internal/tui/dashboard/icicle.go @@ -239,7 +239,7 @@ func drawIcicleLabel(grid [][]treemapCell, tile icicleTile, selected bool) { } width := len(grid[0]) maxLabel := tile.w - 1 - label := abbreviateTreemapLabel(tile.node.name, maxLabel) + label := abbreviateTreemapLabel(rootPathLabelFromFSPath(tile.node.fullPath), maxLabel) col := tile.x for _, r := range []rune(label) { if col < 0 { @@ -273,7 +273,7 @@ func icicleStatusLine(tiles []icicleTile, selected int, metric bubbleMetric) str "sel:%d/%d %s | %s=%s | accesses=%d | bytes=%s", selected+1, len(tiles), - tile.node.fullPath, + rootPathLabelFromFSPath(tile.node.fullPath), treemapMetricLabel(metric), metricText, tile.node.accesses, diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go index 5949755..e464af8 100644 --- a/internal/tui/dashboard/model.go +++ b/internal/tui/dashboard/model.go @@ -526,6 +526,9 @@ func (m Model) renderActiveContent(width, activeHeight int, streamModel *eventst if m.activeTab == TabFiles && m.filesVizMode == tabVizModeTreemap && m.filesDirGrouped { return renderFilesTreemap(m.latest, width, activeHeight, m.filesChart.Metric(), m.filesDirOffset, m.isDark) } + if m.activeTab == TabFiles && m.filesVizMode == tabVizModeIcicle && m.filesDirGrouped { + return renderFilesIcicle(m.latest, width, activeHeight, m.filesChart.Metric(), m.filesDirOffset, m.isDark) + } if m.activeTab == TabProcesses && m.processesVizMode == tabVizModeTreemap { return renderProcessesTreemap(m.latest, width, activeHeight, m.processesChart.Metric(), m.processesOffset, m.isDark) } @@ -733,7 +736,7 @@ func (m Model) allowedVizModes(tab Tab) []tabVizMode { return []tabVizMode{tabVizModeTable, tabVizModeBubbles, tabVizModeTreemap} case TabFiles: if m.filesDirGrouped { - return []tabVizMode{tabVizModeTable, tabVizModeBubbles, tabVizModeTreemap} + return []tabVizMode{tabVizModeTable, tabVizModeBubbles, tabVizModeTreemap, tabVizModeIcicle} } return []tabVizMode{tabVizModeTable} default: diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go index 8b03b2b..5a3be89 100644 --- a/internal/tui/dashboard/model_test.go +++ b/internal/tui/dashboard/model_test.go @@ -402,7 +402,7 @@ func TestMetricToggleAppliesInFilesTreemapMode(t *testing.T) { } } -func TestFilesTreemapRequiresDirectoryMode(t *testing.T) { +func TestFilesVisualizationRequiresDirectoryMode(t *testing.T) { snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{ {Path: "/tmp/a", Accesses: 3}, {Path: "/tmp/b", Accesses: 1}, @@ -437,6 +437,12 @@ func TestFilesTreemapRequiresDirectoryMode(t *testing.T) { next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'v'}[0], Text: string([]rune{'v'})}) model = next.(Model) + if got := model.filesVizMode; got != tabVizModeIcicle { + t.Fatalf("expected files icicle mode enabled in directory mode") + } + + next, _ = model.Update(tea.KeyPressMsg{Code: []rune{'v'}[0], Text: string([]rune{'v'})}) + model = next.(Model) if got := model.filesVizMode; got != tabVizModeTable { t.Fatalf("expected files mode cycled back to table") } @@ -523,6 +529,25 @@ func TestTreemapModeRendersFilesHeader(t *testing.T) { } } +func TestIcicleModeRendersFilesHeader(t *testing.T) { + snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{ + {Path: "/srv/log/a", Accesses: 9, BytesRead: 400, BytesWritten: 200}, + {Path: "/srv/log/b", Accesses: 4, BytesRead: 100, BytesWritten: 40}, + }, 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 + + out := m.View().Content + if !strings.Contains(out, "Files icicle") { + t.Fatalf("expected icicle header in files view") + } +} + func TestTreemapModeRendersProcessesHeader(t *testing.T) { snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{ {PID: 10, Comm: "worker", Syscalls: 12, Bytes: 500}, diff --git a/internal/tui/dashboard/pathlabel.go b/internal/tui/dashboard/pathlabel.go new file mode 100644 index 0000000..4782dc0 --- /dev/null +++ b/internal/tui/dashboard/pathlabel.go @@ -0,0 +1,17 @@ +package dashboard + +import ( + "path/filepath" + "strings" +) + +func rootPathLabelFromFSPath(path string) string { + cleaned := filepath.ToSlash(filepath.Clean(strings.TrimSpace(path))) + if cleaned == "" || cleaned == "." || cleaned == "/" { + return "root" + } + if strings.HasPrefix(cleaned, "/") { + return "root" + cleaned + } + return "root/" + cleaned +} diff --git a/internal/tui/dashboard/pathlabel_test.go b/internal/tui/dashboard/pathlabel_test.go new file mode 100644 index 0000000..66d3ff0 --- /dev/null +++ b/internal/tui/dashboard/pathlabel_test.go @@ -0,0 +1,81 @@ +package dashboard + +import ( + "testing" + + "ior/internal/statsengine" +) + +func TestRootPathLabelFromFSPath(t *testing.T) { + cases := []struct { + in string + want string + }{ + {in: "", want: "root"}, + {in: "/", want: "root"}, + {in: "/var/log", want: "root/var/log"}, + {in: "var/log", want: "root/var/log"}, + {in: "./tmp", want: "root/tmp"}, + } + for _, tc := range cases { + if got := rootPathLabelFromFSPath(tc.in); got != tc.want { + t.Fatalf("rootPathLabelFromFSPath(%q)=%q want %q", tc.in, got, tc.want) + } + } +} + +func TestTreemapRootPathTextDirectoryOnly(t *testing.T) { + snap := statsengine.NewSnapshot( + nil, + nil, + nil, + []statsengine.SyscallSnapshot{{Name: "read", Count: 3, Bytes: 12}}, + []statsengine.FileSnapshot{{Path: "/var/log/app.log", Accesses: 4, BytesRead: 16, BytesWritten: 8}}, + []statsengine.ProcessSnapshot{{PID: 42, Comm: "proc", Syscalls: 5, Bytes: 20}}, + statsengine.HistogramSnapshot{}, + statsengine.HistogramSnapshot{}, + ) + + sysItems := buildSyscallTreemapItems(&snap, bubbleMetricCount) + if len(sysItems) == 0 || sysItems[0].Name != "read" { + t.Fatalf("expected syscall treemap label to stay native, got %#v", sysItems) + } + + fileItems := buildFilesTreemapItems(&snap, bubbleMetricCount) + if len(fileItems) == 0 || fileItems[0].Name != "root/var/log" { + t.Fatalf("expected files treemap label root path, got %#v", fileItems) + } + + procItems := buildProcessesTreemapItems(&snap, bubbleMetricCount) + if len(procItems) == 0 || procItems[0].Name != "42:proc" { + t.Fatalf("expected process treemap label to stay native, got %#v", procItems) + } +} + +func TestBubbleRootPathTextDirectoryOnly(t *testing.T) { + snap := statsengine.NewSnapshot( + nil, + nil, + nil, + []statsengine.SyscallSnapshot{{Name: "write", Count: 2, Bytes: 64}}, + []statsengine.FileSnapshot{{Path: "/home/paul/.config/a", Accesses: 1, BytesRead: 5, BytesWritten: 7}}, + []statsengine.ProcessSnapshot{{PID: 7, Comm: "worker", Syscalls: 9, Bytes: 70}}, + statsengine.HistogramSnapshot{}, + statsengine.HistogramSnapshot{}, + ) + + sys := syscallBubbleData(&snap) + if len(sys) == 0 || sys[0].Label != "write" { + t.Fatalf("expected syscall bubble label to stay native, got %#v", sys) + } + + files := filesDirBubbleData(&snap) + if len(files) == 0 || files[0].Label != "root/home/paul/.config" { + t.Fatalf("expected files bubble label full root path, got %#v", files) + } + + procs := processBubbleData(&snap) + if len(procs) == 0 || procs[0].Label != "7:worker" { + t.Fatalf("expected process bubble label to stay native, got %#v", procs) + } +} diff --git a/internal/tui/dashboard/treemap.go b/internal/tui/dashboard/treemap.go index 24b7f55..7193952 100644 --- a/internal/tui/dashboard/treemap.go +++ b/internal/tui/dashboard/treemap.go @@ -4,7 +4,6 @@ import ( "fmt" "image/color" "math" - "path/filepath" "sort" "strings" "unicode/utf8" @@ -151,13 +150,10 @@ func buildFilesTreemapItems(snap *statsengine.Snapshot, metric bubbleMetric) []s dirs := aggregateFilesByDir(snap.Files()) items := make([]syscallTreemapItem, 0, len(dirs)) for _, dir := range dirs { - label := filepath.Base(dir.Dir) - if label == "." || label == "/" || label == "" { - label = dir.Dir - } + pathLabel := rootPathLabelFromFSPath(dir.Dir) totalBytes := dir.BytesRead + dir.BytesWritten item := syscallTreemapItem{ - Name: label, + Name: pathLabel, Count: dir.Accesses, Bytes: totalBytes, Detail: fmt.Sprintf( @@ -485,6 +481,7 @@ func renderTreemapRow(cells []treemapCell, palette []color.Color) string { } var b strings.Builder styleCache := make(map[string]lipgloss.Style, 8) + selectedColor := lipgloss.Color("129") for _, cell := range cells { if cell.colorSlot < 0 { if cell.bold { @@ -503,6 +500,9 @@ func renderTreemapRow(cells []treemapCell, palette []color.Color) string { if !ok { style = lipgloss.NewStyle().Foreground(palette[slot]) if cell.bold { + style = style.Foreground(selectedColor) + } + if cell.bold { style = style.Bold(true) } styleCache[key] = style |
