From 7688a1362062728f6a917907e6c9502438380584 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 24 Aug 2026 16:08:41 -0400 Subject: [PATCH] Make checkout picker height dynamic --- internal/tui/checkoutview/model.go | 23 +++++++------ internal/tui/checkoutview/model_test.go | 44 ++++++++++++++++++++----- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/internal/tui/checkoutview/model.go b/internal/tui/checkoutview/model.go index 602de5c..23853dc 100644 --- a/internal/tui/checkoutview/model.go +++ b/internal/tui/checkoutview/model.go @@ -253,23 +253,22 @@ func (m *Model) moveCursor(delta int) { m.ensureVisible() } -// maxVisibleRows caps how many stack rows the inline picker shows at once. The -// rest are reached by scrolling, so the picker never takes over the screen. -const maxVisibleRows = 10 +// initialVisibleRows caps the picker before the terminal size is known, so the +// first frame cannot overflow a large stack. +const initialVisibleRows = 10 // bodyHeight returns the number of table rows shown at once: the row count -// capped at maxVisibleRows, and further shrunk to fit a short terminal. It never -// returns less than 1 (a line is reserved for the empty-state message). +// capped by the available terminal height. Before the terminal size is known, +// it falls back to initialVisibleRows. It never returns less than 1 (a line is +// reserved for the empty-state message). func (m Model) bodyHeight() int { rows := len(m.filtered) if rows < 1 { rows = 1 } - limit := maxVisibleRows + limit := initialVisibleRows if m.height > 0 { - if avail := m.height - m.chromeHeight(); avail < limit { - limit = avail - } + limit = m.height - m.chromeHeight() } if limit < 1 { limit = 1 @@ -281,9 +280,9 @@ func (m Model) bodyHeight() int { } // chromeHeight is the number of lines reserved around the table body when -// fitting the picker to a short terminal: title, tabs, blank, header, footer -// (5), plus one line of breathing room so the inline picker never exactly fills -// the terminal (which would make it scroll). The search line adds one more. +// fitting the picker to the terminal: title, tabs, blank, header, footer (5), +// plus one line of breathing room so the inline picker never exactly fills the +// terminal (which would make it scroll). The search line adds one more. func (m Model) chromeHeight() int { chrome := 6 if m.searching { diff --git a/internal/tui/checkoutview/model_test.go b/internal/tui/checkoutview/model_test.go index 4fb1168..e0962f7 100644 --- a/internal/tui/checkoutview/model_test.go +++ b/internal/tui/checkoutview/model_test.go @@ -317,21 +317,49 @@ func manyRows(n int) []StackRow { return rows } -func TestView_InlineHeightIsBounded(t *testing.T) { - // A long list on a tall terminal must not take over the screen: it shows at - // most maxVisibleRows rows plus a little chrome, and offers a scroll hint. - m := drive(New(manyRows(30)), tea.WindowSizeMsg{Width: 90, Height: 50}) +func TestBodyHeight_UsesAvailableTerminalSpace(t *testing.T) { + tests := []struct { + name string + rows int + height int + searching bool + want int + }{ + {name: "unknown terminal size uses fallback", rows: 30, want: initialVisibleRows}, + {name: "small list stays compact", rows: 3, height: 50, want: 3}, + {name: "tall terminal expands past fallback", rows: 100, height: 50, want: 44}, + {name: "short terminal shrinks", rows: 30, height: 12, want: 6}, + {name: "search reserves another line", rows: 100, height: 50, searching: true, want: 43}, + {name: "empty list reserves one row", height: 50, want: 1}, + {name: "tiny terminal reserves one row", rows: 30, height: 3, want: 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New(manyRows(tt.rows)) + m.height = tt.height + m.searching = tt.searching + assert.Equal(t, tt.want, m.bodyHeight()) + }) + } +} + +func TestView_ExpandsToAvailableTerminalHeight(t *testing.T) { + const terminalHeight = 50 + m := drive(New(manyRows(100)), tea.WindowSizeMsg{Width: 90, Height: terminalHeight}) + + assert.Greater(t, m.bodyHeight(), initialVisibleRows) + assert.Equal(t, terminalHeight-m.chromeHeight(), m.bodyHeight()) lines := len(strings.Split(m.View(), "\n")) - assert.LessOrEqual(t, lines, maxVisibleRows+6, "picker must not fill a tall terminal") - assert.GreaterOrEqual(t, lines, maxVisibleRows, "shows up to maxVisibleRows rows") - assert.Contains(t, stripANSI(m.View()), "of 30", "shows a scroll position indicator") + assert.Equal(t, terminalHeight-1, lines, "picker keeps one safety row free") + assert.Contains(t, stripANSI(m.View()), "of 100", "shows a scroll position indicator") } func TestView_ShrinksToShortTerminal(t *testing.T) { m := drive(New(manyRows(30)), tea.WindowSizeMsg{Width: 90, Height: 12}) lines := len(strings.Split(m.View(), "\n")) + assert.Equal(t, 12-m.chromeHeight(), m.bodyHeight()) assert.LessOrEqual(t, lines, 12, "must fit within a short terminal") - assert.Less(t, lines, maxVisibleRows+6) } func TestView_ClearsOnExit(t *testing.T) {