░█▀▀░█░█░█▀▀░█▀█
░█▀▀░█░█░█░█░█▀█
░▀░░░▀▀▀░▀▀▀░▀░▀
Terminal-native music library aggregator. One TUI, one queue, many sources: local files (via MPD), internet radio, SomaFM, Spotify, YouTube. Inline album-art thumbnails on every row when run in a Kitty-graphics-capable terminal.
- Inline album-art thumbs on every list row (Kitty Unicode placeholders, with a halfblocks fallback for non-Kitty terminals)
- Five sources, one unified queue: local files (MPD), Spotify, YouTube, SomaFM, and user-defined internet radio
- Synced lyrics pane (
B) — timestamped lyrics scroll with playback for local, Spotify, and YouTube tracks via the free lrclib.net API; local files with embedded lyric tags use those instead of the network - Vim-style keybinds, mouse support, MPRIS bridge, lifecycle hooks, and a
unix-socket IPC control plane (
fuga play <uri>,fuga next, …) - Source-plugin trait — adding a new backend is one file implementing
MusicSource - Embedded
librespotfor Spotify (nospotifydsubprocess) andyt-dlpshell-out for YouTube (fuga itself never talks to Google) - Per-source theme palette and a per-source tab bar —
tcycles the active source and the tab list flips with it; each mode is user-customizable via[ui.tabs] - See docs/architecture.md for the source-plugin design and audio-routing notes
v0.3.0 — Local, radio, SomaFM, Spotify, and YouTube all work end-to-end
(browse, play, queue, search, control). Synced lyrics (B) for local,
Spotify, and YouTube tracks. Remote sources stream rows into the view as
each page arrives. macOS media keys + Now Playing widget are wired
through MPRemoteCommandCenter. See
CHANGELOG.md for the full list.
- Linux or macOS (developed on Void Linux; also builds and runs on macOS)
- Rust 1.75+ to build
mpdrunning onlocalhost:6600with your library indexed- A Kitty-graphics-capable terminal for inline thumbs:
kitty,ghostty,wezterm,konsole(recent), orstpatched with kitty-graphics-protocol- Anything else falls back to
halfblocks(low-resolution but renders) - Sixel terminals (
xterm -ti vt340,mlterm,foot) supported viathumb_mode = "sixel"for now-playing art only — row thumbs are disabled because sixel cells don't anchor to scrolling rows. WIP: sixel image overflows one row above its panel border in xterm; use kitty mode where possible.
- Spotify Premium + a developer app (
client_id) — only if you want Spotify
Homebrew (macOS, Linuxbrew):
brew install crodorg/fuga/fugaThe tap formula builds from source via cargo and pins to the latest
tagged release. Runtime extras — brew install mpd yt-dlp — are opt-in
depending on which sources you want.
From source:
cargo build --release
install -Dm755 target/release/fuga ~/.local/bin/fugaOr use the install script: ./scripts/install.sh.
mkdir -p ~/.config/fuga
cp examples/config.toml ~/.config/fuga/config.toml
$EDITOR ~/.config/fuga/config.tomlDefaults work for local-only use as long as MPD is on localhost:6600.
Path resolution: fuga checks $XDG_CONFIG_HOME/fuga first, then
~/.config/fuga if it already exists, otherwise the platform default
(~/.config/fuga on Linux, ~/Library/Application Support/fuga on
macOS). The same order applies to cache and data dirs with
$XDG_CACHE_HOME and $XDG_DATA_HOME. macOS users who prefer the
XDG layout just need to create ~/.config/fuga/ before first run.
The top tab bar swaps per source. t cycles the active source and
the tab list flips to that source's layout. Each mode ships a sensible
default; override any of them via [ui.tabs]:
[ui]
tab_alignment = "center" # center | left | right
radio_split = false # true → separate Radio + SomaFM tabs
# Keep [ui.tabs] as the LAST block under [ui] — it's a TOML sub-table,
# so any flat [ui] keys placed below it would be parsed into it.
[ui.tabs]
local = ["directories", "albums", "playlists", "queue", "search"]
spotify = ["spotify", "albums", "artists", "playlists", "podcasts", "queue", "search"]
youtube = ["youtube", "queue", "search"]
radio = ["stations", "queue"]
somafm = ["stations", "queue"]Recognized tab ids: queue, directories, albums, artists,
playlists, stations, radio, somafm, spotify, podcasts,
youtube, search. Sources omitted from [ui.tabs] (or with empty /
all-invalid lists) use the hard-coded default, so you can't lock
yourself out of navigation.
The bottom-right art panel resizes via two percentage knobs. Vertical 100% sits flush under the tab bar; horizontal 100% runs from the 24-cell text margin to the right edge. Aspect is preserved — whichever axis hits its limit first constrains the other.
[ui]
art_height_pct = 70 # clamped to [20, 100]
art_width_pct = 40 # clamped to [15, 100]
# art_collapsed = false # start with the panel shrunk into the bottom barQuick version:
- Create an app at https://developer.spotify.com/dashboard. Add
http://127.0.0.1:8888/callbackto the app's redirect URIs. - Set
[spotify] enabled = trueandclient_id = "..."in your config. - Run
fuga --spotify-authonce. A browser opens; approve. Token persists at~/.local/share/fuga/spotify_tokens.json(mode 0600). - Run
fuganormally.
Stuck? cat docs/spotify-setup.md (or read it on
GitHub) —
full walkthrough with troubleshooting + every scope explained.
If Spotify and MPD compete for the same ALSA device, route both through
PulseAudio or PipeWire (both expose a pulse device that mixes for you).
| Key | Action |
|---|---|
q |
Quit |
j / k |
Down / up |
C-d / C-u |
Page down / up |
g g |
Top |
G |
Bottom |
Tab / S-Tab |
Cycle tabs |
C-n / C-p |
Cycle tabs (alt) |
1–9 |
Jump to tab N |
Enter / l |
Activate (descend / play) |
a |
Enqueue (add to queue without playing) |
Esc / h |
Back one level |
Space |
Play / pause |
n / p |
Next / previous track |
H / L |
Seek -10s / +10s |
S |
Stop |
+ / - |
Volume up / down |
z |
Toggle shuffle |
x |
Cycle repeat (off → all → track) |
o |
Sort modal |
d |
Spotify Connect device picker |
T |
Cycle thumbnail mode |
t |
Cycle active source (Local → Spotify → …) |
r |
Refresh current view |
s |
Focus search input |
/ |
Filter rows in current page |
: |
Focus command bar |
? |
Toggle help overlay |
F |
Like / unlike current track (Spotify) |
f |
Follow playing track (jump cursor to it) |
C |
Clear queue |
D |
Remove hovered row from queue |
m |
Open contextual action menu |
P |
Pin / unpin hovered item |
v |
Expand hovered album art |
B |
Toggle synced-lyrics view |
Y |
Download hovered track (YouTube) |
Plus leader chords: g g (top) and g {l,s,r,f,y} to jump to Local /
Spotify / Radio / SomaFM / YouTube. All keys are user-configurable —
the example config lists every default explicitly with grouped
comments.
Mouse:
- Click a tab label → switch tab
- Click a row → play / descend
- Scroll wheel anywhere → cursor up/down
- Scroll wheel over the volume cell → volume up/down
- Click on the progress bar → seek to that fraction
- Right-click on the progress bar → pause/resume
- Album-art panel swallows clicks (rows behind it don't fire)
Type : then:
| Command | Effect |
|---|---|
:q / :quit |
Quit |
:add <uri> |
Append <uri> to the queue (scheme picks source) |
:play <uri> |
Push <uri>, play immediately |
:goto <n> |
Jump to queue index n |
:vol <0..100> |
Set master volume |
URI scheme determines the source: local:, radio:, somafm:, spotify:,
youtube:.
A running fuga listens on a unix socket ($XDG_RUNTIME_DIR/fuga.sock).
From another shell:
fuga play spotify:track:11dFghVXANMlKmJXsNCbNl
fuga next
fuga prev
fuga pause
fuga stop
fuga vol 60
fuga statusfuga status prints title | artist | mm:ss/mm:ss | source so it composes
into status bars and waybar modules.
Linux media keys and system mixers (KDE Plasma, GNOME, playerctl) drive
fuga via the MPRIS D-Bus bridge automatically — no setup. Volume changes
from outside fuga (e.g. KDE's mixer) sync back to the bottom bar.
Set shell commands in [hooks] to run on lifecycle events. They receive
state via FUGA_* env vars:
[hooks]
on_track_change = "notify-send 'Now playing' \"$FUGA_TITLE — $FUGA_ARTIST\""
on_source_switch = "logger fuga: $FUGA_SOURCE_FROM -> $FUGA_SOURCE_TO"
on_startup = "echo started >> ~/.cache/fuga/runs.log"Press /, type, Enter. Query fans out across every registered source in
parallel; results are grouped by source. j/k to navigate, Enter to play.
- No audio after switching to Spotify — check
~/.cache/fuga/fuga.logforlibrespot stop timed out. If MPD and librespot sharedefaultALSA device they fight; use PulseAudio/PipeWire or different ALSA devices inmpd.conf/ your PA config. - Inline thumbs invisible — your terminal probably isn't Kitty-capable.
Press
Tto cycle to halfblocks (works anywhere). Verify your terminal with:printf '\e_Gi=31337,s=1,v=1,a=q,t=d,f=24;AAAA\e\\'. - Under tmux — set
set -g allow-passthrough onintmux.confand use tmux ≥ 3.4. Otherwise the graphics protocol is silently dropped. - Spotify auth fails — delete
~/.local/share/fuga/spotify_tokens.jsonand re-runfuga --spotify-auth. Confirm the redirect URI in your Spotify developer dashboard matchesredirect_portin your config. - MPD connection error on startup —
mpc statusto verify MPD is running, thenmpc updateto make sure it sees your library. - Phone doesn't see fuga as a Spotify Connect device — you need a
client_idconfigured (Spotify Web API only registers Connect devices through an authenticated session). Re-runfuga --spotify-authif it's been a while since the token was issued.
~/.cache/fuga/fuga.log. fuga --debug raises log level. The TUI never
writes to stdout (would corrupt the screen).
cargo run -- --debug # dev
cargo test # unit tests
cargo clippy -- -D warnings # lint
cargo fmt # formatSee docs/architecture.md for architecture, the source-plugin trait, audio routing notes, and the phased roadmap.
MIT. See LICENSE.
Authorship. fuga is a hobby project. Large parts of the code were written with AI assistance, reviewed and integrated by the author. Bug reports and pull requests are welcome; clean rewrites of any module are welcome too.
Spotify. fuga uses librespot to stream from Spotify. librespot is an open-source project not approved or endorsed by Spotify; using it outside personal/educational contexts may violate the Spotify Terms of Service. A Spotify Premium account is required. fuga is intended for personal use and is not affiliated with Spotify AB.
YouTube. fuga shells out to a separately-installed
yt-dlp binary to search, stream, and
optionally download tracks from YouTube. fuga itself sends no traffic to
Google/YouTube; it only invokes the local yt-dlp binary. Users are
responsible for compliance with the
YouTube Terms of Service,
including any rules around downloading audio. Downloaded files land in
[youtube] download_dir if you set one, otherwise MPD's
music_directory, otherwise XDG Downloads / ~/Downloads. fuga is not
affiliated with Google LLC or YouTube.
