1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 17:55:08 +00:00

Merge pull request #601 from sibisai/fix/status-resize-ghost-lines

fix(status): pad View output to terminal height to prevent ghost lines on resize
This commit is contained in:
Tw93
2026-03-21 06:46:01 +08:00
committed by GitHub
2 changed files with 56 additions and 33 deletions

View File

@@ -165,6 +165,7 @@ func (m model) View() string {
header, mole := renderHeader(m.metrics, m.errMessage, m.animFrame, termWidth, m.catHidden) header, mole := renderHeader(m.metrics, m.errMessage, m.animFrame, termWidth, m.catHidden)
var cardContent string
if termWidth <= 80 { if termWidth <= 80 {
cardWidth := termWidth cardWidth := termWidth
if cardWidth > 2 { if cardWidth > 2 {
@@ -179,27 +180,31 @@ func (m model) View() string {
} }
rendered = append(rendered, renderCard(c, cardWidth, 0)) rendered = append(rendered, renderCard(c, cardWidth, 0))
} }
// Combine header, mole, and cards with consistent spacing cardContent = lipgloss.JoinVertical(lipgloss.Left, rendered...)
var content []string } else {
content = append(content, header) cardWidth := max(24, termWidth/2-4)
if mole != "" { cards := buildCards(m.metrics, cardWidth)
content = append(content, mole) cardContent = renderTwoColumns(cards, termWidth)
}
content = append(content, lipgloss.JoinVertical(lipgloss.Left, rendered...))
return lipgloss.JoinVertical(lipgloss.Left, content...)
} }
cardWidth := max(24, termWidth/2-4)
cards := buildCards(m.metrics, cardWidth)
twoCol := renderTwoColumns(cards, termWidth)
// Combine header, mole, and cards with consistent spacing // Combine header, mole, and cards with consistent spacing
var content []string parts := []string{header}
content = append(content, header)
if mole != "" { if mole != "" {
content = append(content, mole) parts = append(parts, mole)
} }
content = append(content, twoCol) parts = append(parts, cardContent)
return lipgloss.JoinVertical(lipgloss.Left, content...) 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 { func (m model) collectCmd() tea.Cmd {

View File

@@ -809,16 +809,16 @@ func TestFormatDiskLine(t *testing.T) {
if expectedLabel == "" { if expectedLabel == "" {
expectedLabel = "DISK" expectedLabel = "DISK"
} }
if !contains(got, expectedLabel) { if !strings.Contains(got, expectedLabel) {
t.Errorf("formatDiskLine(%q, ...) = %q, should contain label %q", tt.label, 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) 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) 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) 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) { func TestModelViewErrorRendersSingleMole(t *testing.T) {
m := model{ m := model{
width: 120, width: 120,
@@ -1169,16 +1200,3 @@ func stripANSI(s string) string {
} }
return result.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
}