diff --git a/Cargo.lock b/Cargo.lock index cee96f6..d34275d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,7 @@ name = "breadman" version = "0.3.4" dependencies = [ "anyhow", + "bread-screenshots", "breadpad-shared", "chrono", "dirs 5.0.1", diff --git a/breadman/Cargo.toml b/breadman/Cargo.toml index 577f684..3808281 100644 --- a/breadman/Cargo.toml +++ b/breadman/Cargo.toml @@ -11,6 +11,8 @@ path = "src/main.rs" [dependencies] breadpad-shared = { path = "../breadpad-shared" } +# Capture primitives for `--screenshot` mode — see src/screenshot.rs. +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" } anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/breadman/src/main.rs b/breadman/src/main.rs index d46557a..e165a9e 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -13,6 +13,7 @@ use std::rc::Rc; use std::sync::Arc; mod editor; +mod screenshot; mod views; // ── Args ───────────────────────────────────────────────────────────────────── @@ -23,6 +24,29 @@ mod args { pub view: Option, pub done_id: Option, pub upcoming_plain: bool, + pub screenshot: Option, + pub output: Option, + pub width: u32, + pub height: u32, + } + + impl Args { + /// `None` for a normal run. Exits the process with an error if + /// `--screenshot` was given without `--output`, before any GTK + /// setup happens. + pub fn screenshot_request(&self) -> Option { + let view = self.screenshot.clone()?; + let Some(output) = self.output.clone() else { + eprintln!("breadman: --screenshot requires --output"); + std::process::exit(1); + }; + Some(crate::screenshot::ScreenshotRequest { + view, + output: output.into(), + width: self.width, + height: self.height, + }) + } } pub fn parse() -> Args { @@ -30,6 +54,10 @@ mod args { view: None, done_id: None, upcoming_plain: false, + screenshot: None, + output: None, + width: 1920, + height: 1080, }; let raw: Vec = std::env::args().skip(1).collect(); let mut i = 0; @@ -50,6 +78,26 @@ mod args { } args.view = Some("upcoming".into()); } + "--screenshot" => { + i += 1; + args.screenshot = raw.get(i).cloned(); + } + "--output" => { + i += 1; + args.output = raw.get(i).cloned(); + } + "--width" => { + i += 1; + if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { + args.width = v; + } + } + "--height" => { + i += 1; + if let Some(v) = raw.get(i).and_then(|s| s.parse().ok()) { + args.height = v; + } + } _ => {} } i += 1; @@ -214,7 +262,9 @@ fn main() -> Result<()> { return cmd_upcoming_plain(); } - run_app(args.view, cfg) + let screenshot_req = args.screenshot_request(); + let view = screenshot_req.as_ref().map(|r| r.view.clone()).or(args.view); + run_app(view, cfg, screenshot_req) } fn cmd_done(id: &str) -> Result<()> { @@ -250,10 +300,20 @@ fn cmd_upcoming_plain() -> Result<()> { Ok(()) } -fn run_app(initial_view: Option, cfg: Config) -> Result<()> { - let app = gtk4::Application::builder() - .application_id("com.breadway.breadman") - .build(); +fn run_app( + initial_view: Option, + cfg: Config, + screenshot_req: Option, +) -> Result<()> { + let mut builder = gtk4::Application::builder().application_id("com.breadway.breadman"); + if screenshot_req.is_some() { + // GApplication is single-instance by default; this machine typically + // already has a real breadman instance, so without this a + // screenshot run would activate the *existing* instance instead of + // starting a fresh one that ever sees `screenshot_req`. + builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); + } + let app = builder.build(); let cfg = Arc::new(cfg); let initial_view = Arc::new(initial_view); @@ -261,7 +321,7 @@ fn run_app(initial_view: Option, cfg: Config) -> Result<()> { app.connect_activate(move |app| { let cfg = cfg.as_ref().clone(); let initial_view = initial_view.as_deref().map(|s| s.to_string()); - if let Err(e) = build_app_window(app, cfg, initial_view) { + if let Err(e) = build_app_window(app, cfg, initial_view, screenshot_req.clone()) { tracing::error!("failed to build window: {}", e); } }); @@ -279,6 +339,7 @@ fn build_app_window( app: >k4::Application, cfg: Config, initial_view: Option, + screenshot_req: Option, ) -> Result<()> { apply_css(&cfg); @@ -465,6 +526,10 @@ fn build_app_window( } stack.set_visible_child_name(initial); + if let Some(req) = screenshot_req { + screenshot::dispatch(&window, req); + } + window.present(); Ok(()) } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs new file mode 100644 index 0000000..66da64a --- /dev/null +++ b/breadman/src/screenshot.rs @@ -0,0 +1,58 @@ +//! `--screenshot` CLI mode: render the named view, capture it via +//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s +//! `bread-capture` orchestrator, or run standalone for one-off captures. +//! +//! No clap here, same reasoning as breadpad: extends breadman's own +//! hand-rolled `mod args` instead of bolting on a second parser that would +//! reject its real flags (`--view`, `done`, `upcoming --plain`). +//! +//! `--screenshot ` doubles as the view selector — it's passed through +//! as `initial_view` (the same field `--view` already sets) rather than +//! needing a separate mechanism, since breadman already supports opening +//! directly to a named stack page. + +use gtk4::prelude::*; +use std::path::PathBuf; +use std::time::Duration; + +/// Extra settle time after `map` for the first frame to actually paint +/// before grim runs — `map` fires once the surface exists, not once +/// anything has been drawn into it. +const SETTLE_DELAY: Duration = Duration::from_millis(300); + +#[derive(Clone)] +pub struct ScreenshotRequest { + pub view: String, + pub output: PathBuf, + pub width: u32, + pub height: u32, +} + +/// Wire up the given view's screenshot sequence against an already-built, +/// not-yet-presented window. Every path here ends by exiting the process — +/// it never returns control to the normal note-manager UI. +/// +/// Unlike the other apps' `dispatch`, this doesn't validate `req.view` +/// against a known-views list — an invalid name just falls through to +/// breadman's own `unwrap_or("all")` default (see `build_app_window`), +/// same as `--view` already behaves for a normal run. +pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { + let output = req.output; + let (width, height) = (req.width as i32, req.height as i32); + window.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); +} + +fn finish(result: anyhow::Result<()>) { + match result { + Ok(()) => std::process::exit(0), + Err(e) => { + eprintln!("breadman: screenshot capture failed: {e}"); + std::process::exit(1); + } + } +}