diff --git a/Cargo.lock b/Cargo.lock index 51efaee..5abc856 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,15 +127,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "android-activity" version = "0.6.0" @@ -477,28 +468,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "bindgen" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 1.0.109", - "which", -] - [[package]] name = "bit-set" version = "0.6.0" @@ -540,7 +509,6 @@ dependencies = [ "egui_extras", "gbm", "image", - "leptess", "libc", "memmap2", "png 0.17.16", @@ -701,15 +669,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -737,17 +696,6 @@ dependencies = [ "libc", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.60" @@ -1208,12 +1156,6 @@ dependencies = [ "winit", ] -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "emath" version = "0.29.1" @@ -1571,12 +1513,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "glow" version = "0.13.1" @@ -1773,15 +1709,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "icu_collections" version = "2.1.1" @@ -1995,56 +1922,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "leptess" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8964e3d3270be667dda2d0026e8c77011bafaad33936011b93750489987513" -dependencies = [ - "tesseract-plumbing", - "thiserror 1.0.69", -] - -[[package]] -name = "leptonica-plumbing" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7a74c43d6f090d39158d233f326f47cd8bba545217595c93662b4e31156f42" -dependencies = [ - "leptonica-sys", - "libc", - "thiserror 1.0.69", -] - -[[package]] -name = "leptonica-sys" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da627c72b2499a8106f4dd33143843015e4a631f445d561f3481f7fba35b6151" -dependencies = [ - "bindgen", - "pkg-config", - "vcpkg", -] - [[package]] name = "libc" version = "0.2.182" @@ -2184,12 +2067,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2289,16 +2166,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -2711,12 +2578,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -3045,35 +2906,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -3438,29 +3270,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "tesseract-plumbing" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25fbbb95169954a9262a565fbfb001c4d9dad271d48142e6632a3e2b7314b35" -dependencies = [ - "leptonica-plumbing", - "tesseract-sys", - "thiserror 1.0.69", -] - -[[package]] -name = "tesseract-sys" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd33f6f216124cfaf0fa86c2c0cdf04da39b6257bd78c5e44fa4fa98c3a5857b" -dependencies = [ - "bindgen", - "leptonica-sys", - "pkg-config", - "vcpkg", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -3605,7 +3414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ "memchr", - "nom 8.0.0", + "nom", "petgraph", ] @@ -3695,12 +3504,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -4146,18 +3949,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "widestring" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index f17d820..967daab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ required-features = ["gui"] [features] default = [] gui = ["dep:eframe", "dep:egui", "dep:egui_extras", "dep:ab_glyph"] -ocr = ["dep:leptess"] [dependencies] clap = { version = "4", features = ["derive"] } @@ -45,7 +44,6 @@ ab_glyph = { version = "0.2", optional = true } eframe = { version = "0.29", features = ["wayland"], optional = true } egui = { version = "0.29", optional = true } egui_extras = { version = "0.29", features = ["image"], optional = true } -leptess = { version = "0.14", optional = true } [profile.release] lto = "fat" diff --git a/src/capture.rs b/src/capture.rs index fee84f6..2d05ec4 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -333,19 +333,20 @@ impl AppState { // Capture all monitors in parallel — each thread opens its own // Wayland connection. This turns N sequential captures (each // gated on a compositor roundtrip) into N concurrent ones. - let captures: Vec>> = std::thread::scope(|s| { - let handles: Vec<_> = mons - .iter() - .map(|mon| { - s.spawn(move || { - let lw = (mon.width as f64 / mon.scale).round() as u32; - let lh = (mon.height as f64 / mon.scale).round() as u32; - capture_and_resize(&mon.name, opts, lw, lh, false) + let captures: Vec>> = + std::thread::scope(|s| { + let handles: Vec<_> = mons + .iter() + .map(|mon| { + s.spawn(move || { + let lw = (mon.width as f64 / mon.scale).round() as u32; + let lh = (mon.height as f64 / mon.scale).round() as u32; + capture_and_resize(&mon.name, opts, lw, lh, false) + }) }) - }) - .collect(); - handles.into_iter().map(|h| h.join().unwrap()).collect() - }); + .collect(); + handles.into_iter().map(|h| h.join().unwrap()).collect() + }); let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h); for (mon, c) in mons.iter().zip(captures.into_iter()) { @@ -672,12 +673,11 @@ fn capture_and_resize( let mut state = state; let overlay = if opts.include_cursor { 1 } else { 0 }; - let frame = - state - .screencopy_mgr - .as_ref() - .unwrap() - .capture_output(overlay, &out_handle, &state.qh, ()); + let frame = state + .screencopy_mgr + .as_ref() + .unwrap() + .capture_output(overlay, &out_handle, &state.qh, ()); state.frame = Some(frame); state.frame_state = FrameState::Negotiating; state.dispatch_to_ready()?; diff --git a/src/clipboard.rs b/src/clipboard.rs index a55c3fe..e45ec7f 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -14,12 +14,3 @@ pub fn copy_png(png_bytes: Vec) -> Result<()> { thread::sleep(std::time::Duration::from_millis(50)); Ok(()) } - -#[allow(dead_code)] -pub fn copy_text(text: String) -> Result<()> { - Options::new() - .copy(Source::Bytes(text.into_bytes().into()), MimeType::Text) - .map_err(|e| BlastError::Clipboard(format!("{e}")))?; - thread::sleep(std::time::Duration::from_millis(50)); - Ok(()) -} diff --git a/src/gui/annotations.rs b/src/gui/annotations.rs index 3e44a04..01fd7da 100644 --- a/src/gui/annotations.rs +++ b/src/gui/annotations.rs @@ -213,15 +213,7 @@ impl Annotation for ArrowAnn { fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { let c = to_rgba(self.color); draw_line_img( - img, - self.from[0], - self.from[1], - self.to[0], - self.to[1], - self.thickness, - c, - iw, - ih, + img, self.from[0], self.from[1], self.to[0], self.to[1], self.thickness, c, iw, ih, ); draw_arrowhead( img, @@ -290,14 +282,7 @@ impl Annotation for TextAnn { } fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { draw_text_img( - img, - self.pos[0], - self.pos[1], - &self.text, - self.size, - self.color, - iw, - ih, + img, self.pos[0], self.pos[1], &self.text, self.size, self.color, iw, ih, ); } fn as_text(&self) -> Option<&TextAnn> { @@ -355,9 +340,7 @@ impl Annotation for PixelateAnn { rect_selection(&self.r, painter, to_screen); } fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { - pixelate_region( - img, self.r[0], self.r[1], self.r[2], self.r[3], self.block, iw, ih, - ); + pixelate_region(img, self.r[0], self.r[1], self.r[2], self.r[3], self.block, iw, ih); } } @@ -403,17 +386,7 @@ impl Annotation for PencilAnn { fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { let c = to_rgba(self.color); for w in self.points.windows(2) { - draw_line_img( - img, - w[0][0], - w[0][1], - w[1][0], - w[1][1], - self.thickness, - c, - iw, - ih, - ); + draw_line_img(img, w[0][0], w[0][1], w[1][0], w[1][1], self.thickness, c, iw, ih); } } } diff --git a/src/gui/app.rs b/src/gui/app.rs index 49ffa15..ee68061 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -15,9 +15,8 @@ use crate::{ capture::{self, CaptureOptions}, clipboard, error::BlastError, - freeze, hyprland, paths, region, select, + freeze, hyprland, paths, region, select, wayland_windows, shhh::{self, ShadowOptions}, - wayland_windows, }; #[derive(Debug, Clone, Copy, PartialEq)] @@ -205,25 +204,23 @@ impl BlastApp { Subject::Area => { let geom = hyprland::with_animations_disabled(|bs| { let boxes = wayland_windows::visible_window_hints().unwrap_or_else(|| { - let bar = - hyprland::get_option_int("plugin:hyprbars:bar_height").unwrap_or(0); + let bar = hyprland::get_option_int("plugin:hyprbars:bar_height") + .unwrap_or(0); hyprland::visible_windows() .map(|wins| { wins.iter() .map(|w| { let g = w.to_geometry(bs, bar); select::HintBox { - x: g.x as i32, - y: g.y as i32, - w: g.w as i32, - h: g.h as i32, + x: g.x as i32, y: g.y as i32, + w: g.w as i32, h: g.h as i32, } }) .collect() }) .unwrap_or_default() }); - region::select_area_boxes(boxes, None, true) + region::select_area_boxes(boxes, None) }) .map_err(|e| e.to_string())?; freeze_guard.kill(); @@ -231,7 +228,7 @@ impl BlastApp { (img, "Area".into()) } Subject::Region => { - let geom = region::select_free_region(None, true).map_err(|e| e.to_string())?; + let geom = region::select_free_region(None).map_err(|e| e.to_string())?; freeze_guard.kill(); let img = capture::capture_region(geom, &opts).map_err(|e| e.to_string())?; (img, "Region".into()) diff --git a/src/main.rs b/src/main.rs index a8e56b7..0d947be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,6 @@ mod error; mod freeze; mod hyprland; mod notify; -#[cfg(feature = "ocr")] -mod ocr; mod paths; mod region; mod select; @@ -48,21 +46,6 @@ struct Cli { #[arg(long = "hint-color", value_name = "RRGGBBAA", value_parser = parse_color)] hint_color: Option, - /// Hide the border around the active selection / hovered hint box. - /// Useful when the outline bleeds into the captured image. - #[arg(long = "no-outline")] - no_outline: bool, - - /// Reuse the geometry from the most recent `region` capture (skips interactive selection). - #[arg(long)] - last: bool, - - /// Send notification with clickable actions (requires --notify). - /// On save/copysave, exposes a "Copy path" button. Process briefly blocks - /// awaiting a click or notification timeout. - #[arg(long, requires = "notify")] - actions: bool, - #[command(flatten)] shadow: ShadowArgs, @@ -170,19 +153,6 @@ enum Action { /// Output file path (defaults to /tmp/.png). file: Option, }, - /// OCR the captured region and copy the recognized text to the clipboard. - #[cfg(feature = "ocr")] - Ocr { - #[arg(value_enum, default_value_t = Subject::Region)] - subject: Subject, - /// Tesseract language code(s), e.g. "eng" or "eng+deu" (default "eng"). - #[arg(long, default_value = "eng")] - lang: String, - /// Write text to this file instead of (or in addition to) clipboard. - /// Use '-' for stdout. - #[arg(short = 'o', long)] - output: Option, - }, Check, } @@ -213,9 +183,6 @@ fn main() { wait_secs: cli.wait, scale: cli.scale, hint_color: cli.hint_color, - no_outline: cli.no_outline, - last: cli.last, - actions: cli.actions, shadow: cli.shadow, }; @@ -244,7 +211,6 @@ fn print_usage() { println!(" -w, --wait N Wait N seconds before capture"); println!(" -s, --scale SCALE Scale factor"); println!(" --hint-color RRGGBBAA Hint-box colour for area/region (e.g. ff550080)"); - println!(" --no-outline Hide the selection border (avoids outline in captures)"); println!(" --no-shadow Disable drop shadow and corner rounding"); println!(" --radius px Corner radius (default: from Hyprland config or 0)"); println!(" --shadow-offset x,y Shadow offset (default: -20,-20)"); @@ -260,9 +226,6 @@ struct Context { wait_secs: Option, scale: Option, hint_color: Option, - no_outline: bool, - last: bool, - actions: bool, shadow: ShadowArgs, } @@ -294,31 +257,6 @@ impl Context { } } - /// Notify after a successful save. If `--actions` is enabled, expose a - /// "Copy path" button; on click, copy `saved_path` to the clipboard. - fn notify_saved(&self, summary: &str, saved_path: &str) { - if !self.notify { - return; - } - if self.actions { - let actions = [("copy-path", "Copy path")]; - match notify::notify_with_actions( - summary, - Some(saved_path), - Some(saved_path), - &actions, - 3000, - ) { - Ok(Some(key)) if key == "copy-path" => { - let _ = clipboard::copy_text(saved_path.to_string()); - } - _ => {} - } - } else { - let _ = notify::notify_ok(summary, Some(saved_path), Some(saved_path)); - } - } - /// Encode image to PNG, applying shadow/rounding if enabled. fn finalize_png(&self, img: &DynamicImage) -> Result> { match self.shadow.resolve() { @@ -342,55 +280,6 @@ fn run(action: Action, ctx: Context) -> Result<()> { run_copysave(subject, output.or(file), ctx) } Action::Edit { subject, file } => run_edit(subject, file, ctx), - #[cfg(feature = "ocr")] - Action::Ocr { - subject, - lang, - output, - } => run_ocr(subject, lang, output, ctx), - } -} - -#[cfg(feature = "ocr")] -fn run_ocr(subject: Subject, lang: String, output: Option, ctx: Context) -> Result<()> { - let (img, _what) = capture_subject(&subject, &ctx)?; - let text = ocr::recognize(&img, &lang)?; - - if text.is_empty() { - ctx.notify_err("OCR: no text recognized", None); - return Err(BlastError::Other("no text recognized".into())); - } - - match output.as_deref() { - Some("-") => { - print!("{text}"); - } - Some(path) => { - std::fs::write(path, &text).map_err(BlastError::Io)?; - ctx.notify_ok("Text saved", Some(path), None); - println!("{path}"); - } - None => { - clipboard::copy_text(text.clone()).map_err(|e| { - ctx.notify_err("Clipboard error", Some(&e.to_string())); - e - })?; - ctx.notify_ok("Text copied", Some(&summarize_text(&text)), None); - // also print to stdout so it's pipe-able - println!("{text}"); - } - } - - Ok(()) -} - -#[cfg(feature = "ocr")] -fn summarize_text(t: &str) -> String { - let one_line: String = t.split_whitespace().collect::>().join(" "); - if one_line.len() <= 80 { - one_line - } else { - format!("{}…", &one_line[..77]) } } @@ -451,7 +340,7 @@ fn run_save(subject: Subject, file: Option, ctx: Context) -> Result<()> let png = ctx.finalize_png(&img)?; std::fs::write(&dest, &png).map_err(BlastError::Io)?; let name = dest.display().to_string(); - ctx.notify_saved(&format!("Screenshot of {what}"), &name); + ctx.notify_ok(&format!("Screenshot of {what}"), Some(&name), Some(&name)); println!("{name}"); } } @@ -474,7 +363,11 @@ fn run_copysave(subject: Subject, file: Option, ctx: Context) -> Result< })?; let name = dest.display().to_string(); - ctx.notify_saved(&format!("{what} copied and saved"), &name); + ctx.notify_ok( + &format!("{what} copied and saved"), + Some(&name), + Some(&name), + ); println!("{name}"); Ok(()) } @@ -584,11 +477,11 @@ fn capture_area(ctx: &Context) -> Result<(DynamicImage, String)> { }) .collect() }); - region::select_area_boxes(boxes, ctx.hint_color, !ctx.no_outline) + region::select_area_boxes(boxes, ctx.hint_color) })? } else { let boxes = wayland_windows::visible_window_hints().unwrap_or_default(); - region::select_area_boxes(boxes, ctx.hint_color, !ctx.no_outline)? + region::select_area_boxes(boxes, ctx.hint_color)? }; // If the selection snapped to a known Hyprland window, ask the compositor @@ -613,22 +506,13 @@ fn capture_area(ctx: &Context) -> Result<(DynamicImage, String)> { } fn capture_region_free(ctx: &Context) -> Result<(DynamicImage, String)> { - if ctx.last { - let geom = paths::load_last_region().ok_or_else(|| { - BlastError::Selection("no previous region cached (run `region` once first)".into()) - })?; - let img = capture::capture_region(geom, &ctx.capture_opts())?; - return Ok((img, "Region".into())); - } - let mut freeze_guard = if ctx.freeze { freeze::FreezeGuard::spawn()? } else { freeze::FreezeGuard::none() }; - let geom = region::select_free_region(ctx.hint_color, !ctx.no_outline)?; - paths::save_last_region(&geom); + let geom = region::select_free_region(ctx.hint_color)?; let img = capture::capture_region(geom, &ctx.capture_opts())?; freeze_guard.kill(); diff --git a/src/notify.rs b/src/notify.rs index b8d48ad..3a75802 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -8,7 +8,7 @@ use std::env; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; -use std::time::{Duration, Instant}; +use std::time::Duration; use crate::error::{BlastError, Result}; @@ -28,7 +28,13 @@ pub fn notify_ok(summary: &str, body: Option<&str>, icon: Option<&str>) -> Resul #[allow(dead_code)] pub fn notify_error(summary: &str, body: Option<&str>) -> Result<()> { - send_notify(summary, body.unwrap_or(""), "", URGENCY_CRITICAL, 5000) + send_notify( + summary, + body.unwrap_or(""), + "", + URGENCY_CRITICAL, + 5000, + ) } fn err>(s: S) -> BlastError { @@ -39,7 +45,13 @@ fn io_err(e: std::io::Error) -> BlastError { BlastError::Notify(e.to_string()) } -fn send_notify(summary: &str, body: &str, icon: &str, urgency: u8, timeout_ms: i32) -> Result<()> { +fn send_notify( + summary: &str, + body: &str, + icon: &str, + urgency: u8, + timeout_ms: i32, +) -> Result<()> { let path = session_bus_path().ok_or_else(|| err("no DBus session bus address"))?; let mut sock = UnixStream::connect(&path).map_err(io_err)?; sock.set_read_timeout(Some(Duration::from_secs(2))).ok(); @@ -189,20 +201,6 @@ fn write_empty_string_array(buf: &mut Vec) { buf.extend_from_slice(&0u32.to_le_bytes()); } -/// Marshal an array of strings: u32 byte-length + N (string) entries. -fn write_string_array(buf: &mut Vec, items: &[&str]) { - align_to(buf, 4); - let len_pos = buf.len(); - buf.extend_from_slice(&[0u8; 4]); - // 's' element alignment is 4 (already at 4-aligned position). - let start = buf.len(); - for s in items { - write_string(buf, s); - } - let len = (buf.len() - start) as u32; - buf[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes()); -} - /// Marshal `a{sv}` containing a single "urgency" -> byte entry. fn write_urgency_hint(buf: &mut Vec, urgency: u8) { align_to(buf, 4); @@ -242,7 +240,7 @@ fn build_method_call( // Header fields array a(yv) let fields_len_pos = out.len(); out.extend_from_slice(&[0u8; 4]); // placeholder for array byte-length - // Pad to element alignment (struct = 8); this padding is NOT counted in the array length. + // Pad to element alignment (struct = 8); this padding is NOT counted in the array length. align_to(&mut out, 8); let fields_start = out.len(); @@ -299,173 +297,10 @@ mod tests { notify_ok("blast notify test", Some("hand-rolled DBus works"), None) .expect("notify failed"); } - - /// Live test for the actions path. Pops a notification with a "Copy path" - /// button. Returns Some("copy-path") if you click it, None on timeout. - /// Run with: cargo test --bin blast notify::tests::live_actions -- --ignored --nocapture - #[test] - #[ignore] - fn live_actions() { - let result = notify_with_actions( - "blast actions test", - Some("click \"Copy path\" within 5 seconds"), - None, - &[("copy-path", "Copy path")], - 5000, - ) - .expect("notify_with_actions failed"); - println!("result: {:?}", result); - } } /// Read and discard one DBus message (used to drain the Hello reply). fn drain_message(sock: &mut UnixStream) -> Result<()> { - let _ = read_message(sock)?; - Ok(()) -} - -// === Interactive notifications (actions) === - -/// Send a notification with clickable actions, then wait up to `expire_ms`+500ms for the -/// user to click one. Returns `Some(action_key)` on click, `None` on dismissal or timeout. -/// -/// `actions` is a list of `(key, label)` pairs — e.g. `[("open", "Open"), ("copy-path", "Copy path")]`. -#[allow(dead_code)] -pub fn notify_with_actions( - summary: &str, - body: Option<&str>, - icon: Option<&str>, - actions: &[(&str, &str)], - expire_ms: i32, -) -> Result> { - let path = session_bus_path().ok_or_else(|| err("no DBus session bus address"))?; - let mut sock = UnixStream::connect(&path).map_err(io_err)?; - sock.set_write_timeout(Some(Duration::from_secs(2))).ok(); - - sasl_auth(&mut sock)?; - hello(&mut sock)?; - add_match( - &mut sock, - "type='signal',interface='org.freedesktop.Notifications'", - )?; - let notif_id = do_notify_with_actions( - &mut sock, - summary, - body.unwrap_or(""), - icon.unwrap_or(""), - URGENCY_NORMAL, - expire_ms, - actions, - )?; - - // Wait for ActionInvoked or NotificationClosed for our id, or timeout. - // Bus may interleave other signals (NameAcquired etc) — we just skip them. - let deadline = Instant::now() + Duration::from_millis(expire_ms.max(0) as u64 + 500); - loop { - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - return Ok(None); - } - sock.set_read_timeout(Some(remaining)).ok(); - - let msg = match read_message(&mut sock) { - Ok(m) => m, - Err(_) => return Ok(None), // timed out or socket error - }; - - match msg.member.as_deref() { - Some("ActionInvoked") => { - if let Some((id, key)) = parse_us_body(&msg.body) { - if id == notif_id { - return Ok(Some(key)); - } - } - } - Some("NotificationClosed") => { - if msg.body.len() >= 4 { - let id = u32::from_le_bytes(msg.body[0..4].try_into().unwrap()); - if id == notif_id { - return Ok(None); - } - } - } - _ => {} - } - } -} - -fn add_match(sock: &mut UnixStream, rule: &str) -> Result<()> { - let mut body = Vec::new(); - write_string(&mut body, rule); - let msg = build_method_call( - 3, - "/org/freedesktop/DBus", - "org.freedesktop.DBus", - "AddMatch", - "org.freedesktop.DBus", - Some("s"), - &body, - 0, - ); - sock.write_all(&msg).map_err(io_err)?; - drain_message(sock)?; - Ok(()) -} - -fn do_notify_with_actions( - sock: &mut UnixStream, - summary: &str, - body: &str, - icon: &str, - urgency: u8, - timeout_ms: i32, - actions: &[(&str, &str)], -) -> Result { - let mut payload = Vec::new(); - write_string(&mut payload, "blast"); - write_u32(&mut payload, 0); - write_string(&mut payload, icon); - write_string(&mut payload, summary); - write_string(&mut payload, body); - - // Flatten (key,label) pairs into a flat string array. - let mut flat: Vec<&str> = Vec::with_capacity(actions.len() * 2); - for (k, l) in actions { - flat.push(k); - flat.push(l); - } - write_string_array(&mut payload, &flat); - write_urgency_hint(&mut payload, urgency); - align_to(&mut payload, 4); - payload.extend_from_slice(&timeout_ms.to_le_bytes()); - - let msg = build_method_call( - 4, - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications", - "Notify", - "org.freedesktop.Notifications", - Some("susssasa{sv}i"), - &payload, - 0, // NOT fire-and-forget — we need the reply to learn our notification id. - ); - sock.write_all(&msg).map_err(io_err)?; - - let reply = read_message(sock)?; - if reply.body.len() < 4 { - return Err(err("notify reply: body too short")); - } - Ok(u32::from_le_bytes(reply.body[0..4].try_into().unwrap())) -} - -// === Message unmarshalling === - -struct Message { - member: Option, - body: Vec, -} - -fn read_message(sock: &mut UnixStream) -> Result { let mut fixed = [0u8; 16]; sock.read_exact(&mut fixed).map_err(io_err)?; if fixed[0] != b'l' { @@ -473,100 +308,10 @@ fn read_message(sock: &mut UnixStream) -> Result { } let body_len = u32::from_le_bytes(fixed[4..8].try_into().unwrap()) as usize; let fields_len = u32::from_le_bytes(fixed[12..16].try_into().unwrap()) as usize; + // Header is always padded to 8 before body (whether body is empty or not). let header_pad = (8 - ((16 + fields_len) % 8)) % 8; - - let mut fields = vec![0u8; fields_len]; - sock.read_exact(&mut fields).map_err(io_err)?; - let mut pad = vec![0u8; header_pad]; - if !pad.is_empty() { - sock.read_exact(&mut pad).map_err(io_err)?; - } - let mut body = vec![0u8; body_len]; - if !body.is_empty() { - sock.read_exact(&mut body).map_err(io_err)?; - } - - let member = parse_member(&fields); - Ok(Message { member, body }) -} - -/// Walk the header fields array looking for the MEMBER field (code=3, sig "s"). -/// Returns None if not present or if parsing falls off the end of a known type. -fn parse_member(fields: &[u8]) -> Option { - let mut pos = 0; - while pos < fields.len() { - pos = align_up(pos, 8); - if pos >= fields.len() { - break; - } - let code = fields[pos]; - pos += 1; - if pos >= fields.len() { - return None; - } - let sig_len = fields[pos] as usize; - pos += 1; - if pos + sig_len + 1 > fields.len() { - return None; - } - let sig = fields[pos..pos + sig_len].to_vec(); - pos += sig_len + 1; // include trailing NUL - - if code == 3 && sig.first() == Some(&b's') { - pos = align_up(pos, 4); - if pos + 4 > fields.len() { - return None; - } - let slen = u32::from_le_bytes(fields[pos..pos + 4].try_into().ok()?) as usize; - pos += 4; - if pos + slen > fields.len() { - return None; - } - return Some(String::from_utf8_lossy(&fields[pos..pos + slen]).into_owned()); - } - pos = skip_variant_value(&sig, fields, pos)?; - } - None -} - -fn skip_variant_value(sig: &[u8], fields: &[u8], mut pos: usize) -> Option { - match sig.first()? { - b'o' | b's' => { - pos = align_up(pos, 4); - if pos + 4 > fields.len() { - return None; - } - let slen = u32::from_le_bytes(fields[pos..pos + 4].try_into().ok()?) as usize; - Some(pos + 4 + slen + 1) - } - b'g' => { - if pos >= fields.len() { - return None; - } - let glen = fields[pos] as usize; - Some(pos + 1 + glen + 1) - } - b'u' | b'i' => { - pos = align_up(pos, 4); - Some(pos + 4) - } - _ => None, - } -} - -fn align_up(pos: usize, n: usize) -> usize { - (pos + n - 1) & !(n - 1) -} - -/// Parse a DBus body with signature "us": u32 id + string. Used for ActionInvoked. -fn parse_us_body(body: &[u8]) -> Option<(u32, String)> { - if body.len() < 8 { - return None; - } - let id = u32::from_le_bytes(body[0..4].try_into().ok()?); - let slen = u32::from_le_bytes(body[4..8].try_into().ok()?) as usize; - if body.len() < 8 + slen { - return None; - } - Some((id, String::from_utf8_lossy(&body[8..8 + slen]).into_owned())) + let total_after = fields_len + header_pad + body_len; + let mut discard = vec![0u8; total_after]; + sock.read_exact(&mut discard).map_err(io_err)?; + Ok(()) } diff --git a/src/ocr.rs b/src/ocr.rs deleted file mode 100644 index 29444b3..0000000 --- a/src/ocr.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! OCR via libtesseract (dynamic link, gated by the `ocr` cargo feature). -//! -//! `leptess` wraps `tesseract-sys` (bindgen-generated bindings to libtesseract + -//! libleptonica). At build time bindgen reads /usr/include/tesseract/*.h and emits -//! the Rust extern declarations; at runtime we dynamic-link against -//! `libtesseract.so` / `libleptonica.so` already present on the system. - -use image::DynamicImage; -use leptess::LepTess; - -use crate::error::{BlastError, Result}; -use crate::shhh::encode_png; - -/// Run OCR on `img` using the given language tag (e.g. "eng", "eng+deu"). -/// Returns the recognized text with trailing whitespace trimmed. -/// -/// leptess only exposes `set_image_from_mem` (decodes via leptonica), so we -/// round-trip through PNG. Re-encoding is cheap relative to OCR runtime. -pub fn recognize(img: &DynamicImage, language: &str) -> Result { - let mut lt = LepTess::new(None, language) - .map_err(|e| BlastError::Other(format!("tesseract init: {e}")))?; - - let png = encode_png(img).map_err(|e| BlastError::Image(e.to_string()))?; - lt.set_image_from_mem(&png) - .map_err(|e| BlastError::Other(format!("tesseract set_image: {e}")))?; - - let text = lt - .get_utf8_text() - .map_err(|e| BlastError::Other(format!("tesseract recognize: {e}")))?; - - Ok(text.trim().to_string()) -} diff --git a/src/paths.rs b/src/paths.rs index 1eb566e..18f6c63 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -1,11 +1,9 @@ //! File path helpers: XDG screenshot dir, random names, editor tmp dir. -use std::{env, fs, path::PathBuf}; +use std::{env, path::PathBuf}; use rand::{distributions::Alphanumeric, Rng}; -use crate::hyprland::Geometry; - /// Resolve the target directory for saving screenshots. /// /// Priority: $XDG_SCREENSHOTS_DIR -> $XDG_PICTURES_DIR -> $HOME. @@ -62,31 +60,3 @@ pub fn default_editor_path() -> PathBuf { pub fn resolve_editor() -> String { env::var("BLAST_EDITOR").unwrap_or_else(|_| "gimp".into()) } - -fn last_region_path() -> Option { - dirs::cache_dir().map(|d| d.join("blast").join("last-region")) -} - -pub fn save_last_region(g: &Geometry) { - let Some(path) = last_region_path() else { - return; - }; - if let Some(parent) = path.parent() { - let _ = fs::create_dir_all(parent); - } - let _ = fs::write(path, format!("{} {} {} {}\n", g.x, g.y, g.w, g.h)); -} - -pub fn load_last_region() -> Option { - let path = last_region_path()?; - let s = fs::read_to_string(path).ok()?; - let mut parts = s.trim().split_whitespace(); - let x = parts.next()?.parse().ok()?; - let y = parts.next()?.parse().ok()?; - let w = parts.next()?.parse().ok()?; - let h = parts.next()?.parse().ok()?; - if w <= 0 || h <= 0 { - return None; - } - Some(Geometry { x, y, w, h }) -} diff --git a/src/region.rs b/src/region.rs index 6276d4f..28c555c 100644 --- a/src/region.rs +++ b/src/region.rs @@ -5,20 +5,16 @@ use crate::{ }; /// Area selection from a pre-built list of hint boxes (logical coords). -pub fn select_area_boxes( - boxes: Vec, - hint_rgba: Option, - outline: bool, -) -> Result { +pub fn select_area_boxes(boxes: Vec, hint_rgba: Option) -> Result { if boxes.is_empty() { - select::select_region(vec![], false, hint_rgba, outline) + select::select_region(vec![], false, hint_rgba) } else { - select::select_region(boxes, true, hint_rgba, outline) + select::select_region(boxes, true, hint_rgba) } } -pub fn select_free_region(hint_rgba: Option, outline: bool) -> Result { - select::select_region(vec![], false, hint_rgba, outline) +pub fn select_free_region(hint_rgba: Option) -> Result { + select::select_region(vec![], false, hint_rgba) } #[allow(dead_code)] pub fn select_area( @@ -26,10 +22,9 @@ pub fn select_area( border_size: i64, bar_height: i64, hint_rgba: Option, - outline: bool, ) -> Result { if windows.is_empty() { - return select::select_region(vec![], false, hint_rgba, outline); + return select::select_region(vec![], false, hint_rgba); } let boxes = windows .iter() @@ -43,5 +38,5 @@ pub fn select_area( } }) .collect(); - select::select_region(boxes, true, hint_rgba, outline) + select::select_region(boxes, true, hint_rgba) } diff --git a/src/select.rs b/src/select.rs index 8daac2f..b438d84 100644 --- a/src/select.rs +++ b/src/select.rs @@ -123,7 +123,6 @@ struct St { hover: Option, restrict: bool, c_hint: [u8; 4], - outline: bool, running: bool, result: Option, @@ -346,7 +345,6 @@ impl St { let buf_size = s.buf_size; let c_hint = self.c_hint; - let outline = self.outline; let box_data: Vec<(i32, i32, i32, i32, bool)> = self .boxes .iter() @@ -371,9 +369,7 @@ impl St { if bx0 < bx1 && by0 < by1 { if *is_hov { fill(data, stride, bx0, by0, bx1, by1, C_SEL); - if outline { - draw_border(data, stride, pw, ph, bx0, by0, bx1, by1, BDR * scale, C_BDR); - } + draw_border(data, stride, pw, ph, bx0, by0, bx1, by1, BDR * scale, C_BDR); } else { fill(data, stride, bx0, by0, bx1, by1, c_hint); } @@ -387,9 +383,7 @@ impl St { let y1 = ((gy + gh - ly) * scale).max(0).min(ph); if x0 < x1 && y0 < y1 { fill(data, stride, x0, y0, x1, y1, C_SEL); - if outline { - draw_border(data, stride, pw, ph, x0, y0, x1, y1, BDR * scale, C_BDR); - } + draw_border(data, stride, pw, ph, x0, y0, x1, y1, BDR * scale, C_BDR); } } @@ -760,7 +754,6 @@ pub fn select_region( boxes: Vec, restrict: bool, hint_rgba: Option, - outline: bool, ) -> Result { let conn = Connection::connect_to_env().map_err(|e| BlastError::Selection(format!("connect: {e}")))?; @@ -793,9 +786,12 @@ pub fn select_region( let pending: Vec = out_globals .iter() .map(|&(gname, ver)| PendingOut { - wl: globals - .registry() - .bind::(gname, ver.min(4), &qh, ()), + wl: globals.registry().bind::( + gname, + ver.min(4), + &qh, + (), + ), name: None, wl_name: gname, ox: 0, @@ -838,7 +834,6 @@ pub fn select_region( hover: None, restrict, c_hint, - outline, running: true, result: None, }; diff --git a/src/shhh.rs b/src/shhh.rs index a12ec8b..601094a 100644 --- a/src/shhh.rs +++ b/src/shhh.rs @@ -237,8 +237,8 @@ fn create_shadow( for (i, &a) in mask.iter().enumerate() { out_rgba[i * 4 + 3] = lut[a as usize]; } - let buf: ImageBuffer, Vec> = - ImageBuffer::from_raw(new_w, new_h, out_rgba).expect("shadow buffer construction"); + let buf: ImageBuffer, Vec> = ImageBuffer::from_raw(new_w, new_h, out_rgba) + .expect("shadow buffer construction"); DynamicImage::ImageRgba8(buf) } diff --git a/src/toplevel_capture.rs b/src/toplevel_capture.rs index 02c8127..9b0c557 100644 --- a/src/toplevel_capture.rs +++ b/src/toplevel_capture.rs @@ -26,9 +26,7 @@ use wayland_protocols::wp::linux_dmabuf::zv1::client::{ zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, }; use wayland_protocols_wlr::foreign_toplevel::v1::client::{ - zwlr_foreign_toplevel_handle_v1::{ - self, State as WlrToplevelState, ZwlrForeignToplevelHandleV1, - }, + zwlr_foreign_toplevel_handle_v1::{self, State as WlrToplevelState, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, }; @@ -696,7 +694,8 @@ impl Dispatch for St { } hyprland_toplevel_export_frame_v1::Event::Flags { flags } => { if let wayland_client::WEnum::Value(f) = flags { - state.y_invert = f.contains(hyprland_toplevel_export_frame_v1::Flags::YInvert); + state.y_invert = f + .contains(hyprland_toplevel_export_frame_v1::Flags::YInvert); } } hyprland_toplevel_export_frame_v1::Event::Ready { .. } => { diff --git a/src/wayland_windows.rs b/src/wayland_windows.rs index 5cbe7af..fbf3053 100644 --- a/src/wayland_windows.rs +++ b/src/wayland_windows.rs @@ -23,9 +23,7 @@ use wayland_protocols::xdg::xdg_output::zv1::client::{ zxdg_output_v1::{self, ZxdgOutputV1}, }; use wayland_protocols_wlr::foreign_toplevel::v1::client::{ - zwlr_foreign_toplevel_handle_v1::{ - self, State as WlrToplevelState, ZwlrForeignToplevelHandleV1, - }, + zwlr_foreign_toplevel_handle_v1::{self, State as WlrToplevelState, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, };