Lock the main goroutine to OS thread 1 in init() and defer
MediaKeysInit to RunMainLoop so MPRemoteCommandCenter handlers
are registered on the correct thread for CFRunLoop delivery.
Extract a platform-agnostic MediaController interface from the MPRIS
integration so that media key support can be added per-platform without
modifying the core TUI code.
- mediactl/ package: Controller interface (Update, EmitSeeked, Close),
MainLoopRunner interface for controllers that need the main OS thread,
and a self-registration registry via init()
- MPRIS refactored to implement mediactl.Controller, self-registers via
init() with blank import in main.go
- TrackInfo and SetPositionMsg moved to shared packages (mediactl and
internal/control) so platform code doesn't cross-import
- Model uses []mediactl.Controller instead of *mpris.Service, with
notifyMediaControllers() and emitSeeked() helpers replacing direct
mpris calls
- prog.Run() moved to a goroutine with mediactl.RunMainLoop() on the
main thread, enabling macOS NSRunLoop (no-op on Linux)
- Full macOS CGo scaffold: MPRemoteCommandCenter for receiving media
key events, MPNowPlayingInfoCenter for Now Playing info, with change
detection to skip redundant CGo calls on the per-second tick
- Key handlers upgraded from notifyMPRIS() to notifyAll() so Lua
plugins also receive state updates on keyboard actions
- Notification hot path uses cached position/duration instead of
re-acquiring the speaker lock
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
Arrow-key seeking for HTTP podcast streams never ran because the key handlers called m.doSeek(...) without returning its command. That meant seek-by-reconnect worked via IPC and jump-to-time, but not via Left/Right in the TUI.
Return the command from the arrow-key handlers and add a regression test for the HTTP stream seek path.
Three new built-in themes filling gaps in the hue spectrum:
- dracula: canonical purple palette (draculatheme.com), 265° accent
- ember: deep warm hearth with copper-to-crimson spectrum, 20° accent
- neon-blade-runner: Cronenweth's 1982 neon noir — hot pink neon signs,
teal rain reflections, amber CRT readouts, 334° accent
Updates docs/themes.md count from 17 to 20 and changes the custom
theme example from Dracula (now built-in) to Solarized.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
When silent token refresh failed in the silentOnly path, credentials
were re-saved without the RefreshToken field, permanently losing the
ability to refresh silently. This caused a death spiral where every
subsequent launch fell back to the spclient token, which gets
rate-limited on Web API calls.
Preserve the original refresh token so the next launch can retry.
The KSXGitHub/github-actions-deploy-aur action broke due to an Arch
Linux util-linux update. Replace it with a self-contained workflow
that handles SSH setup, AUR clone, .SRCINFO generation, and push
directly.
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
Playlists() results are cached for 5 minutes to avoid redundant
API calls when switching providers. Mutations (add track, create
playlist) invalidate the cache.
Replace fmt.Fprintf(os.Stderr) calls in the spotify provider with
applog.Printf() so messages appear styled in the TUI footer instead
of corrupting the alternate screen buffer. Log entries auto-expire
after 6 seconds.
ReconnectInteractive was deleting the credentials file before
attempting the new OAuth flow. If that flow failed (e.g. port in use),
credentials were lost permanently, causing every subsequent silent
reconnect to fail with "no stored credentials". Since
newInteractiveSession overwrites the file on success via saveCreds,
the pre-delete was unnecessary and destructive.
When the OAuth2 refresh token is expired but the spclient session is
valid, continue without a Web API token source instead of failing
entirely. The Web API layer already falls back to the spclient token
when tokenSource is nil, so this avoids unnecessary interactive
reconnect (and the port 19872 binding issue that comes with it).
Render keybinding hints with a background-highlighted pill style
instead of dim brackets for a cleaner, modern look. Replace the ⏯
icon with ▶❚❚ for better visual clarity.
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
The Model held a concrete *player.Player and called config.Save()
directly, making it untestable without a live audio backend and
real filesystem. This extracts two interfaces:
- player.Engine: 30-method interface covering all Player methods the
TUI uses. The Model now accepts player.Engine, enabling mock-based
testing of the playback state machine.
- model.ConfigSaver: single-method interface for config persistence.
All 11 config.Save() calls in ui/model/ now go through
m.configSaver, removing the direct filesystem dependency.
The Model held a concrete *player.Player and called config.Save()
directly, making it untestable without a live audio backend and
real filesystem. This extracts two interfaces:
- player.Engine: 30-method interface covering all Player methods the
TUI uses. The Model now accepts player.Engine, enabling mock-based
testing of the playback state machine.
- model.ConfigSaver: single-method interface for config persistence.
All 11 config.Save() calls in ui/model/ now go through
m.configSaver, removing the direct filesystem dependency.
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
ESC/backspace exit path never restored PanelWidth, leaving the layout
at full terminal width. Shift+V exit path always capped at 80 columns
regardless of compact mode. Extract restorePanelWidth() so all paths
use the same compact-aware logic.
Runtime control of playback settings that were previously only
accessible via UI keybindings. All commands use the existing IPC
Unix socket protocol.
New commands:
cliamp shuffle [on|off|toggle]
cliamp repeat [off|all|one|cycle]
cliamp mono [on|off|toggle]
cliamp speed <ratio>
cliamp eq <preset> / cliamp eq --band <0-9> <dB>
cliamp device <name|list>
Status response now includes shuffle, repeat, mono, speed, and EQ
preset fields. Site and docs updated with new remote control section.
* feat: CLI playlist management, Unix socket IPC, and SSH streaming
Three features that make cliamp agent-native and scriptable:
1. `cliamp playlist {list,create,add,show,remove,delete}` — manage TOML
playlists from the CLI with --json output and --ssh HOST for remote
directory walking
2. Unix socket IPC at ~/.config/cliamp/cliamp.sock — play/pause/next/prev/
stop/status/volume/seek/load/queue commands over JSON-RPC, following
the MPRIS prog.Send() pattern
3. SSH streaming via ssh:// URL scheme — pipe audio from remote machines
through the existing decoder pipeline, zero filesystem mounts needed
Zero new Go dependencies. All existing tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(ui): SSH track glyph + duration metadata fallback
- Show ↗ prefix on SSH-streamed tracks in the playlist view
- Store/read duration_secs in local TOML playlists
- Fall back to track metadata duration when player reports 0
(piped SSH streams can't determine total length from audio frames)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: album separators, favorites, playlist resume, enrich command
- Album group separators in playlist view (triggers on album field change)
- Favorites system: * key toggles ★, [★ N] count in header, CLI `playlist favorite`
- [67/123] position counter in playlist header
- Playlist resume: saves and restores playlist + track + position across sessions
- `cliamp playlist enrich` probes duration via afinfo and derives album from path
- `ConnectTimeout=5` on all SSH commands to prevent stale hangs
- `SavePlaylist()` exported on local provider
- `*` added to keymap overlay and bottom help bar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: persist TUI favorites to disk, fix truncation, add cross-playlist favorites view
- TUI * key now persists favorites to TOML via FavoriteSetter interface
- ★ prefix deducted from truncation budget (prevents line overflow)
- JSON output includes duration_secs and favorite fields
- `cliamp playlist favorites` lists all ★ tracks across all playlists
- FavoriteSetter interface in provider/interfaces.go
- Bottom help bar: [*]Fav (no extra ★ symbol)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: accent-color favorites, atomic TOML writes, strict --index parsing
Kotharat design review + silent failure audit fixes:
- ★ renders in accent color (yellow) independently from line style — scannable in long playlists
- savePlaylist uses atomic write (tmp + rename) — no data loss on crash
- --index parsing uses strconv.Atoi — rejects fractional/malformed input
- Bounds check on PlaylistFavorite re-read — no panic on concurrent modification
- Help bar priority 80 → 75 for [*]Fav — yields to navigation hints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(ipc): remote theme switching via cliamp theme <name>
- `cliamp theme list` — list all available themes (built-in + custom)
- `cliamp theme <name>` — change theme in running TUI via IPC
- ThemeMsg with reply channel for error reporting ("theme not found")
- TUI handler reloads themes from disk before applying (picks up new custom themes)
- Added Name field to IPC Request for theme name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(theme): clean up theme CLI UX from review findings
- `cliamp theme` (no args) prints usage cleanly instead of printing playback state
- `theme list` matching is case-insensitive (prevents routing confusion)
- Documented LoadAll() precedent in ThemeMsg handler (matches openThemePicker pattern)
- Updated CLAUDE.md and agent docs with theme commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: Blade Runner visualizers, theme-conditional UI chrome, IPC vis command
Two new visualizer modes inspired by the film's on-screen electronics:
- Esper: phosphor grid scan with CRT afterglow decay (4-level persistence)
- VoightKampff: concentric iris rings with bass-driven bellows indicator
Theme-conditional UI chrome (active when neon-blade-runner theme is set):
- Seek bar: ▰▱ blocks with ◆ thumb (Voight-Kampff analog meter)
- Album separators: ── ◈ ALBUM ◈ ── (LAPD terminal data headers)
- SSH track glyph: ◉ (replicant eye) instead of ↗
- Time format: T+MM:SS (mission elapsed time)
- Position counter: [SUBJ 003/123] (interrogation notation)
- Loading spinner: cycles ENHANCE / SCANNING / PROCESSING
- Title flicker: 2% chance per frame of 2-3 char CRT glitch cluster
- Track transition: 4-frame ░▒▓█ static burst
IPC visualizer command:
- cliamp vis <name> — switch visualizer by name
- cliamp vis next — cycle to next mode (same as v key)
- cliamp vis list — list all modes with * active marker
Also adds neon-blade-runner-amber theme (1982 LAPD terminal amber phosphor).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor PR: urfave/cli v3, consolidate duplications, fix SSH+ffmpeg bug
CLI framework:
- Replace hand-rolled flag parser with urfave/cli v3 command tree
- Move command definitions to commands.go, keep main.go focused on TUI startup
- Rename --volume/--theme flags to --vol/--start-theme to avoid subcommand collisions
- Hide unhelpful (default: 0) on int flags where 0 means auto/config
Code consolidation (cmd/playlist.go):
- Use player.SupportedExts instead of duplicate audioExtensions (adds 4 missing formats)
- Use resolve.CollectAudioFiles instead of duplicate walkDir
- Use playlist.TrackFromFilename instead of local copy (includes sanitizeTag)
- Add bulk AddTracks() to avoid N+1 file open/close per track
- Add provider.Exists() using os.Stat instead of parsing all TOML files
- Extract newProvider() and collectLocalAudio() helpers
Architecture fixes:
- Unify MPRIS/IPC message types via internal/control package with type aliases
- Remove ~30 lines of duplicated handler cases in update.go
- Add SSH+ffmpeg guard in pipeline.go (was silently passing ssh:// to ffmpeg)
- Create internal/sshurl for proper URL parsing with port support
- Replace macOS-only afinfo with cross-platform ffprobe for remote duration probing
- Add consistent StrictHostKeyChecking=yes to all SSH call sites
- Fix StrictHostKeyChecking docs to match code (yes, not accept-new)
IPC fixes:
- Remove TOCTOU os.Stat before DialTimeout in ipc/client.go
- Remove unnecessary 64KB scanner buffer in ipc/server.go
- Add nil-guards on msg.Reply for ThemeMsg and StatusRequestMsg
- Change ipcSend/overridesFromFlags to return errors instead of os.Exit
Removals:
- Remove CLAUDE.md
- Remove Esper and Voight-Kampff visualizers
- Remove neon-blade-runner theme-conditional chrome
- Delete hand-rolled ParseFlags and helpers from config/flags.go
* Remove review file
* Add Local provider pill, L keybinding, default radio startup
- Register Local as a provider pill tab (after Radio)
- Add L keybinding to switch to Local provider for browsing TOML playlists
- Load 3 cliamp radio streams directly on default startup (no async M3U)
- Skip playlist resume on default startup so radio streams aren't replaced
- Only allow position resume (SetResume) when CLI args given, never ResumePlaylist
- Update keybinding docs, keymap overlay, and site
---------
Co-authored-by: Tom di Mino <contact@tomdimino.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>