Skip to content

Latest commit

Ā 

History

385 Commits

Folders and files

NameName
Last commit message
Last commit date
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 

Repository files navigation

mpdtui logo

mpdtui

A lazygit-style terminal UI for MPD (Music Player Daemon).

Go version License: MIT


Bordered panels, single-key contextual actions, vim-style navigation — no mouse required. Two run modes: a full panel UI, and a lightweight single-line inline player for a shell or tmux pane.

Screenshot

mpdtui full panel UI: Library tree, Album Art, Playlists with Name/Count columns, Queue with Title/Album/Artist/Year/Genre/Composer/Type/Duration columns, live stats, and the Now Playing equalizer visualizer

Demo

mpdtui demo -- click to play the video
Click the screenshot to play the demo (GitHub doesn't inline-render repo video files).

Features

  • Library — expandable directory tree (MPD's actual filesystem layout, lazily loaded per folder), or free-text tag search; o cycles name/most-recently-modified sort. Search (/, and the global f artist/album/track search) is case- and accent-insensitive, so "bruno" matches "Bruno" and "buble" matches "BublĆ©"
  • Album art — shown for the currently playing track, updated automatically on track change. Renders as a crisp real image via the Kitty graphics protocol when the terminal supports it (detected from $TERM containing kitty, or $KITTY_WINDOW_ID being set -- covers kitty itself and kitty-compatible terminals that keep TERM as xterm-kitty, even if it's been overridden to something like xterm-256color for remote-host terminfo compatibility). Everywhere else -- including terminals that do support the Kitty protocol but don't signal it either way, e.g. some WezTerm/Konsole configurations -- falls back to a fixed 30x15 ASCII-art rendering; there's no capability probe, since one risks hanging against a terminal that never answers it. "No Album Art" is shown if MPD has none embedded or alongside the track.
  • Playlists — a Name/Count table, mirroring the Queue panel's own column layout: a pinned header row, Name (šŸŽµ icon prefix, truncated to 24 characters with "...") on the left, Count right-aligned as the last column, populated once fetched and refreshed automatically every 10 minutes in the background or on demand with R. Load, append, save, delete stored playlists; o cycles between most-recently-updated and alphabetical
  • Queue — reorder, remove, clear, jump to any track, add the selected track to an existing playlist (a, fuzzy-search by name in a popup -- writes directly into that playlist's own file, rejecting a track already in it rather than writing a duplicate); a pinned header row (Title/Album/Artist/Rating/Lyr/Plays/Mark/Year/Genre/Composer/ Type/Duration) stays visible while scrolling. Title, Album and Artist are also pinned horizontally: they stay on screen at any terminal width, while h/l scroll everything from Rating rightward into view, so a narrow terminal hides no column permanently. Title is bold and colored WhatsApp green; Title/Album/Artist/Genre/Composer are truncated (30/20/40/9/14 characters) with "..." if longer; Type shows a color-coded format badge (MP3/FLAC/M4A/...); Type and Duration are right-aligned. A narrow Lyr column, right after Rating, shows a colored tick per lyrics format found for the track -- green for synced (.lrc), orange for plain (.txt), both adjacent (no gap) if both exist -- only present at all when music_dir is configured and actually exists; otherwise the Queue looks exactly as it would without the lyrics feature -- see Lyrics
  • Lyrics (y) — a viewer (bordered the same green as a focused panel, muted-yellow text), positioned over the Queue's own Artist-through-Type columns, for the currently playing track's lyrics, read from a .txt sidecar file next to the track on disk -- or, if a same-named .lrc (synced/timestamped) file exists instead, real karaoke-style highlighting: the currently-singing line is colored and auto-scrolled into view live as the track plays; see Lyrics for setup
  • Lyrics search (f → l <words>, I) — find tracks by a phrase in their lyrics, matched against a prebuilt on-disk index of every .txt/ .lrc sidecar. I (re)builds the index in the background with a progress overlay; it's incremental and cancellable. See Searching by lyrics
  • Track metadata (1-5, m) — local play count, 1-5 star rating, and a mark-with-reason flag (e.g. "mark for deletion"), stored in a SQLite database separate from MPD's own library; see Track metadata for setup
  • Settings (e) — a two-tab overlay: a read-only Config tab (MPD connection, music_dir, track_metadata status) and a Database tab -- a table-selector wizard over the mark-reason/tag catalogs, with a proper bordered edit box to add entries and confirm-before-delete, without touching the SQLite file by hand
  • Library stats — live total tracks (green) / artists (sky blue) / playlists (cyan), shown alongside the Queue search box, refreshed on library/playlist changes; its own border shows the running mpdtui version, right-aligned
  • Now Playing bar — a vibrant play/pause/stop glyph (bright green/yellow/red), title (bold WhatsApp green) - artist (bold sky blue), live progress (cyan bar), volume (colored along a green-to-red gradient by level), repeat/random/single/consume flags (bold, green when on/red when off), and the current track's local rating and play count (when track_metadata is active) -- updated instantly via MPD's idle protocol (stays in sync even when playback changes from another client, e.g. mpc); its right half shows a small visualization (v cycles Balance / Equalizer / Cliamp) -- real FFT spectrum analyzers when MPD's fifo output is configured (see Visualizations), falling back to a playback-driven animation when it isn't
  • Lightweight inline mode (-mini) — two live status lines (queue/ playlist counts, then track/progress), no alt-screen takeover, for tmux status panes or a quick glance
  • Fuzzy pickers (-p / -t) — fzf-style playlist/track search from the shell, no panels involved
  • Track info and metadata updates (-i / -iu) — print details for the currently playing track (-i), or update track properties such as rating (-iu -r 1-5) directly from the command line
  • Confirmation prompts on destructive actions (clear queue, delete playlist)

Install

Homebrew

brew install susamn/tap/mpdtui

Ships as a prebuilt binary, so this needs no Go toolchain and takes seconds.

Previously this lived in its own tap, susamn/mpdtui. That tap is deprecated and no longer updated -- if you installed from it, move over with:

brew uninstall mpdtui && brew untap susamn/mpdtui
brew install susamn/tap/mpdtui

From source

go build -o mpdtui ./cmd/mpdtui

Requires Go 1.26+ and a reachable MPD server.

Usage

./mpdtui            # full panel UI
./mpdtui -mini      # lightweight inline player
./mpdtui -p         # fuzzy-search playlists; Enter clears the queue and plays it
./mpdtui -t         # fuzzy-search tracks; Enter adds it to the queue and plays it
./mpdtui -i         # print info for the currently playing track
./mpdtui -iu -r 4   # update rating of the currently playing track (1-5)
./mpdtui -v       # print version and exit

-mini, -p, -t, -lyrics-line, -i, and -iu are mutually exclusive.

Connects using the same environment variables as mpc:

Variable Default Notes
MPD_HOST localhost may be password@host
MPD_PORT 6600

Press ? inside the full UI for the in-app keybinding list.

Theming

mpdtui reads its color palette from a plain file -- borders, the selected-row highlight, format badges, ratings, and every other accent color come from it, rather than being fixed in the binary. This is entirely config-driven: mpdtui itself has no built-in notion of Omarchy, matugen, or any other specific desktop -- it only knows about one setting, theme_file, in its own config file (~/.config/mpdtui/config, alongside music_dir/track_metadata).

The first time mpdtui ever runs (any mode -- full UI, -mini, -p, -t), it creates both of these if they don't already exist yet, and never touches either again once they do:

  • ~/.config/mpdtui/colors.toml -- mpdtui's own default color file, seeded with its original built-in colors, so there's a real, inspectable, editable file on disk from the start rather than a value only visible in the source.
  • ~/.config/mpdtui/config -- with theme_file = ./colors.toml already pointing at that default file (a relative path resolves against ~/.config/mpdtui itself, whatever $XDG_CONFIG_HOME is set to; ~/... and absolute paths both work too), plus commented examples for music_dir/track_metadata/visualizer_fifo.

So out of the box, on any generic Linux system, mpdtui looks exactly like it always did -- just now backed by a file you can open and edit directly, rather than a fixed scheme. To follow a live desktop theme instead, point theme_file at wherever that theme's colors actually live -- see below for Omarchy and matugen specifically, or hand-edit ~/.config/mpdtui/colors.toml itself for anything else.

File format -- flat key = "value" pairs, no sections/arrays/ nesting:

mode = "dark"

accent = "#89b4fa"
selection = "#cdd6f4"
muted = "#45475a"

background = "#1e1e2e"
dark_background = "#171723"
darker_background = "#0f0f17"
lighter_background = "#353543"

foreground = "#cdd6f4"
dark_foreground = "#9aa1b7"
light_foreground = "#d5dcf6"
bright_foreground = "#dae0f7"

red = "#f38ba8"
yellow = "#f9e2af"
orange = "#f59cb5"
green = "#a6e3a1"
cyan = "#94e2d5"
blue = "#89b4fa"
magenta = "#cba6f7"
brown = "#935e6d"

bright_red = "#f38ba8"
bright_yellow = "#f9e2af"
bright_green = "#a6e3a1"
bright_cyan = "#94e2d5"
bright_blue = "#89b4fa"
bright_magenta = "#cba6f7"

(this is deliberately the same shape Omarchy's own theme files use -- any subset of these keys is fine, missing ones just keep the built-in default for that slot; see internal/theme.Default)

theme_file is read once at startup. To make an already-running mpdtui (full UI or -mini) pick up a change to that file without restarting it, send it SIGUSR1 -- the same convention e.g. Omarchy's own theme-set flow already uses for kitty/ghostty/btop/helix (see omarchy-restart-terminal and friends): each app opts in by handling that signal itself and re-reading its own config, rather than something else pushing color data at it. Whatever regenerates theme_file should send this after writing it:

pkill -SIGUSR1 mpdtui

Also visible at a glance from the Settings overlay (e) -- the Config tab shows the resolved theme_file path and whether it was actually found and read.

Omarchy

Point theme_file at Omarchy's own live theme file, which it keeps in sync on every omarchy theme set ...:

theme_file = ~/.local/state/omarchy/current/theme/colors.toml

Then drop this in ~/.config/omarchy/hooks/theme-set.d/reload-mpdtui.hook (and chmod +x it) so mpdtui re-reads it automatically on every switch:

#!/bin/bash
pkill -SIGUSR1 mpdtui

matugen

Unlike Omarchy, matugen has no single fixed output file of its own: it only produces whatever template you configure, written to wherever you tell it. Add a template that emits mpdtui's own file shape (~/.config/matugen/templates/mpdtui-colors.toml):

accent = "{{colors.primary.default.hex}}"
selection = "{{colors.secondary_container.default.hex}}"
muted = "{{colors.outline.default.hex}}"

background = "{{colors.surface.default.hex}}"
dark_background = "{{colors.surface_dim.default.hex}}"
lighter_background = "{{colors.surface_bright.default.hex}}"

foreground = "{{colors.on_surface.default.hex}}"
dark_foreground = "{{colors.on_surface_variant.default.hex}}"

red = "{{colors.error.default.hex}}"
yellow = "{{colors.secondary.default.hex}}"
orange = "{{colors.tertiary_container.default.hex}}"
green = "{{colors.tertiary.default.hex}}"
cyan = "{{colors.secondary_fixed.default.hex}}"
blue = "{{colors.primary.default.hex}}"
magenta = "{{colors.tertiary_fixed.default.hex}}"

bright_red = "{{colors.on_error_container.default.hex}}"
bright_yellow = "{{colors.on_secondary_container.default.hex}}"
bright_green = "{{colors.on_tertiary_container.default.hex}}"
bright_cyan = "{{colors.secondary_fixed_dim.default.hex}}"
bright_blue = "{{colors.primary_container.default.hex}}"
bright_magenta = "{{colors.tertiary_fixed_dim.default.hex}}"

(the red/green/yellow/blue/magenta/cyan role choices mirror matugen's own usual kitty-template ANSI-color mapping, so it stays consistent with the rest of an existing matugen setup) then wire it into ~/.config/matugen/config.toml:

[templates.mpdtui]
input_path = '~/.config/matugen/templates/mpdtui-colors.toml'
output_path = '~/.cache/mpdtui/colors.toml'
post_hook = "pkill -SIGUSR1 mpdtui"

post_hook there is exactly the same SIGUSR1 nudge as the Omarchy hook above -- matugen already runs it on every regenerate. Then point theme_file in mpdtui's own config at that same output_path:

theme_file = ~/.cache/mpdtui/colors.toml

Keybindings

Global (any panel):

Key Action
Space Toggle play/pause
s Stop
n Next track
p Back / previous track
, / . Seek -5s / +5s
- / = Volume down / up
z Toggle random (shuffle)
x Toggle repeat
c Toggle consume
Z Toggle single
D Clear entire queue (confirm)
Tab, 1/2/3 Cycle / jump focus between panels
/ Search (contextual: filters Library/Playlists, jumps to a match in Queue)
f Global search from any panel -- type a/al/l/p/t + a term (artist/album/lyrics/playlist/track); matches appear live in a results table laid out per kind: track = šŸŽµ Track + šŸŽ¤ Artist, lyrics = those two plus šŸ“ a matched-lyrics excerpt, album = šŸ’æ Album + šŸŽ¤ Album Artist ("Not available" when untagged), artist and playlist = a single column. Up/Down (or Ctrl-P/Ctrl-N) move the highlight while typing; Tab (or f to return) switches focus to the table for j/k/g/G navigation, within the popup only. Enter acts on the highlight and closes the popup (track adds+plays, playlist loads+plays, artist/album jump into that group in the Library); from the table, a instead adds without playing (track) or appends (playlist) and leaves the popup open, so several tracks can be queued back-to-back. Stays open with "no X found" if nothing matches. l (lyrics) matches the term as a plain case/accent-insensitive substring against the words of each track's .txt/.lrc sidecar, read from the prebuilt lyrics index (I builds/refreshes it -- this search never touches the filesystem; an unbuilt index just returns nothing), with the matched term colored in the excerpt; hits otherwise behave exactly like track hits (add+play on Enter, add on a)
I Rebuild the lyrics search index (needs music_dir set, see Lyrics) -- a background scan of every track's .txt/.lrc sidecar with a live progress overlay; incremental, so a rebuild after adding a few lyrics files only re-reads those. Esc cancels a run in flight (the existing index is left intact). The index lives at ~/.config/mpdtui/lyrics_index.db
F Clear any active search/filter, in every panel at once (Library search, Playlists filter) -- unlike a panel's own Esc, works regardless of which panel is currently focused
i Track info card for the currently playing track (or, when nothing is playing, the track selected in the Queue -- see Which track an action applies to) -- Track/Album/Artist/Genre/Year, colored "LRC"/"TXT" text for whichever lyrics format(s) are found (needs music_dir set), live audio quality (bitrate, sample rate/bit depth/channels), the stored playlists the track belongs to (up to 4 listed, the rest summarised as a count -- Tab toggles the card between that summary and the full list, growing it upwards within the Queue panel; from the same background playlist scan that fills the Playlists panel's Count column, so it costs no extra MPD traffic, and reads "loading…" rather than "none" until that scan lands), and, when track_metadata is active, a Rating/Plays table plus Marks and Tags lists (same treatment as the playlists: capped, one Tab expands all three together). A small fixed-size card anchored to the bottom-right quadrant of the Queue panel
y Lyrics viewer for the currently playing track (needs music_dir set, see Lyrics below) -- j/k/g/G/Ctrl-F/Ctrl-B to scroll, y or Esc to close. Transport controls (Space/s/n/p/,/./-/=/z/x/c/Z) keep working while it's open. Shows synced (.lrc) lyrics with the current line auto-highlighted and scrolled into view when available, otherwise plain .txt; a colored LRC/TXT badge sits top-right in the title. t switches between whichever formats exist for the track (choice sticks across track changes); before an .lrc's first timestamp, a big blinking block-letter "STARTING" banner shows instead of the lyrics list
v Cycle Now Playing visualizations (right half of the Now Playing bar)
w Story card for the currently playing track (or the Queue selection when nothing is playing) -- the cover on the left, and on the right the story, behind-the-scenes notes, bootlegs, links and source attribution. Read from a wiki/ folder beside the audio; mpdtui never goes online, the stories are gathered ahead of time by music-tui's wiki-fetch and copied in by wiki-push. The picture is real pixels on a Kitty terminal and colored half-blocks everywhere else. j/k scrolls, w or Esc closes, transport keys stay live. A track with no story says so in the hint bar rather than opening an empty card
M Library card, centered on the Queue panel: a summary of the whole collection. Collection -- tracks, albums, artists, stored playlists, total playtime, and when MPD last updated its database. Lyrics and stories -- how many tracks have a sidecar (synced .lrc vs plain .txt, both, and sidecars matching no track), how many have a fetched story, how many story images sit beside them, and the state of the lyrics search index: whether it has been built at all, how many tracks are in it and when it was built (red-flagged if it was built against a different music_dir). The sidecar counts need music_dir; the index line does not. Local metadata -- rated (with the average), played, plays, bookmarks, marked and tagged, each against the size of its catalog, when track_metadata is active. Playlist fallouts -- entries a stored playlist lists that the library has no track for; MPD skips those silently when loading a playlist, so they are invisible everywhere else in the app. Recently added -- the newest tracks by file modification time (MPD records no "added" date). The border carries the snapshot's age. Opens on the last scan and fills in behind itself, since the bottom half is several whole-library round-trips; r rescans, j/k scrolls, M or Esc closes, transport keys stay live. A snapshot under a minute old is reused rather than rescanned. The card is sized to the Queue panel and re-lays its two-column rows into one column when that panel is too narrow for them
L Locate the currently playing track: selects it in the Queue and moves focus there, from any panel, and also reveals it in the Library tree (expanding every folder along its path and selecting it there, without moving focus away from Queue). Both panels scroll the track to their vertical middle rather than leaving it on the top or bottom line, so it lands with its neighbours visible around it, and both the Queue row and the Library node flash briefly to draw the eye to it. The Queue-selecting part also happens automatically, whenever the playing track actually changes (explicit play action or natural auto-advance alike) -- except while an overlay is open, or on startup; the centering, the flash and the Library reveal are only on the explicit keypress, so a natural auto-advance never re-scrolls the Queue under you
e Settings: a two-tab overlay -- Config (read-only: MPD host/port, music_dir, track_metadata status and file paths) and Database (browse/add/delete mark_reason/tags catalog rows, only when track_metadata is active; otherwise explains why it isn't). Tab/Backtab switches tabs; on Database, Left/Right switches which catalog table, j/k/g/G navigates rows, a adds (bordered edit box), d deletes (y/n to confirm); Esc closes
? Help overlay
q Quit

Panel-local:

Panel Key Action
Library Enter Expand/collapse a folder, or add+play a track
Library a Add selected folder (recursively) or track to queue (no play), then move focus to the Queue -- what you just added is there, and the next thing you do is almost always to it. Focus only moves on a real add, so pressing a on a node with nothing behind it stays put
Library A Add every current search result to the queue at once. Only active on search results (/ in this panel, or an artist/album opened from global search) -- in browse mode the top level is the whole library, so it says so instead. Moves focus to the Queue afterwards, like a
Library Backspace Collapse folder, or go up to its parent
Library j/k/g/G Native tree navigation (also J/K to jump in/out a level)
Library o Cycle sort: name / most recently modified (browse mode only)
Library Esc Clear active search
Playlists Enter Load playlist into queue and play
Playlists a Append playlist to queue
Playlists d Delete playlist (confirm)
Playlists S Save current queue as a new playlist
Playlists R Refresh track counts now (also happens automatically every 10 minutes in the background)
Playlists o Cycle sort: most recently updated / name
Playlists Esc Clear active filter
Queue Enter Play selected track
Queue a Add the currently playing track (or the selected one when nothing is playing -- see Which track an action applies to) to an existing playlist: fuzzy-search its name in a popup (type to filter, j/k/g/G/Up/Down to navigate, Enter to add). Writes directly into that playlist's own file; rejected with an error, no duplicate written, if the track is already in it
Queue d Remove selected track
Queue J / K Move selected track down / up
Queue / Search: focuses the always-visible "Search track:" box above the queue, Enter jumps to first match (Esc cancels)
Queue 1-5 Rate 1-5 stars (needs track_metadata set, see Track metadata below): the currently playing track, or the selected one when nothing is playing -- so scrolling the Queue away from what's playing doesn't redirect the rating. Note: this means 1/2 no longer jump to Library/Playlists from inside Queue -- Tab/Backtab still cycle panels regardless of focus
Queue t Tag the currently playing track (or the selected one when nothing is playing), from the same kind of popup as m -- a checklist of the tag catalog, Enter toggles, Esc closes, "(clear all tags)" at the top. Edit the catalog itself in Settings (e, Database tab)
Queue m Mark the currently playing track (or the selected one when nothing is playing -- see Which track an action applies to), from a small popup -- j/k/g/G to navigate, Enter toggles the highlighted reason on or off, Esc closes. A track can carry several marks at once, so the popup is a checklist (a āœ“ against each mark that is set) that stays open as you toggle, with a "(clear all marks)" entry at the top. Transport controls keep working while it's open

Mini mode (-mini): Space play/pause, n/p next/prev, s stop, -/= volume, 1-5 rate whatever's currently playing (needs track_metadata set, see Track metadata), q/Ctrl-C quit.

Which track an action applies to

Four of the Queue panel's keys act on a track rather than on a row of the list: 1-5 (rate), m (mark), a (add to a playlist) and i (the track info card). All four target the same thing:

  • the track currently playing if there is one, wherever the Queue's own cursor happens to be
  • the selected track, when nothing is playing -- paused or stopped

Rating, marking and filing a track away are judgements about the music you're listening to, and scrolling down the queue mid-track to see what's coming up is a completely normal thing to do -- so while a track plays, the cursor's position shouldn't quietly redirect them onto whatever row it was left on.

Once playback stops or pauses there is nothing being listened to, and the cursor is the only track you are actually pointing at, so it takes over. Paused used to count as playing here, on the reasoning that it's still "the track you're on"; that was reversed, because it meant pausing and scrolling elsewhere left i stubbornly showing the paused track. (MPD also keeps reporting a "current song" while stopped -- the position it would resume from -- which likewise isn't a track anyone is listening to.)

The m and a popups both name their target in their own title, and both stay pinned to it: transport controls keep working while a popup is open and a track can auto-advance on its own, so the mark or the playlist entry still lands on the track the title promised.

The Queue's positional keys are deliberately unaffected, because for them the selection genuinely is the subject: Enter (play), d (remove) and J/K (move) all still act on the selected row.

Lyrics

Put a .txt file next to a track (same directory, e.g. /a/b/Some Track [84934].mp3 → /a/b/Some Track [84934].txt) and mpdtui will show it. Matching is normalized rather than exact: special characters are stripped from both the track's and the .txt's filename before comparing, so Some Track [84934].txt and some_track-84934.txt both match the same track despite differing punctuation.

MPD's own protocol has no command to serve an arbitrary sidecar file's content (unlike album art, which MPD serves natively), so this requires mpdtui to read the file directly off disk -- it only works if mpdtui runs somewhere that can actually see the same files MPD does (typically the same host, or a mounted/synced path). Point it at the right directory by creating ~/.config/mpdtui/config (respects $XDG_CONFIG_HOME if set) with:

music_dir = /path/to/your/music

(~/ is expanded.) If the config file doesn't exist, has no music_dir line, or music_dir names a path that doesn't actually exist (a typo, a stale setting, an unmounted drive), the lyrics feature stays inactive: no Lyr column in the Queue at all (not even an empty one -- the table looks exactly as it would without this feature), and y still opens the viewer but explains what's missing rather than erroring.

Lyrics availability is rechecked live every time the Queue repopulates (adding a track, loading/appending a playlist, even another client like mpc changing the queue), not cached from when a track was first added -- so a .txt file dropped in later shows up on the next Queue refresh without needing to requeue anything.

Synced lyrics (auto-highlighted current line)

Same idea, but with a .lrc file instead of (or alongside) the .txt -- same directory, same base filename, e.g. /a/b/Some Track [84934].lrc. Standard LRC format: one line per timestamp, [mm:ss.xx]lyric text (hundredths or milliseconds both work, e.g. [02:15.30] or [02:15.300]); metadata tags like [ar:Artist]/[ti:Title] at the top are recognized and ignored rather than shown as lyrics. Free synced lyrics for a lot of music are available from sites like lrclib.net.

When a .lrc exists for the currently playing track, a colored "LRC" badge appears in the top-right corner of the viewer's title, and the line matching the current playback position is highlighted (a solid background band) and kept in view as playback advances, auto-scrolling a few lines ahead of pinning it to the very top. Before the .lrc's first timestamp -- an instrumental intro -- a big blinking block-letter "STARTING" banner (a terminal has no real font-size control, so this is drawn out of block characters across several rows rather than just being bigger text) plus a blinking ..... shows in place of the lyrics list, with a blank line beneath it, until the first real line's timestamp arrives; this is .lrc-only, plain .txt is never affected. If only a .txt exists, or the .lrc has no lines mpdtui can parse a timestamp from, it falls back to plain, unhighlighted text exactly as before (badge shows "TXT" instead).

If a track has both a .txt and a .lrc, press t (while the viewer is open) to switch between them -- synced is preferred by default, t toggles to plain and back, and the title's badge flips to match. The choice sticks across track changes (not reset per song), so switching once keeps applying as you skip through the queue, falling back gracefully on any individual track that doesn't have your preferred format. A future word-level/enhanced-LRC format (sometimes called A2) would slot into the same t cycle once supported.

Searching by lyrics

f then l <words> finds tracks by a phrase in their lyrics -- e.g. l never gonna give you up. The term is matched as a plain case/accent-insensitive substring against the combined text of each track's .txt and flattened .lrc sidecar; a hit is added and played (or, with a, just added) exactly like a track hit.

Results land in a three-column table -- šŸŽµ Track, šŸŽ¤ Artist, šŸ“ a one-line excerpt of the matching lyrics with the search term itself colored -- so it's obvious at a glance why a track matched:

šŸŽµ Sweet Child o' Mine     šŸŽ¤ Guns N' Roses   šŸ“ …she's got a smile that it seems to me …
šŸŽµ Cats in the Cradle      šŸŽ¤ Harry Chapin    šŸ“ …my child arrived just the other day …

This search reads a prebuilt index only -- it never walks the music directory itself, because that scan is thousands of syscalls and stalls the UI on a large library. Build or refresh the index with I: a background scan with a live progress overlay (Esc cancels; the existing index is left intact on cancel or failure). It's incremental -- a sidecar unchanged since the last run is not re-read -- so refreshing after dropping in a few new lyrics files is near-instant. The index is a single SQLite file at ~/.config/mpdtui/lyrics_index.db (next to config and mpdtui.db); deleting it just means the next I rebuilds from scratch. Until the first I, l searches return nothing.

Which format(s) exist for a track is also visible without opening the viewer at all: the Queue's Lyr column shows a green tick for .lrc, an orange tick for .txt (both adjacent, no gap, if both exist -- a terminal can't blend the two colors into one glyph, so this is the closest a character grid gets to "overlapping"), and the track info card (i) shows the same colors as text -- "LRC" and/or "TXT" -- right next to the rest of the track's details.

Track metadata

Local, per-track bookkeeping -- play count, a 1-5 star rating, and a "marked with a reason" flag -- that MPD itself has no concept of, kept in a SQLite database entirely separate from MPD's own library (mpdtui never writes to anything MPD manages; it only ever adds its own opinions about a track MPD already reports).

Off by default. Enable it by adding to ~/.config/mpdtui/config (the same file music_dir lives in):

track_metadata = true

The database itself lives at ~/.config/mpdtui/mpdtui.db (next to config, nowhere else) and is created automatically the first time it's needed.

When active, the Queue table gains three right-aligned columns right before Type, in this order: Plays, Mark (one colored tick per mark, blank if unmarked -- different mark reasons get different tick colors, and past three marks it shows a single tick plus the count so one heavily-marked track can't widen the column for the whole queue), and Rating (gold stars, filled/unfilled). All database reads/writes happen in the background -- rating or marking a track flashes its confirmation immediately, and the relevant column repaints as soon as the write lands, without ever blocking a keypress on disk I/O.

Tracks rated above 3 stars also get a single star in the Queue's left gutter, in the space between the panel border and the index number that the "ā–¶" playing marker already occupies -- so it costs no width, and the rows don't shift. Five stars take the Rating column's own color at full strength and four a weakened version of it, so the two tiers read apart at a glance while scrolling. Unlike the Rating column, the gutter star survives a narrow terminal, since dropping columns to save space never drops it.

Both tiers come from a single theme color, with the weaker one dimmed toward the background to a guaranteed luminance separation (at least 2.23x across the themes tested, while keeping the dimmest 4-star star 2.01x above its own background) rather than read from a second palette field. Pairing "yellow" with "bright_yellow" seems like the obvious approach and does not work: across the 15 Omarchy themes this was tested against, 8 had those two within 1.2x luminance of each other and 3 had them byte-identical.

  • Rating (1-5, Queue panel): rates the track that's currently playing, or the selected one when nothing is playing -- see Which track an action applies to. In -mini mode, which has no separate selection, 1-5 always rate whatever's currently playing.

  • Play count (Plays column in the full UI): tracked automatically, no keybinding. A track counts as played once you've listened to at least 50% of it (by elapsed/duration, not just "it started"), counted once per queue song id so ticking past the halfway point on every refresh, or seeking back across it, doesn't inflate the count. -mini mode tracks this independently the same way -- if you run both a full-UI and a -mini instance against the same MPD server at once, a single play-through can be double-counted.

  • Mark (m, Queue panel): opens a small popup listing mark reasons (e.g. "mark for deletion") for the currently playing track -- or the selected one when nothing is playing, exactly like Rating above (see Which track an action applies to) -- plus a "(clear all marks)" entry.

    A track can carry several marks at once, and a mark applies to as many tracks as you like -- the same shape tags already had. So the popup is a checklist rather than a one-of-N choice: Enter toggles the highlighted reason and the popup stays open, since the natural thing after adding one mark is to add another. Each row shows whether that reason is currently set, which makes the popup double as the answer to "what is this track marked with?".

    The Track Info card (i) lists a track's marks one per line, each in its own color, capped like the playlist list and expanded by the same Tab. They were a single row in that card's metadata table until marks became a set -- comma-joined into one cell, several reasons ran straight off the side of the card.

  • Tags (t, Queue panel): the same thing for tags, which have been a many-to-many relation in the database since before marks were -- but with nothing to set them: Settings could edit the tag catalog and the card could display a track's tags, with no way in between to actually put one on a track. t is that way, using the same checklist popup as m, and the card lists tags exactly like marks.

    Databases from before this existed are migrated automatically on first launch: each track's single mark moves into the new join table, and the old column is only dropped once every mark is provably accounted for -- a mismatch rolls the migration back and leaves the old data untouched rather than continuing.

    The popup names the track in its title, and stays pinned to it: if the track auto-advances while the popup is open, the mark still lands on the one it was opened for. This is bookkeeping only -- mpdtui never deletes or moves a file itself, marking one just records your own intent for you to act on later. -mini mode shows the currently playing track's marks (if any) but has no way to set them -- that needs the full UI's popup.

Tracks are matched by their file path, normalized the same way lyrics sidecar files are (special characters stripped per path segment, lowercased) -- so minor path differences don't create duplicate rows, but each directory still stays distinct (a track named the same as another in a different folder is still tracked separately).

Add or delete mark-reason/tag catalog entries in-app via the Settings overlay (e, Database tab) -- see Settings below. Renaming an existing entry still needs the sqlite3 CLI directly against ~/.config/mpdtui/mpdtui.db; only add/delete are exposed in the UI so far.

Visualizations

The right half of the Now Playing bar draws a visualization; v cycles through them.

MPD's client protocol carries no audio data at all -- no spectrum, no waveform, nothing but playback status -- so a client can't analyze what's playing through the connection it already has. MPD the server can be told to duplicate its decoded output into a named pipe, which is the standard way around this (it's what ncmpcpp's visualizer reads too). Add this to your mpd.conf, alongside your existing audio_output, and restart MPD:

audio_output {
    type   "fifo"
    name   "Visualizer feed"
    path   "/tmp/mpd.fifo"
    format "44100:16:2"
}

That's all the setup there is: /tmp/mpd.fifo is mpdtui's default, so it picks the feed up on its own. The format line has to match what's above -- MPD gives no way to discover the pipe's actual format, so mpdtui assumes it. Point mpdtui somewhere else with visualizer_fifo = /some/ path in ~/.config/mpdtui/config, or set visualizer_fifo = off to never read one.

With the feed live, mpdtui reads the PCM, runs an FFT over a ~46ms window, and maps the result onto logarithmically spaced frequency bands, redrawn at 25fps. Three visualizations draw from that, in v's cycle order:

  • Balance (the default) -- ten wide, roughly octave-sized bands, each drawn as its level relative to the average across all of them rather than its absolute loudness. See below for why this is the one shown first.
  • Equalizer -- one narrow frequency band per column, absolute level. The densest view of the spectrum.
  • Cliamp -- wider bars in Winamp colors, with peak caps that hold briefly and then fall. Absolute level too.

Equalizer and Cliamp scale with volume: the fifo carries the stream at full scale, before the mixer MPD's volume setting drives, so that is applied on top.

Why Balance is different, and why it is the default

On an absolute display, the biggest thing moving is overall loudness -- as a track gets louder every bar rises together, and that shared movement is larger than the differences between bands. But it's those differences, the spectral balance, that actually distinguish one moment of a track from another.

Balance subtracts the shared movement out. Each frame it takes the mean level across the bands and draws every bar as its own deviation from that mean, so turning the track up changes nothing on screen and only the balance between bands moves. At the panel's two rows the mean lands exactly on the row boundary: a full bottom row with an empty top row reads as average, anything reaching into the top row is louder than average, a partial bottom row is quieter. Color reinforces the direction -- warm above the mean, blue below -- since most bars sit near the middle where heights are similar.

The bands are few and wide on purpose. Narrow bands over real music track their neighbours closely, so a wide display of them shows the same signal many times over with noise on top; a band spanning roughly an octave averages that noise down and moves distinguishably from the band beside it.

The full/empty points are +/-15dB from the mean, measured against real playback rather than picked: band deviations came out with a median of ~6dB and a 95th percentile of ~14dB, so this puts a typical bar around 70% height while clipping under 4% of them. (Muting still blanks it -- a muted player showing a dancing display would be plainly wrong.)

Without the feed -- no audio_output block, MPD on another machine (a named pipe is local-only), or simply nothing playing -- the visualizations fall back to an animation driven by playback state alone. It looks alive, but it isn't following the music; that's the tell that the fifo isn't being read.

Settings

e opens a two-tab overlay, Tab/Backtab switches between tabs, Esc closes it:

  • Config -- read-only, shown as a bordered Setting/Value table (matching the Database tab's own look). Shows exactly what mpdtui resolved at startup: MPD host/port (and whether a password is set, never the password itself), music_dir (or a note that it's not configured), track_metadata's status, the config/database file paths, and theme_file's resolved path plus whether it was actually found and read (see Theming). There's no way to edit any of this from here -- it's a snapshot for reference, not a settings form; change the underlying environment variables or ~/.config/mpdtui/config and restart mpdtui instead.
  • Database -- a small table-selector wizard over the mark_reason/ tags catalog tables (see Track metadata above), only shown when track_metadata is active (otherwise this tab just explains that it isn't, matching every other track-metadata feature in this app):
    • Left/Right switches which catalog table you're looking at (Mark Reasons / Tags), highlighted in the sub-tab bar
    • The rows themselves are a normal selectable table -- j/k/g/G to navigate, same as every other table in mpdtui
    • a opens a small bordered edit box to type a new entry; Enter adds it and returns to the table, updated immediately
    • d deletes the currently selected row, after a y/n confirmation (matching Playlists' own delete confirmation) -- deleting a mark-reason or tag that's still applied to tracks clears it from them first, rather than leaving a dangling reference
    • No sqlite3 CLI needed for either add or delete anymore

Test

go test ./...

internal/mpdclient and internal/ui's integration tests need a reachable MPD server (they skip automatically if there isn't one).

License

MIT, see LICENSE.

About

A terminal based mpd player. Inspired by lazygit, this player has vim motions. It also has a inline player if you don't want to open the big player.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages