summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-26 09:47:28 +0200
committerPaul Buetow <paul@buetow.org>2026-02-26 09:47:28 +0200
commit81ffb947201690088ef25a1839a8993bbfc27f03 (patch)
tree948400add2b7df214c1587f04fe4ec9bd51c439a /internal/tui
parentad4d7fca20d80f71ccabef3281e3f80081f4db62 (diff)
tui: fix responsive layout and stream viewport chrome
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/common/styles.go4
-rw-r--r--internal/tui/dashboard/histogram.go15
-rw-r--r--internal/tui/dashboard/histogram_test.go31
-rw-r--r--internal/tui/dashboard/layout.go3
-rw-r--r--internal/tui/dashboard/model.go23
-rw-r--r--internal/tui/dashboard/model_test.go22
-rw-r--r--internal/tui/dashboard/overview.go46
-rw-r--r--internal/tui/dashboard/overview_test.go23
-rw-r--r--internal/tui/dashboard/sparkline.go22
-rw-r--r--internal/tui/dashboard/sparkline_test.go8
-rw-r--r--internal/tui/dashboard/tabs.go103
-rw-r--r--internal/tui/dashboard/tabs_test.go23
12 files changed, 269 insertions, 54 deletions
diff --git a/internal/tui/common/styles.go b/internal/tui/common/styles.go
index ed6a191..d4c75ff 100644
--- a/internal/tui/common/styles.go
+++ b/internal/tui/common/styles.go
@@ -16,8 +16,7 @@ var (
var (
// ScreenStyle is the base style for full-screen models.
ScreenStyle = lipgloss.NewStyle().
- Foreground(ColorText).
- Background(ColorBackground)
+ Foreground(ColorText)
// HeaderStyle is used by top-level titles and screen headers.
HeaderStyle = lipgloss.NewStyle().
@@ -34,7 +33,6 @@ var (
// TabInactiveStyle is applied to non-selected tabs.
TabInactiveStyle = lipgloss.NewStyle().
Foreground(ColorMuted).
- Background(ColorPanel).
Padding(0, 1)
// PanelStyle is used for boxed sections.
diff --git a/internal/tui/dashboard/histogram.go b/internal/tui/dashboard/histogram.go
index 47942fe..7613230 100644
--- a/internal/tui/dashboard/histogram.go
+++ b/internal/tui/dashboard/histogram.go
@@ -14,8 +14,11 @@ func renderLatencyTab(snap *statsengine.Snapshot, width, height int) string {
return common.PanelStyle.Render("Latency: waiting for stats...")
}
+ panelInner := panelInnerWidth(width)
hist := renderHistogram(snap.LatencyHistogram, "Latency Histogram", width, height)
- spark := common.PanelStyle.Render(renderLabeledSparkline("Latency sparkline:", snap.LatencySeriesNs(), sparklineWidth(width)))
+ spark := common.PanelStyle.Width(panelInner).Render(
+ renderOverviewSparkline("Latency sparkline:", snap.LatencySeriesNs(), panelInner),
+ )
return strings.Join([]string{hist, spark}, "\n")
}
@@ -24,8 +27,11 @@ func renderGapsTab(snap *statsengine.Snapshot, width, height int) string {
return common.PanelStyle.Render("Gaps: waiting for stats...")
}
+ panelInner := panelInnerWidth(width)
hist := renderHistogram(snap.GapHistogram, "Gap Histogram", width, height)
- spark := common.PanelStyle.Render(renderLabeledSparkline("Gap sparkline:", snap.GapSeriesNs(), sparklineWidth(width)))
+ spark := common.PanelStyle.Width(panelInner).Render(
+ renderOverviewSparkline("Gap sparkline:", snap.GapSeriesNs(), panelInner),
+ )
return strings.Join([]string{hist, spark}, "\n")
}
@@ -47,6 +53,7 @@ func renderHistogram(hist statsengine.HistogramSnapshot, title string, width, he
if width <= 0 {
width = 80
}
+ panelInner := panelInnerWidth(width)
if height > 0 {
maxRows := height - 3
@@ -73,7 +80,7 @@ func renderHistogram(hist statsengine.HistogramSnapshot, title string, width, he
}
}
- barWidth := width - labelWidth - countWidth - 10
+ barWidth := panelInner - labelWidth - countWidth - 6
if barWidth < 8 {
barWidth = 8
}
@@ -86,7 +93,7 @@ func renderHistogram(hist statsengine.HistogramSnapshot, title string, width, he
}
lines = append(lines, "Scale: █▓▒░")
- return common.PanelStyle.Render(strings.Join(lines, "\n"))
+ return common.PanelStyle.Width(panelInner).Render(strings.Join(lines, "\n"))
}
func renderHistogramBar(count, maxCount uint64, width int) string {
diff --git a/internal/tui/dashboard/histogram_test.go b/internal/tui/dashboard/histogram_test.go
index 9da1c47..7790394 100644
--- a/internal/tui/dashboard/histogram_test.go
+++ b/internal/tui/dashboard/histogram_test.go
@@ -5,6 +5,8 @@ import (
"testing"
"ior/internal/statsengine"
+
+ "github.com/charmbracelet/lipgloss"
)
func TestRenderHistogramNoBuckets(t *testing.T) {
@@ -74,3 +76,32 @@ func TestRenderHistogramTruncatesForSmallHeight(t *testing.T) {
t.Fatalf("expected histogram rows to be truncated for small height: %q", out)
}
}
+
+func TestRenderLatencyGapsTabDoesNotOverflowWidth(t *testing.T) {
+ snap := statsengine.NewSnapshot(
+ []float64{10, 20, 15, 30, 18, 35},
+ []float64{2, 4, 3, 5, 7, 6},
+ nil,
+ nil,
+ nil,
+ nil,
+ statsengine.NewHistogramSnapshot(6, []statsengine.HistogramBucketSnapshot{
+ {Label: "[0,1us)", Count: 1},
+ {Label: "[1us,10us)", Count: 2},
+ {Label: "[10us,100us)", Count: 3},
+ }),
+ statsengine.NewHistogramSnapshot(6, []statsengine.HistogramBucketSnapshot{
+ {Label: "[0,1us)", Count: 1},
+ {Label: "[1us,10us)", Count: 2},
+ {Label: "[10us,100us)", Count: 3},
+ }),
+ )
+
+ const width = 100
+ out := renderLatencyGapsTab(&snap, width, 24)
+ for _, line := range strings.Split(out, "\n") {
+ if lipgloss.Width(line) > width {
+ t.Fatalf("latency/gaps line exceeds width %d: got %d in %q", width, lipgloss.Width(line), line)
+ }
+ }
+}
diff --git a/internal/tui/dashboard/layout.go b/internal/tui/dashboard/layout.go
new file mode 100644
index 0000000..38cce18
--- /dev/null
+++ b/internal/tui/dashboard/layout.go
@@ -0,0 +1,3 @@
+package dashboard
+
+const panelHorizontalChrome = 4
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 9b425b1..2ed53a1 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -14,6 +14,7 @@ import (
const defaultRefreshMs = 1000
const streamRefreshMs = 200
+const streamChromeRows = 4
// SnapshotSource is the dashboard data source.
type SnapshotSource interface {
@@ -73,7 +74,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
- m.streamModel.SetViewport(msg.Width, msg.Height)
+ streamWidth, streamHeight := streamViewport(msg.Width, msg.Height)
+ m.streamModel.SetViewport(streamWidth, streamHeight)
return m, nil
case refreshTickMsg:
snap := m.snapshot()
@@ -188,7 +190,7 @@ func (m *Model) handleScrollKey(msg tea.KeyMsg) bool {
case TabProcesses:
return scrollOffset(keyStr, &m.processesOffset, m.maxProcessesRows())
case TabStream:
- streamWidth, streamHeight := common.EffectiveViewport(m.width, m.height)
+ streamWidth, streamHeight := streamViewport(m.width, m.height)
m.streamModel.SetViewport(streamWidth, streamHeight)
return m.streamModel.HandleTeaKey(msg)
default:
@@ -267,6 +269,10 @@ func (m *Model) SetStreamSource(source *eventstream.RingBuffer) {
// View renders the tab bar, active tab scaffold, and help bar.
func (m Model) View() string {
width, height := common.EffectiveViewport(m.width, m.height)
+ activeHeight := height
+ if m.activeTab == TabStream {
+ _, activeHeight = streamViewport(width, height)
+ }
var b strings.Builder
b.WriteString(renderTabBar(m.activeTab, width))
@@ -276,7 +282,7 @@ func (m Model) View() string {
m.latest,
&m.streamModel,
width,
- height,
+ activeHeight,
m.syscallsOffset,
m.filesOffset,
m.filesDirGrouped,
@@ -286,7 +292,7 @@ func (m Model) View() string {
b.WriteString("\n")
b.WriteString(common.HighlightStyle.Render("Press ? for help"))
b.WriteString("\n")
- b.WriteString(renderHelpBar(m.keys))
+ b.WriteString(renderHelpBar(m.keys, width))
return common.ScreenStyle.Render(b.String())
}
@@ -328,3 +334,12 @@ func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstre
func streamTickCmd() tea.Cmd {
return tea.Tick(streamRefreshMs*time.Millisecond, func(time.Time) tea.Msg { return streamTickMsg{} })
}
+
+func streamViewport(width, height int) (int, int) {
+ width, height = common.EffectiveViewport(width, height)
+ height -= streamChromeRows
+ if height < 1 {
+ height = 1
+ }
+ return width, height
+}
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index 1e54b27..6baa62c 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -390,3 +390,25 @@ func TestRenderActiveTabUsesDirectoryFilesViewWhenGrouped(t *testing.T) {
t.Fatalf("expected grouped directory files view header, got %q", out)
}
}
+
+func TestStreamTabViewKeepsTabAndHelpChromeVisible(t *testing.T) {
+ rb := eventstream.NewRingBuffer()
+ for i := 0; i < 200; i++ {
+ rb.Push(eventstream.StreamEvent{Syscall: "read"})
+ }
+
+ m := NewModelWithConfig(nil, rb, 1000, common.DefaultKeyMap())
+ m.activeTab = TabStream
+ m.width = 120
+ m.height = 30
+ m.streamModel.SetSource(rb)
+ m.streamModel.Refresh()
+
+ out := m.View()
+ if !strings.Contains(out, "1:Overview") {
+ t.Fatalf("expected tab bar to remain visible in stream view")
+ }
+ if !strings.Contains(out, "Press ? for help") {
+ t.Fatalf("expected help hint to remain visible in stream view")
+ }
+}
diff --git a/internal/tui/dashboard/overview.go b/internal/tui/dashboard/overview.go
index f4ec5bc..9a77da0 100644
--- a/internal/tui/dashboard/overview.go
+++ b/internal/tui/dashboard/overview.go
@@ -6,6 +6,7 @@ import (
common "ior/internal/tui/common"
"strings"
"time"
+ "unicode/utf8"
"github.com/charmbracelet/lipgloss"
)
@@ -32,16 +33,17 @@ func renderOverview(snap *statsengine.Snapshot, width, height int) string {
trendWithArrow(snap.ThroughputTrend),
)
- latencySpark := renderLabeledSparkline("Latency:", snap.LatencySeriesNs(), sparklineWidth(width))
- gapSpark := renderLabeledSparkline("Gap:", snap.GapSeriesNs(), sparklineWidth(width))
- throughputSpark := renderLabeledSparkline("Throughput:", snap.ThroughputSeriesB(), sparklineWidth(width))
+ panelInner := panelInnerWidth(width)
+ latencySpark := renderOverviewSparkline("Latency:", snap.LatencySeriesNs(), panelInner)
+ gapSpark := renderOverviewSparkline("Gap:", snap.GapSeriesNs(), panelInner)
+ throughputSpark := renderOverviewSparkline("Throughput:", snap.ThroughputSeriesB(), panelInner)
topSyscalls := "Top syscalls: " + summarizeTopSyscalls(snap)
topFiles := "Top files: " + summarizeTopFiles(snap)
topProcesses := "Top processes: " + summarizeTopProcesses(snap)
latencyHist := "Latency buckets: " + summarizeHistogramBrief(snap.LatencyHistogram)
gapHist := "Gap buckets: " + summarizeHistogramBrief(snap.GapHistogram)
- panel := common.PanelStyle.Width(width)
+ panel := common.PanelStyle.Width(panelInner)
sparkPanel := panel.Render(strings.Join([]string{latencySpark, "", gapSpark, "", throughputSpark}, "\n"))
topPanel := panel.Render(strings.Join([]string{topSyscalls, topFiles, topProcesses}, "\n"))
histPanel := panel.Render(strings.Join([]string{latencyHist, gapHist}, "\n"))
@@ -70,7 +72,7 @@ func renderSyscallBox(snap *statsengine.Snapshot, width int) string {
snap.SyscallRatePerSec,
generatedAt,
)
- return common.PanelStyle.Width(width).Height(5).Render(content)
+ return common.PanelStyle.Width(summaryBoxInnerWidth(width)).Height(5).Render(content)
}
func renderBytesBox(snap *statsengine.Snapshot, width int) string {
@@ -80,7 +82,7 @@ func renderBytesBox(snap *statsengine.Snapshot, width int) string {
formatBytes(snap.WriteBytesPerSec),
formatBytes(float64(snap.TotalBytes)),
)
- return common.PanelStyle.Width(width).Height(5).Render(content)
+ return common.PanelStyle.Width(summaryBoxInnerWidth(width)).Height(5).Render(content)
}
func renderErrorBox(snap *statsengine.Snapshot, width int) string {
@@ -96,7 +98,7 @@ func renderErrorBox(snap *statsengine.Snapshot, width int) string {
snap.LatencyMeanNs,
snap.GapMeanNs,
)
- return common.PanelStyle.Width(width).Height(5).Render(content)
+ return common.PanelStyle.Width(summaryBoxInnerWidth(width)).Height(5).Render(content)
}
func trendWithArrow(trend statsengine.Trend) string {
@@ -202,20 +204,36 @@ func summaryBoxWidth(width int) int {
if width <= 0 {
return 24
}
- w := (width - 4) / 3
+ w := width / 3
if w < 18 {
return 18
}
return w
}
-func sparklineWidth(width int) int {
- if width <= 0 {
- return 20
+func summaryBoxInnerWidth(width int) int {
+ inner := width - panelHorizontalChrome
+ if inner < 14 {
+ return 14
}
- w := width - 14
+ return inner
+}
+
+func renderOverviewSparkline(label string, data []float64, panelInner int) string {
+ w := panelInner - utf8.RuneCountInString(label) - 1
if w < 8 {
- return 8
+ w = 8
}
- return w
+ return renderLabeledSparkline(label, data, w)
+}
+
+func panelInnerWidth(width int) int {
+ if width <= 0 {
+ width = 80
+ }
+ inner := width - panelHorizontalChrome
+ if inner < 20 {
+ return 20
+ }
+ return inner
}
diff --git a/internal/tui/dashboard/overview_test.go b/internal/tui/dashboard/overview_test.go
index cee6cf2..89f00fb 100644
--- a/internal/tui/dashboard/overview_test.go
+++ b/internal/tui/dashboard/overview_test.go
@@ -6,6 +6,8 @@ import (
"time"
"ior/internal/statsengine"
+
+ "github.com/charmbracelet/lipgloss"
)
func TestRenderOverviewIncludesCoreMetrics(t *testing.T) {
@@ -94,3 +96,24 @@ func TestOverviewSummariesIncludeFilesProcessesAndHistograms(t *testing.T) {
}
}
}
+
+func TestRenderOverviewDoesNotOverflowWidth(t *testing.T) {
+ snap := statsengine.NewSnapshot(
+ []float64{10, 20, 15, 30, 18, 35, 40},
+ []float64{2, 4, 3, 5, 7, 6, 8},
+ []float64{1024, 2048, 4096, 2048, 1024},
+ []statsengine.SyscallSnapshot{{Name: "read", Count: 20}},
+ []statsengine.FileSnapshot{{Path: "/tmp/very/long/path/to/file.log", Accesses: 10}},
+ []statsengine.ProcessSnapshot{{PID: 42, Comm: "proc", Syscalls: 30}},
+ statsengine.HistogramSnapshot{},
+ statsengine.HistogramSnapshot{},
+ )
+
+ const width = 120
+ out := renderOverview(&snap, width, 30)
+ for _, line := range strings.Split(out, "\n") {
+ if lipgloss.Width(line) > width {
+ t.Fatalf("overview line exceeds width %d: got %d in %q", width, lipgloss.Width(line), line)
+ }
+ }
+}
diff --git a/internal/tui/dashboard/sparkline.go b/internal/tui/dashboard/sparkline.go
index fad86e2..5cc445d 100644
--- a/internal/tui/dashboard/sparkline.go
+++ b/internal/tui/dashboard/sparkline.go
@@ -11,23 +11,15 @@ func renderSparkline(data []float64, width int) string {
}
samples := sampleForWidth(data, width)
- leftPad := 0
- if len(samples) < width {
- leftPad = width - len(samples)
- }
min, max := minMax(samples)
if min == max {
top := repeatRune(' ', width)
- bottom := repeatRune(' ', leftPad) + repeatRune('█', len(samples))
+ bottom := repeatRune('█', width)
return top + "\n" + bottom
}
top := make([]rune, width)
bottom := make([]rune, width)
- for i := 0; i < leftPad; i++ {
- top[i] = ' '
- bottom[i] = ' '
- }
scale := 16.0
denom := max - min
for i, value := range samples {
@@ -51,7 +43,7 @@ func renderSparkline(data []float64, width int) string {
bottomLevel = 1
}
- col := leftPad + i
+ col := i
top[col] = sparkRowChars[topLevel]
bottom[col] = sparkRowChars[bottomLevel]
}
@@ -72,12 +64,16 @@ func renderLabeledSparkline(label string, data []float64, width int) string {
}
func sampleForWidth(data []float64, width int) []float64 {
- if width >= len(data) {
- return append([]float64(nil), data...)
- }
if width == 1 {
return []float64{data[len(data)-1]}
}
+ if len(data) == 1 {
+ out := make([]float64, width)
+ for i := range out {
+ out[i] = data[0]
+ }
+ return out
+ }
last := len(data) - 1
samples := make([]float64, width)
diff --git a/internal/tui/dashboard/sparkline_test.go b/internal/tui/dashboard/sparkline_test.go
index e1fb316..66d1673 100644
--- a/internal/tui/dashboard/sparkline_test.go
+++ b/internal/tui/dashboard/sparkline_test.go
@@ -16,7 +16,7 @@ func TestRenderSparklineEmptyOrInvalidWidth(t *testing.T) {
func TestRenderSparklineSingleValue(t *testing.T) {
got := renderSparkline([]float64{10}, 8)
- if got != " \n █" {
+ if got != " \n████████" {
t.Fatalf("expected two-line constant sparkline, got %q", got)
}
}
@@ -28,14 +28,14 @@ func TestRenderSparklineAllEqualValues(t *testing.T) {
}
}
-func TestRenderSparklineRightAlignsShortHistory(t *testing.T) {
+func TestRenderSparklineStretchesShortHistoryToWidth(t *testing.T) {
got := renderSparkline([]float64{1, 2, 3}, 6)
lines := strings.Split(got, "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 lines, got %q", got)
}
- if !strings.HasPrefix(lines[1], " ") {
- t.Fatalf("expected left padding for short history, got %q", lines[1])
+ if strings.HasPrefix(lines[1], " ") {
+ t.Fatalf("expected short history to fill width, got %q", lines[1])
}
}
diff --git a/internal/tui/dashboard/tabs.go b/internal/tui/dashboard/tabs.go
index a2fe366..7f1908a 100644
--- a/internal/tui/dashboard/tabs.go
+++ b/internal/tui/dashboard/tabs.go
@@ -4,6 +4,7 @@ import (
"fmt"
common "ior/internal/tui/common"
"strings"
+ "unicode/utf8"
"github.com/charmbracelet/lipgloss"
)
@@ -77,28 +78,108 @@ func tabIndex(tab Tab) int {
}
func renderTabBar(active Tab, width int) string {
- parts := make([]string, 0, len(allTabs))
- for i, tab := range allTabs {
- label := fmt.Sprintf("%d:%s", i+1, tab.String())
- if tab == active {
- parts = append(parts, common.TabActiveStyle.Render(label))
- } else {
- parts = append(parts, common.TabInactiveStyle.Render(label))
+ if width > 0 && width < 90 {
+ return renderTabBarPlain(active, width)
+ }
+ build := func(short bool) string {
+ parts := make([]string, 0, len(allTabs))
+ for i, tab := range allTabs {
+ label := fmt.Sprintf("%d:%s", i+1, tabLabel(tab, short))
+ if tab == active {
+ parts = append(parts, common.TabActiveStyle.Render(label))
+ } else {
+ parts = append(parts, common.TabInactiveStyle.Render(label))
+ }
}
+ return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
}
- bar := lipgloss.JoinHorizontal(lipgloss.Left, parts...)
+ bar := build(false)
+ if width > 0 && lipgloss.Width(bar) > width {
+ bar = build(true)
+ }
+ if width > 0 && lipgloss.Width(bar) > width {
+ label := fmt.Sprintf("%d:%s", tabIndex(active)+1, tabLabel(active, false))
+ bar = common.TabActiveStyle.Render(label)
+ }
if width <= 0 {
return bar
}
- return lipgloss.NewStyle().Width(width).Render(bar)
+ styled := lipgloss.NewStyle().Width(width).Render(bar)
+ if strings.Contains(styled, "\n") {
+ return renderTabBarPlain(active, width)
+ }
+ return styled
}
-func renderHelpBar(keys common.KeyMap) string {
+func renderHelpBar(keys common.KeyMap, width int) string {
parts := make([]string, 0, len(keys.DashboardShortHelp()))
for _, binding := range keys.DashboardShortHelp() {
help := binding.Help()
parts = append(parts, help.Key+" "+help.Desc)
}
- return common.HelpBarStyle.Render(strings.Join(parts, " • "))
+ text := strings.Join(parts, " • ")
+ if width > 0 {
+ text = truncatePlain(text, width)
+ }
+ if width > 0 && width < 90 {
+ return text
+ }
+ return common.HelpBarStyle.Width(width).Render(text)
+}
+
+func tabLabel(tab Tab, short bool) string {
+ if !short {
+ return tab.String()
+ }
+ switch tab {
+ case TabOverview:
+ return "Ovr"
+ case TabSyscalls:
+ return "Sys"
+ case TabFiles:
+ return "Fil"
+ case TabProcesses:
+ return "Pro"
+ case TabLatency:
+ return "Lat"
+ case TabStream:
+ return "Str"
+ default:
+ return "Unk"
+ }
+}
+
+func truncatePlain(s string, width int) string {
+ if width <= 0 {
+ return ""
+ }
+ if utf8.RuneCountInString(s) <= width {
+ return s
+ }
+ if width == 1 {
+ return "…"
+ }
+ r := []rune(s)
+ return string(r[:width-1]) + "…"
+}
+
+func renderTabBarPlain(active Tab, width int) string {
+ parts := make([]string, 0, len(allTabs))
+ for i, tab := range allTabs {
+ label := fmt.Sprintf("%d:%s", i+1, tabLabel(tab, true))
+ if tab == active {
+ label = "[" + label + "]"
+ }
+ parts = append(parts, label)
+ }
+ text := strings.Join(parts, " ")
+ if width > 0 {
+ text = truncatePlain(text, width)
+ padding := width - utf8.RuneCountInString(text)
+ if padding > 0 {
+ text += strings.Repeat(" ", padding)
+ }
+ }
+ return text
}
diff --git a/internal/tui/dashboard/tabs_test.go b/internal/tui/dashboard/tabs_test.go
index 0fc36f2..bf96864 100644
--- a/internal/tui/dashboard/tabs_test.go
+++ b/internal/tui/dashboard/tabs_test.go
@@ -3,6 +3,8 @@ package dashboard
import (
"strings"
"testing"
+
+ common "ior/internal/tui/common"
)
func TestTabNavigationWraps(t *testing.T) {
@@ -18,10 +20,29 @@ func TestTabNavigationWraps(t *testing.T) {
}
func TestRenderTabBarContainsLabels(t *testing.T) {
- out := renderTabBar(TabOverview, 80)
+ out := renderTabBar(TabOverview, 100)
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)
}
}
}
+
+func TestRenderTabBarSmallWidthUsesSingleLine(t *testing.T) {
+ out := renderTabBar(TabOverview, 70)
+ lines := strings.Split(out, "\n")
+ if len(lines) != 1 {
+ t.Fatalf("expected single-line tab bar at width 70, got %d lines", len(lines))
+ }
+ if strings.Contains(out, "6:Strea") {
+ t.Fatalf("tab label should not be wrapped/split in small width output")
+ }
+}
+
+func TestRenderHelpBarSmallWidthUsesSingleLine(t *testing.T) {
+ out := renderHelpBar(common.DefaultKeyMap(), 70)
+ lines := strings.Split(out, "\n")
+ if len(lines) != 1 {
+ t.Fatalf("expected single-line help bar at width 70, got %d lines", len(lines))
+ }
+}