fix(ui): keep Spotify errors within scroll budget

This commit is contained in:
Bjarne Øverli
2026-08-20 19:06:48 +02:00
parent 0a1842097f
commit cce9b4aaad
4 changed files with 52 additions and 11 deletions
+11 -4
View File
@@ -38,7 +38,7 @@ func (m *Model) handleSpotSearchInputKey(msg tea.KeyPressMsg) tea.Cmd {
m.closeSpotSearch()
case tea.KeyEnter:
if m.spotSearch.query == "" {
m.spotSearch.err = "Enter a search query."
m.setSpotSearchError("Enter a search query.")
return nil
}
if !m.spotSearch.loading {
@@ -123,7 +123,7 @@ func (m *Model) handleSpotSearchResultsKey(msg tea.KeyPressMsg) tea.Cmd {
// The playlist picker adds one track; an album is many, and Spotify
// has no single call to add a whole record.
if track.IsAlbum() {
m.spotSearch.err = "Open the album with Enter, then add tracks from the queue."
m.setSpotSearchError("Open the album with Enter, then add tracks from the queue.")
return nil
}
m.spotSearch.selTrack = track
@@ -166,13 +166,20 @@ func (m *Model) spotSearchBusy() bool {
return m.spotSearch.loading || m.spotSearch.albumLoading
}
func (m *Model) setSpotSearchError(message string) {
m.spotSearch.err = message
if m.spotSearch.screen == spotSearchResults {
m.spotSearchResultsMaybeAdjustScroll(m.spotSearchResultsVisible())
}
}
// expandSpotAlbum fetches the tracks of the selected album placeholder. The
// overlay stays open while it runs: closing it would bump the request
// generation and drop the response.
func (m *Model) expandSpotAlbum(album playlist.Track, action spotAlbumAction) tea.Cmd {
loader, ok := m.spotSearch.prov.(provider.AlbumTrackLoader)
if !ok {
m.spotSearch.err = "This provider cannot open albums."
m.setSpotSearchError("This provider cannot open albums.")
return nil
}
m.spotSearch.albumLoading = true
@@ -249,7 +256,7 @@ func (m *Model) handleSpotSearchNewNameKey(msg tea.KeyPressMsg) tea.Cmd {
m.spotSearchPlaylistMaybeAdjustScroll(m.spotSearchPlaylistVisible())
case tea.KeyEnter:
if strings.TrimSpace(m.spotSearch.newName) == "" {
m.spotSearch.err = "Playlist name is required."
m.setSpotSearchError("Playlist name is required.")
return nil
}
if !m.spotSearch.loading {
+5 -1
View File
@@ -300,7 +300,11 @@ func (m *Model) spotSearchResultsHelpLine() string {
}
func (m *Model) spotSearchResultsVisible() int {
return m.effectivePlaylistVisible()
visible := m.effectivePlaylistVisible()
if m.spotSearch.err != "" {
visible--
}
return max(0, visible)
}
func (m *Model) spotSearchPlaylistHelpLine() string {
+30
View File
@@ -1,6 +1,7 @@
package model
import (
"fmt"
"strings"
"testing"
@@ -54,6 +55,35 @@ func TestSpotSearchErrorFitsBodyBudget(t *testing.T) {
}
}
func TestSpotSearchErrorKeepsCursorVisible(t *testing.T) {
m := newLayoutTestModel(80, 18)
m.spotSearch.visible = true
m.spotSearch.screen = spotSearchResults
m.recomputeLayout()
visible := m.effectivePlaylistVisible()
if visible < 3 {
t.Fatalf("body rows = %d, want at least 3", visible)
}
for i := range visible - 1 {
m.spotSearch.results = append(m.spotSearch.results, trackResult(fmt.Sprintf("Track %d", i)))
}
m.spotSearch.cursor = len(m.spotSearch.results) - 1
m.setSpotSearchError("Search failed")
resultRows := m.spotSearchResultsVisible()
if resultRows != visible-1 {
t.Fatalf("result rows = %d, want %d", resultRows, visible-1)
}
if rows := spotSearchRowsToCursor(m.spotSearch.results, m.spotSearch.scroll, m.spotSearch.cursor); rows > resultRows {
t.Fatalf("cursor sits %d rows below scroll %d, result window is %d", rows, m.spotSearch.scroll, resultRows)
}
selected := m.spotSearch.results[m.spotSearch.cursor].Title
if body := stripAnsi(m.renderSpotSearchBody()); !strings.Contains(body, selected) {
t.Fatalf("body = %q, want selected result %q", body, selected)
}
}
func trackResult(name string) playlist.Track {
return playlist.Track{Title: name, Artist: "NOFX"}
}
+6 -6
View File
@@ -723,7 +723,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.spotSearch.cursor = 0
m.spotSearch.scroll = 0
if msg.err != nil {
m.spotSearch.err = msg.err.Error()
m.setSpotSearchError(msg.err.Error())
return m, nil
}
m.spotSearch.results = msg.tracks
@@ -739,11 +739,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.spotSearch.albumLoading = false
if msg.err != nil {
m.spotSearch.err = msg.err.Error()
m.setSpotSearchError(msg.err.Error())
return m, nil
}
if len(msg.tracks) == 0 {
m.spotSearch.err = "That album has no tracks available here."
m.setSpotSearchError("That album has no tracks available here.")
return m, nil
}
album := msg.album
@@ -766,7 +766,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.spotSearch.cursor = 0
m.spotSearch.scroll = 0
if msg.err != nil {
m.spotSearch.err = msg.err.Error()
m.setSpotSearchError(msg.err.Error())
return m, nil
}
m.spotSearch.playlists = msg.playlists
@@ -783,7 +783,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cancelSpotRequest()
m.spotSearch.loading = false
if msg.err != nil {
m.spotSearch.err = "Add failed: " + msg.err.Error()
m.setSpotSearchError("Add failed: " + msg.err.Error())
return m, nil
}
m.status.Showf(statusTTLDefault, "Added to %q", msg.name)
@@ -797,7 +797,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cancelSpotRequest()
m.spotSearch.loading = false
if msg.err != nil {
m.spotSearch.err = "Create failed: " + msg.err.Error()
m.setSpotSearchError("Create failed: " + msg.err.Error())
return m, nil
}
m.status.Showf(statusTTLDefault, "Created %q & added track", msg.name)