From 02c0e1e78a201781e787cd5e56cf1958e5baf1c8 Mon Sep 17 00:00:00 2001 From: sibisai Date: Thu, 19 Mar 2026 18:38:09 -0700 Subject: [PATCH] fix(status): pad View output to terminal height to prevent ghost lines on resize --- cmd/status/main.go | 37 ++++++++++++++++------------- cmd/status/view_test.go | 52 +++++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/cmd/status/main.go b/cmd/status/main.go index a31a8ba..6df38fc 100644 --- a/cmd/status/main.go +++ b/cmd/status/main.go @@ -165,6 +165,7 @@ func (m model) View() string { header, mole := renderHeader(m.metrics, m.errMessage, m.animFrame, termWidth, m.catHidden) + var cardContent string if termWidth <= 80 { cardWidth := termWidth if cardWidth > 2 { @@ -179,27 +180,31 @@ func (m model) View() string { } rendered = append(rendered, renderCard(c, cardWidth, 0)) } - // Combine header, mole, and cards with consistent spacing - var content []string - content = append(content, header) - if mole != "" { - content = append(content, mole) - } - content = append(content, lipgloss.JoinVertical(lipgloss.Left, rendered...)) - return lipgloss.JoinVertical(lipgloss.Left, content...) + cardContent = lipgloss.JoinVertical(lipgloss.Left, rendered...) + } else { + cardWidth := max(24, termWidth/2-4) + cards := buildCards(m.metrics, cardWidth) + cardContent = renderTwoColumns(cards, termWidth) } - cardWidth := max(24, termWidth/2-4) - cards := buildCards(m.metrics, cardWidth) - twoCol := renderTwoColumns(cards, termWidth) // Combine header, mole, and cards with consistent spacing - var content []string - content = append(content, header) + parts := []string{header} if mole != "" { - content = append(content, mole) + parts = append(parts, mole) } - content = append(content, twoCol) - return lipgloss.JoinVertical(lipgloss.Left, content...) + parts = append(parts, cardContent) + output := lipgloss.JoinVertical(lipgloss.Left, parts...) + + // Pad output to exactly fill the terminal height so every frame fully + // overwrites the alt screen buffer, preventing ghost lines on resize. + if m.height > 0 { + contentHeight := lipgloss.Height(output) + if contentHeight < m.height { + output += strings.Repeat("\n", m.height-contentHeight) + } + } + + return output } func (m model) collectCmd() tea.Cmd { diff --git a/cmd/status/view_test.go b/cmd/status/view_test.go index 79f6796..f9b47ea 100644 --- a/cmd/status/view_test.go +++ b/cmd/status/view_test.go @@ -809,16 +809,16 @@ func TestFormatDiskLine(t *testing.T) { if expectedLabel == "" { expectedLabel = "DISK" } - if !contains(got, expectedLabel) { + if !strings.Contains(got, expectedLabel) { t.Errorf("formatDiskLine(%q, ...) = %q, should contain label %q", tt.label, got, expectedLabel) } - if !contains(got, tt.wantUsed) { + if !strings.Contains(got, tt.wantUsed) { t.Errorf("formatDiskLine(%q, ...) = %q, should contain used value %q", tt.label, got, tt.wantUsed) } - if !contains(got, tt.wantFree) { + if !strings.Contains(got, tt.wantFree) { t.Errorf("formatDiskLine(%q, ...) = %q, should contain free value %q", tt.label, got, tt.wantFree) } - if tt.wantNoSubstr != "" && contains(got, tt.wantNoSubstr) { + if tt.wantNoSubstr != "" && strings.Contains(got, tt.wantNoSubstr) { t.Errorf("formatDiskLine(%q, ...) = %q, should not contain %q", tt.label, got, tt.wantNoSubstr) } }) @@ -1133,6 +1133,37 @@ func TestRenderMemoryCardShowsSwapSizeOnWideWidth(t *testing.T) { } } +func TestModelViewPadsToTerminalHeight(t *testing.T) { + tests := []struct { + name string + width int + height int + }{ + {"narrow terminal", 60, 40}, + {"wide terminal", 120, 40}, + {"tall terminal", 120, 80}, + {"short terminal", 120, 10}, + {"zero height", 120, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := model{ + width: tt.width, + height: tt.height, + ready: true, + metrics: MetricsSnapshot{}, + } + + view := m.View() + got := lipgloss.Height(view) + if got < tt.height { + t.Errorf("View() height = %d, want >= %d (terminal height)", got, tt.height) + } + }) + } +} + func TestModelViewErrorRendersSingleMole(t *testing.T) { m := model{ width: 120, @@ -1169,16 +1200,3 @@ func stripANSI(s string) string { } return result.String() } - -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr))) -} - -func containsMiddle(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -}