Merge feature/screenshot-mode: add --screenshot CLI mode for automated capture
All checks were successful
dev release / build (push) Successful in 3m49s

This commit is contained in:
Breadway 2026-07-29 16:50:55 +08:00
commit ba7a43ad62
4 changed files with 136 additions and 2 deletions

24
src/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -1,12 +1,19 @@
mod commands;
mod screenshot;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let args: Vec<String> = 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![

101
src/src/screenshot.rs Normal file
View file

@ -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<ScreenshotRequest> {
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);
}
}
}