| 1 | # Contributor and agent instructions for rmptui |
| 2 | |
| 3 | ## Build, test, and lint |
| 4 | |
| 5 | - `cargo build` — compile the application. |
| 6 | - `cargo run -- --config FILE` — run with a specific config (MPD at |
| 7 | `127.0.0.1:6600` by default; `MPD_HOST`/`MPD_PORT` override it). |
| 8 | - `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test`. |
| 9 | - `cargo test mpd::protocol` — run the tests of one module; `tests/mpd_client.rs` |
| 10 | exercises the client against a scripted fake MPD server. |
| 11 | |
| 12 | ## Architecture |
| 13 | |
| 14 | - `mpd/` is a self-contained MPD client. |
| 15 | - `protocol.rs`: pure command encoding (every argument quoted and escaped) |
| 16 | and response parsing. |
| 17 | - `connection.rs`: async TCP and Unix-socket connections. |
| 18 | - `types.rs`: typed models. |
| 19 | - `commands.rs`: command constructors. |
| 20 | - `worker.rs`: two background tasks. The *idle* connection reports changed |
| 21 | subsystems. The *command* connection runs `Request`s from an `MpdHandle` |
| 22 | and answers `Query`s with `Reply`s. Both reconnect on their own. |
| 23 | - `app.rs` owns the state and runs one `tokio::select!` loop over terminal |
| 24 | input, `AppEvent`s (MPD, lyrics, tag writes) and visualizer frames. UI code |
| 25 | never awaits MPD: it sends requests and reacts to the resulting events. |
| 26 | - `state.rs` (`MpdState`) mirrors the server: status, current song, the queue |
| 27 | (updated incrementally via `plchanges`), stored playlists, and a database |
| 28 | version used to invalidate caches. |
| 29 | - `screens/` holds one component per screen behind the `Screen` trait (`draw`, |
| 30 | `handle_action`, `on_mpd`, `on_enter`, `on_mouse`, `search`). |
| 31 | - Screens receive a `Ctx` with read access to config and state, the |
| 32 | `MpdHandle`, and an effect queue (`Effect`) for things only the app can do: |
| 33 | notifications, popups, tag writes, lyric reloads. |
| 34 | - Every screen sees every `MpdEvent`, so caches stay valid while hidden. |
| 35 | - `popups/` holds modal components (`Popup` trait); the topmost popup gets all |
| 36 | keys. Destructive actions go through `ConfirmPopup`. |
| 37 | - `ui/` holds the frame chrome (header, tabs, progress, status line), the |
| 38 | virtualized searchable `ListView`, and the `LineInput` editor. |
| 39 | - `config/`: |
| 40 | - `default.toml` is embedded and is the single source of truth for |
| 41 | defaults; user files are merged over it table by table. |
| 42 | - `keys.rs` parses key specs and builds the per-context `Keymap`. |
| 43 | - `action.rs` defines the `Action`, `Context` and `ScreenId` enums. Each action |
| 44 | declares the contexts it may be bound in. Lookup order is: popup or screen |
| 45 | context → `navigation` → `global`. |
| 46 | - `lyrics/`: LRC parser and the async lookup service (local `.lrc` → cache → |
| 47 | LRCLIB). `visualizer/`: fifo reader thread with FFT, plus bar smoothing. |
| 48 | `tags.rs`: lofty-based tag writing. |
| 49 | |
| 50 | ## Conventions |
| 51 | |
| 52 | - Build MPD commands only through `mpd::commands` or `Command::arg`; never |
| 53 | format raw command strings from user data. Filter expressions go through |
| 54 | `protocol::filter_eq`/`filter_and`. |
| 55 | - Prefer position-based commands (`move`, `delete`) for actions users repeat |
| 56 | quickly. They stay correct even when sent before the local queue refreshes. |
| 57 | - Every new action needs: |
| 58 | - an `Action` variant with its contexts and description; |
| 59 | - a default binding in `config/default.toml` (a test enforces this); |
| 60 | - a README entry. |
| 61 | - Every new option needs: |
| 62 | - a documented entry in `default.toml`; |
| 63 | - a field in the matching config struct (`deny_unknown_fields` catches |
| 64 | typos); |
| 65 | - validation in `Config::validate` if it has limits; |
| 66 | - README documentation. |
| 67 | - Screens must not block: do file and network I/O in tasks (see lyrics) or |
| 68 | `spawn_blocking` (see tag writes) and report back through `AppEvent`. |