# Plan — seek Executes `architecture/seek.md` against `quality/seek.md`. One commit: the change is a single thin path from key to sink, and half-wiring it would leave a dead RPC. ## A — The engine (where the only real logic is) - [x] **A1 — `PlayerEngine::seek_target`** — a pure function of (current position, delta millis, known duration) returning the absolute target: saturating on both ends, 0 as the floor, `duration - 1 s` as the ceiling when the duration is known, no ceiling when it is 0. Extracted as a free function so the arithmetic is testable without an audio device. *Verifies:* G1, G5, G6, G7. - [x] **A2 — Rewrite `PlayerEngine::seek_to`** to route through the same helper, dropping the `clamp(1s, duration)` that panics whenever the duration is unknown or under a second. *Verifies:* G1. - [x] **A3 — `PlayerEngine::seek_by`** — reads `sink.get_pos()`, applies A1, calls `try_seek`, and on success notifies `PlayerMessage::Elapsed` with the new position so a paused seek is visible. Refuses when stopped. *Verifies:* G4, G8, G9. - [x] **A4 — `PlayerEngineCommand::SeekBy` + `Player::seek_by`** on the existing bounded reply pattern. *Verifies:* G3. - [x] **A5 — Engine tests**: extremes (`i64::MIN`/`MAX`), zero duration, short track, composition, near-end saturation. *Verifies:* G1, G4–G7. ## B — Wire and server - [x] **B1 — Proto**: `rpc Seek(SeekRequest) returns (SeekResponse)` with `sint32 delta_millis`, documented with the clamping contract. *Verifies:* G10. - [x] **B2 — `PlaybackCommand::Seek { delta_millis }`** + its `name()` arm, and the playback handler that forwards to `player.seek_by` and warns on failure. No state. *Verifies:* G2, G12. - [x] **B3 — `RpcService::seek`** following the `change_volume` shape (record the field, send, return empty). *Verifies:* G10. - [x] **B4 — Server test** that the command reaches the player with the delta unchanged. *Verifies:* G11. ## C — Clients - [x] **C1 — TUI**: `Action::SeekForward`/`SeekBackward`, `Ctrl-f`/`Ctrl-b` bindings in `Scope::Global` with help text, `MessageFromUi::Seek(i64)`, the `lib.rs` arm, `rpc.rs` wrapper, `SEEK_STEP` constant, dispatch tests. *Verifies:* G13, G14, G15. - [x] **C2 — Web**: the same two actions in `keymap.rs` (global, with `HELP` rows), `Rpc::seek`, the `app.rs` dispatch arms, and `⏪`/`⏩` buttons in the transport bar. *Verifies:* G13, G14, G15. - [x] **C3 — CLI**: `GlobalCmd::Seek { seconds: i32 }` with `allow_negative_numbers`, and its executor arm. *Verifies:* G14. ## D — Docs and gates - [x] **D1 — Docs**: key tables (`docs/src/clients/tui.md`, `docs/src/clients/web.md`), the CLI page's `global` list, and the within-track/end-of-track/HLS sentences in `docs/src/queue.md`. *Verifies:* G16. - [x] **D2 — Gates**: workspace tests, `check-features`, `cargo fmt`, a wasm build of `cbd-web`, and the book build. *Verifies:* G17. - [x] **D3 — `plan/summary.md`** entry, then commit. ## Deferred (recorded, not dropped) - Absolute seek + click-to-seek on the web progress gauge (the gauge is already rendered; needs one proto field and a click handler). - Configurable step size, and a larger step on `<`/`>`. - Chapter-aware seek for podcasts/audiobooks — no provider exposes chapter marks through the library model.