A fast, minimal, cross-platform terminal music player — search, stream, and organize music from the command line, with a hybrid recommendation engine and first-class support for self-hosted libraries.
- Fast & tiny — a single ~10 MB Go binary, minimal dependencies, no Electron, no daemon.
- Instant playback — streams resolve natively in-process (~0.2s), no Python helper on the hot path; preloading and an on-disk URL cache make most plays start near-instantly.
- Gapless playback — the next queued track is handed to the running mpv ahead of time, so natural track boundaries play straight through with no respawn and no silence (the iOS app does the same, plus optional crossfade).
- Works anywhere — macOS, Linux, and Windows (amd64 / arm64).
- Few third parties — streams from YouTube Music out of the box; optionally point it at your own Subsonic/Navidrome server or a folder of local files.
- Yours to keep — likes, playlists, and history are stored as open, portable files (M3U8 / XSPF / ListenBrainz JSON) you can take anywhere.
- Synced lyrics — karaoke-style, auto-scrolling lyrics via LRCLIB (free, no account); works for YouTube, Subsonic, and local tracks alike.
- Artist & album pages — drill into any artist (top songs · albums · singles, with Last.fm listener stats) or album (ordered tracklist, year), like the big apps.
- Scrobbling — submit plays to Last.fm and/or ListenBrainz, with an offline spool so listens made without a connection aren't lost.
- Requirements
- Install
- Quick start
- Controls
- Scrobbling
- Commands
- Configuration
- Sources
- Downloads
- Library & interop
- Data directory
- Build from source
- Troubleshooting
- License
| Tool | Required? | Purpose |
|---|---|---|
| mpv | recommended | Pause / seek / volume + OS "Now Playing". Without it, audio still plays via ffplay/afplay but transport controls are off. |
| yt-dlp | optional | Powers downloads (d key) and is the stream-resolution fallback. Streaming itself resolves natively in-process (InnerTube, ~0.2s) — no yt-dlp needed to play. |
| ffmpeg | optional | ffprobe reads tags for local files; ffplay is a playback fallback. |
pixeltui can install and repair these for you — see doctor.
Streaming a Subsonic/Navidrome server or playing local files needs none of the above beyond a player (those stream directly).
curl -fsSL https://raw.githubusercontent.com/dotjarden/pixeltui/main/install.sh | shAuto-detects your CPU, downloads the right prebuilt binary, puts pixeltui on
your PATH, and installs the optional tools (mpv for controls, yt-dlp for
downloads). No Go needed.
irm https://raw.githubusercontent.com/dotjarden/pixeltui/main/install.ps1 | iexSkip the automatic dependency check/install with --nodoctor
(curl … | sh -s -- --nodoctor) or PIXELTUI_NO_DOCTOR=1. You can always run
pixeltui doctor --fix later.
Grab the binary for your platform from the latest release:
| Platform | Asset |
|---|---|
| macOS, Apple Silicon (M1–M4) | pixeltui-darwin-arm64 |
| macOS, Intel | pixeltui-darwin-amd64 |
| Linux x86-64 | pixeltui-linux-amd64 |
| Linux ARM64 | pixeltui-linux-arm64 |
| Windows | pixeltui-windows-amd64.exe |
These are command-line programs — run them from a terminal, not by double-clicking (double-clicking a binary just opens it in a text editor). On macOS:
chmod +x pixeltui-darwin-arm64
xattr -d com.apple.quarantine pixeltui-darwin-arm64 # clear Gatekeeper
sudo mv pixeltui-darwin-arm64 /usr/local/bin/pixeltui
pixeltui doctor --fixgo install github.com/dotjarden/pixeltui/tui@latest # binary installs as `tui`; rename to `pixeltui` if you like
pixeltui doctor --fix # install yt-dlp + mpvpixeltui update # latest release
pixeltui update v0.2.4 # a specific tag — also how you roll backDownloads the release build for your platform, verifies its checksum, and
replaces the running binary in place. With no argument it tracks the latest
release; pass a tag (with or without the v) to pin or roll back. All
releases: https://github.com/dotjarden/pixeltui/releases. (You can also just
re-run the install one-liner, or go install …@latest.)
pixeltui uninstall # remove the binary, data dir, and bundled yt-dlp/mpv
pixeltui uninstall --keep-data # keep your library + config; remove everything else
pixeltui uninstall -y # skip the confirmation promptA full clean by default: removes the pixeltui binary, the ~/.pixeltui data
directory (cache, graph, library, config) and the self-contained tools it
installed there. On Windows it also strips the PATH entry the installer added.
mpv installed via your system package manager is left in place (remove it
yourself, e.g. sudo apt-get remove mpv).
pixeltui # open the interactive player (search-first)
pixeltui "Get Lucky" "Daft Punk" # start seeded from a track
pixeltui setup # configure Last.fm key, Subsonic, local & download folders
pixeltui doctor # check everything is wired upRecommendations use a free Last.fm API key. Search and playback work without one.
One rule keeps the keymap predictable for track actions:
lowercase acts on the highlighted track · SHIFT acts on what's playing. (e.g.
flikes the selected track,Flikes the one currently playing.)
Playback (always the now-playing track)
| Key | Action |
|---|---|
↵ |
play selected track |
space |
pause / resume |
← → (or h l) |
seek (step configurable in settings, default 10s) |
n |
next track · ; jump to the playing track |
+ / − |
volume up / down |
Track — lowercase = highlighted, Shift = now-playing
| Key | Action |
|---|---|
f / F |
like / unlike (♥) |
a |
add to queue (A = play next) |
p / P |
add to playlist |
d / D |
download |
x / X |
mute artist for this session (X also skips) |
. |
actions menu — everything above, plus go to artist / album & start station |
o / O |
start an endless station (from selection / playing) |
Artist & album pages
| Key | Action |
|---|---|
/ then !a <artist> |
artist page: top songs · albums · singles (+ Last.fm listener stats) |
/ then !al <album> |
album search → full album page (ordered tracklist, year, durations) |
↵ on an album row |
open the album |
esc |
back through pages (album → artist → results) |
e |
queue everything on the page |
Queue (Up Next pane — Tab switches focus)
| Key | Action |
|---|---|
↑ ↓ |
navigate · j k reorder |
del |
remove · s shuffle · r repeat · c clear |
u |
undo the last clear / remove / shuffle |
View & modes
| Key | Action |
|---|---|
/ |
search the current source |
' |
filter the current list in place (fuzzy) |
b |
browse: Liked · playlists · charts · stats · Local · Subsonic (o on a playlist starts a blended station · u restores a deleted playlist) |
y |
lyrics — [ ] nudge the sync, 0 resets |
z autoplay · t sleep timer · , settings |
|
Tab |
switch pane · ? all keys · q quit · esc back |
Press ? in the app for the full list at any time.
pixeltui can submit your plays to Last.fm and/or ListenBrainz:
pixeltui setup→ Scrobbling — paste your Last.fm API key + shared secret (both on the same page) and/or a ListenBrainz token, and enable it.- For Last.fm, a one-time browser authorization runs at the end of setup
(redo anytime with
pixeltui scrobble-auth).
Plays submit at the standard 50% / 4-minute mark, with a now-playing update at
track start. Plays made offline are spooled and retried on the next launch.
Toggle scrobbling live in the in-app settings (,).
pixeltui open the player (search-first)
pixeltui [track] [artist] start seeded from a track
pixeltui setup interactive config wizard
pixeltui scrobble-auth authorize Last.fm scrobbling (one-time)
pixeltui serve [--tunnel …] companion-app server (see "Companion server")
pixeltui devices list devices paired with the server
pixeltui devices revoke <id> unpair a device (running serve needs a restart)
pixeltui update [version] self-update: latest, or a tag like v0.2.4
pixeltui doctor [--fix] check setup; --fix auto-installs/repairs deps
pixeltui reset [target] wipe data: cache | graph | library | config | all
pixeltui uninstall [--keep-data] remove pixeltui, data, and bundled tools
pixeltui export <playlist> [file] write a playlist as XSPF (portable)
pixeltui build-graph build the offline recommendation graph (once)
pixeltui cache warm --artist X pre-fetch an artist for offline use
pixeltui cache stats | clear show / wipe the cache
pixeltui help full usage and flags
Common flags for the recommend/seed mode: -explore 0..10, -deep-cuts,
-no-artist "A,B", -n N, -offline, -no-tui, -key <lastfm>, -dev.
doctor --fix self-resolves the optional tools: it installs mpv — a bundle
on macOS, the standalone build on Windows, your package manager on Linux — and a
self-contained yt-dlp binary into ~/.pixeltui/bin (no Python needed; used
for downloads and as the streaming fallback).
Run pixeltui setup, or edit ~/.pixeltui/config.json:
{
"lastfm_key": "",
"scrobble": {
"enabled": false,
"lastfm_secret": "",
"lastfm_session": "",
"lastfm_user": "",
"listenbrainz_token": ""
},
"subsonic": { "url": "", "user": "", "pass": "" },
"local_dirs": [],
"download_dir": "",
"theme": "",
"explore": 5,
"autoplay": true,
"seek_step": 10,
"charts": { "global": true, "country": "" },
"server": { "addr": ":8787", "name": "", "public_url": "", "tunnel": "" }
}The server block sets the defaults for pixeltui serve — bind address,
advertised name, and remote access (tunnel: "tailscale", "cloudflare",
"ngrok", or a fixed public_url for a tunnel you run yourself). See
Companion server.
Current charts (live YouTube Music top tracks — no API key needed) show in
Browse and on the For You landing: charts.global (worldwide Top, on by
default) and charts.country — a country name or 2-letter code, e.g.
"United States" or "GB".
Every value can also be set by environment variable (env wins over the file):
| Variable | Meaning |
|---|---|
LASTFM_API_KEY |
Last.fm API key for recommendations |
LASTFM_API_SECRET |
Last.fm shared secret (scrobbling) |
LISTENBRAINZ_TOKEN |
ListenBrainz user token (scrobbling) |
PIXELTUI_SUBSONIC_URL / _USER / _PASS |
Subsonic/Navidrome server |
PIXELTUI_LOCAL_DIRS |
local music folders (PATH-style list) |
PIXELTUI_DOWNLOAD_DIR |
where downloads are saved |
PIXELTUI_THEME |
accent theme (default · ocean · matrix · amber · rose · mono) |
PIXELTUI_SERVE_ADDR / _URL / _TUNNEL |
pixeltui serve bind address / public URL / tunnel |
PIXELTUI_YTDLP / PIXELTUI_MPV |
override the yt-dlp / mpv binary path |
The config file is written 0600 since it can hold a password.
pixeltui can pull music from three places; switch between them in the b browse menu.
- YouTube Music (default) — search, radio, and lyrics with no account.
- Subsonic / Navidrome — point it at your server in
setup. Starred songs and playlists are browsable and stream directly (no yt-dlp). - Local files — add folders in
setup; tags are read viaffprobewith a filename fallback. Plays directly.
/ searches whichever source you're currently browsing.
Set a download folder (in setup), then press d on any YouTube track. Files are
saved with embedded tags and cover art in a standard layout:
<download_dir>/Artist/Album/Title.ext
That's exactly what Subsonic/Navidrome expect to scan — drop the folder into your server's music library and it just works.
Everything pixeltui stores about your listening is kept in open formats under
~/.pixeltui/library/, so nothing is locked in:
- Likes & playlists → M3U8 (export any playlist to XSPF with
pixeltui export) - Listening history → ListenBrainz-style JSON
- Up Next + session → restored on the next launch
Everything lives under ~/.pixeltui/:
config.json configuration
cache.db stream-URL + API cache (bbolt)
graph.bin offline recommendation graph
library/ likes, playlists, history
bin/ self-contained tools pixeltui installed (yt-dlp)
ytdlp-venv/ fast pip yt-dlp (optional)
mpv.app/ self-contained mpv (macOS, optional)
Reset any of it with pixeltui reset <target>. Installed tools (mpv, yt-dlp) are
kept even on reset all.
pixeltui serve exposes your library and sources over HTTP so a phone (or any
client) can browse, search, and stream from anywhere. A native iOS companion
app (PixelPal, SwiftUI) lives in clients/ios with full feature parity —
offline downloads, two-way like/playlist sync, daily mixes, stations, lyrics,
and stats. The full HTTP API is documented in docs/API.md if you
want to build your own client.
pixeltui serve # LAN: prints a pairing QR + code
pixeltui serve --tunnel tailscale # advertise your tailnet address
pixeltui serve --tunnel cloudflare # public trycloudflare.com URL, no account
pixeltui serve --tunnel ngrok # public ngrok URL (authtoken required)
pixeltui serve --url https://music.example.com # BYO tunnel / reverse proxyAll of these can be saved as defaults in pixeltui setup (Companion server
step) — or in the server section of ~/.pixeltui/config.json — so a plain
pixeltui serve does the right thing:
- Pairing: scan the QR (or enter the URL + code) once; the device gets a
saved token (hashed at rest in
~/.pixeltui/devices.json, revocable). Pairing codes are single-use and rotate after repeated bad attempts. - Transport: REST for actions, Server-Sent Events for live state.
- Streaming: everything is served range-aware — Subsonic and local files directly, YouTube via a natively resolved AAC/m4a CDN URL (instant seek, no transcoding; ffmpeg only as a last-resort fallback).
- Shared library: likes and playlists live in
~/.pixeltui/libraryas plain M3U8 — the TUI and every paired client read and write the same files, so changes made anywhere show up everywhere. - Shared history: clients report plays back (
/api/played), landing in the samehistory.jsonlthe TUI writes — so phone listening feeds Recently Played, listening stats, recommendations, and scrobbling (Last.fm / ListenBrainz) exactly like TUI plays. - Full feature surface: search (all sources), artist/album pages, charts,
radio stations, engine recommendations, synced lyrics, listening stats,
likes/playlists, play history — everything the TUI offers, over
/api/*endpoints guarded by per-device tokens.
| Tunnel | URL | Privacy | Needs |
|---|---|---|---|
tailscale |
http://<host>.ts.net:8787 |
private mesh (WireGuard) — recommended | Tailscale on both devices |
cloudflare |
random *.trycloudflare.com (HTTPS) |
public URL, bearer token is the gate | cloudflared binary |
ngrok |
ngrok domain (HTTPS) | public URL, bearer token is the gate | ngrok + authtoken |
--url |
whatever you run | up to you | your own tunnel/proxy |
--tunnel cloudflare and --tunnel ngrok spawn the tunnel as a child
process, wait for the public URL, bake it into the pairing QR, and tear the
tunnel down when the server stops. The tunnel is supervised: if it dies
mid-session it's restarted automatically (with backoff), and if the restart
comes back on a different URL the server re-advertises it live — new pairing
QR in the terminal, updated /api/sources endpoints, and a push to connected
apps so they learn the new address without re-pairing. --tunnel tailscale
starts nothing — it just detects your tailnet DNS name and advertises it.
Note that a Cloudflare quick tunnel gets a new random URL on every start. Paired clients handle this automatically when they can: the server advertises all its addresses, and the app falls back across them (e.g. to the LAN address when you're home). If the app can't reach any known address — a fresh tunnel URL while you're away — paste the new URL in the app under Settings → Server → Change Address; the pairing carries over. For a stable address, use Tailscale.
The server binds plain HTTP, so don't port-forward it to the open internet — put it on a private tunnel. Tailscale is the zero-config option (free for personal use, WireGuard-encrypted end to end):
-
Install Tailscale on the machine running
pixeltui serveand sign in (brew install --cask tailscaleon macOS, or your package manager). -
Install the Tailscale app on the phone, sign in to the same account, and leave its VPN toggle on.
-
Find the machine's tailnet name:
tailscale status(e.g.mymac.tail1234.ts.net). -
Start the server — it detects the tailnet name itself:
pixeltui serve --tunnel tailscale
-
Pair by scanning the QR. The phone now reaches your library from any network — home Wi-Fi, LTE, hotel — as long as Tailscale is connected.
The traffic is HTTP inside an encrypted WireGuard tunnel, and only devices
on your tailnet can reach the port at all. Other tunnels (Cloudflare Tunnel,
ngrok) also work — pass whatever public URL they give you via --url — but
they expose the endpoint publicly, leaving only the bearer token as the gate,
so a private mesh like Tailscale is the recommended default.
tui/ the pixeltui Go app — go.mod at root
main.go CLI entry point (all subcommands)
tui/ interactive player (Bubble Tea) + playback
server/ companion-app HTTP server (`pixeltui serve`)
engine/ recommendation scoring engine
store/ hybrid data source: graph + bbolt cache + Last.fm
innertube/ native YouTube stream resolution (no yt-dlp)
library/ likes, playlists, history (M3U8 / JSONL on disk)
lastfm/ ytm/ subsonic/ local/ music sources & metadata clients
scrobble/ lyrics/ download/ config/ supporting services
docs/ architecture, server API reference, user guide
clients/ios/ PixelPal — the iOS companion app (SwiftUI; local-only for now)
Developer docs: docs/ARCHITECTURE.md · server HTTP API: docs/API.md · extended user guide: docs/GUIDE.md
Requires Go 1.25+.
make build # → ./pixeltui (builds ./tui)
make install # → $PREFIX/pixeltui (default /usr/local/bin)
make release # cross-compile for all platforms → dist/
make help # all targetsThe build version is embedded via -ldflags -X main.version (pixeltui version);
plain make build derives it from git describe.
scripts/release.sh v0.2.0 # tags + pushes; CI does the restPushing a v* tag triggers .github/workflows/release.yml, which cross-builds
every platform, writes SHA256SUMS, and publishes the GitHub release that
pixeltui update pulls from. Tags with a suffix (e.g. v0.2.0-rc1) publish as
pre-releases.
Run pixeltui doctor — it reports the status of every dependency, your API key,
configured sources, the graph, and the cache, and tells you exactly how to fix
anything that's missing. Add --fix to auto-resolve what it can.
pixeltui is released under the MIT License.
The Go libraries compiled into the binary are all permissive (MIT / BSD-3-Clause). The external tools pixeltui drives — yt-dlp, mpv, and ffmpeg — are run as separate, user-installed programs; they are not bundled with or linked into pixeltui, so their licenses (Unlicense, GPL/LGPL) apply only to those tools. Full attribution and per-license details are in THIRD_PARTY_NOTICES.md.