halpradio is a keyboard-driven, terminal-based Internet Radio streaming application built in Go using the Bubble Tea TUI framework and Lipgloss styling library.
halpradio strictly follows the Model-View-Update (MVU) pattern mandated by Bubble Tea:
flowchart TD
User([User Keyboard / Mouse]) -->|Keypress / Mouse Event| Update[Update Loop `pkg/ui/update.go`]
AsyncEvents([Async Events: ICY Metadata / API]) -->|Tea Cmd / Msg| Update
Update -->|State Mutation| Model[Application State `pkg/ui/model.go`]
Model -->|Render Layout| View[View Orchestrator `pkg/ui/view.go`]
View -->|Lipgloss Styled Output| Terminal([Terminal Screen])
Update -->|Player Actions| PlayerMgr[Player Manager `pkg/player/player.go`]
PlayerMgr -->|ICY Metadata Callback| AsyncEvents
-
Model (
pkg/ui/model.go)
Houses all application state including:- Active tab index (
1: Activities,2: Catalog,3: Countries,4: Genres,5: Favorites,6: RadioBrowser,7: Custom,8: History,9: Globe,0: Tuner) - Station catalog & store handle (
pkg/radio/store.go) - Audio player handle (
pkg/player/player.go) - UI focus states (
FocusMainList,FocusSidebar), list cursor selections, and search queries - Active modals (Theme picker, Add station modal, WhichKey overlay, PR export modal)
- Current theme token definition (
pkg/theme/theme.go)
- Active tab index (
-
Update (
pkg/ui/update.go)
Processes user inputs (LazyVim keybindings, search input) and asynchronous messages:TrackUpdatedMsg: Fired when ICY metadata detects a new song title (sanitized viaradio.SanitizeTrackTitle).TrackIdentifiedMsg: Fired when acoustic fingerprinting identifies track via AcoustID/MusicBrainz.RadioBrowserResultsMsg: Fired when online station search returns results.tea.WindowSizeMsg: Terminal resize events to dynamically compute layout dimensions.tea.SetWindowTitle: Dispatches OSC 2 escape sequences to update native terminal window/tab titles dynamically.
-
View (
pkg/ui/view.go)
Orchestrates sub-component renderers and applies active theme colors to construct the terminal frame.
halpradio/
├── main.go # Entry point; embeds stations.yaml & invokes app.Run()
├── stations.yaml # Bundled station catalog
├── docs/ # Detailed technical documentation
└── pkg/
├── app/ # CLI flag parsing, configuration loading & app bootstrap
├── art/ # Cover art providers plus Kitty / iTerm2 / Sixel / half-block / Braille renderers
├── lyrics/ # LRCLIB & NetEase lyric providers, LRC parser, RAM and disk caches
├── player/ # Multi-backend audio playback engine & ICY stream reader
│ └── fingerprint/ # Acoustic stream recognition, Chromaprint / AcoustID client, LRU cache
├── party/ # P2P mesh synchronized radio rooms, Argon2id/AES-256-GCM E2EE & protocol engine
├── plugin/ # Wazero Wasm sandboxing engine, capability permissions, host API, registry client
├── radio/ # Station catalog store, YAML parser, RadioBrowser client & metadata sanitizer
├── theme/ # Theme definitions & color palette registry
├── timer/ # Pomodoro focus engine, sleep timer with volume fade, and OS event dispatcher
├── ui/ # Main Bubble Tea Model, Update, View, and Keymap logic
│ └── components/ # Modular UI sub-views (Header, StationList, PlayerBar, Visualizer, Modals, PartyBar, LyricsDrawer, AlbumArt)
└── util/ # OS configuration directory resolution & clipboard utilities
| Package | Key Types / Files | Responsibilities |
|---|---|---|
pkg/app |
Run(), RunPluginCLI() |
Parses CLI flags (--backend, --theme, --version, --fingerprint, --auto-identify), handles CLI subcommands (remote, plugin, party), sets up store, instantiates player.Manager, initializes tea.Program. |
pkg/party |
Session, MeshNode, Packet, Crypto |
P2P mesh synchronized radio rooms, WebRTC data channels, Argon2id key derivation & AES-256-GCM encryption, sub-second playback sync, host election, ASCII reaction bus. |
pkg/art |
Client, Cover, Renderer, Protocol |
Resolves high-resolution cover art from iTunes, Deezer, MusicBrainz / Cover Art Archive and Last.fm, then encodes it for the terminal's best image transport with RAM and disk caching. |
pkg/lyrics |
Client, Sheet, Line, ParseLRC() |
Queries LRCLIB and falls back to NetEase, parses .lrc timestamps, resolves the active line for a playback offset, and caches sheets in RAM and on disk. |
pkg/player |
Player, Manager, TrackInfo |
Detects audio CLI backends (mpv, vlc, ffplay, etc.) or falls back to native Go audio. Runs ICY metadata streaming goroutine. |
pkg/player/fingerprint |
Client, Result, LRUCache |
Captures 5s audio buffers, computes Chromaprint subfingerprints, queries AcoustID & MusicBrainz APIs with LRU caching. |
pkg/plugin |
Manager, Sandbox, Manifest, RegistryClient |
Executes sandboxed WebAssembly plugins via Wazero with capability checks (network, storage, events). Fetches and verifies official registry packages. |
pkg/radio |
Store, Station, Sanitizer, History |
Manages bundled, local, and favorite stations. Interfaces with RadioBrowser HTTP API. Cleans dirty metadata and maintains track history. |
pkg/theme |
Theme, GetTheme() |
Defines 6 color palettes (Tokyo Night, Catppuccin, Synthwave, Nord, Gruvbox, Dracula). |
pkg/timer |
Timer, Event, DispatchEvent() |
Powers Pomodoro focus interval state machine, sleep timer countdown with volume fade-out, and cross-platform desktop notifications. |
pkg/ui |
Model, KeyMap, Update() |
Coordinates global navigation state, search filtering, modal popups, and keybindings. |
pkg/ui/components |
Header, StationList, PlayerBar, Visualizer, Modals |
Render pure, reusable Lipgloss UI components. |
pkg/util |
GetConfigDir(), CopyToClipboard() |
Provides platform-agnostic file paths for ~/.config/halpradio/ and clipboard integration. |
-
Audio Playback Subprocess / Goroutine:
Audio playback runs asynchronously in a separate goroutine managed byplayer.Manager. This prevents audio streaming or network delays from blocking the Bubble Tea UI event loop. -
ICY Metadata Extraction & Sanitization:
When a station starts playing,player.Managerlaunches anhttp.Clientrequest with headerIcy-MetaData: 1. As metadata frames arrive, the thread extracts theStreamTitle, passes it throughradio.SanitizeTrackTitleto remove ads and station promo noise, and dispatchesTrackUpdatedMsgto the Bubble Tea program thread (program.Send()). -
Asynchronous Acoustic Stream Fingerprinting:
Triggered on demand (I) or via--auto-identifywhen a stream lacks ICY metadata. An asynchronoustea.Cmdcaptures 5 seconds of stream audio, calculates Chromaprint subfingerprints, queries AcoustID/MusicBrainz, and dispatchesTrackIdentifiedMsgto update the player bar, song history, and clipboard without stalling playback. -
Asynchronous Plugin Event Bus:
When tracks or playback states change,plugin.Managerdispatches lifecycle payloads to running WebAssembly sandboxes in parallel background goroutines with timeout limits. Slow or misbehaving plugins can never stall the UI or audio loop. -
Local Persistence:
Favorites, custom user stations, track history, and plugin states are saved to disk under~/.config/halpradio/in JSON/YAML format using non-blocking file operations. -
Terminal Viewport & Tab Compatibility:
To prevent vertical scrolling and title clipping across diverse terminal emulators (Ghostty, WezTerm, Kitty, iTerm2, Alacritty, Tmux, Apple Terminal, Windows Terminal):- Total rendered height is strictly bounded (
lipgloss.Height(view) <= terminal_height - 1). - Component inner dimensions explicitly account for Lipgloss borders and padding (
innerHeight := height - 2). - Column allocation is dynamically responsive, dropping secondary columns (Bitrate/Codec/Flag) when width is constrained.
- Dynamic terminal window titles are emitted via
tea.SetWindowTitle()(OSC 2 sequence), syncing active tab and track status with the host terminal's native tab bar.
- Total rendered height is strictly bounded (