Skip to content

Commit 788a3cd

Browse files
committed
fix(tui): prevent terminal scrolling by truncating status bar instead of wrapping and clamping total View() line count
1 parent 7f12e60 commit 788a3cd

2 files changed

Lines changed: 77 additions & 16 deletions

File tree

go/tui/helpers.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,10 +388,52 @@ func countLocalDirs(root, subdir string, excludePrefixes ...string) int {
388388
}
389389

390390
func truncate(s string, max int) string {
391-
if len(s) <= max {
391+
if max <= 0 {
392+
return ""
393+
}
394+
395+
// If the printable width fits, return as-is
396+
if lipgloss.Width(s) <= max {
392397
return s
393398
}
394-
return s[:max-1] + "…"
399+
400+
var result strings.Builder
401+
visibleLen := 0
402+
inEsc := false
403+
404+
runes := []rune(s)
405+
n := len(runes)
406+
407+
for i := 0; i < n; i++ {
408+
r := runes[i]
409+
if r == '\x1b' {
410+
inEsc = true
411+
result.WriteRune(r)
412+
continue
413+
}
414+
if inEsc {
415+
result.WriteRune(r)
416+
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
417+
inEsc = false
418+
}
419+
continue
420+
}
421+
422+
w := 1
423+
if r > 0x1100 && (r < 0x115f || r > 0x2329) {
424+
w = 2
425+
}
426+
427+
if visibleLen+w > max-1 {
428+
result.WriteString("\x1b[0m…")
429+
return result.String()
430+
}
431+
432+
result.WriteRune(r)
433+
visibleLen += w
434+
}
435+
436+
return result.String()
395437
}
396438

397439
func padRight(s string, width int) string {

go/tui/view.go

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (m *TuiModel) View() string {
2828

2929
// Heights
3030
topH := 9 // top panels row (with borders = 9 rows)
31-
barH := 4 // status bar lines (including prepended/trailing newlines)
31+
barH := 5 // status bar: 3 blank lines + line1 + line2 = 5 rows
3232
// catalog gets the remaining height
3333
catalogH := h - topH - barH
3434
if catalogH < 4 {
@@ -114,7 +114,16 @@ func (m *TuiModel) View() string {
114114
// ── Status bar (borderless) ───────────────────────────
115115
statusBar := m.renderStatusBar(w)
116116

117-
return mainContent + statusBar
117+
out := mainContent + statusBar
118+
119+
// Hard clamp: ensure the final output never exceeds termHeight lines.
120+
// This is the last line of defence against terminal scrolling.
121+
outLines := strings.Split(out, "\n")
122+
if len(outLines) > m.termHeight {
123+
out = strings.Join(outLines[:m.termHeight], "\n")
124+
}
125+
126+
return out
118127
}
119128

120129
// ─────────────────────────────────────────────
@@ -587,10 +596,10 @@ func (m *TuiModel) renderStatusBar(w int) string {
587596
statusIcon + barKeyStyle.Render("Select packages") + barTextStyle.Render(" · "),
588597
}
589598
if m.hasPendingChanges() {
590-
line1Parts = append(line1Parts, barKeyStyle.Render("s") + barTextStyle.Render(" save changes · "))
599+
line1Parts = append(line1Parts, barKeyStyle.Render("s")+barTextStyle.Render(" save changes · "))
591600
}
592601
line1Parts = append(line1Parts,
593-
barKeyStyle.Render("g") + barTextStyle.Render(" settings"),
602+
barKeyStyle.Render("g")+barTextStyle.Render(" settings"),
594603
)
595604

596605
var serverShortcut string
@@ -607,15 +616,21 @@ func (m *TuiModel) renderStatusBar(w int) string {
607616
}
608617

609618
_ = mode
610-
line1 := barStyle.Width(w).Render(strings.Join(line1Parts, ""))
619+
620+
// Render line1 and line2 WITHOUT Width() to prevent wrapping.
621+
// Then truncate to exactly w columns using lipgloss-aware truncation.
622+
line1Raw := barStyle.PaddingLeft(2).Render(strings.Join(line1Parts, ""))
623+
if lipgloss.Width(line1Raw) > w {
624+
line1Raw = truncate(line1Raw, w)
625+
}
611626

612627
// Line 2: shortcuts
613628
sep := barSepStyle.Render(" │ ")
614629
var shortcutParts []string
615630
shortcutParts = append(shortcutParts,
616-
barKeyStyle.Render("↑↓") + barTextStyle.Render(" move"),
617-
barKeyStyle.Render("Space") + barTextStyle.Render(" toggle"),
618-
barKeyStyle.Render("c") + barTextStyle.Render(" manage"),
631+
barKeyStyle.Render("↑↓")+barTextStyle.Render(" move"),
632+
barKeyStyle.Render("Space")+barTextStyle.Render(" toggle"),
633+
barKeyStyle.Render("c")+barTextStyle.Render(" manage"),
619634
)
620635

621636
items := m.getActiveItems()
@@ -624,18 +639,22 @@ func (m *TuiModel) renderStatusBar(w int) string {
624639
hasHoveredInstalled = items[m.selectedIndex].Installed
625640
}
626641
if hasHoveredInstalled {
627-
shortcutParts = append(shortcutParts, barKeyStyle.Render("u") + barTextStyle.Render(" update"))
642+
shortcutParts = append(shortcutParts, barKeyStyle.Render("u")+barTextStyle.Render(" update"))
628643
}
629644

630645
shortcutParts = append(shortcutParts,
631646
serverShortcut,
632-
barKeyStyle.Render("r") + barTextStyle.Render(" refresh"),
633-
barKeyStyle.Render("n") + barTextStyle.Render(" new"),
634-
barKeyStyle.Render("q") + barTextStyle.Render(" quit"),
647+
barKeyStyle.Render("r")+barTextStyle.Render(" refresh"),
648+
barKeyStyle.Render("n")+barTextStyle.Render(" new"),
649+
barKeyStyle.Render("q")+barTextStyle.Render(" quit"),
635650
)
636-
line2 := barStyle.Width(w).Render(strings.Join(shortcutParts, sep))
651+
line2Raw := barStyle.PaddingLeft(2).Render(strings.Join(shortcutParts, sep))
652+
if lipgloss.Width(line2Raw) > w {
653+
line2Raw = truncate(line2Raw, w)
654+
}
637655

638-
return "\n\n\n" + line1 + "\n" + line2
656+
// Always return exactly 5 lines: 3 blank + line1 + line2
657+
return "\n\n\n" + line1Raw + "\n" + line2Raw
639658
}
640659

641660
func (m *TuiModel) getActiveItems() []bridge.CatalogEntry {

0 commit comments

Comments
 (0)