breadpad: add --screenshot CLI mode for automated capture

Same reasoning as breadbar/breadbox/breadclip/breadsearch, but plumbed
through breadpad's own hand-rolled arg parser (mod args) instead of clap
— breadpad doesn't use clap at all, and adding a second, separate clap
parser alongside the existing one would reject every one of breadpad's
real flags (--type, --no-classify, --status, fire, calendar, ...) the
moment it saw one clap didn't recognize. --screenshot/--output/--width/
--height are just three more fields on the same Args struct.

Only the "popup" view (the compose window) is wired up — the reminder
window needs a real stored Note to render, not worth fabricating one
just for a screenshot pass.

Also fixes the same single-instance footgun as the other apps: without
NON_UNIQUE, a screenshot run would just toggle-close the operator's real
already-open popup instead of getting its own window.
This commit is contained in:
Breadway 2026-07-29 11:43:55 +08:00
parent 1289c05c47
commit 879ec1e7f4
4 changed files with 166 additions and 9 deletions

21
Cargo.lock generated
View file

@ -302,6 +302,16 @@ dependencies = [
"piper",
]
[[package]]
name = "bread-screenshots"
version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27"
dependencies = [
"anyhow",
"bread-utils",
"tracing",
]
[[package]]
name = "bread-theme"
version = "0.2.3"
@ -313,6 +323,16 @@ dependencies = [
"serde_json",
]
[[package]]
name = "bread-utils"
version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#1a3475bd2358202f60e29c9bd27d06b1428b1a27"
dependencies = [
"dirs 5.0.1",
"serde",
"serde_json",
]
[[package]]
name = "breadman"
version = "0.3.4"
@ -335,6 +355,7 @@ name = "breadpad"
version = "0.3.4"
dependencies = [
"anyhow",
"bread-screenshots",
"breadpad-shared",
"chrono",
"dirs 5.0.1",

View file

@ -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
ort.workspace = true
tracing.workspace = true

View file

@ -13,6 +13,8 @@ use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, Once};
mod screenshot;
static ORT_INIT: Once = Once::new();
fn init_ort_once(cfg: &Config) {
@ -41,6 +43,29 @@ mod args {
pub model_info: bool,
pub calendar_test: bool,
pub calendar_list_uid: Option<String>,
pub screenshot: Option<String>,
pub output: Option<String>,
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<crate::screenshot::ScreenshotRequest> {
let view = self.screenshot.clone()?;
let Some(output) = self.output.clone() else {
eprintln!("breadpad: --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 {
@ -53,6 +78,10 @@ mod args {
model_info: false,
calendar_test: false,
calendar_list_uid: None,
screenshot: None,
output: None,
width: 1920,
height: 1080,
};
let raw: Vec<String> = std::env::args().skip(1).collect();
let mut i = 0;
@ -82,6 +111,26 @@ mod args {
_ => {}
}
}
"--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;
@ -120,7 +169,8 @@ fn main() -> Result<()> {
return cmd_calendar_list_uid(&note_id, &cfg);
}
run_popup(args.note_type, args.no_classify, cfg)
let screenshot_req = args.screenshot_request();
run_popup(args.note_type, args.no_classify, cfg, screenshot_req)
}
fn cmd_status(cfg: &Config) -> Result<()> {
@ -510,22 +560,36 @@ fn build_reminder_window(
window.present();
}
fn run_popup(preset_type: Option<String>, no_classify: bool, cfg: Config) -> Result<()> {
fn run_popup(
preset_type: Option<String>,
no_classify: bool,
cfg: Config,
screenshot_req: Option<screenshot::ScreenshotRequest>,
) -> Result<()> {
// Try to get current Hyprland workspace
let workspace = get_active_workspace();
let app = gtk4::Application::builder()
.application_id("com.breadway.breadpad")
.build();
let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad");
if screenshot_req.is_some() {
// GApplication is single-instance by default; this machine typically
// already has a real breadpad instance, so without this a
// screenshot run would just toggle-close the *existing* instance's
// window instead of starting a fresh one that ever sees
// `screenshot_req` (see the `app.windows().first()` toggle below).
builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE);
}
let app = builder.build();
let cfg = Arc::new(cfg);
app.connect_activate(move |app| {
if screenshot_req.is_none() {
if let Some(win) = app.windows().first().cloned() {
win.close();
return;
}
build_window(app, cfg.clone(), workspace.clone(), preset_type.clone(), no_classify);
}
build_window(app, cfg.clone(), workspace.clone(), preset_type.clone(), no_classify, screenshot_req.clone());
});
let code = app.run_with_args::<String>(&[]);
@ -551,6 +615,7 @@ fn build_window(
workspace: Option<String>,
preset_type: Option<String>,
no_classify: bool,
screenshot_req: Option<screenshot::ScreenshotRequest>,
) {
let window = gtk4::ApplicationWindow::builder()
.application(app)
@ -710,6 +775,10 @@ fn build_window(
});
window.add_controller(key_ctrl);
if let Some(req) = screenshot_req {
screenshot::dispatch(&window, req);
}
window.present();
entry.grab_focus();
}

View file

@ -0,0 +1,65 @@
//! `--screenshot` CLI mode: render breadpad's compose popup, 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 (unlike breadbar/breadbox/breadclip/breadsearch): breadpad
//! already has its own small hand-rolled flag parser (`mod args`) covering
//! `--type`/`--no-classify`/`--status`/`fire`/`calendar`/etc, and clap's
//! default "reject unknown flags" behavior would break every one of those
//! if bolted on as a second, separate parser. `--screenshot`/`--output`/
//! `--width`/`--height` are just three more fields on that same `Args`
//! struct instead.
//!
//! Only the "popup" view (the compose window from `run_popup`) is wired up
//! — the reminder window (`run_reminder_window`, reached via `fire <id>`)
//! needs a real stored `Note` to render, which isn't worth fabricating for
//! a screenshot pass.
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 popup UI.
pub fn dispatch(window: &gtk4::ApplicationWindow, req: ScreenshotRequest) {
match req.view.as_str() {
"popup" => {
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));
});
});
}
other => {
eprintln!("breadpad: unknown screenshot view '{other}' (known: popup)");
std::process::exit(1);
}
}
}
fn finish(result: anyhow::Result<()>) {
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("breadpad: screenshot capture failed: {e}");
std::process::exit(1);
}
}
}