The audio engine could already seek and nothing called it: no rpc, no playback command, no binding. This wires it from every client. The one real decision was where the arithmetic lives. A seek is relative but the engine seeks to an absolute position, so either the client computes a target from the last position update or it sends an offset and the engine adds it to the live position. The offset wins on the ordinary case of pressing the key twice: positions are broadcast on a 250 ms tick and then cross the network, so three quick presses would all read the same stale base and jump 15 s instead of 45. It also keeps the clamping policy in one place instead of three clients, and matters more while paused, where no position updates arrive at all. So the wire carries sint32 delta_millis and the step is a client constant. It also uncovered a live panic: seek_to did `time.clamp(Duration::from_secs(1), duration)`, and `Ord::clamp` asserts min <= max while `duration()` returns 0 for any source that reported no length (HLS, some streams). That panicked the engine thread, killing audio. Unreachable only because nothing called it; wiring seek made it reachable from user input. It is now saturating arithmetic in a pure, exhaustively tested function. Boundaries: backwards saturates at 0 and never enters the previous track; forwards stops 1 s short of the end so the track finishes through the ordinary end-of-stream path (which advances the queue) instead of relying on seek-to-exact-end, which decoders disagree about; an unknown duration has no upper clamp. The engine emits Elapsed from the seek path itself, because tick() skips a paused sink and a paused seek would otherwise show the old position until playback resumed. An unseekable source (SoundCloud HLS) warns server-side and changes nothing. Ctrl-b/Ctrl-f join the existing control-chord family; plain f still toggles the spectrum because lookup compares every modifier but SHIFT exactly. In the browser Ctrl-f would open the find bar, but the keydown handler already prevent_defaults any chord that resolves. Seek is deliberately not tested through the playback loop: every test there builds a real Player whose engine thread opens an audio device, so a test that awaits a player reply passes or hangs depending on whether the machine has working audio. The arithmetic is tested as a pure function, and the rpc -> command mapping (the layer the paste bug lived in) in rpc.rs. Verified: 20 audio-player tests (5 new: i64::MIN/MAX, zero duration, sub-second tracks, composition, near-end saturation), 95 crabidy-server, 119 cbd-tui, 21 cbd-web, 58 server tests with --no-default-features, workspace clippy clean under -D warnings, fmt clean, wasm bundle and book build. Not exercised: an actual seek through an audio device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| README.md | ||
| Trunk.toml | ||
| index.html | ||
| style.css | ||
README.md
cbd-web — the browser client
A Leptos client-side WASM app with the same
functionality as cbd-tui, served by crabidy-server itself.
How it works
- Transport: gRPC-web (
tonic-web-wasm-client) over the same generated client and proto types the TUI uses (crabidy-core). No second API surface — feature parity is structural. The server wraps its existing gRPC service intonic-web, so the browser and the TUI hit identical/crabidy.v1.CrabidyService/…paths, and the same role auth layer gates both. - Serving: the built bundle (
cbd-web/dist) is embedded intocrabidy-serverat compile time behind the default-onweb-uifeature and served as the fallback route on port 50051. gRPC and static assets share one origin, so there is no CORS story. - Local-first: pure client-side rendering, every asset in the
bundle (no CDN, no external fonts), library listings cached in memory
like the TUI, credentials and theme in
localStorage, and the update stream reconnects with backoff when the server disappears. There is no CRDT layer — this is a remote control for one live server state, not an offline-editing app (a deliberate departure from theweb_client_example_workspacetemplate that informed the toolchain).
Functionality
Everything the TUI does: browse the library (j/k/h/l, click),
marks, create/rename/delete nodes (%/e/d), bookmark and capture
(w/W, with live progress lines and skipped-track marking), the full queue
and playback controls, volume, shuffle/repeat, and a ? help overlay listing
the keys. Keys mirror the TUI; every key also has a clickable control. A
light/dark theme follows the OS and can be toggled (persisted). The accent
color is the crab orange-red.
When the server requires credentials, a login form collects the role
(owner / queue-owner / queue-appender) and password; they are
stored in localStorage and sent as the gRPC-web authorization
header on every request.
Building
The WASM toolchain (trunk, wasm-bindgen, the wasm32-unknown-unknown
target) is provided by devenv. From the repo root:
devenv shell -- build-web # release bundle → cbd-web/dist
cargo build -p crabidy-server # embeds cbd-web/dist
build-web clears RUSTFLAGS first: the native toolchain sets the
mold linker, which rust-lld (the wasm linker) cannot parse.
Building crabidy-server without a cbd-web/dist present is fine — it
embeds a placeholder page telling you to run build-web. To drop the web
client (and the tonic-web layer) entirely, build the server without its
web-ui cargo feature — e.g. --no-default-features --features all-providers,opus,spectrum; see docs/src/build-features.md.
Dev loop
Run a server, then a live-reloading trunk server that proxies gRPC-web to it:
cargo run -p crabidy-server # or `cbd`
devenv shell -- serve-web # trunk serve on http://127.0.0.1:8080
Trunk.toml proxies /crabidy.v1.CrabidyService to 127.0.0.1:50051,
so the app behaves as if served from the server.
Tests
The DOM-free logic (pane/selection state machines, the keymap, capture
progress formatting) lives in src/state.rs and src/keymap.rs and is
unit-tested on the native target:
cargo test -p cbd-web
Components in src/app.rs stay thin over that logic. The server-side
serving and the gRPC-web + auth routing are tested in crabidy-server
(src/web.rs, tests/web_server.rs).