From fe4a455cef0169879865168abe7952ca70e0ead7 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 16:47:47 +0800 Subject: [PATCH] bos-settings: first-pass --screenshot CLI mode for automated capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First bread-ecosystem app on the Tauri/webview stack rather than raw GTK4 — proves the isolated-headless-Sway capture pipeline works against a plain (non-layer-shell) tao/wry window with no gtk4-rs-specific signal to hook. No connect_map here: since the window isn't owned by gtk4-rs directly, this instead waits a fixed settle delay (2s — longer than the native apps need, since a webview's first paint means a full page load plus an async Tauri-command round-trip for real content, not just GTK widget layout — confirmed by an initial 800ms attempt capturing the sidebar correctly but the About panel's data still blank) on Tauri's own async runtime, then captures the known-size canvas and exits. Only one view ("default", the initial landing section) is wired up. Capturing a specific sidebar section would mean routing the Svelte frontend to a URL/hash on window creation — real, separate frontend work, deferred past this first pass. bread-screenshots is pulled from bread-ecosystem's dev branch, unlike this repo's existing bread-theme/bread-utils deps which point at main — it doesn't exist on main yet. --- src/Cargo.lock | 24 +++++++++- src/Cargo.toml | 4 ++ src/src/lib.rs | 9 +++- src/src/screenshot.rs | 101 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/src/screenshot.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index ed1c9de..3492adf 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -290,8 +290,10 @@ dependencies = [ name = "bos-settings" version = "0.8.0" dependencies = [ + "anyhow", + "bread-screenshots", "bread-theme", - "bread-utils", + "bread-utils 0.3.1 (git+https://github.com/Breadway/bread-ecosystem?branch=main)", "notify", "regex", "serde", @@ -304,6 +306,16 @@ dependencies = [ "toml_edit 0.22.27", ] +[[package]] +name = "bread-screenshots" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#c93f4cf4d0ea1d49e639a1b5b4507e5a2497f52d" +dependencies = [ + "anyhow", + "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev)", + "tracing", +] + [[package]] name = "bread-theme" version = "0.3.1" @@ -314,6 +326,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bread-utils" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#c93f4cf4d0ea1d49e639a1b5b4507e5a2497f52d" +dependencies = [ + "dirs 5.0.1", + "serde", + "serde_json", +] + [[package]] name = "bread-utils" version = "0.3.1" diff --git a/src/Cargo.toml b/src/Cargo.toml index 16d19d7..5ad9ccc 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -43,4 +43,8 @@ regex = "1" # checkout exists on the runner) since no tag has these functions yet. bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", branch = "main" } bread-utils = { git = "https://github.com/Breadway/bread-ecosystem", branch = "main", features = ["toml"] } +# Capture primitives for `--screenshot` mode — see src/screenshot.rs. On +# "dev", not "main" like the two deps above: it doesn't exist on main yet. +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" } +anyhow = "1" diff --git a/src/src/lib.rs b/src/src/lib.rs index 9530e92..a10efe4 100644 --- a/src/src/lib.rs +++ b/src/src/lib.rs @@ -1,12 +1,19 @@ mod commands; +mod screenshot; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + let args: Vec = std::env::args().collect(); + let screenshot_req = screenshot::parse(&args); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) - .setup(|app| { + .setup(move |app| { commands::theme::watch_and_emit(app.handle()); + if let Some(req) = screenshot_req { + screenshot::dispatch(req); + } Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/src/src/screenshot.rs b/src/src/screenshot.rs new file mode 100644 index 0000000..9c32abd --- /dev/null +++ b/src/src/screenshot.rs @@ -0,0 +1,101 @@ +//! `--screenshot` CLI mode: capture the settings window via +//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s +//! `bread-capture` orchestrator, or run standalone for one-off captures. +//! +//! First pass at a Tauri (webview, not raw GTK4) target: there's no +//! `connect_map`/`glib` signal to hook here the way every other bread- +//! ecosystem app's screenshot mode does, since the window is owned by +//! tao/wry, not gtk4-rs directly. Instead this just waits a fixed +//! [`SETTLE_DELAY`] on Tauri's own async runtime after `setup()` runs — +//! longer than the native apps' settle delays, since a webview's first +//! paint means a full page load (JS bundle parse + Svelte mount), not just +//! GTK widget layout. The window itself is a plain, non-layer-shell +//! toplevel (per `tauri.conf.json`'s fixed 960x640 size), so — same +//! reasoning as breadman/breadhelp — a full known-size canvas capture is +//! enough; no geometry to track. +//! +//! Only one view ("default", the settings window's initial landing +//! section) is wired up so far. The frontend is a Svelte SPA with its own +//! sidebar routing for each settings section (Appearance, Network, +//! Bluetooth, ...) — capturing one of those specifically would mean +//! passing a route via a URL/hash on window creation and is real, separate +//! frontend work, deferred past this first pass (which exists to prove the +//! isolated-headless-Sway pipeline works against a Tauri/webview window at +//! all, not to reach full view parity with the native GTK4 apps). + +use std::path::PathBuf; +use std::time::Duration; + +const SETTLE_DELAY: Duration = Duration::from_millis(2000); +const KNOWN_VIEWS: &[&str] = &["default"]; + +pub struct ScreenshotRequest { + pub output: PathBuf, + pub width: u32, + pub height: u32, +} + +/// `None` for a normal run. Exits the process with an error for an unknown +/// view, or if `--screenshot` was given without `--output` — before any +/// Tauri setup happens. +pub fn parse(args: &[String]) -> Option { + let mut view = None; + let mut output = None; + let mut width = 1920u32; + let mut height = 1080u32; + let mut it = args.iter().skip(1); + while let Some(arg) = it.next() { + match arg.as_str() { + "--screenshot" => view = it.next().cloned(), + "--output" => output = it.next().cloned(), + "--width" => { + if let Some(v) = it.next().and_then(|s| s.parse().ok()) { + width = v; + } + } + "--height" => { + if let Some(v) = it.next().and_then(|s| s.parse().ok()) { + height = v; + } + } + _ => {} + } + } + let view = view?; + if !KNOWN_VIEWS.contains(&view.as_str()) { + eprintln!( + "bos-settings: unknown screenshot view '{view}' (known: {})", + KNOWN_VIEWS.join(", ") + ); + std::process::exit(1); + } + let Some(output) = output else { + eprintln!("bos-settings: --screenshot requires --output"); + std::process::exit(1); + }; + Some(ScreenshotRequest { output: output.into(), width, height }) +} + +/// Schedule the capture-then-exit sequence. Called once from `setup()`. +pub fn dispatch(req: ScreenshotRequest) { + tauri::async_runtime::spawn(async move { + tokio::time::sleep(SETTLE_DELAY).await; + finish(bread_screenshots::capture_region( + 0, + 0, + req.width as i32, + req.height as i32, + &req.output, + )); + }); +} + +fn finish(result: anyhow::Result<()>) { + match result { + Ok(()) => std::process::exit(0), + Err(e) => { + eprintln!("bos-settings: screenshot capture failed: {e}"); + std::process::exit(1); + } + } +}