Skip to content

Commit ccf40ce

Browse files
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 -->
2 parents eed3d21 + 372043a commit ccf40ce

58 files changed

Lines changed: 3176 additions & 789 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- **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.
89

910
### Fixed
1011

PLUGINS.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ spotatui plugin remove <name> # uninstall
1717
spotatui plugin new <name> # scaffold a new plugin to start from
1818
```
1919

20-
Plugins are cloned into `~/.config/spotatui/plugins/<name>/` and loaded at startup. Restart
21-
spotatui after installing, and bind any commands the plugin registers under `plugin_commands` in
22-
`config.yml`.
20+
Plugins are cloned into `plugins/<name>/` under the spotatui app config directory
21+
(`$XDG_CONFIG_HOME/spotatui` when `XDG_CONFIG_HOME` is set to an absolute path,
22+
or `~/.config/spotatui` when it is unset or not absolute) and loaded at startup.
23+
Restart spotatui after installing, and bind any commands the plugin registers
24+
under `plugin_commands` in `config.yml`.
2325

2426
Plugins are not sandboxed and run with full app privileges and network access, so only install
2527
ones you trust. See [Trust and safety](docs/scripting.md#trust-and-safety).
2628

27-
You can also drop a single `.lua` file into `~/.config/spotatui/plugins/` by hand.
29+
You can also drop a single `.lua` file into the app config directory's `plugins/`
30+
folder by hand. Manual examples use `${XDG_CONFIG_HOME:-$HOME/.config}` for
31+
brevity. That matches spotatui when `XDG_CONFIG_HOME` is unset or absolute; if
32+
it is relative, spotatui ignores it and loads plugins from
33+
`$HOME/.config/spotatui/plugins`, so copy there instead.
2834

2935
## First-party examples
3036

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,13 @@ Prefer setting the password via the `SPOTATUI_SUBSONIC_PASSWORD` environment var
212212

213213
### Internet Radio
214214

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.
216216

217217
### YouTube
218218

219219
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.
220220

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`.
222222

223223
## Native Streaming
224224

@@ -232,15 +232,15 @@ See the [Native Streaming Wiki](https://github.com/LargeModGames/spotatui/wiki/N
232232

233233
## Configuration
234234

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.
236236

237237
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.
238238

239239
- Customization guide: [`docs/configuration.md`](docs/configuration.md), with a commented [`examples/config.example.yml`](examples/config.example.yml)
240240
- Full config reference: [Configuration Wiki](https://github.com/LargeModGames/spotatui/wiki/Configuration)
241241
- Built-in themes (Spotify, Dracula, Nord, …): [Themes Wiki](https://github.com/LargeModGames/spotatui/wiki/Themes)
242242

243-
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.
244244

245245
### Discord Rich Presence
246246

@@ -256,7 +256,7 @@ You can also override the app ID via `SPOTATUI_DISCORD_APP_ID`, or disable it in
256256

257257
### Anonymous Song Counter
258258

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.
260260

261261
### GitHub Profile Widget
262262

@@ -339,13 +339,17 @@ Follow the spotifyd documentation to get set up. After that:
339339
If you used the original `spotify-tui` before:
340340

341341
- The binary name changed from `spt` to `spotatui`.
342-
- Config paths changed: `~/.config/spotify-tui/` → `~/.config/spotatui/`.
342+
- 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.
343343

344344
You can copy your existing config:
345345

346346
```bash
347-
mkdir -p ~/.config/spotatui
348-
cp -r ~/.config/spotify-tui/* ~/.config/spotatui/
347+
case "${XDG_CONFIG_HOME:-}" in
348+
/*) config_home="$XDG_CONFIG_HOME" ;;
349+
*) config_home="$HOME/.config" ;;
350+
esac
351+
mkdir -p "$config_home/spotatui"
352+
cp -r ~/.config/spotify-tui/* "$config_home/spotatui/"
349353
```
350354

351355
You may be asked to re-authenticate with Spotify the first time.

docs/configuration.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
spotatui reads `config.yml` from the app config directory:
44

5-
- Linux / macOS: `~/.config/spotatui/config.yml`
6-
- Windows: `C:\Users\<you>\.config\spotatui\config.yml`
5+
- `$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>`.
79

810
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).
911

1012
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.
1113

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+
1216
## Safe by default
1317

1418
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:
3539

3640
# Volume
3741
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
3943

4044
# Scrolling
4145
table_scroll_padding: 5 # rows kept visible below the selection before
@@ -82,20 +86,34 @@ Valid fields per screen:
8286

8387
`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.
8488

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+
85102
## Layout
86103

87104
```yaml
88105
behavior:
89106
sidebar_position: left # left | right | hidden
90107
playbar_position: bottom # bottom | top
91-
sidebar_width_percent: 20 # 0 hides the sidebar entirely
92-
library_height_percent: 30
93-
playbar_height_rows: 6 # 0 hides the playbar
108+
sidebar_width_percent: 20 # initial sidebar width default; saved runtime size wins once present
109+
library_height_percent: 30 # initial library height default; saved runtime size wins once present
110+
playbar_height_rows: 6 # initial playbar height default, 0..=50; 0 hides it; saved runtime size wins once present
94111
small_terminal_width: 150
95112
small_terminal_height: 45
96113
```
97114

98115
- `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.
99117
- `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.
100118
- Unknown position strings fall back to the default with a warning.
101119
- Mouse hit-testing follows every arrangement automatically.

docs/keybindings.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ Press `Enter` to apply the filter and `Esc` to clear it.
4040

4141
## Customizing Keybindings
4242

43-
Edit `~/.config/spotatui/config.yml`:
43+
Edit `config.yml` in the spotatui app config directory (`$XDG_CONFIG_HOME/spotatui`
44+
when `XDG_CONFIG_HOME` is set to an absolute path, or `~/.config/spotatui` when
45+
it is unset or not absolute):
4446

4547
```yaml
4648
keybindings:

docs/native-streaming.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ Native streaming uses 320 kbps by default. To select a different quality, set
2626
streaming_bitrate: 320 # 96, 160, or 320 kbps
2727
```
2828
29-
`client.yml` is in the same app config directory as `config.yml` (for example,
30-
`~/.config/spotatui/client.yml` on Linux and macOS). This setting controls the
31-
librespot native player directly; the Spotify app may still describe a Connect
32-
device's quality as "Automatic".
29+
`client.yml` is in the spotatui app config directory (for example,
30+
`$XDG_CONFIG_HOME/spotatui/client.yml` when `XDG_CONFIG_HOME` is set to an
31+
absolute path, or `~/.config/spotatui/client.yml` when it is unset or not
32+
absolute). This setting controls the librespot native player directly; the
33+
Spotify app may still describe a Connect device's quality as "Automatic".
34+
35+
Native-streaming credentials and audio cache are stored in the app cache
36+
directory, for example `$XDG_CACHE_HOME/spotatui/streaming_cache` when
37+
`XDG_CACHE_HOME` is set to an absolute path, or
38+
`~/.cache/spotatui/streaming_cache` when it is unset or not absolute.
3339

3440
## Notes
3541

docs/scripting.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ feature, which is enabled in the default build.
66

77
## File locations
88

9-
Plugins are loaded from your config directory (`~/.config/spotatui/`) at startup, in this order:
9+
Plugins are loaded from your app config directory (`$XDG_CONFIG_HOME/spotatui`
10+
when `XDG_CONFIG_HOME` is set to an absolute path, or `~/.config/spotatui`
11+
when it is unset or not absolute) at startup, in this order:
1012

1113
1. `init.lua`, if present.
1214
2. Single-file plugins: every `plugins/*.lua` file, sorted by filename.
@@ -54,10 +56,11 @@ spotatui plugin remove <name> # uninstall
5456
spotatui plugin new <name> # scaffold a new plugin to start from
5557
```
5658

57-
`add` clones the repository into `~/.config/spotatui/plugins/<name>/` (a shallow clone) and
58-
records it in `~/.config/spotatui/plugins.lock`. `update` fast-forwards each clone to the remote's
59-
latest commit. Restart spotatui after installing or updating for changes to take effect, and bind
60-
any commands the plugin registers under `plugin_commands` in `config.yml`.
59+
`add` clones the repository into `plugins/<name>/` under the spotatui app config
60+
directory (a shallow clone) and records it in `plugins.lock` in that same
61+
directory. `update` fast-forwards each clone to the remote's latest commit.
62+
Restart spotatui after installing or updating for changes to take effect, and
63+
bind any commands the plugin registers under `plugin_commands` in `config.yml`.
6164

6265
Single-file plugins you drop into `plugins/` by hand are not tracked in the lockfile; `plugin list`
6366
shows them under "untracked".
@@ -71,9 +74,9 @@ your config directory:
7174
spotatui plugin new my-plugin
7275
```
7376

74-
This writes `~/.config/spotatui/plugins/my-plugin/main.lua` (with a `require_api` guard, a sample
75-
command, and a suggested key binding) plus a `README.md`. Edit it, then `git init` and push to
76-
share it.
77+
This writes `plugins/my-plugin/main.lua` under the spotatui app config directory
78+
(with a `require_api` guard, a sample command, and a suggested key binding) plus
79+
a `README.md`. Edit it, then `git init` and push to share it.
7780

7881
A shareable plugin is a git repository with a `main.lua` (or `init.lua`) entry point at its root:
7982

@@ -289,8 +292,9 @@ tick. If the app stalls past several interval periods, the interval fires once a
289292
### Persistent storage
290293

291294
Each plugin gets a private key-value store persisted as plain JSON at
292-
`~/.config/spotatui/plugin-data/<plugin>.json`. Values must be JSON-serializable (tables,
293-
strings, numbers, booleans); functions and userdata raise.
295+
`plugin-data/<plugin>.json` under the spotatui app config directory. Values must
296+
be JSON-serializable (tables, strings, numbers, booleans); functions and userdata
297+
raise.
294298

295299
- `spotatui.storage_get(key)` - the stored value, or `nil`.
296300
- `spotatui.storage_set(key, value)` - store a value. `nil` removes the key.

docs/themes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ spotatui comes with several built-in theme presets. Access them via `Alt-,` > Th
2121

2222
## Custom Themes
2323

24-
You can create custom themes in `~/.config/spotatui/config.yml`:
24+
You can create custom themes in `config.yml` in the spotatui app config directory
25+
(`$XDG_CONFIG_HOME/spotatui` when `XDG_CONFIG_HOME` is set to an absolute path,
26+
or `~/.config/spotatui` when it is unset or not absolute):
2527

2628
```yaml
2729
theme:

examples/plugins/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,24 @@ privileges, so read anything you install from elsewhere (see
1818

1919
## Installing
2020

21-
Single-file plugins go straight into `plugins/`:
21+
Single-file plugins go straight into `plugins/` under the spotatui app config
22+
directory (see [`PLUGINS.md`](../../PLUGINS.md) for config path rules):
2223

2324
```bash
24-
cp track-notifier.lua ~/.config/spotatui/plugins/
25+
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/spotatui/plugins"
26+
cp track-notifier.lua "${XDG_CONFIG_HOME:-$HOME/.config}/spotatui/plugins/"
2527
```
2628

2729
Directory plugins (a folder with a `main.lua` entry point) are copied as a whole:
2830

2931
```bash
30-
cp -r session-stats ~/.config/spotatui/plugins/
32+
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/spotatui/plugins"
33+
cp -r session-stats "${XDG_CONFIG_HOME:-$HOME/.config}/spotatui/plugins/"
3134
```
3235

3336
Restart spotatui after installing. Plugins that register commands need a key binding; add one to
34-
`~/.config/spotatui/config.yml` under `plugin_commands` (each plugin documents a suggested key in
35-
its header comment).
37+
`config.yml` in the spotatui app config directory under `plugin_commands` (each
38+
plugin documents a suggested key in its header comment).
3639

3740
To install a plugin published as a git repository, use the built-in installer instead:
3841

examples/plugins/accent-cycler.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
--
33
-- Theme overrides from set_theme are runtime-only; they reset when spotatui restarts.
44
--
5-
-- Install (single file):
6-
-- cp accent-cycler.lua ~/.config/spotatui/plugins/
5+
-- Install (single file; see PLUGINS.md for config path rules):
6+
-- cp accent-cycler.lua "${XDG_CONFIG_HOME:-$HOME/.config}/spotatui/plugins/"
77
--
8-
-- Suggested binding, in ~/.config/spotatui/config.yml:
8+
-- Suggested binding, in config.yml in the spotatui app config directory:
99
-- plugin_commands:
1010
-- cycle_accent: "ctrl-y"
1111

0 commit comments

Comments
 (0)