summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-05 21:50:58 +0200
committerPaul Buetow <paul@buetow.org>2026-03-05 21:50:58 +0200
commita4298701546b09fccb15ce30db7c7e3f4070525c (patch)
treeb3433014284ccd354be48efb2ce125ccaf236d7e /internal
parent2bd89ced830f97fd12a672fddb6978d204a014fd (diff)
fix(tui): stabilize full-width layout and sparkline rendering
Diffstat (limited to 'internal')
-rw-r--r--internal/tui/common/viewport.go22
-rw-r--r--internal/tui/common/viewport_test.go41
-rw-r--r--internal/tui/dashboard/histogram.go9
-rw-r--r--internal/tui/dashboard/layout.go4
-rw-r--r--internal/tui/dashboard/overview.go38
-rw-r--r--internal/tui/dashboard/overview_test.go31
-rw-r--r--internal/tui/dashboard/sparkline.go85
-rw-r--r--internal/tui/dashboard/sparkline_test.go82
-rw-r--r--internal/tui/pidpicker/model.go10
-rw-r--r--internal/tui/pidpicker/model_test.go10
-rw-r--r--internal/tui/tui.go3
-rw-r--r--internal/tui/tui_test.go17
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)
+ }
+}