You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: Move generated app state to XDG state/cache dirs (#407)
# Summary
Move generated runtime/app state out of the config directory and into
XDG state/cache locations.
This separates user-authored configuration from app-generated state.
`config.yml` remains suitable for hand editing or declarative
management, while runtime changes such as volume, layout, active source,
radio favorites, history, and token/cache files move to the appropriate
XDG state/cache locations.
- Keeps user-authored config in `$XDG_CONFIG_HOME/spotatui`.
- Stores `state.yml`, listening history, and Spotify OAuth token cache
under `$XDG_STATE_HOME/spotatui`.
- Stores native streaming credentials/cache under
`$XDG_CACHE_HOME/spotatui/streaming_cache`.
- Persists volume changes through runtime state and flushes pending
state saves before CLI command exit.
- Documents the updated config/state/cache ownership.
# Testing
- `cargo fmt --all`
- `cargo test --no-default-features --features telemetry`
- 504 passed
- `cargo clippy --no-default-features --features telemetry -- -D
warnings`
- `cargo test`
- 764 passed
- `cargo test --features all-sources`
- 837 passed, 15 ignored
- `cargo clippy --features all-sources -- -D warnings`
- `cargo check`
# Additional notes
Existing config-dir app data is migrated on first use when the new
target path does not already exist. This includes legacy runtime
fields/radio favorites from `config.yml`, listening history,
`last_session.yml`, Spotify OAuth token caches, and native streaming
credentials/audio cache.
If a new state/cache target already exists, the legacy file or directory
is left in place instead of being merged or overwritten; users may need
to move or remove legacy files manually only in that conflict case.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## New Features
* Added XDG-compliant locations for configuration, runtime state,
history, cache, credentials, plugins, and streaming data.
* Runtime settings now persist separately, including volume, shuffle,
layout, active source, announcements, and saved radio stations.
* Added migration support for existing configuration and state files.
## Bug Fixes
* Improved radio-station merging, deduplication, sanitization, and
protection of configured stations.
* Invalid configuration values now fall back to defaults with a warning.
* Improved secure persistence for sensitive files and streaming data.
## Documentation
* Updated configuration, plugin, keybinding, theme, streaming,
scripting, setup, playlist, and migration guidance, including local
playlist locations and persisted runtime settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,7 @@
5
5
### Added
6
6
7
7
-**Help search highlights its matches**: While filtering the Help menu (search key, `/` by default), every occurrence of your search terms is now highlighted in the visible rows, so you can see at a glance which part of a row matched. Highlighting follows the same smart-case rule as the filter itself ([#408](https://github.com/LargeModGames/spotatui/issues/408)).
8
+
-**Generated app state now uses XDG state/cache directories**: `config.yml` stays in the app config directory for user-authored settings, while runtime-managed state (`state.yml`), listening history, last playback session, and Spotify token caches move to the app state directory. Native streaming credentials and audio cache move to the app cache directory. Existing config-dir runtime fields, radio favorites, listening history, playback-session files, Spotify token caches, and legacy native streaming credentials and audio cache are migrated on first use when the new target path does not already exist, preserving free-source startup, existing in-app radio favorites, Spotify login sessions, and native streaming setup during upgrade. If a new state/cache target already exists, the legacy file or directory is left in place instead of being merged or overwritten.
Copy file name to clipboardExpand all lines: README.md
+12-8Lines changed: 12 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -212,13 +212,13 @@ Prefer setting the password via the `SPOTATUI_SUBSONIC_PASSWORD` environment var
212
212
213
213
### Internet Radio
214
214
215
-
Search the radio-browser.info directory in-app (Enter plays a station directly), and press the save key (`F` by default) to keep a station in your sidebar. Saved stations live under `behavior.radio_stations`; the playbar shows a `LIVE` badge with the stream's now-playing title.
215
+
Search the radio-browser.info directory in-app (Enter plays a station directly), and press the save key (`F` by default) to keep a station in your sidebar. Stations can also be preconfigured in `config.yml`; stations saved in-app live in `state.yml`. The playbar shows a `LIVE` badge with the stream's now-playing title.
216
216
217
217
### YouTube
218
218
219
219
Requires the [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) binary (`ffmpeg` recommended). No Google account, API key, or cookies — search and playback are anonymous. If playback breaks after a YouTube change, updating yt-dlp (`yt-dlp -U`) is the fix; no spotatui update needed.
220
220
221
-
**Local YouTube playlists** live in `~/.config/spotatui/youtube_playlists.yml`, a plain human-editable file you can back up or share. Create one from the sidebar, add tracks with `w`, and play a playlist as a queue with `Enter`.
221
+
**Local YouTube playlists** live in the spotatui config directory as `youtube_playlists.yml`, a plain human-editable file you can back up or share. Create one from the sidebar, add tracks with `w`, and play a playlist as a queue with `Enter`.
222
222
223
223
## Native Streaming
224
224
@@ -232,15 +232,15 @@ See the [Native Streaming Wiki](https://github.com/LargeModGames/spotatui/wiki/N
232
232
233
233
## Configuration
234
234
235
-
The config file is at `${HOME}/.config/spotatui/config.yml`. You can also configure spotatui in-app by pressing `Alt-,` to open Settings.
235
+
The config file is at `$XDG_CONFIG_HOME/spotatui/config.yml` when `XDG_CONFIG_HOME` is set to an absolute path, falling back to `${HOME}/.config/spotatui/config.yml` when it is unset or not absolute. You can also configure spotatui in-app by pressing `Alt-,` to open Settings.
236
236
237
237
Nearly everything is customizable: keybindings, themes, icons, playbar button labels, status-line and window-title format templates, table columns (reorder/rename/resize), default sorting per screen, startup screen, and layout (sidebar/playbar position). Invalid values fall back to defaults with a logged warning — a config typo never blocks startup.
238
238
239
239
- Customization guide: [`docs/configuration.md`](docs/configuration.md), with a commented [`examples/config.example.yml`](examples/config.example.yml)
240
240
- Full config reference: [Configuration Wiki](https://github.com/LargeModGames/spotatui/wiki/Configuration)
spotatui also stores local listening history at `${HOME}/.config/spotatui/history/listens.jsonl`, which powers `spotatui history recap`. Short or skipped plays are stored but excluded from recap totals.
243
+
spotatui also stores local listening history at `$XDG_STATE_HOME/spotatui/history/listens.jsonl` when `XDG_STATE_HOME` is set to an absolute path, falling back to `${HOME}/.local/state/spotatui/history/listens.jsonl` when it is unset or not absolute. This powers `spotatui history recap`. Short or skipped plays are stored but excluded from recap totals.
244
244
245
245
### Discord Rich Presence
246
246
@@ -256,7 +256,7 @@ You can also override the app ID via `SPOTATUI_DISCORD_APP_ID`, or disable it in
256
256
257
257
### Anonymous Song Counter
258
258
259
-
spotatui includes an opt-in global counter showing how many songs have been played by all users worldwide (the badge and chart at the top of this README). It is **completely anonymous** — no personal information, song names, artists, or listening history is collected; it only sends a simple increment when a new song starts. It is enabled by default and can be disabled with `enable_global_song_count: false` in `~/.config/spotatui/config.yml`. This is purely a fun community metric with zero tracking of individual users.
259
+
spotatui includes an opt-in global counter showing how many songs have been played by all users worldwide (the badge and chart at the top of this README). It is **completely anonymous** — no personal information, song names, artists, or listening history is collected; it only sends a simple increment when a new song starts. It is enabled by default and can be disabled with `enable_global_song_count: false` in `config.yml`. This is purely a fun community metric with zero tracking of individual users.
260
260
261
261
### GitHub Profile Widget
262
262
@@ -339,13 +339,17 @@ Follow the spotifyd documentation to get set up. After that:
339
339
If you used the original `spotify-tui` before:
340
340
341
341
- The binary name changed from `spt` to `spotatui`.
- Config paths changed: `~/.config/spotify-tui/`-> `$XDG_CONFIG_HOME/spotatui/` when `XDG_CONFIG_HOME` is set to an absolute path, or `~/.config/spotatui/` when it is unset or not absolute.
-`$XDG_CONFIG_HOME/spotatui/config.yml` when `XDG_CONFIG_HOME` is set to an absolute path.
6
+
-`${HOME}/.config/spotatui/config.yml` otherwise.
7
+
8
+
You can also point spotatui at a specific config file with `--config <path>`.
7
9
8
10
All fields are optional; omitted values use the built-in defaults. A complete, commented example lives in [`examples/config.example.yml`](../examples/config.example.yml).
9
11
10
12
Simple values (numbers, toggles, icons, positions) can also be changed live in the in-app **Settings** screen (see the hint in the top-right of the UI). Structured config — `format:` templates, `tables:` columns, and `playbar_control_labels` — is file-only. Edit the file while the app is closed: saving from the Settings screen rewrites the `behavior`, `theme`, and `keybindings` sections, but your `format:`, `tables:`, and `plugin_commands:` sections survive in-app saves untouched.
11
13
14
+
Machine-managed runtime state lives separately in `$XDG_STATE_HOME/spotatui/state.yml` when `XDG_STATE_HOME` is set to an absolute path, or `${HOME}/.local/state/spotatui/state.yml` when it is unset or not absolute. This includes volume, shuffle, active source, seen announcements, resized pane dimensions, and saved radio stations. The Spotify OAuth token cache and local listening history also live under the app state directory. Native streaming credentials and audio cache live under `$XDG_CACHE_HOME/spotatui/streaming_cache` when `XDG_CACHE_HOME` is set to an absolute path, or `${HOME}/.cache/spotatui/streaming_cache` when it is unset or not absolute. `sync_token` remains user config and is stored in `config.yml`.
15
+
12
16
## Safe by default
13
17
14
18
A typo in `config.yml` never prevents the app from starting. Structural mistakes — an unknown sort field, a bad template placeholder, an invalid column id, an icon that is too wide — are logged as warnings and the affected value falls back to its built-in default. Warnings go to the log file whose path is printed at startup (`/tmp/spotatui_logs/spotatuilog<pid>`).
@@ -35,7 +39,7 @@ behavior:
35
39
36
40
# Volume
37
41
volume_increment: 10# step for + / - (0..=100, fatal if outside)
38
-
volume_percent: 100#startup volume
42
+
volume_percent: 100#initial volume default; saved runtime volume wins once present
39
43
40
44
# Scrolling
41
45
table_scroll_padding: 5# rows kept visible below the selection before
@@ -82,20 +86,34 @@ Valid fields per screen:
82
86
83
87
`default`keeps the order the API returns (playlist order, date saved, play order). A field that is not valid for that screen falls back to `default` with a warning.
84
88
89
+
## Internet Radio
90
+
91
+
Preconfigured stations can be declared in `config.yml`:
92
+
93
+
```yaml
94
+
behavior:
95
+
radio_stations:
96
+
- name: SomaFM Groove Salad
97
+
url: https://ice1.somafm.com/groovesalad-128-mp3
98
+
```
99
+
100
+
Stations saved from inside the app are stored in `state.yml`. The sidebar merges configured stations first and app-saved stations second, deduped by stream URL. To remove a configured station, edit `config.yml`; the in-app remove action only removes app-saved stations from `state.yml`.
101
+
85
102
## Layout
86
103
87
104
```yaml
88
105
behavior:
89
106
sidebar_position: left # left | right | hidden
90
107
playbar_position: bottom # bottom | top
91
-
sidebar_width_percent: 20 # 0 hides the sidebar entirely
- `sidebar_position: hidden` gives the content the full width, but the sidebar auto-reveals while the Library or Playlists panel has keyboard focus or is hovered, so it never becomes unreachable.
116
+
- `sidebar_width_percent`, `library_height_percent`, and `playbar_height_rows` are configured initial defaults. Runtime resize changes use `{` / `}` for sidebar width, `(` / `)` for playbar height, `(` / `)` while hovering Library or Playlists for the library/sidebar split, and `|` to reset sizes; those changes persist in `state.yml` and are not overwritten by configured defaults after state exists. Configured `playbar_height_rows` is capped at 50 rows when applied at startup or when resetting the layout.
99
117
- `small_terminal_width` / `small_terminal_height` are the responsive-layout breakpoints. At or above `small_terminal_width` columns the app uses the wide layout (search box inside the sidebar); below it the search box gets its own full-width top row. `enforce_wide_search_bar: true` forces the full-width search row regardless of width.
100
118
- Unknown position strings fall back to the default with a warning.
101
119
- Mouse hit-testing follows every arrangement automatically.
0 commit comments