A lazygit-style terminal UI for MPD (Music Player Daemon).
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.
Click the screenshot to play the demo (GitHub doesn't inline-render repo video files).
- Library ā expandable directory tree (MPD's actual filesystem layout,
lazily loaded per folder), or free-text tag search;
ocycles name/most-recently-modified sort. Search (/, and the globalfartist/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
$TERMcontainingkitty, or$KITTY_WINDOW_IDbeing set -- covers kitty itself and kitty-compatible terminals that keepTERMasxterm-kitty, even if it's been overridden to something likexterm-256colorfor 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;ocycles 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, whileh/lscroll 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 whenmusic_diris 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.txtsidecar 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/.lrcsidecar.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_metadatastatus) 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_metadatais active) -- updated instantly via MPD'sidleprotocol (stays in sync even when playback changes from another client, e.g.mpc); its right half shows a small visualization (vcycles Balance / Equalizer / Cliamp) -- real FFT spectrum analyzers when MPD'sfifooutput 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)
brew install susamn/tap/mpdtuiShips 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/mpdtuigo build -o mpdtui ./cmd/mpdtuiRequires Go 1.26+ and a reachable MPD server.
./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.
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-- withtheme_file = ./colors.tomlalready pointing at that default file (a relative path resolves against~/.config/mpdtuiitself, whatever$XDG_CONFIG_HOMEis set to;~/...and absolute paths both work too), plus commented examples formusic_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 mpdtuiAlso 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.
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 mpdtuiUnlike 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
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.
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.
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.
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.
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.
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-minimode, which has no separate selection,1-5always 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.
-minimode tracks this independently the same way -- if you run both a full-UI and a-miniinstance 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:
Entertoggles 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 sameTab. 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.tis that way, using the same checklist popup asm, 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.
-minimode 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.
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.
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.
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, andtheme_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/configand restart mpdtui instead. - Database -- a small table-selector wizard over the
mark_reason/tagscatalog tables (see Track metadata above), only shown whentrack_metadatais active (otherwise this tab just explains that it isn't, matching every other track-metadata feature in this app):Left/Rightswitches 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/Gto navigate, same as every other table in mpdtui aopens a small bordered edit box to type a new entry;Enteradds it and returns to the table, updated immediatelyddeletes the currently selected row, after ay/nconfirmation (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
sqlite3CLI needed for either add or delete anymore
go test ./...internal/mpdclient and internal/ui's integration tests need a
reachable MPD server (they skip automatically if there isn't one).
MIT, see LICENSE.
