Documentation
¶
Overview ¶
Package app contains the root Bubble Tea model that wires together all panes, the central store, and the active theme. It is the single entry point for the TUI application.
Package app — command factories extracted from app.go. All functions in this file are methods on *App or package-level helpers that create tea.Cmd values. No routing or state-mutation logic lives here — this file is a pure command factory for the Spotify API calls.
Elm Architecture contract: build*Cmd and fetch*Cmd functions MUST NOT write to the Store. All Store mutations happen in Update() when the returned Msg is processed. Commands return data in Msg payloads; Update() decides what to store.
central message dispatch for the root Bubble Tea model — called by Update() for every incoming message; routes Msg payloads to Store writes and returns follow-up commands.
preference persistence — theme, preset, and visualizer changes are debounced and flushed to disk via a generation-counter pattern to avoid excessive writes during rapid key presses.
Package app — rendering extracted from app.go. This file contains View() and all render* helper methods on *App. No state mutation or command dispatch lives here — only pure string rendering.
Package app — key and message routing helpers extracted from app.go. These methods handle the routing logic within Update() so that app.go stays focused on the Bubble Tea lifecycle (Init/Update/View structure) rather than the full dispatch table.
Index ¶
- Constants
- func BuildSearchPageCmd(ctx context.Context, client api.SearchAPI, query string, types []string, ...) tea.Cmd
- func ConvertSearchResult(r *api.SearchResult) ([]panes.SearchListItem, int)
- type App
- func (a *App) ActivePresetIndex() int
- func (a *App) AlbumTracksID() string
- func (a *App) BackoffTicks() int
- func (a *App) CallSearchCancel()
- func (a *App) DeviceOverlayOpen() bool
- func (a *App) FocusedPane() layout.PaneID
- func (a *App) GatewayHealthPane() *panes.GatewayHealthPane
- func (a *App) GatewayLivePane() *panes.GatewayLivePane
- func (a *App) GridViewOpen() bool
- func (a *App) HelpOpen() bool
- func (a *App) Init() tea.Cmd
- func (a *App) InitAPIClients(token string)
- func (a *App) IsIdle() bool
- func (a *App) Layout() *layout.Manager
- func (a *App) NeedsRegister() bool
- func (a *App) NetworkLogPane() *panes.NetworkLogPane
- func (a *App) NowPlayingFocused() bool
- func (a *App) NowPlayingPane() *panes.NowPlayingPane
- func (a *App) OnboardingAuthURL() string
- func (a *App) OnboardingError() string
- func (a *App) OnboardingStep() int
- func (a *App) PlaylistTracksID() string
- func (a *App) PlaylistsFocused() bool
- func (a *App) PollIntervals() (playbackInterval, queueInterval int)
- func (a *App) PollingTrafficPane() *panes.PollingTrafficPane
- func (a *App) Prefs() *prefs.PreferenceStore
- func (a *App) PrefsDirtyGen() int
- func (a *App) ProfileOverlayOpen() bool
- func (a *App) QueueFocused() bool
- func (a *App) SchedulePrefsFlush() tea.Cmd
- func (a *App) SearchCancelCtx() context.Context
- func (a *App) SearchLoading() bool
- func (a *App) SearchOpen() bool
- func (a *App) SearchPage() int
- func (a *App) SearchPane() *panes.SearchOverlay
- func (a *App) SearchQuery() string
- func (a *App) SetAlbumTracksID(id string)
- func (a *App) SetDevices(devices api.DevicesAPI)
- func (a *App) SetLastInteraction(t time.Time)
- func (a *App) SetLibrary(library api.LibraryAPI)
- func (a *App) SetPlayer(player api.PlayerAPI)
- func (a *App) SetPlaylistTracksID(id string)
- func (a *App) SetPlaylistsAPI(p api.PlaylistsAPI)
- func (a *App) SetSearch(search api.SearchAPI)
- func (a *App) SetSearchSession(query string, page int, loading bool)
- func (a *App) SetUserAPI(userAPI api.UserAPI)
- func (a *App) Store() *state.Store
- func (a *App) Theme() theme.Theme
- func (a *App) ThemeSwitcherOpen() bool
- func (a *App) TickCount() int
- func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd)
- func (a *App) View() string
- type AppOptions
Constants ¶
const SearchPageSize = 10
SearchPageSize is the number of results fetched per page. Matches Spotify's recommended default; named for test clarity.
Variables ¶
This section is empty.
Functions ¶
func BuildSearchPageCmd ¶
func BuildSearchPageCmd(ctx context.Context, client api.SearchAPI, query string, types []string, page int) tea.Cmd
BuildSearchPageCmd is an exported wrapper around buildSearchPageCmd for testing. Exported for tests only — production code must use buildSearchPageCmd.
func ConvertSearchResult ¶
func ConvertSearchResult(r *api.SearchResult) ([]panes.SearchListItem, int)
ConvertSearchResult is an exported wrapper around convertSearchResult for testing. Exported for tests only.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is the root application model. It owns the active theme, the central store, the API clients, and all pane models. It is the ONLY layer that calls the Spotify API — panes emit request messages and app.go dispatches them.
func New ¶
func New(cfg *config.Config, opts AppOptions) *App
New creates a new App, loading the theme from cfg.Preferences.Theme.
func (*App) ActivePresetIndex ¶
ActivePresetIndex returns the active preset index from the layout manager (exported for testing).
func (*App) AlbumTracksID ¶
AlbumTracksID returns the current album tracks staleness key (album ID). Exported for tests.
func (*App) BackoffTicks ¶
BackoffTicks returns the remaining backoff ticks (exported for testing).
func (*App) CallSearchCancel ¶
func (a *App) CallSearchCancel()
CallSearchCancel calls the current searchCancel function. Exported for tests to verify cancellation without exposing context.CancelFunc directly.
func (*App) DeviceOverlayOpen ¶
DeviceOverlayOpen returns true while the device switcher overlay is visible.
func (*App) FocusedPane ¶
FocusedPane returns the PaneID of the pane that currently has keyboard focus. Returns layout.PaneID(-1) if no pane is focused.
func (*App) GatewayHealthPane ¶
func (a *App) GatewayHealthPane() *panes.GatewayHealthPane
GatewayHealthPane returns the GatewayHealthPane from the panes map (exported for testing).
func (*App) GatewayLivePane ¶
func (a *App) GatewayLivePane() *panes.GatewayLivePane
GatewayLivePane returns the GatewayLivePane from the panes map (exported for testing).
func (*App) GridViewOpen ¶
GridViewOpen returns true while the grid view is the active top-level view.
func (*App) Init ¶
Init starts the splash timer. If the user is already authenticated, it also starts data fetching and the polling loop. If not, those are deferred until auth succeeds.
func (*App) InitAPIClients ¶
InitAPIClients constructs and wires all Spotify API clients with the gateway and event recorder. Called from cmd/ for pre-authenticated startup.
func (*App) IsIdle ¶
IsIdle returns true if no user input has been received within idleThreshold. Exported for testing.
func (*App) NeedsRegister ¶
NeedsRegister returns true when the app was started without a client ID in config (exported for testing).
func (*App) NetworkLogPane ¶
func (a *App) NetworkLogPane() *panes.NetworkLogPane
NetworkLogPane returns the NetworkLogPane from the panes map (exported for testing).
func (*App) NowPlayingFocused ¶
NowPlayingFocused returns true if the NowPlaying pane currently has keyboard focus.
func (*App) NowPlayingPane ¶
func (a *App) NowPlayingPane() *panes.NowPlayingPane
NowPlayingPane returns the NowPlayingPane from the panes map (exported for testing).
func (*App) OnboardingAuthURL ¶
OnboardingAuthURL returns the full OAuth authorization URL for the onboarding flow (exported for testing).
func (*App) OnboardingError ¶
OnboardingError returns the onboarding error message (exported for testing).
func (*App) OnboardingStep ¶
OnboardingStep returns the current onboarding sub-step (exported for testing).
func (*App) PlaylistTracksID ¶
PlaylistTracksID returns the current playlist tracks staleness key (playlist ID). Exported for tests.
func (*App) PlaylistsFocused ¶
PlaylistsFocused returns true if the Playlists pane currently has keyboard focus.
func (*App) PollIntervals ¶
PollIntervals returns the current playback and queue polling intervals based on user activity and playback state. Exported for testing.
func (*App) PollingTrafficPane ¶
func (a *App) PollingTrafficPane() *panes.PollingTrafficPane
PollingTrafficPane returns the PollingTrafficPane from the panes map (exported for testing).
func (*App) Prefs ¶
func (a *App) Prefs() *prefs.PreferenceStore
Prefs returns the underlying PreferenceStore for inspection in tests.
func (*App) PrefsDirtyGen ¶
PrefsDirtyGen returns the current preference dirty generation counter. Used in tests to verify that schedulePrefsFlush increments it correctly.
func (*App) ProfileOverlayOpen ¶
ProfileOverlayOpen returns true while the user profile overlay is visible.
func (*App) QueueFocused ¶
QueueFocused returns true if the Queue pane currently has keyboard focus.
func (*App) SchedulePrefsFlush ¶
SchedulePrefsFlush is the exported test accessor for schedulePrefsFlush. It increments the generation counter and returns the debounce tick Cmd.
func (*App) SearchCancelCtx ¶
SearchCancelCtx returns the context associated with the current in-flight search. Returns nil when no search is in-flight (searchCancel is the no-op sentinel). Exported for tests to observe context cancellation.
func (*App) SearchLoading ¶
SearchLoading returns true while a search HTTP call is in-flight. Exported for tests.
func (*App) SearchOpen ¶
SearchOpen returns true while the search overlay is visible.
func (*App) SearchPage ¶
SearchPage returns the current search staleness key page. Exported for tests.
func (*App) SearchPane ¶
func (a *App) SearchPane() *panes.SearchOverlay
SearchPane returns the search overlay pane. Exported for tests that need to inspect overlay state after openSearch().
func (*App) SearchQuery ¶
SearchQuery returns the current search staleness key query. Exported for tests.
func (*App) SetAlbumTracksID ¶
SetAlbumTracksID sets the album tracks staleness key for testing. Exported for tests only.
func (*App) SetDevices ¶
func (a *App) SetDevices(devices api.DevicesAPI)
SetDevices injects the Spotify Connect devices API client into the app.
func (*App) SetLastInteraction ¶
SetLastInteraction sets the lastInteraction timestamp (exported for testing).
func (*App) SetLibrary ¶
func (a *App) SetLibrary(library api.LibraryAPI)
SetLibrary injects the Spotify API library client into the app.
func (*App) SetPlaylistTracksID ¶
SetPlaylistTracksID sets the playlist tracks staleness key for testing. Exported for tests only.
func (*App) SetPlaylistsAPI ¶
func (a *App) SetPlaylistsAPI(p api.PlaylistsAPI)
SetPlaylistsAPI injects the Spotify playlists mutation client into the app.
func (*App) SetSearchSession ¶
SetSearchSession sets the search staleness keys and loading flag for testing. This bypasses the normal SearchRequestMsg pathway so tests can set up state directly. Exported for tests only.
func (*App) SetUserAPI ¶
SetUserAPI injects the Spotify user identity and statistics API client into the app.
func (*App) ThemeSwitcherOpen ¶
ThemeSwitcherOpen returns true while the theme switcher overlay is visible.
type AppOptions ¶
type AppOptions struct {
// NeedsRegister is true when no client_id is present in config.
// The TUI will show the onboarding flow (stepRegister) on first launch.
NeedsRegister bool
NeedsAuth bool
ClientID string
TokenStore keychain.TokenStore
// TokenBaseURL overrides the Spotify token endpoint for tests.
// Leave empty for production (uses the real Spotify endpoint).
TokenBaseURL string
// Version is the build-time injected version string (e.g. "v0.1.0").
// Falls back to "dev" when not provided.
Version string
// CallbackPort is the port the OAuth callback server is listening on.
// Non-zero when the server was started early (needsRegister || needsAuth).
CallbackPort int
// CallbackCodeCh receives the OAuth authorization code from the callback server.
// Non-nil when the server was started early.
CallbackCodeCh <-chan api.CallbackResult
// CallbackClose closes the callback server. Non-nil when started early.
CallbackClose func()
}
AppOptions carries optional startup configuration into the app. Zero value means the user is already authenticated and no auth flow is needed.