# Terminal UI — cbd-tui `cbd-tui` is the terminal client: a ratatui/crossterm application that connects to a running server over gRPC and redraws from the pushed update stream. It reads `cbd-tui.toml` for its connection defaults (see [Configuration](../config.md)); flags before a subcommand override the file, and running it with no subcommand starts the TUI (see [Command line](./cli.md)). ## Layout The screen has two focusable panes side by side and a now-playing pane: - **Library** (left) — the current [library node](../library.md): the child folders and tracks under the path you have browsed to. Entering a folder replaces the listing. - **Queue** (right) — the play [queue](../queue.md), with the playing track highlighted. - **Now playing** — the current track, a progress gauge, and the frequency-spectrum bars below it (see [Spectrum](#frequency-spectrum), fills the rest of the right column). Its top line reports the server's shuffle, repeat and output level — `Volume: 85%`, or `Volume: 85% (muted)`, which keeps the level you would unmute to on screen. The level tops out at 110%, and it is shown even with nothing loaded, since it belongs to the server rather than the track. `Tab` cycles focus between the library and the queue; keys are routed to whichever pane has focus (plus the global keys, which apply in either). ## Navigating Navigation is vim-style. In the library, `j`/`k` move the selection, `l` enters the selected folder, `h` goes back to the parent, and `g`/`G` and `Ctrl-d`/`Ctrl-u` jump. `Enter` **replaces** the queue with the current selection; `a` appends it and `L` queues it after the current track. In the queue, `Enter` plays the selected track, `o` jumps the selection to the playing track, `d` removes a track, `p` inserts the library selection after the selected track, and `c`/`C` clear the queue. `%` creates a child of the open node where that is allowed (e.g. a search term under `/tidal/search` — see [Search](../providers/search.md)); `e` renames such a node; `d` deletes the selection. Playback keys (`Space`, `r`, `Ctrl-n`/`Ctrl-p`, volume, `m`, `z`, `x`) are global. Press `?` at any time for the full binding table; `Esc` or `?` closes it, and while it is open every other key is inert. ## Marks and saving - `s` toggles a **mark** on the selected item. Marks live on the full item list (they survive filtering) so a marked-but-hidden row still counts when you queue or save. - `v` (or `V`) enters **visual mode**: movement then marks or unmarks the rows you sweep over, like vim's visual selection. The selection is anchored where you entered it, so moving back over a row reverses it. `Esc` leaves visual mode (keeping the marks and any `/` filter), and so does any non-movement action. The pane title shows `— VISUAL` while it is on. Both panes have it. - `w` **saves** the selection — a library subtree, or in the queue pane the whole queue — as a new folder of link files under `/crabidy/`; playback replays it from the source provider. - `W` **captures** the selection into `/crabidy/`: like `w`, but each track's audio is fetched into the shared content store (de-duplicated), so it plays back fully local afterwards. It works on a library subtree and on the queue. Downloads can take a while; progress shows in the library pane. See [The crabidy store](../store.md). - Captured rows are marked with a trailing `↓` (down-arrow) at the end of the row, visible even while you browse another provider, so you can see what you already have. ```admonish note Saves and captures all live under the one `/crabidy` provider. Inside `/crabidy`, `d` deletes a folder or track immediately with no confirmation — it removes only the metadata toml, never the shared store audio, which other saves may reference. ``` ## The register: `y`, `d`, and `p`/`P` The queue has marks and visual mode too, and they feed a **register** — one in-memory slot, like vim's unnamed register: - `y` **yanks** the selection into the register without changing anything. It works in both panes: in the library it yanks the paths you have marked (or the row under the cursor), in the queue the marked tracks. - `d` in the queue **deletes** every marked row (or the cursor row) in one go — and puts them in the register first. - `c` and `C` also fill the register with what they clear, so emptying the queue by accident is recoverable. - `p` **pastes** the register after the cursor, `P` before it. The register survives, so you can paste again. That gives you the two vim moves: `d` then `P` puts the tracks back exactly where they were, and `d` … `p` moves them somewhere else. The queue title shows `register: n` while something is in it, so a paste is never blind. ```admonish note `p` used to insert the **library** selection at the cursor. It now pastes the register, so that flow is `y` in the library, then `p` in the queue. `a`, `L`, and `Enter` still queue the library selection directly. ``` ```admonish warning The register lives in **your client**, in memory, one slot deep: another client cannot undo your delete, a restart forgets it, and each write overwrites the last. It holds *paths*, so a paste re-resolves them — a yanked album node expands to its tracks at paste time, and a path that no longer resolves (a search term you deleted meanwhile) does not come back. ``` ## The `/` live filter Pressing `/` opens a search input that filters the focused pane's items live as you type (case-insensitive substring). It **narrows** the visible list rather than jumping to a match, so you can filter and then act on what is left. `Enter` keeps the filter applied and returns to navigation; `Esc` clears it. Movement, marks, and queue actions all map back to the real underlying rows, so acting on a filtered row affects the right track. ```admonish tip This `/` filter is a local view filter over items already listed. It is distinct from the search-node feature (`%` on `/tidal/search` and friends), which asks a provider for results. See [Search](../providers/search.md). ``` Entering a library folder resets the filter (a fresh listing); the queue's filter is preserved across the constant queue updates and only its visible set is recomputed. ## Frequency spectrum A row of frequency bars is drawn under the track progress while audio plays, as block glyphs (`▁▂▃▄▅▆▇█`). The bars are produced on the **server**: it taps its own audio output, runs an FFT (~20 fps), folds the result into a few log-spaced bins, and streams them on the update stream like every other bit of live state. So the bars work whether the server is local or on another machine, and when nothing is playing they fall to the floor. Press `f` to show or hide them at runtime; `spectrum` in the client config sets the startup default (`spectrum = true` is the default; `false` starts hidden). Either way it is only a display choice — the server always computes and streams the bars while audio flows. `spectrum_color` in the client config picks their color, as a `#rrggbb` hex triple, a color name (`red`, `light-blue`, …), or a 0-255 index into the terminal palette. It defaults to the red the queue marks the playing track with, so the two agree on what "now" looks like. A palette index or a name leaves the shade to your terminal theme, which is the point of them; a hex triple asks for one exact color, which a 256-color terminal will approximate. An unparsable value warns on stderr and falls back to the default rather than stopping the client. ### Shading The bars are **shaded** by height: dim at the floor, brightening toward the top, so a loud bar reads as hot and not merely tall. Each row gets its own shade, interpolated from 55% of `spectrum_color` at the bottom to `spectrum_top_color` — the secondary purple — at the very top. Set `spectrum_top_color = "none"` to ramp brightness alone and keep one hue, or `spectrum_gradient = false` for one flat color. Shading needs colors it can compute with, so it applies only to **hex** values. A name or a palette index is a reference into your terminal's own theme — its actual RGB value is the terminal's business, not the client's — so a named `spectrum_color` renders flat whatever `spectrum_gradient` says, and a named `spectrum_top_color` is ignored in favour of a brightness ramp. ### Segments and seams The bars form one connected field, divided two ways so the individual segments stand out: - **Between bars**, a seam: the last `spectrum_bar_gap` columns of each bar are the same bar drawn at 35% brightness, so neighbours are told apart without the field breaking into separate sticks. `spectrum_bar_gap = 0` removes it. (A named `spectrum_color` cannot be dimmed, so its seam is an empty column instead.) - **Between value rows**, a line: the top `spectrum_row_gap` eighths of every cell are left unlit, so a full row draws `▇` rather than `█` and each segment of a bar reads separately. `spectrum_row_gap = 0` stacks them solid; 2 or more make the line heavier, at a little vertical resolution. `spectrum_bar_width` is a **minimum**, not a width: bars take any spare columns beyond it, which is what keeps the field connected. Raise it to force wider bars — at the cost of showing fewer bands, since there is then room for fewer bars than the server sends and each covers a group of neighbouring bands (taking the loudest of them). ### Peak shadows Each bar trails a **peak-hold shadow**: the gap between the bar and the highest level it lately reached, filled in `spectrum_peak_color` — the primary blue by default, so it reads as a shadow of the red bar rather than part of it. It shows what a transient reached after the bar has dropped away. - `spectrum_peak_fall` is how long, in seconds, a full-scale shadow takes to fall to the floor (10 by default). It is real time, not frames, so the server's frame rate does not change the feel. - `spectrum_peak_fill = false` draws a thin rule (`▔`) at the peak instead of filling the space below it — also the thing to do if your terminal font lacks `▔`… in which case leave it `true`. - `spectrum_peak_color = "none"` (or `"off"`) leaves shadows out entirely. A shadow never appears in a row the bar itself occupies: a terminal cell holds one glyph, so a shadow there would eat bar to repeat what the bar's top edge already shows. Shadows fall away when playback stops, since the server keeps streaming zeroed bars while the audio is idle. ## Media keys and the desktop While the TUI runs it also **is** a media player as far as the desktop is concerned: it publishes the MPRIS2 interfaces on the session bus, so the `XF86Audio*` keys on your keyboard control the server, and status bars can show what is playing. GNOME and KDE route the keys themselves; on sway/i3 they are usually bound to `playerctl`. ```sh playerctl -p crabidy status # Playing playerctl -p crabidy metadata --format \ '{{artist}} - {{title}} ({{duration(mpris:length)}})' playerctl -p crabidy play-pause # same as Space playerctl -p crabidy position 30+ # same as pressing . twice ``` Play, pause, stop, next, previous, seek, shuffle, repeat and the volume are all wired to the same RPCs the keys in the table below send, so it does not matter which one you reach for — and a change made from either shows up in both. Some details worth knowing: - The player appears as `crabidy.instance`, the per-instance name the spec asks for. `playerctl -p crabidy` matches it anyway (it ignores the instance suffix), as does anything built on playerctl — waybar's mpris module among them. Two TUIs on one desktop therefore show up as two players instead of fighting over one name. - **Muting reads as volume 0**, since MPRIS has no mute of its own. Setting the volume to 0 mutes (so unmuting restores your level); setting any other value unmutes and moves to it. - Repeat-one is not offered: the server repeats the queue, so the loop status is `None` or `Playlist` and asking for `Track` is refused. - The desktop cannot quit the client. Closing a terminal UI from a status-bar button is not something a status bar should be able to do. - No metadata leaves the session other than title, artist, album, duration and a queue identifier — in particular no URLs. It exists only while the TUI does: close the client and the media keys go quiet. The feature is `mpris`, on by default; a build without it (`--no-default-features`) touches the bus only for desktop notifications, if those are compiled in. ## Key bindings Global keys work in either pane. Pane keys apply only while that pane is focused (several chords, like `j`/`Enter`/`d`, mean different things per pane). | Scope | Key | Action | | ------- | ----------------- | ----------------------------------------- | | Global | `?` | Show help | | Global | `q` | Quit | | Global | `Tab` | Switch between library and queue | | Global | `Space` | Play/pause | | Global | `r` | Restart current track | | Global | `,` / `.` | Seek back / forward 15 seconds | | Global | `K` | Volume up | | Global | `J` | Volume down | | Global | `m` | Toggle mute | | Global | `z` | Toggle shuffle | | Global | `x` | Toggle repeat | | Global | `Ctrl-n` | Next track | | Global | `Ctrl-p` | Previous track | | Global | `<` / `>` | Previous / next track | | Global | `f` | Toggle the frequency spectrum | | Library | `j` / `k` | Select next / previous item | | Library | `g` / `G` | Select first / last item | | Library | `Ctrl-d` | Jump 15 items down | | Library | `Ctrl-u` | Jump 15 items up | | Library | `h` | Go to parent folder | | Library | `l` | Enter selected folder | | Library | `s` | Mark / unmark selection | | Library | `v` / `V` | Visual mode: movement toggles marks | | Library | `y` | Yank selection into the register | | Library | `Enter` | Replace queue with selection | | Library | `a` | Append selection to queue | | Library | `L` | Queue selection after current track | | Library | `%` | Create node here (e.g. search term) | | Library | `e` | Rename selected node | | Library | `d` | Delete selection | | Library | `w` | Save selection as bookmark | | Library | `W` | Capture selection into /crabidy (audio) | | Library | `/` | Filter this view | | Queue | `j` / `k` | Select next / previous track | | Queue | `g` / `G` | Select first / last track | | Queue | `Ctrl-d` | Jump 15 tracks down | | Queue | `Ctrl-u` | Jump 15 tracks up | | Queue | `s` | Mark / unmark selection | | Queue | `v` / `V` | Visual mode: movement toggles marks | | Queue | `y` | Yank selection into the register | | Queue | `o` | Select the playing track | | Queue | `Enter` | Play selected track | | Queue | `p` / `P` | Paste the register after / before | | Queue | `d` | Remove selection (into the register) | | Queue | `c` | Clear queue except current (to register) | | Queue | `C` | Clear entire queue (to register) | | Queue | `u` | Unique: drop duplicate tracks | | Queue | `U` | Unique by title: one entry per song | | Queue | `S` | Sort the queue (opens a strategy menu) | | Queue | `w` | Save queue under a name | | Queue | `W` | Capture the queue into /crabidy (audio) | | Queue | `/` | Filter this view | | Help | `?` / `Esc` / `q` | Close help |