Merge feature/screenshot-mode: add --screenshot CLI mode for automated capture (breadpad + breadman)
This commit is contained in:
commit
55f5b6c3ae
7 changed files with 298 additions and 15 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -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,11 +323,22 @@ 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"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bread-screenshots",
|
||||
"breadpad-shared",
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
|
@ -335,6 +356,7 @@ name = "breadpad"
|
|||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bread-screenshots",
|
||||
"breadpad-shared",
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub done_id: Option<String>,
|
||||
pub upcoming_plain: bool,
|
||||
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!("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<String> = 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<String>, cfg: Config) -> Result<()> {
|
||||
let app = gtk4::Application::builder()
|
||||
.application_id("com.breadway.breadman")
|
||||
.build();
|
||||
fn run_app(
|
||||
initial_view: Option<String>,
|
||||
cfg: Config,
|
||||
screenshot_req: Option<screenshot::ScreenshotRequest>,
|
||||
) -> 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<String>, 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<String>,
|
||||
screenshot_req: Option<screenshot::ScreenshotRequest>,
|
||||
) -> 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(())
|
||||
}
|
||||
|
|
|
|||
58
breadman/src/screenshot.rs
Normal file
58
breadman/src/screenshot.rs
Normal file
|
|
@ -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 <view>` 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(¬e_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 let Some(win) = app.windows().first().cloned() {
|
||||
win.close();
|
||||
return;
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
65
breadpad/src/screenshot.rs
Normal file
65
breadpad/src/screenshot.rs
Normal 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: >k4::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue