Compare commits
No commits in common. "e03b5ab8cb0f44f52f6a9517979c4bbf470413d9" and "b58c2df1d9b268ba70ad2e00c8ef6cd9bb599285" have entirely different histories.
e03b5ab8cb
...
b58c2df1d9
15 changed files with 82 additions and 778 deletions
211
Cargo.lock
generated
211
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<Result<Option<image::RgbaImage>>> = 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<Result<Option<image::RgbaImage>>> =
|
||||
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()?;
|
||||
|
|
|
|||
|
|
@ -14,12 +14,3 @@ pub fn copy_png(png_bytes: Vec<u8>) -> 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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
134
src/main.rs
134
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<u32>,
|
||||
|
||||
/// 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/<timestamp>.png).
|
||||
file: Option<String>,
|
||||
},
|
||||
/// 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<String>,
|
||||
},
|
||||
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<u64>,
|
||||
scale: Option<f64>,
|
||||
hint_color: Option<u32>,
|
||||
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<Vec<u8>> {
|
||||
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<String>, 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::<Vec<_>>().join(" ");
|
||||
if one_line.len() <= 80 {
|
||||
one_line
|
||||
} else {
|
||||
format!("{}…", &one_line[..77])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,7 +340,7 @@ fn run_save(subject: Subject, file: Option<String>, 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<String>, 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();
|
||||
|
|
|
|||
297
src/notify.rs
297
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: Into<String>>(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<u8>) {
|
|||
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<u8>, 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<u8>, 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<Option<String>> {
|
||||
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<u32> {
|
||||
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<String>,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
fn read_message(sock: &mut UnixStream) -> Result<Message> {
|
||||
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<Message> {
|
|||
}
|
||||
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<String> {
|
||||
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<usize> {
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
32
src/ocr.rs
32
src/ocr.rs
|
|
@ -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<String> {
|
||||
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())
|
||||
}
|
||||
32
src/paths.rs
32
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<PathBuf> {
|
||||
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<Geometry> {
|
||||
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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HintBox>,
|
||||
hint_rgba: Option<u32>,
|
||||
outline: bool,
|
||||
) -> Result<Geometry> {
|
||||
pub fn select_area_boxes(boxes: Vec<HintBox>, hint_rgba: Option<u32>) -> Result<Geometry> {
|
||||
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<u32>, outline: bool) -> Result<Geometry> {
|
||||
select::select_region(vec![], false, hint_rgba, outline)
|
||||
pub fn select_free_region(hint_rgba: Option<u32>) -> Result<Geometry> {
|
||||
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<u32>,
|
||||
outline: bool,
|
||||
) -> Result<Geometry> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,6 @@ struct St {
|
|||
hover: Option<usize>,
|
||||
restrict: bool,
|
||||
c_hint: [u8; 4],
|
||||
outline: bool,
|
||||
|
||||
running: bool,
|
||||
result: Option<Geometry>,
|
||||
|
|
@ -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<HintBox>,
|
||||
restrict: bool,
|
||||
hint_rgba: Option<u32>,
|
||||
outline: bool,
|
||||
) -> Result<Geometry> {
|
||||
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<PendingOut> = out_globals
|
||||
.iter()
|
||||
.map(|&(gname, ver)| PendingOut {
|
||||
wl: globals
|
||||
.registry()
|
||||
.bind::<wl_output::WlOutput, _, _>(gname, ver.min(4), &qh, ()),
|
||||
wl: globals.registry().bind::<wl_output::WlOutput, _, _>(
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<Rgba<u8>, Vec<u8>> =
|
||||
ImageBuffer::from_raw(new_w, new_h, out_rgba).expect("shadow buffer construction");
|
||||
let buf: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::from_raw(new_w, new_h, out_rgba)
|
||||
.expect("shadow buffer construction");
|
||||
DynamicImage::ImageRgba8(buf)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HyprlandToplevelExportFrameV1, ()> 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 { .. } => {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue