summaryrefslogtreecommitdiff
path: root/internal/tui/dashboard
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-25 09:23:19 +0200
committerPaul Buetow <paul@buetow.org>2026-02-25 09:23:19 +0200
commit1279ffb8f2efba54ff005cce91ba65c149cb1ee6 (patch)
tree102483e8d836501b3b935e0674d6608fbe9f4f1f /internal/tui/dashboard
parentb3625cc67c81b4c1bd654a9fcdaf624d76306b07 (diff)
Improve TUI layout and increase sparkline height
Diffstat (limited to 'internal/tui/dashboard')
-rw-r--r--internal/tui/dashboard/files.go18
-rw-r--r--internal/tui/dashboard/files_test.go7
-rw-r--r--internal/tui/dashboard/histogram.go9
-rw-r--r--internal/tui/dashboard/model.go6
-rw-r--r--internal/tui/dashboard/model_test.go6
-rw-r--r--internal/tui/dashboard/overview.go3
-rw-r--r--internal/tui/dashboard/sparkline.go7
-rw-r--r--internal/tui/dashboard/tabs.go7
-rw-r--r--internal/tui/dashboard/tabs_test.go6
9 files changed, 41 insertions, 28 deletions
diff --git a/internal/tui/dashboard/files.go b/internal/tui/dashboard/files.go
index 945869e..faade8d 100644
--- a/internal/tui/dashboard/files.go
+++ b/internal/tui/dashboard/files.go
@@ -17,18 +17,19 @@ func renderFilesWithOffset(snap *statsengine.Snapshot, width, height, offset int
return "Files: waiting for stats..."
}
- rows := fileRows(snap.Files())
+ pathWidth := filePathWidth(width)
+ rows := fileRows(snap.Files(), pathWidth)
if len(rows) == 0 {
return "Files: no data"
}
columns := []table.Column{
- {Title: "Path", Width: filePathWidth(width)},
{Title: "Accesses", Width: 8},
{Title: "Read", Width: 9},
{Title: "Write", Width: 9},
{Title: "Avg Latency", Width: 11},
{Title: "Max Latency", Width: 11},
+ {Title: "Path", Width: pathWidth},
}
tbl := table.New(
@@ -43,16 +44,16 @@ func renderFilesWithOffset(snap *statsengine.Snapshot, width, height, offset int
return tbl.View() + fmt.Sprintf("\nRow %d/%d", cursor+1, len(rows))
}
-func fileRows(files []statsengine.FileSnapshot) []table.Row {
+func fileRows(files []statsengine.FileSnapshot, pathWidth int) []table.Row {
rows := make([]table.Row, 0, len(files))
for _, f := range files {
rows = append(rows, table.Row{
- truncatePathMiddle(f.Path, 48),
strconv.FormatUint(f.Accesses, 10),
formatBytes(float64(f.BytesRead)),
formatBytes(float64(f.BytesWritten)),
formatDurationNs(f.AvgLatencyNs),
formatDurationUintNs(f.MaxLatencyNs),
+ truncatePathMiddle(f.Path, pathWidth),
})
}
return rows
@@ -62,15 +63,12 @@ func filePathWidth(width int) int {
if width <= 0 {
return 24
}
- // Reserve enough room for non-path columns and table separators so
- // latency columns remain visible even on narrower terminals.
- w := width - 70
+ // Keep fixed metrics visible and let path consume the remaining space.
+ // Fixed columns sum to 48 chars; reserve extra for separators/padding.
+ w := width - 58
if w < 14 {
return 14
}
- if w > 52 {
- return 52
- }
return w
}
diff --git a/internal/tui/dashboard/files_test.go b/internal/tui/dashboard/files_test.go
index b0a5dbf..6d73b14 100644
--- a/internal/tui/dashboard/files_test.go
+++ b/internal/tui/dashboard/files_test.go
@@ -49,3 +49,10 @@ func TestTruncatePathMiddle(t *testing.T) {
t.Fatalf("expected head and tail preservation, got %q", got)
}
}
+
+func TestFilePathWidthExpandsOnWideTerminal(t *testing.T) {
+ got := filePathWidth(180)
+ if got <= 80 {
+ t.Fatalf("expected wide path column to use remaining space, got %d", got)
+ }
+}
diff --git a/internal/tui/dashboard/histogram.go b/internal/tui/dashboard/histogram.go
index b2bb88e..1e68a7b 100644
--- a/internal/tui/dashboard/histogram.go
+++ b/internal/tui/dashboard/histogram.go
@@ -29,6 +29,15 @@ func renderGapsTab(snap *statsengine.Snapshot, width, height int) string {
return strings.Join([]string{hist, spark}, "\n")
}
+func renderLatencyGapsTab(snap *statsengine.Snapshot, width, height int) string {
+ if snap == nil {
+ return common.PanelStyle.Render("Latency+Gaps: waiting for stats...")
+ }
+ lat := renderLatencyTab(snap, width, height)
+ gap := renderGapsTab(snap, width, height)
+ return strings.Join([]string{lat, gap}, "\n")
+}
+
func renderHistogram(hist statsengine.HistogramSnapshot, title string, width, height int) string {
buckets := hist.Buckets()
if len(buckets) == 0 {
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 407802f..78da351 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -127,7 +127,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.activeTab = TabLatency
handled = true
case key.Matches(msg, m.keys.Six):
- m.activeTab = TabGaps
+ m.activeTab = TabStream
handled = true
case key.Matches(msg, m.keys.Seven):
m.activeTab = TabStream
@@ -278,9 +278,7 @@ func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstre
case TabProcesses:
return renderProcessesWithOffset(snap, width, height, processesOffset)
case TabLatency:
- return renderLatencyTab(snap, width, height)
- case TabGaps:
- return renderGapsTab(snap, width, height)
+ return renderLatencyGapsTab(snap, width, height)
default:
return common.PanelStyle.Render("Unknown tab")
}
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index b0ce933..29b698d 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -47,6 +47,12 @@ func TestKeySwitchingChangesActiveTab(t *testing.T) {
if model.activeTab != TabStream {
t.Fatalf("expected stream tab on key 7, got %v", model.activeTab)
}
+
+ next, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'6'}})
+ model = next.(Model)
+ if model.activeTab != TabStream {
+ t.Fatalf("expected stream tab on key 6, got %v", model.activeTab)
+ }
}
func TestArrowAndViKeysCycleTabs(t *testing.T) {
diff --git a/internal/tui/dashboard/overview.go b/internal/tui/dashboard/overview.go
index 1bdb64f..9feafab 100644
--- a/internal/tui/dashboard/overview.go
+++ b/internal/tui/dashboard/overview.go
@@ -214,8 +214,5 @@ func sparklineWidth(width int) int {
if w < 8 {
return 8
}
- if w > 80 {
- return 80
- }
return w
}
diff --git a/internal/tui/dashboard/sparkline.go b/internal/tui/dashboard/sparkline.go
index 1531ca6..9c1f2c4 100644
--- a/internal/tui/dashboard/sparkline.go
+++ b/internal/tui/dashboard/sparkline.go
@@ -11,8 +11,10 @@ func renderSparkline(data []float64, width int) string {
samples := sampleForWidth(data, width)
min, max := minMax(samples)
+ line := ""
if min == max {
- return repeatRune('▄', len(samples))
+ line = repeatRune('▄', len(samples))
+ return line + "\n" + line
}
out := make([]rune, len(samples))
@@ -28,7 +30,8 @@ func renderSparkline(data []float64, width int) string {
}
out[i] = sparkChars[idx]
}
- return string(out)
+ line = string(out)
+ return line + "\n" + line
}
func sampleForWidth(data []float64, width int) []float64 {
diff --git a/internal/tui/dashboard/tabs.go b/internal/tui/dashboard/tabs.go
index 9aae218..a2fe366 100644
--- a/internal/tui/dashboard/tabs.go
+++ b/internal/tui/dashboard/tabs.go
@@ -22,8 +22,6 @@ const (
TabProcesses
// TabLatency is the latency histogram tab.
TabLatency
- // TabGaps is the inter-syscall gap tab.
- TabGaps
// TabStream is the live event stream tab.
TabStream
)
@@ -34,7 +32,6 @@ var allTabs = []Tab{
TabFiles,
TabProcesses,
TabLatency,
- TabGaps,
TabStream,
}
@@ -49,9 +46,7 @@ func (t Tab) String() string {
case TabProcesses:
return "Processes"
case TabLatency:
- return "Latency"
- case TabGaps:
- return "Gaps"
+ return "Latency+Gaps"
case TabStream:
return "Stream"
default:
diff --git a/internal/tui/dashboard/tabs_test.go b/internal/tui/dashboard/tabs_test.go
index d40cc68..0fc36f2 100644
--- a/internal/tui/dashboard/tabs_test.go
+++ b/internal/tui/dashboard/tabs_test.go
@@ -6,8 +6,8 @@ import (
)
func TestTabNavigationWraps(t *testing.T) {
- if got := nextTab(TabGaps); got != TabStream {
- t.Fatalf("expected next after gaps to be stream, got %v", got)
+ if got := nextTab(TabLatency); got != TabStream {
+ t.Fatalf("expected next after latency+gaps to be stream, got %v", got)
}
if got := nextTab(TabStream); got != TabOverview {
t.Fatalf("expected wrap to overview from stream, got %v", got)
@@ -19,7 +19,7 @@ func TestTabNavigationWraps(t *testing.T) {
func TestRenderTabBarContainsLabels(t *testing.T) {
out := renderTabBar(TabOverview, 80)
- for _, label := range []string{"Overview", "Syscalls", "Files", "Processes", "Latency", "Gaps", "Stream"} {
+ for _, label := range []string{"Overview", "Syscalls", "Files", "Processes", "Latency+Gaps", "Stream"} {
if !strings.Contains(out, label) {
t.Fatalf("expected tab label %q in tab bar", label)
}