diff options
| -rw-r--r-- | internal/tui/common/viewport.go | 22 | ||||
| -rw-r--r-- | internal/tui/common/viewport_test.go | 41 | ||||
| -rw-r--r-- | internal/tui/dashboard/histogram.go | 9 | ||||
| -rw-r--r-- | internal/tui/dashboard/layout.go | 4 | ||||
| -rw-r--r-- | internal/tui/dashboard/overview.go | 38 | ||||
| -rw-r--r-- | internal/tui/dashboard/overview_test.go | 31 | ||||
| -rw-r--r-- | internal/tui/dashboard/sparkline.go | 85 | ||||
| -rw-r--r-- | internal/tui/dashboard/sparkline_test.go | 82 | ||||
| -rw-r--r-- | internal/tui/pidpicker/model.go | 10 | ||||
| -rw-r--r-- | internal/tui/pidpicker/model_test.go | 10 | ||||
| -rw-r--r-- | internal/tui/tui.go | 3 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 17 |
12 files changed, 235 insertions, 117 deletions
diff --git a/internal/tui/common/viewport.go b/internal/tui/common/viewport.go index 099a4e1..d54c886 100644 --- a/internal/tui/common/viewport.go +++ b/internal/tui/common/viewport.go @@ -1,13 +1,35 @@ package common +import ( + "os" + + xterm "github.com/charmbracelet/x/term" +) + const ( defaultViewportWidth = 80 defaultViewportHeight = 24 ) +var queryTerminalSize = func() (int, int, error) { + return xterm.GetSize(os.Stdout.Fd()) +} + // EffectiveViewport returns a usable terminal viewport size. Missing or invalid // dimensions fall back to defaults. func EffectiveViewport(width, height int) (int, int) { + if width <= 0 || height <= 0 { + terminalWidth, terminalHeight, err := queryTerminalSize() + if err == nil { + if width <= 0 && terminalWidth > 0 { + width = terminalWidth + } + if height <= 0 && terminalHeight > 0 { + height = terminalHeight + } + } + } + if width <= 0 { width = defaultViewportWidth } diff --git a/internal/tui/common/viewport_test.go b/internal/tui/common/viewport_test.go index c90f046..2dda81b 100644 --- a/internal/tui/common/viewport_test.go +++ b/internal/tui/common/viewport_test.go @@ -3,6 +3,14 @@ package common import "testing" func TestEffectiveViewport(t *testing.T) { + originalQuery := queryTerminalSize + t.Cleanup(func() { + queryTerminalSize = originalQuery + }) + queryTerminalSize = func() (int, int, error) { + return 132, 41, nil + } + tests := []struct { name string width int @@ -18,24 +26,24 @@ func TestEffectiveViewport(t *testing.T) { wantHeight: 40, }, { - name: "both missing use defaults", + name: "both missing use terminal size", width: 0, height: 0, - wantWidth: defaultViewportWidth, - wantHeight: defaultViewportHeight, + wantWidth: 132, + wantHeight: 41, }, { - name: "missing height uses default", + name: "missing height uses terminal size", width: 100, height: 0, wantWidth: 100, - wantHeight: defaultViewportHeight, + wantHeight: 41, }, { - name: "missing width uses default", + name: "missing width uses terminal size", width: -1, height: 30, - wantWidth: defaultViewportWidth, + wantWidth: 132, wantHeight: 30, }, } @@ -47,3 +55,22 @@ func TestEffectiveViewport(t *testing.T) { } } } + +func TestEffectiveViewportFallsBackToDefaultsWhenTerminalQueryFails(t *testing.T) { + originalQuery := queryTerminalSize + t.Cleanup(func() { + queryTerminalSize = originalQuery + }) + queryTerminalSize = func() (int, int, error) { + return 0, 0, assertiveError{} + } + + gotWidth, gotHeight := EffectiveViewport(0, 0) + if gotWidth != defaultViewportWidth || gotHeight != defaultViewportHeight { + t.Fatalf("got (%d,%d), want (%d,%d)", gotWidth, gotHeight, defaultViewportWidth, defaultViewportHeight) + } +} + +type assertiveError struct{} + +func (assertiveError) Error() string { return "terminal query failed" } diff --git a/internal/tui/dashboard/histogram.go b/internal/tui/dashboard/histogram.go index 7613230..3511dfb 100644 --- a/internal/tui/dashboard/histogram.go +++ b/internal/tui/dashboard/histogram.go @@ -14,9 +14,10 @@ func renderLatencyTab(snap *statsengine.Snapshot, width, height int) string { return common.PanelStyle.Render("Latency: waiting for stats...") } + panelW := panelWidth(width) panelInner := panelInnerWidth(width) hist := renderHistogram(snap.LatencyHistogram, "Latency Histogram", width, height) - spark := common.PanelStyle.Width(panelInner).Render( + spark := common.PanelStyle.Width(panelW).Render( renderOverviewSparkline("Latency sparkline:", snap.LatencySeriesNs(), panelInner), ) return strings.Join([]string{hist, spark}, "\n") @@ -27,9 +28,10 @@ func renderGapsTab(snap *statsengine.Snapshot, width, height int) string { return common.PanelStyle.Render("Gaps: waiting for stats...") } + panelW := panelWidth(width) panelInner := panelInnerWidth(width) hist := renderHistogram(snap.GapHistogram, "Gap Histogram", width, height) - spark := common.PanelStyle.Width(panelInner).Render( + spark := common.PanelStyle.Width(panelW).Render( renderOverviewSparkline("Gap sparkline:", snap.GapSeriesNs(), panelInner), ) return strings.Join([]string{hist, spark}, "\n") @@ -53,6 +55,7 @@ func renderHistogram(hist statsengine.HistogramSnapshot, title string, width, he if width <= 0 { width = 80 } + panelW := panelWidth(width) panelInner := panelInnerWidth(width) if height > 0 { @@ -93,7 +96,7 @@ func renderHistogram(hist statsengine.HistogramSnapshot, title string, width, he } lines = append(lines, "Scale: █▓▒░") - return common.PanelStyle.Width(panelInner).Render(strings.Join(lines, "\n")) + return common.PanelStyle.Width(panelW).Render(strings.Join(lines, "\n")) } func renderHistogramBar(count, maxCount uint64, width int) string { diff --git a/internal/tui/dashboard/layout.go b/internal/tui/dashboard/layout.go index 0035a9d..75cbafb 100644 --- a/internal/tui/dashboard/layout.go +++ b/internal/tui/dashboard/layout.go @@ -4,7 +4,3 @@ const panelHorizontalChrome = 4 // Keep a small guard so sparkline rows never soft-wrap in panel cells. const sparklineSafetyMargin = 3 - -// Stats engine currently provides 120 time-series slots; cap rendering width -// so wide terminals don't introduce wrap/placement artifacts. -const sparklineMaxWidth = 120 diff --git a/internal/tui/dashboard/overview.go b/internal/tui/dashboard/overview.go index 3ddeaf6..866c5bb 100644 --- a/internal/tui/dashboard/overview.go +++ b/internal/tui/dashboard/overview.go @@ -33,6 +33,7 @@ func renderOverview(snap *statsengine.Snapshot, width, height int) string { trendWithArrow(snap.ThroughputTrend), ) + panelW := panelWidth(width) panelInner := panelInnerWidth(width) labelWidth := maxLabelWidth("Latency:", "Gap:", "Throughput:") latencySpark := renderOverviewSparklineAligned("Latency:", snap.LatencySeriesNs(), panelInner, labelWidth) @@ -44,8 +45,8 @@ func renderOverview(snap *statsengine.Snapshot, width, height int) string { latencyHist := "Latency buckets: " + summarizeHistogramBrief(snap.LatencyHistogram) gapHist := "Gap buckets: " + summarizeHistogramBrief(snap.GapHistogram) - panel := common.PanelStyle.Width(panelInner) - sparkPanel := panel.Render(strings.Join([]string{latencySpark, "", gapSpark, "", throughputSpark}, "\n")) + panel := common.PanelStyle.Width(panelW) + 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")) @@ -73,7 +74,7 @@ func renderSyscallBox(snap *statsengine.Snapshot, width int) string { snap.SyscallRatePerSec, generatedAt, ) - return common.PanelStyle.Width(summaryBoxInnerWidth(width)).Height(5).Render(content) + return common.PanelStyle.Width(width).Height(5).Render(content) } func renderBytesBox(snap *statsengine.Snapshot, width int) string { @@ -83,7 +84,7 @@ func renderBytesBox(snap *statsengine.Snapshot, width int) string { formatBytes(snap.WriteBytesPerSec), formatBytes(float64(snap.TotalBytes)), ) - return common.PanelStyle.Width(summaryBoxInnerWidth(width)).Height(5).Render(content) + return common.PanelStyle.Width(width).Height(5).Render(content) } func renderErrorBox(snap *statsengine.Snapshot, width int) string { @@ -99,7 +100,7 @@ func renderErrorBox(snap *statsengine.Snapshot, width int) string { snap.LatencyMeanNs, snap.GapMeanNs, ) - return common.PanelStyle.Width(summaryBoxInnerWidth(width)).Height(5).Render(content) + return common.PanelStyle.Width(width).Height(5).Render(content) } func trendWithArrow(trend statsengine.Trend) string { @@ -212,19 +213,8 @@ func summaryBoxWidth(width int) int { return w } -func summaryBoxInnerWidth(width int) int { - inner := width - panelHorizontalChrome - if inner < 14 { - return 14 - } - return inner -} - func renderOverviewSparkline(label string, data []float64, panelInner int) string { w := panelInner - utf8.RuneCountInString(label) - 1 - sparklineSafetyMargin - if w > sparklineMaxWidth { - w = sparklineMaxWidth - } if w < 8 { w = 8 } @@ -234,9 +224,6 @@ func renderOverviewSparkline(label string, data []float64, panelInner int) strin func renderOverviewSparklineAligned(label string, data []float64, panelInner int, labelWidth int) string { paddedLabel := padLabelRight(label, labelWidth) w := panelInner - labelWidth - 1 - sparklineSafetyMargin - if w > sparklineMaxWidth { - w = sparklineMaxWidth - } if w < 8 { w = 8 } @@ -262,13 +249,20 @@ func padLabelRight(label string, width int) string { return label + strings.Repeat(" ", pad) } -func panelInnerWidth(width int) int { +func panelWidth(width int) int { if width <= 0 { width = 80 } - inner := width - panelHorizontalChrome - if inner < 20 { + if width < 20 { return 20 } + return width +} + +func panelInnerWidth(width int) int { + inner := panelWidth(width) - panelHorizontalChrome + if inner < 16 { + return 16 + } return inner } diff --git a/internal/tui/dashboard/overview_test.go b/internal/tui/dashboard/overview_test.go index 7de411c..6ac3704 100644 --- a/internal/tui/dashboard/overview_test.go +++ b/internal/tui/dashboard/overview_test.go @@ -6,6 +6,7 @@ import ( "time" "ior/internal/statsengine" + common "ior/internal/tui/common" "charm.land/lipgloss/v2" ) @@ -121,23 +122,22 @@ func TestRenderOverviewDoesNotOverflowWidth(t *testing.T) { func TestRenderOverviewSparklineHasSafetyMargin(t *testing.T) { const panelInner = 80 out := renderOverviewSparkline("Latency:", []float64{1, 2, 3, 4, 5}, panelInner) - lines := strings.Split(out, "\n") - if len(lines) != 2 { - t.Fatalf("expected 2-line sparkline, got %q", out) + if strings.Contains(out, "\n") { + t.Fatalf("expected single-line sparkline, got %q", out) } - if got, max := lipgloss.Width(lines[0]), panelInner-sparklineSafetyMargin; got > max { + if got, max := lipgloss.Width(out), panelInner-sparklineSafetyMargin; got > max { t.Fatalf("expected sparkline width <= %d with safety margin, got %d", max, got) } } -func TestRenderOverviewSparklineCapsWidth(t *testing.T) { +func TestRenderOverviewSparklineUsesAvailableWidth(t *testing.T) { out := renderOverviewSparkline("Latency:", make([]float64, 120), 400) - lines := strings.Split(out, "\n") - if len(lines) != 2 { - t.Fatalf("expected 2-line sparkline, got %q", out) + if strings.Contains(out, "\n") { + t.Fatalf("expected single-line sparkline, got %q", out) } - if got := lipgloss.Width(lines[0]) - len("Latency: "); got > sparklineMaxWidth { - t.Fatalf("expected capped sparkline width <= %d, got %d", sparklineMaxWidth, got) + want := 400 - len("Latency:") - 1 - sparklineSafetyMargin + if got := lipgloss.Width(out) - len("Latency: "); got != want { + t.Fatalf("expected sparkline width %d, got %d", want, got) } } @@ -164,3 +164,14 @@ func TestRenderOverviewSparklineAlignedUsesSameSparkStartColumn(t *testing.T) { t.Fatalf("unexpected throughput prefix: %q", thrTop) } } + +func TestRenderOverviewSparklineAlignedFitsSinglePanelRow(t *testing.T) { + panelW := panelWidth(220) + panelInner := panelInnerWidth(220) + labelWidth := maxLabelWidth("Latency:", "Gap:", "Throughput:") + line := renderOverviewSparklineAligned("Latency:", []float64{0, 10, 5, 10, 0}, panelInner, labelWidth) + rendered := common.PanelStyle.Width(panelW).Render(line) + if got := len(strings.Split(rendered, "\n")); got != 3 { + t.Fatalf("expected sparkline to fit one panel row (3 total lines with border), got %d lines", got) + } +} diff --git a/internal/tui/dashboard/sparkline.go b/internal/tui/dashboard/sparkline.go index 2ce8c90..ab78cce 100644 --- a/internal/tui/dashboard/sparkline.go +++ b/internal/tui/dashboard/sparkline.go @@ -1,9 +1,8 @@ package dashboard import "math" -import "strings" -var sparkRowChars = []rune(" ▁▂▃▄▅▆▇█") +var sparkChars = []rune("▁▂▃▄▅▆▇█") func renderSparkline(data []float64, width int) string { if len(data) == 0 || width <= 0 { @@ -11,23 +10,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)) - return top + "\n" + bottom + if min == 0 { + return repeatRune(' ', width) + } + return repeatRune('▁', width) } - top := make([]rune, width) - bottom := make([]rune, width) - for i := 0; i < leftPad; i++ { - top[i] = ' ' - bottom[i] = ' ' - } + row := make([]rune, width) scale := 16.0 denom := max - min for i, value := range samples { @@ -39,20 +30,17 @@ func renderSparkline(data []float64, width int) string { level = 16 } - topLevel := level - 8 - if topLevel < 0 { - topLevel = 0 + // Collapse the previous two-row 0..16 scale to a single-row 0..7 scale. + oneRow := level / 2 + if oneRow < 0 { + oneRow = 0 } - bottomLevel := level - if bottomLevel > 8 { - bottomLevel = 8 + if oneRow > 7 { + oneRow = 7 } - - col := leftPad + i - top[col] = sparkRowChars[topLevel] - bottom[col] = sparkRowChars[bottomLevel] + row[i] = sparkChars[oneRow] } - return string(top) + "\n" + string(bottom) + return string(row) } func renderLabeledSparkline(label string, data []float64, width int) string { @@ -60,20 +48,47 @@ func renderLabeledSparkline(label string, data []float64, width int) string { if spark == "" { return label } - lines := strings.Split(spark, "\n") - if len(lines) == 1 { - return label + " " + lines[0] - } - pad := repeatRune(' ', len([]rune(label))+1) - return label + " " + lines[0] + "\n" + pad + lines[1] + return label + " " + spark } func sampleForWidth(data []float64, width int) []float64 { - if width >= len(data) { + if width <= 0 || len(data) == 0 { + return nil + } + + if width < len(data) { + start := len(data) - width + return append([]float64(nil), data[start:]...) + } + + if width == len(data) { return append([]float64(nil), data...) } - start := len(data) - width - return append([]float64(nil), data[start:]...) + + if len(data) == 1 { + out := make([]float64, width) + for i := range out { + out[i] = data[0] + } + return out + } + + out := make([]float64, width) + srcLast := len(data) - 1 + dstLast := width - 1 + for i := 0; i < width; i++ { + // Nearest-neighbor upsampling preserves the original series shape + // without introducing interpolated spikes between samples. + srcIdx := int(math.Round(float64(i) * float64(srcLast) / float64(dstLast))) + if srcIdx < 0 { + srcIdx = 0 + } + if srcIdx > srcLast { + srcIdx = srcLast + } + out[i] = data[srcIdx] + } + return out } func minMax(values []float64) (float64, float64) { diff --git a/internal/tui/dashboard/sparkline_test.go b/internal/tui/dashboard/sparkline_test.go index d7acd33..6f549d1 100644 --- a/internal/tui/dashboard/sparkline_test.go +++ b/internal/tui/dashboard/sparkline_test.go @@ -16,37 +16,52 @@ func TestRenderSparklineEmptyOrInvalidWidth(t *testing.T) { func TestRenderSparklineSingleValue(t *testing.T) { got := renderSparkline([]float64{10}, 8) - if got != " \n █" { - t.Fatalf("expected two-line constant sparkline, got %q", got) + if got != "▁▁▁▁▁▁▁▁" { + t.Fatalf("expected single-line constant sparkline, got %q", got) } } func TestRenderSparklineAllEqualValues(t *testing.T) { got := renderSparkline([]float64{5, 5, 5, 5}, 4) - if got != " \n████" { - t.Fatalf("expected two-line flat sparkline, got %q", got) + if got != "▁▁▁▁" { + t.Fatalf("expected single-line flat sparkline, got %q", got) } } -func TestRenderSparklineRightAlignsShortHistory(t *testing.T) { +func TestRenderSparklineAllZeroValuesRendersBlank(t *testing.T) { + got := renderSparkline([]float64{0, 0, 0}, 5) + if got != " " { + t.Fatalf("expected blank sparkline for all-zero series, got %q", got) + } +} + +func TestRenderSparklineLeftAlignsShortHistory(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) + first := strings.IndexFunc(got, func(r rune) bool { return r != ' ' }) + last := strings.LastIndexFunc(got, func(r rune) bool { return r != ' ' }) + if first < 0 || last < 0 { + t.Fatalf("expected visible sparkline cells, got %q", got) + } + if strings.HasPrefix(got, " ") { + t.Fatalf("expected sparkline not to use old right-aligned padding, got %q", got) } - if !strings.HasPrefix(lines[1], " ") { - t.Fatalf("expected left padding for short history, got %q", lines[1]) +} + +func TestRenderSparklineUsesRightmostColumn(t *testing.T) { + got := renderSparkline([]float64{1, 3, 2, 5}, 20) + row := []rune(got) + if len(row) != 20 { + t.Fatalf("expected 20 columns, got %d", len(row)) + } + if row[19] == ' ' { + t.Fatalf("expected rightmost column to contain sparkline data, got %q", got) } } func TestRenderSparklineRespectsWidthTruncation(t *testing.T) { got := renderSparkline([]float64{1, 2, 3, 4, 5, 6, 7, 8}, 4) - lines := strings.Split(got, "\n") - if len(lines) != 2 { - t.Fatalf("expected 2 lines, got %q", got) - } - if len([]rune(lines[0])) != 4 || len([]rune(lines[1])) != 4 { - t.Fatalf("expected 4 runes per line, got %q", got) + if len([]rune(got)) != 4 { + t.Fatalf("expected 4 runes, got %q", got) } } @@ -63,27 +78,32 @@ func TestSampleForWidthUsesRecentTail(t *testing.T) { } } +func TestSampleForWidthUpsamplesToFullWidth(t *testing.T) { + got := sampleForWidth([]float64{10, 20, 30}, 7) + if len(got) != 7 { + t.Fatalf("expected 7 samples, got %d", len(got)) + } + if got[0] != 10 { + t.Fatalf("expected first sample to preserve series start, got %v", got[0]) + } + if got[len(got)-1] != 30 { + t.Fatalf("expected last sample to preserve series end, got %v", got[len(got)-1]) + } +} + func TestRenderSparklineSpansLowToHigh(t *testing.T) { got := renderSparkline([]float64{0, 10}, 2) - lines := strings.Split(got, "\n") - if len(lines) != 2 { - t.Fatalf("expected 2 lines, got %q", got) - } - if !strings.Contains(got, "█") { - t.Fatalf("expected high bar, got %q", got) + if got != "▁█" { + t.Fatalf("expected low-to-high one-row sparkline, got %q", got) } } -func TestRenderLabeledSparklineAlignsSecondRow(t *testing.T) { +func TestRenderLabeledSparklineSingleLine(t *testing.T) { got := renderLabeledSparkline("Latency:", []float64{0, 10}, 2) - lines := strings.Split(got, "\n") - if len(lines) != 2 { - t.Fatalf("expected 2 lines, got %q", got) - } - if !strings.HasPrefix(lines[0], "Latency: ") { - t.Fatalf("expected label prefix on first row, got %q", lines[0]) + if strings.Contains(got, "\n") { + t.Fatalf("expected single-line labeled sparkline, got %q", got) } - if !strings.HasPrefix(lines[1], " ") { - t.Fatalf("expected padding on second row to align sparkline, got %q", lines[1]) + if !strings.HasPrefix(got, "Latency: ") { + t.Fatalf("expected label prefix, got %q", got) } } diff --git a/internal/tui/pidpicker/model.go b/internal/tui/pidpicker/model.go index 87c200c..cfd0c0f 100644 --- a/internal/tui/pidpicker/model.go +++ b/internal/tui/pidpicker/model.go @@ -129,7 +129,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - m.input.SetWidth(clamp(msg.Width-16, 10, 100)) + inputWidth := msg.Width - 16 + if inputWidth < 10 { + inputWidth = 10 + } + m.input.SetWidth(inputWidth) return m, nil case processesLoadedMsg: m.processes = msg.processes @@ -276,7 +280,9 @@ func (m Model) View() tea.View { } b.WriteString("\n") - b.WriteString(helpBarStyle.Render(renderHelp(m.keys.PickerShortHelp()))) + viewWidth, _ := common.EffectiveViewport(m.width, m.height) + helpStyle := helpBarStyle.Copy().Width(viewWidth) + b.WriteString(helpStyle.Render(renderHelp(m.keys.PickerShortHelp()))) return tea.NewView(screenStyle.Render(b.String())) } diff --git a/internal/tui/pidpicker/model_test.go b/internal/tui/pidpicker/model_test.go index c47e59b..038575b 100644 --- a/internal/tui/pidpicker/model_test.go +++ b/internal/tui/pidpicker/model_test.go @@ -152,3 +152,13 @@ func TestRenderRowsKeepsSelectionVisible(t *testing.T) { t.Fatalf("expected selected row to remain visible, got:\n%s", rows) } } + +func TestWindowSizeDoesNotCapInputWidthOnWideTerminals(t *testing.T) { + m := NewWithKeys(DefaultKeyMap()) + next, _ := m.Update(tea.WindowSizeMsg{Width: 160, Height: 40}) + updated := next.(Model) + + if got, want := updated.input.Width(), 144; got != want { + t.Fatalf("expected input width %d for 160-col terminal, got %d", want, got) + } +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 3551e72..f4e45a1 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -695,9 +695,6 @@ func renderHelpOverlay(width, height int, groups [][]key.Binding) string { lines = append(lines, "", "Esc/? close") boxWidth := width - 6 - if boxWidth > 110 { - boxWidth = 110 - } if boxWidth < 72 { boxWidth = 72 } diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index e15c937..68bfca0 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -20,6 +20,7 @@ import ( "charm.land/bubbles/v2/key" tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) type fakeProbeManager struct { @@ -680,3 +681,19 @@ func TestViewSetsDynamicWindowTitle(t *testing.T) { t.Fatalf("unexpected default window title: %q", view.WindowTitle) } } + +func TestRenderHelpOverlayUsesWideViewport(t *testing.T) { + groups := [][]key.Binding{{key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help"))}} + out := renderHelpOverlay(160, 40, groups) + + maxWidth := 0 + for _, line := range strings.Split(out, "\n") { + if w := lipgloss.Width(line); w > maxWidth { + maxWidth = w + } + } + + if maxWidth <= 110 { + t.Fatalf("expected wide help overlay to exceed previous 110-col cap, got %d", maxWidth) + } +} |
