SpotTUI is a terminal-based Spotify client built with Go 1.25+ using the Charmbracelet TUI ecosystem. It provides playlist browsing, playback controls, search, and device management through a Spotify-themed terminal interface.
This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged.
- Performance first.
- Reliability first.
- Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams).
If a tradeoff is required, choose correctness and robustness over short-term convenience.
Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.
- TUI Framework: Bubble Tea v2
- Styling: Lip Gloss
- Components: Bubbles (lists, spinners, text inputs)
- Spotify API: zmb3/spotify/v2
- Auth: OAuth 2.0 PKCE flow (no client secret required)
- spotify_player: https://github.com/aome510/spotify-player (prefered resource)
- spotify-tui: https://github.com/Rigellute/spotify-tui
Use these as implementation references when designing protocol handling, UX flows, and operational safeguards.
- Single root
Modelininternal/tui/model.go - Views are enum states (
ViewLoading,ViewPlaylists,ViewTracks, etc.) - Sub-models embedded for Bubbles components (list.Model, spinner.Model, textinput.Model)
- All Spotify API calls wrapped in
tea.Cmdfunctions incommands.go - Results returned as typed messages (
TracksLoadedMsg,PlaybackStateMsg, etc.) - Error handling via
ErrMsgwith auto-dismiss after 3s
- Each list type has its own Item + Delegate implementation
- Delegates handle rendering with current track highlighting
- Examples:
PlaylistDelegate,TrackDelegate,SearchItemDelegate
- All styles centralized in
styles.Stylesstruct - Single theme (
SpotifyTheme) with adaptive colors - Styles passed to views, not imported directly
- Message types:
*Msgsuffix (e.g.,TracksLoadedMsg) - Command functions: verb-based (e.g.,
fetchTracks,pollPlaybackState) - View render functions:
render*prefix (e.g.,renderPlaylists)
- API errors wrapped in
ErrMsg{Err: err}and returned - Non-critical errors (e.g., no active player) return nil or empty state
- User-facing errors displayed in styled error box
- Background contexts for user-initiated actions
- Timeout contexts (5-15s) for data fetching operations
- Context stored on Model for reuse
# Build
go build -o spottui .
# Run
./spottui
# or
go run main.go| Variable | Required | Description |
|---|---|---|
SPOTIFY_CLIENT |
Yes | Spotify app client ID |
SPOTIFY_CALLBACK |
No | OAuth redirect URI (default: http://localhost:8080/callback) |
Tokens persisted to ~/.config/spottui/token.json with 0600 permissions. Auto-refresh handled by autoSaveTokenSource.
This project currently has no test files. When adding tests:
- Use standard Go testing (
*_test.gofiles) - testify is available as a dependency
- Run with
go test ./...
- Add view constant to
Viewenum inmodel.go - Add case to
View()switch statement - Create
render*function - Add navigation logic in
handleKeyPress
- Create message type in
commands.go - Create
tea.Cmdfunction that calls API and returns message - Handle message in
Update()switch
- Create Item struct implementing
list.Iteminterface inviews/list.go - Create Delegate struct with
Rendermethod - Create
Create*Listfactory function