This document is for contributors and maintainers. It describes the core components, how state flows through the program, and the tradeoffs behind a few important design choices.
src/main.rs: application startup + event loop + wiring (UI ↔ audio ↔ MPRIS)src/app.rs: UI model/state (tracks, selection, filter, cursor mode)src/ui.rs: drawing/layout (ratatui)src/audio.rs: audio thread + queue semantics (rodio)src/library.rs: scanning directories + extracting tags/durationsrc/mpris.rs: MPRIS D-Bus service (zbus)src/library.rs)Responsibility: given a directory, return a sorted Vec<Track>.
Track.display string used by the UI and filtering.src/app.rs)Responsibility: hold all UI state that isn’t “owned” by the audio output.
Key fields (conceptually):
tracks: full libraryselected: selected track indexfilter_mode + filter_query: / search modefollow_playback: cursor mode (follow vs free-roam)shuffle, loop_mode: playback behavior settingsmetadata_window: whether the metadata popup is visibleImportant invariant: display_indices() is the authoritative list for “what you see” and “what actions operate on”. If rendering, navigation, and playback use different filtering/shuffle logic, you will get hard-to-debug mismatches.
src/ui.rs)Responsibility: render the screen. The UI is stateless beyond what’s in App.
src/audio.rs)Responsibility: keep an OutputStream alive and respond to AudioCmd messages.
Reasons for a dedicated thread:
Queue semantics:
Seeking/scrubbing:
Source::skip_duration).Soft quit:
src/mpris.rs)The MPRIS service maps external commands (media keys, playerctl) into the same control flow as keyboard input. This keeps behavior consistent.
See MPRIS.md for details and troubleshooting.