You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TuiTunes is a terminal music player + podcast client built on OpenTUI (Zig native rendering + React 19) with mpv as the playback engine. 45 source files, 92 tests, strict TypeScript.
bun run start # Launch
bun run dev # Watch mode
bun test# 92 tests
bunx tsc --noEmit # Type check
bun build --compile --minify src/index.tsx --outfile dist/tuimusic # Binary
Architecture
Two-Level Section Model
TuiTunes has two top-level sections: music and podcast (Ctrl+1 / Ctrl+2). Each section has its own sidebar views and search routing.
Section: music Section: podcast
Views: search, queue, Views: search, feeds, episodes
library, explore
Search: YouTube Music Search: iTunes podcast API
Playback: yt-dlp URL Playback: YouTube URL (if found)
or RSS enclosure URL
Lyrics: LRCLIB synced Transcript: YouTube auto-captions
+ YouTube fallback (same source as audio)
Single Keyboard Handler (CRITICAL)
ALL keyboard logic lives in one useKeyboard() call in app.tsx. No other component registers useKeyboard. This is intentional.
Why: OpenTUI's useKeyboard fires ALL registered handlers for every key press. Multiple handlers cause key leaks (typing 's' in a search input also toggles shuffle). The single handler has a strict priority chain:
Every overlay block does return; — nothing leaks through.
DO NOT use useCallback with useKeyboard
OpenTUI's useKeyboard uses useEffectEvent internally (ref-based, always calls latest handler). Wrapping the handler in useCallbackfreezes the closure and causes stale state bugs. Pass a plain function.
Overlay Components Are Pure Renderers
CommandPalette, QuitConfirm, SeekInput, TranscriptUrlInput have NO useKeyboard. They receive state and callbacks as props. All their key handling is in app.tsx's single handler.
<select> is broken — items invisible due to buffered rendering. Use <text> elements for all lists.
<scrollbox focused={true}> intercepts Enter key before useKeyboard. Never pass focused to scrollbox.
<input> captures characters — j/k/space etc. get inserted as text AND fire in useKeyboard. That's why the search gate blocks all single keys.
<input> onSubmit type mismatch — requires onSubmit={handler as never} cast.
<input> Enter key — maps to newLine action (returns false), NOT submit. Enter in input does nothing by default. All Enter handling must be in useKeyboard.
mpv
observe_property absent data — when property unavailable, event has NO data field (not null). Check 'data' in event.
loadfile is async — returns before file loads. Monitor file-loaded / end-file events.
Stale socket — always unlinkSync(socketPath) before spawning mpv.
Bun.file().exists() does NOT detect Unix sockets. Use existsSync().
MpvEvent index signature — [key: string]: unknown for extra fields (reason, file_error).
React / Jotai
DO NOT wrap useKeyboard callback in useCallback — causes stale closures. OpenTUI handles stability internally.
Hooks before early return — useTheme() and all hooks must be called before any if (!visible) return null.
Store type import — import type { Store } from 'jotai/vanilla/store' (not from 'jotai').
Playback
Queue is user-controlled — playing from search does NOT add to queue. Only q key adds.
playingFromQueueAtom — tracks if current playback is from queue. n/p/auto-advance only work when true.
Podcast YouTube-backed playback — when possible, plays YouTube version of podcast for synced transcript.
YouTube auto-captions have no time gaps — 99.9% of segment gaps are <0.5s. Paragraph splitting uses sentence-ending punctuation (. ? !) as primary signal, with 20-segment safety cap.
Transcript sources — user can switch via command palette: Auto, Custom URL, Reload.
When Modifying — Checklist
Before any feature change, check these:
Does it need a new keybinding? → Add to app.tsx handler + HelpOverlay + commands.ts
Does it add state? → Add atom to correct store file, import in app.tsx
Does it touch the search flow? → Test both music AND podcast sections
Does it affect playback? → Test with queue playback AND one-off playback
Does it add an overlay? → NO useKeyboard in the overlay. Handle keys in app.tsx's handler. Add a gate block.
Does it change a list? → Test scroll behavior (page-based, not continuous)
Does it change types/interfaces? → Run bun test — tests cover types, queue logic, IPC parsing, format utils, DB queries
Does it add a new file? → Update this AGENTS.md file map
Does it affect themes? → Use useTheme() hook, no hardcoded hex colors
Does it affect NowPlaying? → Check all player states: playing, paused, buffering, stopped