budctl1978b2f
README: screenshots of the desktop app, and the custom-preset commands
- Screenshots of the Overview, Equalizer, Gestures and Earbud settings pages and the keys overlay, in docs/screenshots/. They show --demo's made-up buds. `cargo test -p budctl-gui readme_screenshots -- --ignored` renders them again; it is ignored by default because it writes into docs/. pages_render and it share one render helper. - The command list gains `eq new`, `eq rename` and `eq delete`. The page list says where the bass level now is, and what App settings shows. - CLAUDE.md says how to refresh the screenshots. No new dependency.
+66 −3
8 files changed
- M CLAUDE.md +1 −0
- M README.md +23 −3
- M crates/budctl-gui/src/main.rs +42 −0
- A docs/screenshots/earbuds.png
- A docs/screenshots/equalizer.png
- A docs/screenshots/gestures.png
- A docs/screenshots/keys.png
- A docs/screenshots/overview.png
| @@ -127,6 +127,7 @@ Exit codes: | |||
| 127 | 127 | - The cursor shows only after a key press, and the first key only shows it, so nothing changes where it cannot be seen. A click hides it. | |
| 128 | 128 | - **Buds:** `worker.rs` owns the session, on a thread of its own, and answers each command with one event, in order. It opens the session when a command needs it, answers the buds' own requests while it waits (`Session::serve`), and closes it 10 s after the last command. A write shows "Saving…" until the buds' read-back arrives, and takes no second write meanwhile. A failure says why, with "Try again". `--demo` shows made-up buds; `--debug` hex-dumps frames like the CLI. | |
| 129 | 129 | - **Logic:** keys and clicks both become messages for one `update` function, which stays testable without a display. `iced_test` renders pages to PNG, so the look can be checked without the owner. | |
| 130 | + | - **Screenshots:** the README's come from the demo buds: `cargo test -p budctl-gui readme_screenshots -- --ignored` renders them again into `docs/screenshots/`. | |
| 130 | 131 | ||
| 131 | 132 | ## Working with Claude | |
| 132 | 133 | ||
| @@ -9,6 +9,10 @@ touch controls, battery and more. There is a command-line tool, `budctl`, and a | |||
| 9 | 9 | ||
| 10 | 10 | **Supported buds:** realme Buds T310. More are added as the author gets them. | |
| 11 | 11 | ||
| 12 | + | <p align="center"> | |
| 13 | + | <img src="docs/screenshots/overview.png" width="560" alt="budctl-gui's Overview page: battery of each bud, noise control, and switches such as game mode and 3D sound"> | |
| 14 | + | </p> | |
| 15 | + | ||
| 12 | 16 | ## Install | |
| 13 | 17 | ||
| 14 | 18 | You need Rust 1.88 or newer, BlueZ, and for the desktop app a Wayland desktop. | |
| @@ -31,6 +35,9 @@ budctl anc light|medium|deep # noise cancelling, at this strength | |||
| 31 | 35 | budctl eq # the EQ presets, and the one in use | |
| 32 | 36 | budctl eq deep-bass # select a preset | |
| 33 | 37 | budctl eq custom-4 50=+3 16000=-2 # change bands of a custom preset (dB) | |
| 38 | + | budctl eq new Gym # create a custom preset: flat, and put to use | |
| 39 | + | budctl eq rename custom-4 "Gym mix" # rename a custom preset | |
| 40 | + | budctl eq delete custom-4 # delete a custom preset | |
| 34 | 41 | budctl game on|off # game mode (low latency) | |
| 35 | 42 | budctl set bass-boost|spatial|wind|dual on|off | |
| 36 | 43 | budctl dual # the phones and computers dual connection keeps | |
| @@ -60,11 +67,24 @@ Exit codes: | |||
| 60 | 67 | ||
| 61 | 68 | `budctl-gui` has five pages: | |
| 62 | 69 | ||
| 63 | − | - **Overview:** battery, noise control, the switches. | |
| 64 | − | - **Equalizer:** presets, the bands of your custom presets, the bass level. | |
| 70 | + | - **Overview:** battery, noise control and its strength, the switches, the bass level. | |
| 71 | + | - **Equalizer:** the presets; create, rename and delete your own, and change their bands. | |
| 65 | 72 | - **Gestures:** what each tap and hold does on each bud, and what a hold steps through. | |
| 66 | 73 | - **Earbud settings:** dual connection and the devices it keeps, find my earbuds, firmware, audio codec. | |
| 67 | − | - **App settings.** | |
| 74 | + | - **App settings:** where the colors come from, the keys, the version. | |
| 75 | + | ||
| 76 | + | <table> | |
| 77 | + | <tr> | |
| 78 | + | <td valign="top"><img src="docs/screenshots/equalizer.png" alt="Equalizer: the built-in presets, a custom preset with its pencil and bin, and its six bands open below"></td> | |
| 79 | + | <td valign="top"><img src="docs/screenshots/gestures.png" alt="Gestures: double tap, triple tap and hold for each bud, and the modes a hold steps through"></td> | |
| 80 | + | </tr> | |
| 81 | + | <tr> | |
| 82 | + | <td valign="top"><img src="docs/screenshots/earbuds.png" alt="Earbud settings: the dual connection switch, the devices the buds keep, find my earbuds, firmware and codec"></td> | |
| 83 | + | <td valign="top"><img src="docs/screenshots/keys.png" alt="The keys overlay, shown with ?"></td> | |
| 84 | + | </tr> | |
| 85 | + | </table> | |
| 86 | + | ||
| 87 | + | The screenshots show `--demo`'s made-up buds. | |
| 68 | 88 | ||
| 69 | 89 | The app connects when it needs to, and frees the buds' control channel 10 s after your last | |
| 70 | 90 | change, so other apps can use it. A change shows "Saving…" until the buds confirm it. | |
| @@ -2347,6 +2347,48 @@ mod tests { | |||
| 2347 | 2347 | let _ = saving.update(Message::Switch("spatial", false)); | |
| 2348 | 2348 | let _ = saving.update(Message::Noise(NoiseMode::Off)); | |
| 2349 | 2349 | shots.push(("saving".to_owned(), 1240.0, saving)); | |
| 2350 | + | render(&dir, font, shots); | |
| 2351 | + | } | |
| 2352 | + | ||
| 2353 | + | /// The README's screenshots: the demo buds in the built-in palette, written to | |
| 2354 | + | /// `docs/screenshots/` by `cargo test -p budctl-gui readme_screenshots -- --ignored`. | |
| 2355 | + | #[test] | |
| 2356 | + | #[ignore = "writes into docs/; run it when the look changes"] | |
| 2357 | + | fn readme_screenshots() { | |
| 2358 | + | let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); | |
| 2359 | + | let scratch = root.join("target/readme-screenshots"); | |
| 2360 | + | let _ = fs::remove_dir_all(&scratch); | |
| 2361 | + | let font = theme::system_font(); | |
| 2362 | + | let on = |page: Page, keys: &str| { | |
| 2363 | + | let mut app = App::new(None, font, None, true); | |
| 2364 | + | app.page = page; | |
| 2365 | + | press(&mut app, keys); | |
| 2366 | + | app | |
| 2367 | + | }; | |
| 2368 | + | let mut equalizer = on(Page::Equalizer, ""); | |
| 2369 | + | let _ = equalizer.update(Message::EditPreset(4)); | |
| 2370 | + | let shots = vec![ | |
| 2371 | + | ("overview".to_owned(), 1140.0, on(Page::Overview, "")), | |
| 2372 | + | ("equalizer".to_owned(), 900.0, equalizer), | |
| 2373 | + | ("gestures".to_owned(), 840.0, on(Page::Gestures, "")), | |
| 2374 | + | ("earbuds".to_owned(), 800.0, on(Page::Earbuds, "")), | |
| 2375 | + | ("keys".to_owned(), 800.0, on(Page::Overview, "?")), | |
| 2376 | + | ]; | |
| 2377 | + | let names: Vec<String> = shots.iter().map(|(name, _, _)| name.clone()).collect(); | |
| 2378 | + | render(&scratch, font, shots); | |
| 2379 | + | let out = root.join("docs/screenshots"); | |
| 2380 | + | fs::create_dir_all(&out).unwrap(); | |
| 2381 | + | for name in names { | |
| 2382 | + | fs::rename( | |
| 2383 | + | scratch.join(format!("{name}-tiny-skia.png")), | |
| 2384 | + | out.join(format!("{name}.png")), | |
| 2385 | + | ) | |
| 2386 | + | .unwrap(); | |
| 2387 | + | } | |
| 2388 | + | } | |
| 2389 | + | ||
| 2390 | + | /// Renders each (name, window height, app) 1040 px wide, to `<dir>/<name>-tiny-skia.png`. | |
| 2391 | + | fn render(dir: &Path, font: Font, shots: Vec<(String, f32, App)>) { | |
| 2350 | 2392 | for (name, height, app) in shots { | |
| 2351 | 2393 | let settings = iced::Settings { | |
| 2352 | 2394 | default_font: font, | |
[Binary file]
[Binary file]
[Binary file]
[Binary file]
[Binary file]