Design
Product rationale for the receiver UI, terminal adaptation, playback backends, and provider adapters.
RadioCLI is meant to feel like a product, not a terminal proof of concept.
Why Ink
Ink gives the project React's component model while staying native to terminal constraints. That makes it easier to keep the app screen-oriented, testable, and incrementally adaptable as the UI grows.
Why mpv
Radio streams are messy: redirects, playlists, HLS, odd codecs, and flaky
metadata are normal. mpv already handles that complexity well and exposes JSON
IPC for controls and metadata.
ffplay and VLC provide playback fallbacks when mpv is unavailable. Their
limited control and state reporting is reflected in the UI. mpv is the
intended backend for pause, mute, volume, media keys, metadata, and readiness
checks.
Why Provider Adapters
Public radio directories change, block, rate limit, and disagree. Keeping providers behind adapters prevents the UI from depending on one API shape.
Radio Browser is primary because it is public and has broad station metadata. Radio Garden stays experimental because its public web experience does not mean its API is stable for terminal clients.
Why Location Is Controllable
Nearby stations are useful, but IP-based location lookup is privacy-sensitive.
The feature is enabled for new libraries and performs no lookup until Nearby is
opened. It remains directly controllable through Settings, l, or
:location off, and that local choice persists.
Receiver Aesthetic
The UI borrows from old receiver and car head-unit displays: direct labels, compact hierarchy, visible status, and no fake copy. The app should be usable in a terminal session, not a landing page rendered in ANSI.
On terminals with sufficient color support, the app shell paints a near-black
background that keeps the receiver, tabs, stats panels, and footer visually
cohesive. Transparent Background, limited-color terminals, and NO_COLOR
let the terminal keep its own background.
The Now Playing screen supports multiple visualizer styles so the display can feel like a receiver without locking the product into one animation. The default Ultracode style is fast and direct, while the retained alternates include the restored spinning ASCII cube plus generated multi-flame fire, fireworks, spinning donut, starfield, high-resolution braille visuals, and generative motion scenes. Every retained style is deterministic, pure-frame terminal rendering and still freezes to a truthful zero-signal frame when playback is inactive.
Receiver motion is tied to playback truth rather than screen presence. Visualizers
animate only when playback is playing and backend-ready; idle, loading, paused,
stopped, and error states render a flat zero-signal frame so the display never
implies live audio when nothing is playing.
Explore uses a dense braille world map next to the station list on wide terminals. That layout keeps global discovery spatial without forcing the user to switch away from the playable list. The map has one movable scan cursor instead of many station pins; clicking the map places it directly, WASD makes fine nudges, Shift+WASD makes larger jumps, and the station list reloads around that coordinate. Mouse reporting is activated only for Explore and overflowing menus or lists, then released on other screens so native terminal scrollback continues to work.
The Explore list is distance-truthful rather than popularity-first. RadioCLI builds a cached atlas from all available Radio Browser stations with coordinates and computes local distance inside the app. The list reports the radius covered by the returned stations so the user can tell whether a cursor position is dense, sparse, or limited by provider geotag coverage.
Terminal Adaptation
Full, compact, and micro layouts keep the current selection and keyboard actions available as space shrinks. Compact and micro views shorten labels and adjust the receiver and list areas. The selected station's full name remains available to screen-reader output.
Unicode and rich color are retained when the terminal supports them. Explicit
ASCII settings or a bare C/POSIX locale replace decorative glyphs; UTF-8
locales and SSH sessions retain Unicode. ASCII mode preserves international
station text. Sixteen-color terminals drop the dark fills, and NO_COLOR
removes color entirely.
Reduce Motion stops receiver animation, loading animation, and scrolling labels.
Screen-reader mode also omits receiver visualizers and mouse reporting while
keeping keyboard controls and text status. TERM=dumb produces static frames
with ASCII decoration and no color. These environment adaptations apply to the
current launch and leave saved appearance preferences intact. The exact
overrides are listed in TUI Controls.
Visual Regression Captures
The deterministic Ink snapshot suite covers every screen and settings group, all receiver styles at full size, representative receivers at smaller sizes, Unicode and ASCII decoration, output without color, and focused ANSI color and selection checks. The fixtures use these sizes:
| Layout | Columns × rows |
|---|---|
| Full | 100 × 30 |
| Compact | 50 × 16 |
| Micro | 24 × 8 |
Time, timezone, station data, and animation frames are fixed so failures show real rendering differences. ASCII fixtures use ASCII only for decoration while preserving international station names. The snapshot suite verifies renderer output; native fonts, physical displays, audio, and desktop integration are outside its scope. See Platforms for native feature coverage.
Navigation And Stats
The horizontal tab rail is intentionally dense: it keeps the app feeling like a terminal tool with real surfaces instead of a stack of disconnected menus. On wide terminals the full rail is visible; on narrower terminals it keeps the active tab and nearby tabs in view.
Listening stats use the same direct, boxed language as the rest of the app. The contribution graph begins in January and shows the current local calendar year, while the numeric stats report real locally persisted sessions, including favorite station, thresholded stations listened, sessions, streaks, active days, and total hours listened.
The stats graph palette follows the selected display color, so Stats feels like part of the same configured receiver surface.