summaryrefslogtreecommitdiff
path: root/internal/tui
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
parentb3625cc67c81b4c1bd654a9fcdaf624d76306b07 (diff)
Improve TUI layout and increase sparkline height
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/common/keys.go6
-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
-rw-r--r--internal/tui/eventstream/render.go42
-rw-r--r--internal/tui/eventstream/render_test.go7
12 files changed, 76 insertions, 48 deletions
diff --git a/internal/tui/common/keys.go b/internal/tui/common/keys.go
index 1abf214..7b70d5a 100644
--- a/internal/tui/common/keys.go
+++ b/internal/tui/common/keys.go
@@ -33,8 +33,8 @@ func DefaultKeyMap() KeyMap {
Two: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "syscalls")),
Three: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "files")),
Four: key.NewBinding(key.WithKeys("4"), key.WithHelp("4", "processes")),
- Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "latency")),
- Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "gaps")),
+ Five: key.NewBinding(key.WithKeys("5"), key.WithHelp("5", "lat+gaps")),
+ Six: key.NewBinding(key.WithKeys("6"), key.WithHelp("6", "stream")),
Seven: key.NewBinding(key.WithKeys("7"), key.WithHelp("7", "stream")),
Export: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "export")),
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
@@ -64,7 +64,7 @@ func (k KeyMap) DashboardFullHelp() [][]key.Binding {
controls = append(controls, k.Refresh, k.Help, k.Quit)
return [][]key.Binding{
- {k.One, k.Two, k.Three, k.Four, k.Five, k.Six, k.Seven},
+ {k.One, k.Two, k.Three, k.Four, k.Five, k.Six},
controls,
{
helpTextBinding("left/right", "tab"),
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)
}
diff --git a/internal/tui/eventstream/render.go b/internal/tui/eventstream/render.go
index 1f748a3..e1781f8 100644
--- a/internal/tui/eventstream/render.go
+++ b/internal/tui/eventstream/render.go
@@ -94,30 +94,38 @@ func computeColumnLayout(width int) columnLayout {
width = 100
}
- gap := 8
- latency := 9
- comm := 14
- pidTid := 12
- syscall := 11
- ret := 6
- bytes := 10
+ // Keep non-file columns compact so file paths can use most of the row.
+ gap := 7
+ latency := 8
+ comm := 10
+ pidTid := 10
+ syscall := 9
+ ret := 5
+ bytes := 8
fixed := gap + latency + comm + pidTid + syscall + ret + bytes + 7
file := width - fixed
- if file >= 18 {
+ if file >= 28 {
+ // On wider terminals, give a little more room back to descriptive columns.
+ if width >= 140 {
+ comm = 12
+ syscall = 11
+ pidTid = 11
+ fixed = gap + latency + comm + pidTid + syscall + ret + bytes + 7
+ file = width - fixed
+ }
return columnLayout{gap: gap, latency: latency, comm: comm, pidTid: pidTid, syscall: syscall, ret: ret, bytes: bytes, file: file}
}
- if width < 90 {
- comm = 10
- syscall = 9
- } else {
- comm = 12
- syscall = 10
- }
+ // Very narrow widths: compress further but keep file column readable.
+ comm = 8
+ pidTid = 9
+ syscall = 8
+ ret = 4
+ bytes = 7
fixed = gap + latency + comm + pidTid + syscall + ret + bytes + 7
file = width - fixed
- if file < 8 {
- file = 8
+ if file < 12 {
+ file = 12
}
return columnLayout{gap: gap, latency: latency, comm: comm, pidTid: pidTid, syscall: syscall, ret: ret, bytes: bytes, file: file}
}
diff --git a/internal/tui/eventstream/render_test.go b/internal/tui/eventstream/render_test.go
index 89c2029..c7f32cd 100644
--- a/internal/tui/eventstream/render_test.go
+++ b/internal/tui/eventstream/render_test.go
@@ -93,3 +93,10 @@ func TestRenderEventRowIsSingleLineWithControlCharsAndLongValues(t *testing.T) {
t.Fatalf("expected truncation ellipsis in narrow row, got %q", row)
}
}
+
+func TestComputeColumnLayoutGivesFileMoreSpace(t *testing.T) {
+ cols := computeColumnLayout(120)
+ if cols.file < 55 {
+ t.Fatalf("expected file column to get most width, got %d", cols.file)
+ }
+}