From 76f324cb011db0c4c3dbdf0cccdce9503712fc43 Mon Sep 17 00:00:00 2001 From: "kidkool850@gmail.com" Date: Sat, 2 Nov 2024 01:57:55 -0500 Subject: [PATCH 01/16] start working on cli args --- src/args.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/context.rs | 14 +++++++------- src/main.rs | 11 ++++++----- 3 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..3cc2e85 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +use image::ImageFormat; + +use crate::context::SelectionMode; + +pub struct Region { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +fn parse_region(s: &str) -> Result { + let coords: Vec = s + .split(',') + .map(|s| s.parse().map_err(|_| "Invalid region format")) + .collect::, _>>()?; + + if coords.len() != 4 { + return Err("Region must be in format: x,y,width,height".into()); + } + + Ok(Region { + x: coords[0], + y: coords[1], + width: coords[2], + height: coords[3], + }) +} + +pub struct Args { + output_dir: PathBuf, + image_format: ImageFormat, + mode: SelectionMode, + monitor: Option, + region: Option<(u32, u32, u32, u32)>, + filename: Option, + clipboard: bool, + delay: u64, + monitor_list: bool, + config_path: Option, + optimize: bool, + scale: f32, + notify: bool, + +} \ No newline at end of file diff --git a/src/context.rs b/src/context.rs index f303756..ce0bb7b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -11,7 +11,7 @@ use winit::{ // use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; use cleave_graphics::prelude::*; -pub enum MoveMode { +pub enum SelectionMode { Move, // Move the selection InverseResize, // Make the selection smaller Resize, // Make the selection larger @@ -100,7 +100,7 @@ pub struct AppContext { last_frame: std::time::Instant, graphics: Graphics, bundle: GraphicsBundle, - mode: MoveMode, + mode: SelectionMode, } impl AppContext { @@ -218,7 +218,7 @@ impl AppContext { // window, graphics, mouse_position: DVec2::new(0.0, 0.0), - mode: MoveMode::Resize, + mode: SelectionMode::Resize, }) } @@ -233,17 +233,17 @@ impl AppContext { let selection = self.selection.selection.as_mut()?; match self.mode { - MoveMode::Move => { + SelectionMode::Move => { selection.start.x = (selection.start.x + dx).clamp(0.0, self.size.width as f32); selection.start.y = (selection.start.y + dy).clamp(0.0, self.size.height as f32); selection.end.x = (selection.end.x + dx).clamp(0.0, self.size.width as f32); selection.end.y = (selection.end.y + dy).clamp(0.0, self.size.height as f32); } - MoveMode::Resize => { + SelectionMode::Resize => { selection.end.x = (selection.end.x + dx).clamp(0.0, self.size.width as f32); selection.end.y = (selection.end.y + dy).clamp(0.0, self.size.height as f32); } - MoveMode::InverseResize => { + SelectionMode::InverseResize => { selection.start.x = (selection.start.x + dx).clamp(0.0, self.size.width as f32); selection.start.y = (selection.start.y + dy).clamp(0.0, self.size.height as f32); } @@ -315,7 +315,7 @@ impl AppContext { self.graphics.set_visible(false); } - pub fn set_mode(&mut self, mode: MoveMode) { + pub fn set_mode(&mut self, mode: SelectionMode) { self.mode = mode } diff --git a/src/main.rs b/src/main.rs index 04dca50..ef69326 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,8 @@ use winit::{ }; mod context; -use context::{AppContext, Direction, MoveMode}; +mod args; +use context::{AppContext, Direction, SelectionMode}; pub struct Drag { start: (f64, f64), @@ -125,16 +126,16 @@ impl ApplicationHandler for App { context.handle_move(Direction::Right); } (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { - context.set_mode(MoveMode::InverseResize); + context.set_mode(SelectionMode::InverseResize); } (ElementState::Released, Key::Named(NamedKey::Shift)) => { - context.set_mode(MoveMode::Resize); + context.set_mode(SelectionMode::Resize); } (ElementState::Pressed, Key::Named(NamedKey::Control)) => { - context.set_mode(MoveMode::Move); + context.set_mode(SelectionMode::Move); } (ElementState::Released, Key::Named(NamedKey::Control)) => { - context.set_mode(MoveMode::Resize); + context.set_mode(SelectionMode::Resize); } _ => {} }, From ec30cff50e02d319a784faf31fa479dfc55f9cf4 Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Sat, 2 Nov 2024 10:27:09 -0500 Subject: [PATCH 02/16] start filling out args info --- Cargo.lock | 174 +++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 2 + src/args.rs | 34 +++++++++- src/context.rs | 1 + 4 files changed, 195 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79f881e..ffec0be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -85,11 +91,60 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arbitrary" @@ -363,6 +418,46 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cleave" version = "0.1.0" @@ -370,6 +465,7 @@ dependencies = [ "anyhow", "arboard", "bytemuck", + "clap", "cleave-graphics", "glam", "image", @@ -416,6 +512,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -632,14 +734,15 @@ checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "exr" -version = "1.73.0" +version = "1.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" dependencies = [ "bit_field", + "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -661,7 +764,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "spin", ] [[package]] @@ -918,6 +1030,12 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -1134,6 +1252,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1619,7 +1746,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -2039,6 +2166,15 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -2060,11 +2196,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" -version = "2.0.86" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -2115,18 +2257,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", @@ -2249,6 +2391,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "v_frame" version = "0.3.8" diff --git a/Cargo.toml b/Cargo.toml index c4aa50b..7d32a0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "cleave" version = "0.1.0" edition = "2021" +authors = ["Exotik850"] [workspace] members = ["cleave-graphics"] @@ -18,6 +19,7 @@ pollster = { workspace = true } wgpu = { workspace = true } xcap = { workspace = true } cleave-graphics = { path = "cleave-graphics" } +clap = { version = "4.5.20", features = ["derive"] } [workspace.dependencies] diff --git a/src/args.rs b/src/args.rs index 3cc2e85..2e6229d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,6 +4,7 @@ use image::ImageFormat; use crate::context::SelectionMode; +#[derive(Debug, Copy, Clone)] pub struct Region { pub x: u32, pub y: u32, @@ -29,19 +30,46 @@ fn parse_region(s: &str) -> Result { }) } +fn parse_format(s: &str) -> Result { + match s { + "bmp" => Ok(ImageFormat::Bmp), + "gif" => Ok(ImageFormat::Gif), + "ico" => Ok(ImageFormat::Ico), + "jpeg" => Ok(ImageFormat::Jpeg), + "png" => Ok(ImageFormat::Png), + "tiff" => Ok(ImageFormat::Tiff), + "webp" => Ok(ImageFormat::WebP), + _ => Err("Invalid image format".into()), + } +} + +#[derive(clap::Parser, Debug)] +#[command(version, about, author, long_about=None)] pub struct Args { + #[arg(short, long, default_value = "screenshot.png")] output_dir: PathBuf, + #[arg(value_parser=parse_format)] image_format: ImageFormat, + #[arg(short, long, default_value = "move")] mode: SelectionMode, - monitor: Option, - region: Option<(u32, u32, u32, u32)>, + #[arg(long)] + monitor: Option, // If not provided, the primary monitor is used + #[arg(long, value_parser=parse_region)] + region: Option, + #[arg(long, short='f')] filename: Option, + #[arg(long, short='b')] clipboard: bool, + #[arg(long, short='d')] delay: u64, + #[arg(long, short='l')] monitor_list: bool, + #[arg(long, short='c')] config_path: Option, + #[arg(long, short='p')] optimize: bool, + #[arg(long, short='s')] scale: f32, + #[arg(long, short='n')] notify: bool, - } \ No newline at end of file diff --git a/src/context.rs b/src/context.rs index ce0bb7b..f2092fd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -11,6 +11,7 @@ use winit::{ // use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; use cleave_graphics::prelude::*; +#[derive(Debug, Clone, Copy, clap::ValueEnum)] pub enum SelectionMode { Move, // Move the selection InverseResize, // Make the selection smaller From 1079fb60517db57ca3ad7b33c1d8404535112e80 Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Sat, 2 Nov 2024 10:27:17 -0500 Subject: [PATCH 03/16] refactor: Move application logic to app module and simplify main function --- src/app.rs | 114 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 154 +--------------------------------------------------- 2 files changed, 117 insertions(+), 151 deletions(-) create mode 100644 src/app.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..d1a3dd8 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,114 @@ +use crate::args::Args; +use clap::Parser; +use winit::{ + application::ApplicationHandler, + event::{ElementState, KeyEvent, MouseButton, WindowEvent}, + keyboard::{Key, NamedKey}, +}; + +use crate::context::{AppContext, Direction, SelectionMode}; + + +pub struct App { + args: Args, + context: Option, +} + +impl App { + pub fn new() -> Self { + let args = Args::parse(); + App { + args, + context: None, + } + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + + if self.context.is_some() { + return; + } + + let context = AppContext::new(event_loop).expect("Could not start context"); + self.context = Some(context); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + let Some(context) = &mut self.context else { + return; + }; + if id != context.window_id() { + return; + } + + match event { + WindowEvent::RedrawRequested => { + context.draw(); + } + WindowEvent::CursorMoved { position, .. } => { + context.update_mouse_position(position.x, position.y); + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state, + logical_key: key, + .. + }, + .. + } => match (state, key) { + (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { + event_loop.exit(); + context.destroy(); + } + (ElementState::Pressed, Key::Named(NamedKey::Space)) => { + context.hide_window(); + context.save_selection_to_clipboard(); + event_loop.exit(); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { + context.handle_move(Direction::Down); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowUp)) => { + context.handle_move(Direction::Up); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowLeft)) => { + context.handle_move(Direction::Left); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowRight)) => { + context.handle_move(Direction::Right); + } + (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { + context.set_mode(SelectionMode::InverseResize); + } + (ElementState::Released, Key::Named(NamedKey::Shift)) => { + context.set_mode(SelectionMode::Resize); + } + (ElementState::Pressed, Key::Named(NamedKey::Control)) => { + context.set_mode(SelectionMode::Move); + } + (ElementState::Released, Key::Named(NamedKey::Control)) => { + context.set_mode(SelectionMode::Resize); + } + _ => {} + }, + WindowEvent::MouseInput { state, button, .. } => match (state, button) { + (ElementState::Pressed, MouseButton::Left) => context.start_drag(), + (ElementState::Released, MouseButton::Left) => context.end_drag(), + (_, MouseButton::Right) => context.cancel_drag(), + _ => {} + }, + WindowEvent::CloseRequested => { + event_loop.exit(); + } + _ => {} + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ef69326..51dccd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,160 +1,12 @@ #![windows_subsystem = "windows"] -use winit::{ - application::ApplicationHandler, - event::{ElementState, KeyEvent, MouseButton, WindowEvent}, - keyboard::{Key, NamedKey}, -}; - -mod context; +mod app; mod args; -use context::{AppContext, Direction, SelectionMode}; - -pub struct Drag { - start: (f64, f64), - end: Option<(f64, f64)>, -} - -impl Drag { - fn coords(&self) -> Option<((u32, u32), (u32, u32))> { - let end = self.end?; - let (start_x, start_y) = (self.start.0 as u32, self.start.1 as u32); - let (end_x, end_y) = (end.0 as u32, end.1 as u32); - - let (min_x, max_x) = (start_x.min(end_x), start_x.max(end_x)); - let (min_y, max_y) = (start_y.min(end_y), start_y.max(end_y)); - Some(((min_x, min_y), (max_x, max_y))) - } -} - -pub struct Selection { - start: (f64, f64), - end: (f64, f64), -} - -impl Selection { - fn dimensions(&self) -> (f64, f64) { - let width = (self.end.0 - self.start.0).abs(); - let height = (self.end.1 - self.start.1).abs(); - (width, height) - } - - fn area(&self) -> f64 { - let (width, height) = self.dimensions(); - width * height - } - - // fn aspect_ratio(&self) -> f64 { - // let (width, height) = self.dimensions(); - // width / height - // } - - // fn center(&self) -> (f64, f64) { - // let x = (self.start.0 + self.end.0) / 2.0; - // let y = (self.start.1 + self.end.1) / 2.0; - // (x, y) - // } - - fn coords(&self) -> ((u32, u32), (u32, u32)) { - let (start_x, start_y) = (self.start.0, self.start.1); - let (end_x, end_y) = (self.end.0, self.end.1); - - let (min_x, max_x) = (start_x.min(end_x).ceil(), start_x.max(end_x).floor()); - let (min_y, max_y) = (start_y.min(end_y).ceil(), start_y.max(end_y).floor()); - ((min_x as u32, min_y as u32), (max_x as u32, max_y as u32)) - } -} - -struct App { - context: Option, -} - -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let context = AppContext::new(event_loop).expect("Could not start context"); - self.context = Some(context); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - let Some(context) = &mut self.context else { - return; - }; - if id != context.window_id() { - return; - } +mod context; - match event { - WindowEvent::RedrawRequested => { - context.draw(); - } - WindowEvent::CursorMoved { position, .. } => { - context.update_mouse_position(position.x, position.y); - } - WindowEvent::KeyboardInput { - event: - KeyEvent { - state, - logical_key: key, - .. - }, - .. - } => match (state, key) { - (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { - event_loop.exit(); - context.destroy(); - } - (ElementState::Pressed, Key::Named(NamedKey::Space)) => { - context.hide_window(); - context.save_selection_to_clipboard(); - event_loop.exit(); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { - context.handle_move(Direction::Down); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowUp)) => { - context.handle_move(Direction::Up); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowLeft)) => { - context.handle_move(Direction::Left); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowRight)) => { - context.handle_move(Direction::Right); - } - (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { - context.set_mode(SelectionMode::InverseResize); - } - (ElementState::Released, Key::Named(NamedKey::Shift)) => { - context.set_mode(SelectionMode::Resize); - } - (ElementState::Pressed, Key::Named(NamedKey::Control)) => { - context.set_mode(SelectionMode::Move); - } - (ElementState::Released, Key::Named(NamedKey::Control)) => { - context.set_mode(SelectionMode::Resize); - } - _ => {} - }, - WindowEvent::MouseInput { state, button, .. } => match (state, button) { - (ElementState::Pressed, MouseButton::Left) => context.start_drag(), - (ElementState::Released, MouseButton::Left) => context.end_drag(), - (_, MouseButton::Right) => context.cancel_drag(), - _ => {} - }, - WindowEvent::CloseRequested => { - event_loop.exit(); - } - _ => {} - } - } -} fn main() -> anyhow::Result<()> { - let mut app = App { context: None }; + let mut app = app::App::new(); let event_loop = winit::event_loop::EventLoop::new()?; event_loop.run_app(&mut app)?; Ok(()) From 6017812a4c768a905afec148fe852728399b401f Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Sat, 2 Nov 2024 16:49:50 -0500 Subject: [PATCH 04/16] fmt and checking if inside terminal --- src/app.rs | 195 +++++++++++++++++++++++++++------------------------- src/args.rs | 151 +++++++++++++++++++++++++--------------- src/main.rs | 17 +++-- 3 files changed, 210 insertions(+), 153 deletions(-) diff --git a/src/app.rs b/src/app.rs index d1a3dd8..7981629 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,107 +8,116 @@ use winit::{ use crate::context::{AppContext, Direction, SelectionMode}; - pub struct App { - args: Args, - context: Option, + args: Option, + context: Option, } impl App { - pub fn new() -> Self { - let args = Args::parse(); - App { - args, - context: None, + pub fn new(args: Option) -> Self { + App { + args, + context: None, + } + } + + pub fn run(&mut self) -> anyhow::Result<()> { + if let Some(args) = &self.args { + if let Some(output_dir) = &args.output_dir { + std::fs::create_dir_all(output_dir)?; + } + } + + let event_loop = winit::event_loop::EventLoop::new()?; + event_loop.run_app(self)?; + Ok(()) } - } } impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.context.is_some() { + return; + } - if self.context.is_some() { - return; - } - - let context = AppContext::new(event_loop).expect("Could not start context"); - self.context = Some(context); - } + let context = AppContext::new(event_loop).expect("Could not start context"); + self.context = Some(context); + } - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - let Some(context) = &mut self.context else { - return; - }; - if id != context.window_id() { - return; - } + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + let Some(context) = &mut self.context else { + return; + }; + if id != context.window_id() { + return; + } - match event { - WindowEvent::RedrawRequested => { - context.draw(); - } - WindowEvent::CursorMoved { position, .. } => { - context.update_mouse_position(position.x, position.y); - } - WindowEvent::KeyboardInput { - event: - KeyEvent { - state, - logical_key: key, - .. - }, - .. - } => match (state, key) { - (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { - event_loop.exit(); - context.destroy(); - } - (ElementState::Pressed, Key::Named(NamedKey::Space)) => { - context.hide_window(); - context.save_selection_to_clipboard(); - event_loop.exit(); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { - context.handle_move(Direction::Down); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowUp)) => { - context.handle_move(Direction::Up); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowLeft)) => { - context.handle_move(Direction::Left); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowRight)) => { - context.handle_move(Direction::Right); - } - (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { - context.set_mode(SelectionMode::InverseResize); - } - (ElementState::Released, Key::Named(NamedKey::Shift)) => { - context.set_mode(SelectionMode::Resize); - } - (ElementState::Pressed, Key::Named(NamedKey::Control)) => { - context.set_mode(SelectionMode::Move); - } - (ElementState::Released, Key::Named(NamedKey::Control)) => { - context.set_mode(SelectionMode::Resize); - } - _ => {} - }, - WindowEvent::MouseInput { state, button, .. } => match (state, button) { - (ElementState::Pressed, MouseButton::Left) => context.start_drag(), - (ElementState::Released, MouseButton::Left) => context.end_drag(), - (_, MouseButton::Right) => context.cancel_drag(), - _ => {} - }, - WindowEvent::CloseRequested => { - event_loop.exit(); - } - _ => {} - } - } -} \ No newline at end of file + match event { + WindowEvent::RedrawRequested => { + context.draw(); + } + WindowEvent::CursorMoved { position, .. } => { + context.update_mouse_position(position.x, position.y); + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state, + logical_key: key, + .. + }, + .. + } => match (state, key) { + (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { + event_loop.exit(); + context.destroy(); + } + (ElementState::Pressed, Key::Named(NamedKey::Space)) => { + context.hide_window(); + context.save_selection_to_clipboard(); + event_loop.exit(); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { + context.handle_move(Direction::Down); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowUp)) => { + context.handle_move(Direction::Up); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowLeft)) => { + context.handle_move(Direction::Left); + } + (ElementState::Pressed, Key::Named(NamedKey::ArrowRight)) => { + context.handle_move(Direction::Right); + } + (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { + context.set_mode(SelectionMode::InverseResize); + } + (ElementState::Released, Key::Named(NamedKey::Shift)) => { + context.set_mode(SelectionMode::Resize); + } + (ElementState::Pressed, Key::Named(NamedKey::Control)) => { + context.set_mode(SelectionMode::Move); + } + (ElementState::Released, Key::Named(NamedKey::Control)) => { + context.set_mode(SelectionMode::Resize); + } + _ => {} + }, + WindowEvent::MouseInput { state, button, .. } => match (state, button) { + (ElementState::Pressed, MouseButton::Left) => context.start_drag(), + (ElementState::Released, MouseButton::Left) => context.end_drag(), + (_, MouseButton::Right) => context.cancel_drag(), + _ => {} + }, + WindowEvent::CloseRequested => { + event_loop.exit(); + } + _ => {} + } + } +} diff --git a/src/args.rs b/src/args.rs index 2e6229d..ceb29b1 100644 --- a/src/args.rs +++ b/src/args.rs @@ -6,70 +6,111 @@ use crate::context::SelectionMode; #[derive(Debug, Copy, Clone)] pub struct Region { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, } fn parse_region(s: &str) -> Result { - let coords: Vec = s - .split(',') - .map(|s| s.parse().map_err(|_| "Invalid region format")) - .collect::, _>>()?; + let coords: Vec = s + .split(',') + .map(|s| s.parse().map_err(|_| "Invalid region format")) + .collect::, _>>()?; - if coords.len() != 4 { - return Err("Region must be in format: x,y,width,height".into()); - } + if coords.len() != 4 { + return Err("Region must be in format: x,y,width,height".into()); + } - Ok(Region { - x: coords[0], - y: coords[1], - width: coords[2], - height: coords[3], - }) + Ok(Region { + x: coords[0], + y: coords[1], + width: coords[2], + height: coords[3], + }) } fn parse_format(s: &str) -> Result { - match s { - "bmp" => Ok(ImageFormat::Bmp), - "gif" => Ok(ImageFormat::Gif), - "ico" => Ok(ImageFormat::Ico), - "jpeg" => Ok(ImageFormat::Jpeg), - "png" => Ok(ImageFormat::Png), - "tiff" => Ok(ImageFormat::Tiff), - "webp" => Ok(ImageFormat::WebP), - _ => Err("Invalid image format".into()), - } + match s { + "bmp" => Ok(ImageFormat::Bmp), + "gif" => Ok(ImageFormat::Gif), + "ico" => Ok(ImageFormat::Ico), + "jpeg" => Ok(ImageFormat::Jpeg), + "png" => Ok(ImageFormat::Png), + "tiff" => Ok(ImageFormat::Tiff), + "webp" => Ok(ImageFormat::WebP), + _ => Err("Invalid image format".into()), + } } +/// Cleave - A GPU-accelerated screen capture tool #[derive(clap::Parser, Debug)] -#[command(version, about, author, long_about=None)] +#[command( + name = "cleave", + author, + version, + about, + long_about = "A lightweight, GPU-accelerated screen capture tool that allows users to quickly select and copy portions of their screen" +)] pub struct Args { - #[arg(short, long, default_value = "screenshot.png")] - output_dir: PathBuf, - #[arg(value_parser=parse_format)] - image_format: ImageFormat, - #[arg(short, long, default_value = "move")] - mode: SelectionMode, - #[arg(long)] - monitor: Option, // If not provided, the primary monitor is used - #[arg(long, value_parser=parse_region)] - region: Option, - #[arg(long, short='f')] - filename: Option, - #[arg(long, short='b')] - clipboard: bool, - #[arg(long, short='d')] - delay: u64, - #[arg(long, short='l')] - monitor_list: bool, - #[arg(long, short='c')] - config_path: Option, - #[arg(long, short='p')] - optimize: bool, - #[arg(long, short='s')] - scale: f32, - #[arg(long, short='n')] - notify: bool, -} \ No newline at end of file + /// Output directory for the captured image + /// + /// If not provided, the capture is copied to the clipboard + #[arg(short, long)] + pub output_dir: Option, + /// Output format for the captured image + /// + /// Supported formats: bmp, gif, ico, jpeg, png, tiff, webp + /// + /// Only used when output_dir is provided + #[arg(long="format", value_parser=parse_format)] + pub image_format: Option, + /// Selection mode for the capture tool + #[arg(short, long, default_value = "move")] + pub mode: SelectionMode, + /// Monitor index to capture + /// + /// If not provided, the primary monitor is used + #[arg(long)] + pub monitor: Option, // If not provided, the primary monitor is used + /// Region to capture in the format: x,y,width,height + /// + /// If not provided, the entire screen is captured and the user is prompted to select a region + /// If provided, the user is not prompted and the region is captured immediately + #[arg(long, short='r', value_parser=parse_region)] + pub region: Option, + /// Filename for the captured image + /// + /// If not provided, the image is saved with a timestamp: 'cleave-YYYY-MM-DD-HH-MM-SS' + /// Only used when output_dir is provided + #[arg(long, short = 'f')] + pub filename: Option, + /// Delay in milliseconds before capturing the screen + /// + /// If not provided, the screen is captured immediately + #[arg(long, short = 'd', default_value = "0")] + pub delay: u64, + /// List available monitors and exit + #[arg(long, short = 'l')] + pub monitor_list: bool, + /// Path to the configuration file + /// + /// If not provided, the default configuration is used + #[arg(long, short = 'c')] + pub config_path: Option, + // TODO: Implement these features + // /// Optimize the captured image when applicable + // #[arg(long, short='p')] + // optimize: bool, + /// Scale the captured image by a factor + #[arg(long, short = 's')] + pub scale: Option, + /// Notify the user when the capture is complete + #[arg(long, short = 'n')] + pub notify: bool, + /// Daemon Mode Hotkey + /// + /// If provided, app runs in the background and captures the screen whenever the user presses a hotkey + #[arg(long)] + pub daemon_hotkey: Option, +} diff --git a/src/main.rs b/src/main.rs index 51dccd9..245271f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,20 @@ -#![windows_subsystem = "windows"] +// TODO When opened outside of a terminal, the program should not open the terminal. +// However, when opened inside a terminal, the program should be able to output to the terminal. +// #![windows_subsystem = "windows"] + +use std::io::IsTerminal; + +use args::Args; +use clap::Parser; mod app; mod args; mod context; - fn main() -> anyhow::Result<()> { - let mut app = app::App::new(); - let event_loop = winit::event_loop::EventLoop::new()?; - event_loop.run_app(&mut app)?; + let stdout = std::io::stdout(); + let args = stdout.is_terminal().then(Args::parse); + let mut app = app::App::new(args); + app.run()?; Ok(()) } From 86ba848e9d782ddbc80073f251f42ca3226478e0 Mon Sep 17 00:00:00 2001 From: "kidkool850@gmail.com" Date: Sat, 2 Nov 2024 17:54:05 -0500 Subject: [PATCH 05/16] feat: Add argument verification and monitor listing functionality --- Cargo.lock | 117 ++++++++++++++++++++--------------- Cargo.toml | 3 + src/app.rs | 32 +++++++++- src/args.rs | 62 +++++++++++++++++-- src/context.rs | 161 +++++++++++++++++++++++++++++++++++-------------- 5 files changed, 272 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffec0be..4e0d558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,12 +18,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -82,6 +76,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.91" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" [[package]] name = "arbitrary" @@ -375,9 +375,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.31" +version = "1.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" dependencies = [ "jobserver", "libc", @@ -418,6 +418,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "clap" version = "4.5.20" @@ -465,6 +479,7 @@ dependencies = [ "anyhow", "arboard", "bytemuck", + "chrono", "clap", "cleave-graphics", "glam", @@ -734,15 +749,14 @@ checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "exr" -version = "1.72.0" +version = "1.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" dependencies = [ "bit_field", - "flume", "half", "lebe", - "miniz_oxide 0.7.4", + "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", @@ -764,16 +778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", -] - -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "spin", + "miniz_oxide", ] [[package]] @@ -970,6 +975,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "image" version = "0.25.4" @@ -1252,15 +1280,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1746,7 +1765,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -2166,15 +2185,6 @@ dependencies = [ "serde", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -2204,9 +2214,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.85" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -2257,18 +2267,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" dependencies = [ "proc-macro2", "quote", @@ -2795,6 +2805,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" diff --git a/Cargo.toml b/Cargo.toml index 7d32a0b..8ab9439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ wgpu = { workspace = true } xcap = { workspace = true } cleave-graphics = { path = "cleave-graphics" } clap = { version = "4.5.20", features = ["derive"] } +chrono = "0.4.38" [workspace.dependencies] @@ -39,3 +40,5 @@ lto = "thin" [build] rustflags = ["-C", "target-cpu=native"] + +[features] diff --git a/src/app.rs b/src/app.rs index 7981629..a5d9e65 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,4 @@ use crate::args::Args; -use clap::Parser; use winit::{ application::ApplicationHandler, event::{ElementState, KeyEvent, MouseButton, WindowEvent}, @@ -23,6 +22,23 @@ impl App { pub fn run(&mut self) -> anyhow::Result<()> { if let Some(args) = &self.args { + if let Err(e) = args.verify() { + eprintln!("{}", e); + std::process::exit(1); + } + + if args.monitor_list { + println!("Available monitors:"); + for monitor in xcap::Monitor::all().into_iter().flatten() { + println!("ID: {}", monitor.id()); + } + std::process::exit(0); + } + + if args.delay > 0 { + std::thread::sleep(std::time::Duration::from_millis(args.delay)); + } + if let Some(output_dir) = &args.output_dir { std::fs::create_dir_all(output_dir)?; } @@ -39,8 +55,16 @@ impl ApplicationHandler for App { if self.context.is_some() { return; } + let mut context = AppContext::new(event_loop).expect("Could not start context"); + + if let Some(args) = &self.args { + let Some(arg_context) = context.set_args(args) else { + event_loop.exit(); + return; + }; + context = arg_context; + } - let context = AppContext::new(event_loop).expect("Could not start context"); self.context = Some(context); } @@ -79,7 +103,9 @@ impl ApplicationHandler for App { } (ElementState::Pressed, Key::Named(NamedKey::Space)) => { context.hide_window(); - context.save_selection_to_clipboard(); + if let Err(e) = context.save_selection(self.args.as_ref()) { + eprintln!("{}", e); + }; event_loop.exit(); } (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { diff --git a/src/args.rs b/src/args.rs index ceb29b1..d34af1e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -43,6 +43,17 @@ fn parse_format(s: &str) -> Result { } } +fn parse_filter(s: &str) -> Result { + match s { + "Nearest" => Ok(image::imageops::FilterType::Nearest), + "Triangle" => Ok(image::imageops::FilterType::Triangle), + "CatmullRom" => Ok(image::imageops::FilterType::CatmullRom), + "Gaussian" => Ok(image::imageops::FilterType::Gaussian), + "Lanczos3" => Ok(image::imageops::FilterType::Lanczos3), + _ => Err("Invalid filter type".into()), + } +} + /// Cleave - A GPU-accelerated screen capture tool #[derive(clap::Parser, Debug)] #[command( @@ -93,11 +104,11 @@ pub struct Args { /// List available monitors and exit #[arg(long, short = 'l')] pub monitor_list: bool, - /// Path to the configuration file - /// - /// If not provided, the default configuration is used - #[arg(long, short = 'c')] - pub config_path: Option, + // /// Path to the configuration file + // /// + // /// If not provided, the default configuration is used + // #[arg(long, short = 'c')] + // pub config_path: Option, // TODO: Implement these features // /// Optimize the captured image when applicable // #[arg(long, short='p')] @@ -105,6 +116,14 @@ pub struct Args { /// Scale the captured image by a factor #[arg(long, short = 's')] pub scale: Option, + /// Filter to use when scaling the image + /// + /// Supported filters: Nearest, Triangle, CatmullRom, Gaussian, Lanczos3 + /// + /// Only used when scale is provided + #[arg(long, short = 'q', value_parser=parse_filter)] + pub filter: Option, + /// Notify the user when the capture is complete #[arg(long, short = 'n')] pub notify: bool, @@ -114,3 +133,36 @@ pub struct Args { #[arg(long)] pub daemon_hotkey: Option, } + +impl Args { + pub fn verify(&self) -> Result<(), String> { + if self.monitor_list + && (self.output_dir.is_some() + || self.image_format.is_some() + || self.filename.is_some() + || self.region.is_some() + || self.scale.is_some() + || self.notify + || self.daemon_hotkey.is_some()) + { + return Err("Monitor list option cannot be used with other options".into()); + } + if let Some(scale) = self.scale { + if scale <= 0.0 { + return Err("Scale factor must be greater than 0".into()); + } + } + if let Some(region) = self.region { + if region.width == 0 || region.height == 0 { + return Err("Region width and height must be greater than 0".into()); + } + } + if (self.image_format.is_some() || self.filename.is_some()) && self.output_dir.is_none() { + return Err( + "Output format and filename is only used when output directory is provided".into(), + ); + } + + Ok(()) + } +} diff --git a/src/context.rs b/src/context.rs index f2092fd..fc01bc2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,7 +1,11 @@ +use std::path::{Path, PathBuf}; + use anyhow::Context; use arboard::ImageData; use glam::{DVec2, Vec2}; -use image::{GenericImageView, ImageBuffer, Rgba}; +use image::{ + imageops::FilterType, GenericImageView, ImageBuffer, ImageError, ImageFormat, Rgba, RgbaImage, +}; // use pixels::{Pixels, SurfaceTexture}; use winit::{ dpi::PhysicalSize, @@ -11,6 +15,8 @@ use winit::{ // use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; use cleave_graphics::prelude::*; +use crate::args::Args; + #[derive(Debug, Clone, Copy, clap::ValueEnum)] pub enum SelectionMode { Move, // Move the selection @@ -79,24 +85,41 @@ impl UserSelection { Some(((min_x as u32, min_y as u32), (max_x as u32, max_y as u32))) } - fn sel_dimensions(&self) -> Option<(f32, f32)> { - let selection = self.selection.as_ref()?; - let width = (selection.end.x - selection.start.x).abs(); - let height = (selection.end.y - selection.start.y).abs(); - Some((width, height)) - } + // fn sel_dimensions(&self) -> Option<(f32, f32)> { + // let selection = self.selection.as_ref()?; + // let width = (selection.end.x - selection.start.x).abs(); + // let height = (selection.end.y - selection.start.y).abs(); + // Some((width, height)) + // } // fn get_ } +fn resize_image( + image: &RgbaImage, + scale: f32, + filter: FilterType, +) -> Result { + let new_width = (image.width() as f32 * scale).round() as u32; + let new_height = (image.height() as f32 * scale).round() as u32; + Ok(image::imageops::resize( + image, new_width, new_height, filter, + )) +} + +fn generate_output_path(dir: &Path, filename: &str, format: ImageFormat) -> PathBuf { + let ext = format.extensions_str().first().copied().unwrap_or("png"); + + let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); + let filename = format!("{filename}-{}.{ext}", timestamp); + + dir.join(filename) +} + pub struct AppContext { - size: PhysicalSize, mouse_position: DVec2, selection: UserSelection, - // current_drag: Option, - // selection: Option, image: ImageBuffer, Vec>, - // pixels: Pixels<'static>, total_time: f32, last_frame: std::time::Instant, graphics: Graphics, @@ -133,37 +156,64 @@ impl AppContext { self.selection.selection = None; } - fn get_selection_data(&self) -> Option> { - let ((min_x, min_y), (max_x, max_y)) = self.selection.sel_coords()?; - let img = self - .image - .view(min_x, min_y, max_x.abs_diff(min_x), max_y.abs_diff(min_y)); - let image_data = img.to_image().to_vec(); - Some(image_data) + fn get_selection_data(&self, ax: u32, ay: u32, bx: u32, by: u32) -> RgbaImage { + let img = self.image.view(ax, ay, bx.abs_diff(ax), by.abs_diff(ay)); + img.to_image() } - pub fn save_selection_to_clipboard(&self) { - let (width, height) = self.selection.sel_dimensions().unwrap(); + fn save_to_clipboard(&self, image_data: RgbaImage) { + let mut clipboard = arboard::Clipboard::new().unwrap(); + let image_data = ImageData { + width: image_data.width() as usize, + height: image_data.height() as usize, + bytes: std::borrow::Cow::Owned(image_data.to_vec()), + }; + let _ = clipboard.set_image(image_data); + } - let width = width.floor() as usize; - let height = height.floor() as usize; + pub fn save_selection(&self, args: Option<&Args>) -> anyhow::Result<()> { + // Get dimensions and validate image data + let ((ax, ay), (bx, by)) = if let Some(region) = args.and_then(|a| a.region) { + ( + (region.x, region.y), + ((region.x + region.width), (region.y + region.height)), + ) + } else { + self.selection + .sel_coords() + .ok_or_else(|| anyhow::anyhow!("No selection made"))? + }; - let image_data = self.get_selection_data().unwrap(); + let mut image_data = self.get_selection_data(ax, ay, bx, by); - let mut clipboard = arboard::Clipboard::new().unwrap(); - if width * height != image_data.len() / 4 { - eprintln!( - "Invalid selection size {:?} (w h p)", - (width, height, image_data.len() / 4) - ); - return; + // Handle scaling if requested + if let Some(scale) = args.and_then(|a| a.scale) { + image_data = resize_image( + &image_data, + scale, + args.and_then(|a| a.filter).unwrap_or(FilterType::Nearest), + )?; } - let image_data = ImageData { - width, - height, - bytes: std::borrow::Cow::Owned(image_data), + + // Save to clipboard if no output directory specified + let Some(output_dir) = args.and_then(|f| f.output_dir.as_deref()) else { + return { + self.save_to_clipboard(image_data); + Ok(()) + }; }; - let _ = clipboard.set_image(image_data); + + // Generate filename and save + let format = args + .and_then(|f| f.image_format) + .unwrap_or(ImageFormat::Png); + let path = generate_output_path( + output_dir, + args.and_then(|f| f.filename.as_deref()).unwrap_or("cleave"), + format, + ); + + Ok(image_data.save_with_format(path, format)?) } pub fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> anyhow::Result { @@ -210,7 +260,6 @@ impl AppContext { // let pixels = Pixels::new(size.width, size.height, surface_texture)?; Ok(Self { - size, image: img, bundle, total_time: 0.0, @@ -235,18 +284,18 @@ impl AppContext { match self.mode { SelectionMode::Move => { - selection.start.x = (selection.start.x + dx).clamp(0.0, self.size.width as f32); - selection.start.y = (selection.start.y + dy).clamp(0.0, self.size.height as f32); - selection.end.x = (selection.end.x + dx).clamp(0.0, self.size.width as f32); - selection.end.y = (selection.end.y + dy).clamp(0.0, self.size.height as f32); + selection.start.x = (selection.start.x + dx).clamp(0.0, self.image.width() as f32); + selection.start.y = (selection.start.y + dy).clamp(0.0, self.image.height() as f32); + selection.end.x = (selection.end.x + dx).clamp(0.0, self.image.width() as f32); + selection.end.y = (selection.end.y + dy).clamp(0.0, self.image.height() as f32); } SelectionMode::Resize => { - selection.end.x = (selection.end.x + dx).clamp(0.0, self.size.width as f32); - selection.end.y = (selection.end.y + dy).clamp(0.0, self.size.height as f32); + selection.end.x = (selection.end.x + dx).clamp(0.0, self.image.width() as f32); + selection.end.y = (selection.end.y + dy).clamp(0.0, self.image.height() as f32); } SelectionMode::InverseResize => { - selection.start.x = (selection.start.x + dx).clamp(0.0, self.size.width as f32); - selection.start.y = (selection.start.y + dy).clamp(0.0, self.size.height as f32); + selection.start.x = (selection.start.x + dx).clamp(0.0, self.image.width() as f32); + selection.start.y = (selection.start.y + dy).clamp(0.0, self.image.height() as f32); } } @@ -275,8 +324,8 @@ impl AppContext { fn update_uniforms(&mut self) { self.bundle.uniforms.time = self.total_time; - self.bundle.uniforms.screen_size.x = self.size.width as f32; - self.bundle.uniforms.screen_size.y = self.size.height as f32; + self.bundle.uniforms.screen_size.x = self.image.width() as f32; + self.bundle.uniforms.screen_size.y = self.image.height() as f32; let drag = self.selection.drag; let selection = self.selection.selection; @@ -326,4 +375,24 @@ impl AppContext { drag.end = Some(self.mouse_position.as_vec2()); } } + + pub fn set_args(mut self, args: &crate::args::Args) -> Option { + // self.bundle.uniforms.screen_size.x = args.width as f32; + // self.bundle.uniforms.screen_size.y = args.height as f32; + if let Some(region) = args.region { + self.selection.selection = Some(Selection { + start: Vec2::new(region.x as f32, region.y as f32), + end: Vec2::new( + (region.x + region.width) as f32, + (region.y + region.height) as f32, + ), + }); + if let Err(e) = self.save_selection(Some(args)) { + eprintln!("Error saving selection: {:?}", e); + }; + return None; + } + + Some(self) + } } From 197e918735595500d592e22c648be320c189489a Mon Sep 17 00:00:00 2001 From: "kidkool850@gmail.com" Date: Sun, 3 Nov 2024 19:51:40 -0600 Subject: [PATCH 06/16] feat: Update dependencies and implement hotkey functionality for daemon mode --- Cargo.lock | 79 +++++++++ Cargo.toml | 4 + cleave-graphics/Cargo.toml | 2 +- src/app.rs | 7 + src/args.rs | 4 - src/hotkey.rs | 344 +++++++++++++++++++++++++++++++++++++ src/main.rs | 119 +++++++++++++ 7 files changed, 554 insertions(+), 5 deletions(-) create mode 100644 src/hotkey.rs diff --git a/Cargo.lock b/Cargo.lock index 4e0d558..2598d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitstream-io" @@ -482,9 +485,12 @@ dependencies = [ "chrono", "clap", "cleave-graphics", + "device_query", "glam", "image", + "keyboard-types", "pollster", + "thiserror", "wgpu", "winit", "xcap", @@ -683,6 +689,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "device_query" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafa241a89a5edccff5057d0b85fbc083a781bd03d766c11a688331604980985" +dependencies = [ + "lazy_static", + "macos-accessibility-client", + "pkg-config", + "readkey", + "readmouse", + "windows 0.48.0", + "x11", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1119,6 +1140,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.6.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -1136,6 +1168,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 = "lebe" version = "0.5.2" @@ -1226,6 +1264,16 @@ dependencies = [ "imgref", ] +[[package]] +name = "macos-accessibility-client" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1994,6 +2042,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "readkey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7677f98ca49bc9bb26e04c8abf80ba579e2cb98e8a384a0ff8128ad70670d249" + +[[package]] +name = "readmouse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be105c72a1e6a5a1198acee3d5b506a15676b74a02ecd78060042a447f408d94" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2785,6 +2845,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.57.0" @@ -3186,6 +3255,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index 8ab9439..a35f112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,18 @@ image = { workspace = true } pollster = { workspace = true } wgpu = { workspace = true } xcap = { workspace = true } +thiserror = { workspace = true } cleave-graphics = { path = "cleave-graphics" } clap = { version = "4.5.20", features = ["derive"] } chrono = "0.4.38" +keyboard-types = "0.7.0" +device_query = "2.1.0" [workspace.dependencies] anyhow = "1" arboard = "3.4.1" +thiserror = "1" bytemuck = { version = "1.19.0", features = ["derive"] } glam = { version = "0.29.1", features = ["bytemuck"] } image = "0.25.4" diff --git a/cleave-graphics/Cargo.toml b/cleave-graphics/Cargo.toml index 455f466..e3c4d14 100644 --- a/cleave-graphics/Cargo.toml +++ b/cleave-graphics/Cargo.toml @@ -9,4 +9,4 @@ image = { workspace = true } wgpu = { workspace = true } bytemuck = { workspace = true } winit = { workspace = true } -thiserror = "1" \ No newline at end of file +thiserror = { workspace = true } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index a5d9e65..a614d39 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ + use crate::args::Args; use winit::{ application::ApplicationHandler, @@ -35,6 +36,12 @@ impl App { std::process::exit(0); } + if let Some(hotkey) = &args.daemon_hotkey { + // Wait until the hotkey is pressed + let hotkey: crate::hotkey::HotKey = hotkey.parse()?; + crate::hotkey::wait_until_pressed(hotkey); + } + if args.delay > 0 { std::thread::sleep(std::time::Duration::from_millis(args.delay)); } diff --git a/src/args.rs b/src/args.rs index d34af1e..ac979bb 100644 --- a/src/args.rs +++ b/src/args.rs @@ -124,9 +124,6 @@ pub struct Args { #[arg(long, short = 'q', value_parser=parse_filter)] pub filter: Option, - /// Notify the user when the capture is complete - #[arg(long, short = 'n')] - pub notify: bool, /// Daemon Mode Hotkey /// /// If provided, app runs in the background and captures the screen whenever the user presses a hotkey @@ -142,7 +139,6 @@ impl Args { || self.filename.is_some() || self.region.is_some() || self.scale.is_some() - || self.notify || self.daemon_hotkey.is_some()) { return Err("Monitor list option cannot be used with other options".into()); diff --git a/src/hotkey.rs b/src/hotkey.rs new file mode 100644 index 0000000..d804537 --- /dev/null +++ b/src/hotkey.rs @@ -0,0 +1,344 @@ +use device_query::{DeviceQuery, Keycode}; +pub use keyboard_types::{Code, Modifiers}; +use std::{borrow::Borrow, fmt::Display, hash::Hash, str::FromStr}; + +use crate::keycode_to_code; + +#[cfg(target_os = "macos")] +pub const CMD_OR_CTRL: Modifiers = Modifiers::SUPER; +#[cfg(not(target_os = "macos"))] +pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL; + +#[derive(thiserror::Error, Debug)] +pub enum HotKeyParseError { + #[error("Couldn't recognize \"{0}\" as a valid key for hotkey, if you feel like it should be, please report this to https://github.com/tauri-apps/muda")] + UnsupportedKey(String), + #[error("Found empty token while parsing hotkey: {0}")] + EmptyToken(String), + #[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")] + InvalidFormat(String), +} + +/// A keyboard shortcut that consists of an optional combination +/// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and +/// one key ([`Code`](crate::hotkey::Code)). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct HotKey { + /// The hotkey modifiers. + pub mods: Modifiers, + /// The hotkey key. + pub key: Code, + /// The hotkey id. + pub id: u32, +} + +impl HotKey { + /// Creates a new hotkey to define keyboard shortcuts throughout your application. + /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] + pub fn new(mods: Option, key: Code) -> Self { + let mut mods = mods.unwrap_or_else(Modifiers::empty); + if mods.contains(Modifiers::META) { + mods.remove(Modifiers::META); + mods.insert(Modifiers::SUPER); + } + + Self { + mods, + key, + id: mods.bits() << 16 | key as u32, + } + } + + pub fn check(&self, codes: impl IntoIterator) -> bool { + let mut mods = Modifiers::empty(); + let mut code = None; + for key in codes { + match key { + Keycode::LShift | Keycode::RShift => mods |= Modifiers::SHIFT, + Keycode::LControl | Keycode::RControl => mods |= Modifiers::CONTROL, + Keycode::LAlt | Keycode::RAlt => mods |= Modifiers::ALT, + Keycode::LMeta | Keycode::RMeta => mods |= Modifiers::SUPER, + other => { + code = Some(other); + } + } + } + + if code.is_none() { + return false; + } + + self.matches(mods, keycode_to_code(code.unwrap())) + } + + /// Returns the id associated with this hotKey + /// which is a hash of the string represention of modifiers and key within this hotKey. + pub fn id(&self) -> u32 { + self.id + } + + /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. + pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; + let modifiers = modifiers.borrow(); + let key = key.borrow(); + self.mods == *modifiers & base_mods && self.key == *key + } + + /// Converts this hotkey into a string. + pub fn into_string(self) -> String { + let mut hotkey = String::new(); + if self.mods.contains(Modifiers::SHIFT) { + hotkey.push_str("shift+") + } + if self.mods.contains(Modifiers::CONTROL) { + hotkey.push_str("control+") + } + if self.mods.contains(Modifiers::ALT) { + hotkey.push_str("alt+") + } + if self.mods.contains(Modifiers::SUPER) { + hotkey.push_str("super+") + } + hotkey.push_str(&self.key.to_string()); + hotkey + } +} + +impl Display for HotKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.into_string()) + } +} + +// HotKey::from_str is available to be backward +// compatible with tauri and it also open the option +// to generate hotkey from string +impl FromStr for HotKey { + type Err = HotKeyParseError; + fn from_str(hotkey_string: &str) -> Result { + parse_hotkey(hotkey_string) + } +} + +impl TryFrom<&str> for HotKey { + type Error = HotKeyParseError; + + fn try_from(value: &str) -> Result { + parse_hotkey(value) + } +} + +impl TryFrom for HotKey { + type Error = HotKeyParseError; + + fn try_from(value: String) -> Result { + parse_hotkey(&value) + } +} + +fn parse_hotkey(hotkey: &str) -> Result { + let tokens = hotkey.split('+').collect::>(); + + let mut mods = Modifiers::empty(); + let mut key = None; + + match tokens.len() { + // single key hotkey + 1 => { + key = Some(parse_key(tokens[0])?); + } + // modifiers and key comobo hotkey + _ => { + for raw in tokens { + let token = raw.trim(); + + if token.is_empty() { + return Err(HotKeyParseError::EmptyToken(hotkey.to_string())); + } + + if key.is_some() { + // At this point we have parsed the modifiers and a main key, so by reaching + // this code, the function either received more than one main key or + // the hotkey is not in the right order + // examples: + // 1. "Ctrl+Shift+C+A" => only one main key should be allowd. + // 2. "Ctrl+C+Shift" => wrong order + return Err(HotKeyParseError::InvalidFormat(hotkey.to_string())); + } + + match token.to_uppercase().as_str() { + "OPTION" | "ALT" => { + mods |= Modifiers::ALT; + } + "CONTROL" | "CTRL" => { + mods |= Modifiers::CONTROL; + } + "COMMAND" | "CMD" | "SUPER" => { + mods |= Modifiers::SUPER; + } + "SHIFT" => { + mods |= Modifiers::SHIFT; + } + #[cfg(target_os = "macos")] + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + mods |= Modifiers::SUPER; + } + #[cfg(not(target_os = "macos"))] + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + mods |= Modifiers::CONTROL; + } + _ => { + key = Some(parse_key(token)?); + } + } + } + } + } + + Ok(HotKey::new( + Some(mods), + key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, + )) +} + +fn parse_key(key: &str) -> Result { + use Code::*; + match key.to_uppercase().as_str() { + "BACKQUOTE" | "`" => Ok(Backquote), + "BACKSLASH" | "\\" => Ok(Backslash), + "BRACKETLEFT" | "[" => Ok(BracketLeft), + "BRACKETRIGHT" | "]" => Ok(BracketRight), + "PAUSE" | "PAUSEBREAK" => Ok(Pause), + "COMMA" | "," => Ok(Comma), + "DIGIT0" | "0" => Ok(Digit0), + "DIGIT1" | "1" => Ok(Digit1), + "DIGIT2" | "2" => Ok(Digit2), + "DIGIT3" | "3" => Ok(Digit3), + "DIGIT4" | "4" => Ok(Digit4), + "DIGIT5" | "5" => Ok(Digit5), + "DIGIT6" | "6" => Ok(Digit6), + "DIGIT7" | "7" => Ok(Digit7), + "DIGIT8" | "8" => Ok(Digit8), + "DIGIT9" | "9" => Ok(Digit9), + "EQUAL" | "=" => Ok(Equal), + "KEYA" | "A" => Ok(KeyA), + "KEYB" | "B" => Ok(KeyB), + "KEYC" | "C" => Ok(KeyC), + "KEYD" | "D" => Ok(KeyD), + "KEYE" | "E" => Ok(KeyE), + "KEYF" | "F" => Ok(KeyF), + "KEYG" | "G" => Ok(KeyG), + "KEYH" | "H" => Ok(KeyH), + "KEYI" | "I" => Ok(KeyI), + "KEYJ" | "J" => Ok(KeyJ), + "KEYK" | "K" => Ok(KeyK), + "KEYL" | "L" => Ok(KeyL), + "KEYM" | "M" => Ok(KeyM), + "KEYN" | "N" => Ok(KeyN), + "KEYO" | "O" => Ok(KeyO), + "KEYP" | "P" => Ok(KeyP), + "KEYQ" | "Q" => Ok(KeyQ), + "KEYR" | "R" => Ok(KeyR), + "KEYS" | "S" => Ok(KeyS), + "KEYT" | "T" => Ok(KeyT), + "KEYU" | "U" => Ok(KeyU), + "KEYV" | "V" => Ok(KeyV), + "KEYW" | "W" => Ok(KeyW), + "KEYX" | "X" => Ok(KeyX), + "KEYY" | "Y" => Ok(KeyY), + "KEYZ" | "Z" => Ok(KeyZ), + "MINUS" | "-" => Ok(Minus), + "PERIOD" | "." => Ok(Period), + "QUOTE" | "'" => Ok(Quote), + "SEMICOLON" | ";" => Ok(Semicolon), + "SLASH" | "/" => Ok(Slash), + "BACKSPACE" => Ok(Backspace), + "CAPSLOCK" => Ok(CapsLock), + "ENTER" => Ok(Enter), + "SPACE" => Ok(Space), + "TAB" => Ok(Tab), + "DELETE" => Ok(Delete), + "END" => Ok(End), + "HOME" => Ok(Home), + "INSERT" => Ok(Insert), + "PAGEDOWN" => Ok(PageDown), + "PAGEUP" => Ok(PageUp), + "PRINTSCREEN" => Ok(PrintScreen), + "SCROLLLOCK" => Ok(ScrollLock), + "ARROWDOWN" | "DOWN" => Ok(ArrowDown), + "ARROWLEFT" | "LEFT" => Ok(ArrowLeft), + "ARROWRIGHT" | "RIGHT" => Ok(ArrowRight), + "ARROWUP" | "UP" => Ok(ArrowUp), + "NUMLOCK" => Ok(NumLock), + "NUMPAD0" | "NUM0" => Ok(Numpad0), + "NUMPAD1" | "NUM1" => Ok(Numpad1), + "NUMPAD2" | "NUM2" => Ok(Numpad2), + "NUMPAD3" | "NUM3" => Ok(Numpad3), + "NUMPAD4" | "NUM4" => Ok(Numpad4), + "NUMPAD5" | "NUM5" => Ok(Numpad5), + "NUMPAD6" | "NUM6" => Ok(Numpad6), + "NUMPAD7" | "NUM7" => Ok(Numpad7), + "NUMPAD8" | "NUM8" => Ok(Numpad8), + "NUMPAD9" | "NUM9" => Ok(Numpad9), + "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd), + "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal), + "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide), + "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter), + "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEqual), + "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply), + "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract), + "ESCAPE" | "ESC" => Ok(Escape), + "F1" => Ok(F1), + "F2" => Ok(F2), + "F3" => Ok(F3), + "F4" => Ok(F4), + "F5" => Ok(F5), + "F6" => Ok(F6), + "F7" => Ok(F7), + "F8" => Ok(F8), + "F9" => Ok(F9), + "F10" => Ok(F10), + "F11" => Ok(F11), + "F12" => Ok(F12), + "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown), + "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp), + "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute), + "MEDIAPLAY" => Ok(MediaPlay), + "MEDIAPAUSE" => Ok(MediaPause), + "MEDIAPLAYPAUSE" => Ok(MediaPlayPause), + "MEDIASTOP" => Ok(MediaStop), + "MEDIATRACKNEXT" => Ok(MediaTrackNext), + "MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious), + "F13" => Ok(F13), + "F14" => Ok(F14), + "F15" => Ok(F15), + "F16" => Ok(F16), + "F17" => Ok(F17), + "F18" => Ok(F18), + "F19" => Ok(F19), + "F20" => Ok(F20), + "F21" => Ok(F21), + "F22" => Ok(F22), + "F23" => Ok(F23), + "F24" => Ok(F24), + + _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), + } +} + +pub fn wait_until_pressed(hotkey: HotKey) { + let state = device_query::DeviceState::new(); + println!("Waiting for hotkey: {}", hotkey); + loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + if hotkey.check(state.get_keys()) { + break; + } + } +} + +// struct KeyCatcher { +// pressed: HashSet, +// } diff --git a/src/main.rs b/src/main.rs index 245271f..48bce63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,13 @@ use std::io::IsTerminal; use args::Args; use clap::Parser; +use device_query::Keycode; +use keyboard_types::Code; mod app; mod args; mod context; +mod hotkey; fn main() -> anyhow::Result<()> { let stdout = std::io::stdout(); @@ -18,3 +21,119 @@ fn main() -> anyhow::Result<()> { app.run()?; Ok(()) } + +fn keycode_to_code(keycode: Keycode) -> Code { + match keycode { + Keycode::Key0 => Code::Digit0, + Keycode::Key1 => Code::Digit1, + Keycode::Key2 => Code::Digit2, + Keycode::Key3 => Code::Digit3, + Keycode::Key4 => Code::Digit4, + Keycode::Key5 => Code::Digit5, + Keycode::Key6 => Code::Digit6, + Keycode::Key7 => Code::Digit7, + Keycode::Key8 => Code::Digit8, + Keycode::Key9 => Code::Digit9, + Keycode::A => Code::KeyA, + Keycode::B => Code::KeyB, + Keycode::C => Code::KeyC, + Keycode::D => Code::KeyD, + Keycode::E => Code::KeyE, + Keycode::F => Code::KeyF, + Keycode::G => Code::KeyG, + Keycode::H => Code::KeyH, + Keycode::I => Code::KeyI, + Keycode::J => Code::KeyJ, + Keycode::K => Code::KeyK, + Keycode::L => Code::KeyL, + Keycode::M => Code::KeyM, + Keycode::N => Code::KeyN, + Keycode::O => Code::KeyO, + Keycode::P => Code::KeyP, + Keycode::Q => Code::KeyQ, + Keycode::R => Code::KeyR, + Keycode::S => Code::KeyS, + Keycode::T => Code::KeyT, + Keycode::U => Code::KeyU, + Keycode::V => Code::KeyV, + Keycode::W => Code::KeyW, + Keycode::X => Code::KeyX, + Keycode::Y => Code::KeyY, + Keycode::Z => Code::KeyZ, + Keycode::F1 => Code::F1, + Keycode::F2 => Code::F2, + Keycode::F3 => Code::F3, + Keycode::F4 => Code::F4, + Keycode::F5 => Code::F5, + Keycode::F6 => Code::F6, + Keycode::F7 => Code::F7, + Keycode::F8 => Code::F8, + Keycode::F9 => Code::F9, + Keycode::F10 => Code::F10, + Keycode::F11 => Code::F11, + Keycode::F12 => Code::F12, + Keycode::F13 => Code::F13, + Keycode::F14 => Code::F14, + Keycode::F15 => Code::F15, + Keycode::F16 => Code::F16, + Keycode::F17 => Code::F17, + Keycode::F18 => Code::F18, + Keycode::F19 => Code::F19, + Keycode::F20 => Code::F20, + Keycode::Escape => Code::Escape, + Keycode::Space => Code::Space, + Keycode::LControl => Code::ControlLeft, + Keycode::RControl => Code::ControlRight, + Keycode::LShift => Code::ShiftLeft, + Keycode::RShift => Code::ShiftRight, + Keycode::LAlt => Code::AltLeft, + Keycode::RAlt => Code::AltRight, + Keycode::Command => Code::MetaLeft, + Keycode::LOption => Code::MetaLeft, + Keycode::ROption => Code::MetaRight, + Keycode::LMeta => Code::MetaLeft, + Keycode::RMeta => Code::MetaRight, + Keycode::Enter => Code::Enter, + Keycode::Up => Code::ArrowUp, + Keycode::Down => Code::ArrowDown, + Keycode::Left => Code::ArrowLeft, + Keycode::Right => Code::ArrowRight, + Keycode::Backspace => Code::Backspace, + Keycode::CapsLock => Code::CapsLock, + Keycode::Tab => Code::Tab, + Keycode::Home => Code::Home, + Keycode::End => Code::End, + Keycode::PageUp => Code::PageUp, + Keycode::PageDown => Code::PageDown, + Keycode::Insert => Code::Insert, + Keycode::Delete => Code::Delete, + Keycode::Numpad0 => Code::Numpad0, + Keycode::Numpad1 => Code::Numpad1, + Keycode::Numpad2 => Code::Numpad2, + Keycode::Numpad3 => Code::Numpad3, + Keycode::Numpad4 => Code::Numpad4, + Keycode::Numpad5 => Code::Numpad5, + Keycode::Numpad6 => Code::Numpad6, + Keycode::Numpad7 => Code::Numpad7, + Keycode::Numpad8 => Code::Numpad8, + Keycode::Numpad9 => Code::Numpad9, + Keycode::NumpadSubtract => Code::NumpadSubtract, + Keycode::NumpadAdd => Code::NumpadAdd, + Keycode::NumpadDivide => Code::NumpadDivide, + Keycode::NumpadMultiply => Code::NumpadMultiply, + Keycode::NumpadEquals => Code::NumpadEqual, + Keycode::NumpadEnter => Code::NumpadEnter, + Keycode::NumpadDecimal => Code::NumpadDecimal, + Keycode::Grave => Code::Backquote, + Keycode::Minus => Code::Minus, + Keycode::Equal => Code::Equal, + Keycode::LeftBracket => Code::BracketLeft, + Keycode::RightBracket => Code::BracketRight, + Keycode::BackSlash => Code::Backslash, + Keycode::Semicolon => Code::Semicolon, + Keycode::Apostrophe => Code::Quote, + Keycode::Comma => Code::Comma, + Keycode::Dot => Code::Period, + Keycode::Slash => Code::Slash, + } +} From d54b0666068ef4b0cad7a0bb4960f53f74f367bf Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Mon, 4 Nov 2024 17:59:09 -0600 Subject: [PATCH 07/16] feat: Refactor image handling and split crop functionality --- src/context.rs | 136 ++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 57 deletions(-) diff --git a/src/context.rs b/src/context.rs index fc01bc2..bf9976c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -107,13 +107,13 @@ fn resize_image( )) } -fn generate_output_path(dir: &Path, filename: &str, format: ImageFormat) -> PathBuf { +fn generate_output_path(dir: impl AsRef, filename: &str, format: ImageFormat) -> PathBuf { let ext = format.extensions_str().first().copied().unwrap_or("png"); let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); let filename = format!("{filename}-{}.{ext}", timestamp); - dir.join(filename) + dir.as_ref().join(filename) } pub struct AppContext { @@ -172,66 +172,23 @@ impl AppContext { } pub fn save_selection(&self, args: Option<&Args>) -> anyhow::Result<()> { - // Get dimensions and validate image data - let ((ax, ay), (bx, by)) = if let Some(region) = args.and_then(|a| a.region) { - ( - (region.x, region.y), - ((region.x + region.width), (region.y + region.height)), - ) - } else { - self.selection - .sel_coords() - .ok_or_else(|| anyhow::anyhow!("No selection made"))? - }; - - let mut image_data = self.get_selection_data(ax, ay, bx, by); - - // Handle scaling if requested - if let Some(scale) = args.and_then(|a| a.scale) { - image_data = resize_image( - &image_data, - scale, - args.and_then(|a| a.filter).unwrap_or(FilterType::Nearest), - )?; - } + let image_data = crop_image(&self.image, args, &self.selection)?; - // Save to clipboard if no output directory specified - let Some(output_dir) = args.and_then(|f| f.output_dir.as_deref()) else { - return { - self.save_to_clipboard(image_data); - Ok(()) - }; + let Some(output) = args.and_then(|a| a.output_dir.as_deref()) else { + self.save_to_clipboard(image_data); + return Ok(()); }; - // Generate filename and save - let format = args - .and_then(|f| f.image_format) - .unwrap_or(ImageFormat::Png); - let path = generate_output_path( - output_dir, - args.and_then(|f| f.filename.as_deref()).unwrap_or("cleave"), - format, - ); - - Ok(image_data.save_with_format(path, format)?) + save_selection(image_data, args, &self.selection, output) } pub fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> anyhow::Result { - let monitor = xcap::Monitor::all()? - .into_iter() - .find(|m| m.is_primary()) - .with_context(|| "Could not get primary monitor")?; - let img = monitor.capture_image()?; - let size = PhysicalSize::new(monitor.width(), monitor.height()); - - let icon_bytes = include_bytes!("../icon.png"); - let rgba = image::load_from_memory(icon_bytes)?.to_rgba8(); - let (width, height) = rgba.dimensions(); - let rgba = rgba.into_raw(); + let img = capture_screen()?; + let (width, height, rgba) = load_icon()?; let window = event_loop.create_window( WindowAttributes::default() - .with_inner_size(size) + .with_inner_size(PhysicalSize::new(img.width(), img.height())) .with_title("Cleave") .with_resizable(false) .with_decorations(false) @@ -240,7 +197,7 @@ impl AppContext { .with_window_icon(Some(Icon::from_rgba(rgba, width, height)?)), )?; - let graphics = Graphics::new(window, size.width, size.height); + let graphics = Graphics::new(window, img.width(), img.height()); let graphics = pollster::block_on(graphics)?; let bundle = GraphicsBundle::new( @@ -251,7 +208,8 @@ impl AppContext { graphics.config.format, ); - graphics.window.set_visible(true); + // graphics.window.set_visible(true); + let _ = graphics .window .set_cursor_grab(winit::window::CursorGrabMode::Confined); @@ -361,8 +319,8 @@ impl AppContext { self.graphics.window.set_minimized(true); } - pub fn hide_window(&self) { - self.graphics.set_visible(false); + pub fn set_window_visibility(&self, val: bool) { + self.graphics.set_visible(val); } pub fn set_mode(&mut self, mode: SelectionMode) { @@ -396,3 +354,67 @@ impl AppContext { Some(self) } } + +fn crop_image( + img: &RgbaImage, + args: Option<&Args>, + selection: &UserSelection, +) -> anyhow::Result { + let ((ax, ay), (bx, by)) = if let Some(region) = args.and_then(|a| a.region) { + ( + (region.x, region.y), + ((region.x + region.width), (region.y + region.height)), + ) + } else { + selection + .sel_coords() + .ok_or_else(|| anyhow::anyhow!("No selection made"))? + }; + + Ok(img.view(ax, ay, bx.abs_diff(ax), by.abs_diff(ay)).to_image()) +} + +fn save_selection( + mut image: RgbaImage, + args: Option<&Args>, + selection: &UserSelection, + save_path: impl AsRef, +) -> anyhow::Result<()> { + // Handle scaling if requested + if let Some(scale) = args.and_then(|a| a.scale) { + image = resize_image( + &image, + scale, + args.and_then(|a| a.filter).unwrap_or(FilterType::Nearest), + )?; + } + + // Generate filename and save + let format = args + .and_then(|f| f.image_format) + .unwrap_or(ImageFormat::Png); + let path = generate_output_path( + save_path, + args.and_then(|f| f.filename.as_deref()).unwrap_or("cleave"), + format, + ); + + Ok(image.save_with_format(path, format)?) +} + +fn capture_screen() -> anyhow::Result, Vec>> { + let monitor = xcap::Monitor::all()? + .into_iter() + .find(|m| m.is_primary()) + .with_context(|| "Could not get primary monitor")?; + let img = monitor.capture_image()?; + Ok(img) +} + +fn load_icon() -> Result<(u32, u32, Vec), anyhow::Error> { + let icon_bytes = include_bytes!("../icon.png"); + let rgba = image::load_from_memory(icon_bytes)?.to_rgba8(); + let (width, height) = rgba.dimensions(); + let rgba = rgba.into_raw(); + Ok((width, height, rgba)) +} From d283dd405a27f8fbeb2c8a8c55bb057828588d3d Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Mon, 4 Nov 2024 18:05:13 -0600 Subject: [PATCH 08/16] [broken] temp commit trying to get daemon to save kay callback gaurds --- Cargo.lock | 9 ---- Cargo.toml | 5 ++- src/app.rs | 115 ++++++++++++++++++++++++++++++++++++++++++++------ src/args.rs | 19 +++++++++ src/hotkey.rs | 16 +------ 5 files changed, 125 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2598d33..b27cc8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,10 +692,7 @@ dependencies = [ [[package]] name = "device_query" version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafa241a89a5edccff5057d0b85fbc083a781bd03d766c11a688331604980985" dependencies = [ - "lazy_static", "macos-accessibility-client", "pkg-config", "readkey", @@ -1168,12 +1165,6 @@ 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 = "lebe" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index a35f112..f451776 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,9 @@ cleave-graphics = { path = "cleave-graphics" } clap = { version = "4.5.20", features = ["derive"] } chrono = "0.4.38" keyboard-types = "0.7.0" -device_query = "2.1.0" - +device_query = { path = "../device_query" } +# device_query = { git = "https://github.com/exotik850/device_query", branch = "sleep_duration" } +# device_query = "2.1.0" [workspace.dependencies] anyhow = "1" diff --git a/src/app.rs b/src/app.rs index a614d39..abda228 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,27 +1,62 @@ +use std::sync::{atomic::AtomicBool, mpsc::TryRecvError}; -use crate::args::Args; +use crate::{args::Args, hotkey::HotKey}; +use device_query::{DeviceEvents, DeviceEventsHandler, DeviceQuery, KeyboardCallback, Keycode}; use winit::{ application::ApplicationHandler, event::{ElementState, KeyEvent, MouseButton, WindowEvent}, + event_loop::EventLoop, keyboard::{Key, NamedKey}, }; use crate::context::{AppContext, Direction, SelectionMode}; -pub struct App { +#[derive(Debug)] +struct KeyAction { + key: Keycode, + pressed: bool, +} + +struct Daemon { + pressed: Vec, + rx: std::sync::mpsc::Receiver, + _event_handler: DeviceEventsHandler, + hotkey: HotKey, + key_up: device_query::CallbackGuard, + key_down: device_query::CallbackGuard, +} + +impl Daemon { + fn clear_buffer(&mut self) { + for action in self.rx.iter() { + println!("{:?}", action); + if action.pressed { + self.pressed.push(action.key); + } else { + self.pressed.retain(|&x| x != action.key); + } + } + } +} + +pub struct App { args: Option, + daemon: Option>, context: Option, } -impl App { +impl App { pub fn new(args: Option) -> Self { App { args, context: None, + daemon: None, } } - pub fn run(&mut self) -> anyhow::Result<()> { + pub fn run(mut self) -> anyhow::Result<()> { + let mut start_daemon = None; + if let Some(args) = &self.args { if let Err(e) = args.verify() { eprintln!("{}", e); @@ -39,7 +74,8 @@ impl App { if let Some(hotkey) = &args.daemon_hotkey { // Wait until the hotkey is pressed let hotkey: crate::hotkey::HotKey = hotkey.parse()?; - crate::hotkey::wait_until_pressed(hotkey); + // crate::hotkey::wait_until_pressed(hotkey); + start_daemon = Some(hotkey); } if args.delay > 0 { @@ -50,14 +86,54 @@ impl App { std::fs::create_dir_all(output_dir)?; } } + let daemon = start_daemon.map(|hotkey| { + let (tx, rx) = std::sync::mpsc::channel(); + let _event_handler = + device_query::DeviceEventsHandler::new(std::time::Duration::from_millis(10)) + .expect("Could not start event loop"); + let txa = tx.clone(); + let key_down = _event_handler.on_key_down(move |key| { + let _ = txa.send(KeyAction { + key: *key, + pressed: true, + }); + }); + let key_up = _event_handler.on_key_up(move |key| { + let _ = tx.send(KeyAction { + key: *key, + pressed: false, + }); + }); + Daemon { + _event_handler, + rx, + pressed: Vec::new(), + key_up, + key_down, + hotkey, + } + }); + + // match daemon { + // Some(daemon) => loop { + // if let Ok(_) = daemon.rx.try_recv() { + // self.start_loop()?; + // if daemon.stay_running { + // continue; + // } + // break; + // } + // }, + // None => self.start_loop()?, + // } + self.daemon = daemon; - let event_loop = winit::event_loop::EventLoop::new()?; - event_loop.run_app(self)?; - Ok(()) + let event_loop = EventLoop::new()?; + Ok(event_loop.run_app(&mut self)?) } } -impl ApplicationHandler for App { +impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { if self.context.is_some() { return; @@ -84,10 +160,19 @@ impl ApplicationHandler for App { let Some(context) = &mut self.context else { return; }; + if let Some(daemon) = self.daemon.as_mut() { + daemon.clear_buffer(); + if !daemon.hotkey.check(daemon.pressed.iter().copied()) { + return; + } + daemon.pressed.clear(); + } if id != context.window_id() { return; } + let stay_running = self.args.as_ref().is_some_and(|d| d.stay_running()); + match event { WindowEvent::RedrawRequested => { context.draw(); @@ -105,15 +190,19 @@ impl ApplicationHandler for App { .. } => match (state, key) { (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { - event_loop.exit(); - context.destroy(); + if !stay_running { + event_loop.exit(); + context.destroy(); + } } (ElementState::Pressed, Key::Named(NamedKey::Space)) => { - context.hide_window(); + context.set_window_visibility(false); if let Err(e) = context.save_selection(self.args.as_ref()) { eprintln!("{}", e); }; - event_loop.exit(); + if !stay_running { + event_loop.exit(); + } } (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { context.handle_move(Direction::Down); diff --git a/src/args.rs b/src/args.rs index ac979bb..2376878 100644 --- a/src/args.rs +++ b/src/args.rs @@ -129,6 +129,15 @@ pub struct Args { /// If provided, app runs in the background and captures the screen whenever the user presses a hotkey #[arg(long)] pub daemon_hotkey: Option, + + /// Persistent Daemon Mode + /// + /// If true, the app will continue to run in the background even after the hotkey is pressed, + /// allowing the user to capture the screen multiple times + /// + /// Only used when daemon_hotkey is provided + #[arg(long, short)] + pub persistent: bool, } impl Args { @@ -158,7 +167,17 @@ impl Args { "Output format and filename is only used when output directory is provided".into(), ); } + if self.persistent && self.daemon_hotkey.is_none() { + return Err("Persistent daemon mode can only be used with daemon hotkey".into()); + } + if self.daemon_hotkey.is_some() && self.delay > 0 { + return Err("Delay cannot be used with daemon hotkey".into()); + } Ok(()) } + + pub fn stay_running(&self) -> bool { + self.daemon_hotkey.is_some() && self.persistent + } } diff --git a/src/hotkey.rs b/src/hotkey.rs index d804537..d39ee8e 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -3,6 +3,7 @@ pub use keyboard_types::{Code, Modifiers}; use std::{borrow::Borrow, fmt::Display, hash::Hash, str::FromStr}; use crate::keycode_to_code; +use device_query::{DeviceEvents, DeviceState}; #[cfg(target_os = "macos")] pub const CMD_OR_CTRL: Modifiers = Modifiers::SUPER; @@ -327,18 +328,3 @@ fn parse_key(key: &str) -> Result { _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), } } - -pub fn wait_until_pressed(hotkey: HotKey) { - let state = device_query::DeviceState::new(); - println!("Waiting for hotkey: {}", hotkey); - loop { - std::thread::sleep(std::time::Duration::from_millis(100)); - if hotkey.check(state.get_keys()) { - break; - } - } -} - -// struct KeyCatcher { -// pressed: HashSet, -// } From 2559f7c1a887b28fa325ca59146fa890ad06eb06 Mon Sep 17 00:00:00 2001 From: "kidkool850@gmail.com" Date: Tue, 5 Nov 2024 09:09:41 -0600 Subject: [PATCH 09/16] throw daemon into file --- src/daemon.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/daemon.rs diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..3d333cc --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,73 @@ +use std::sync::{Arc, RwLock}; + +use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; + +use crate::hotkey::HotKey; + +#[derive(Debug)] +pub(crate) struct KeyAction { + key: Keycode, + pressed: bool, +} + +pub(crate) struct Daemon { + pressed: Arc>>, + + // pub rx: std::sync::mpsc::Receiver, + _event_handler: DeviceEventsHandler, + _key_up: device_query::CallbackGuard, + _key_down: device_query::CallbackGuard, + hotkey: HotKey, + listening: bool, +} + +impl Daemon { + pub fn start(hotkey: HotKey) -> Self { + let _event_handler = + device_query::DeviceEventsHandler::new(std::time::Duration::from_millis(10)) + .expect("Could not start event loop"); + let pressed: Arc>> = Default::default(); + let pa: Arc<_> = pressed.clone(); + let _key_down = _event_handler.on_key_down(move |key| { + let mut pressed = pa.write().unwrap(); + pressed.push(key); + }); + let pb: Arc<_> = pressed.clone(); + let _key_up = _event_handler.on_key_up(move |key| { + let mut pressed = pb.write().unwrap(); + pressed.retain(|&k| k != key); + }); + Daemon { + _event_handler, + _key_up, + _key_down, + // rx, + pressed, + hotkey, + listening: true, + } + } + + fn is_pressed(&self) -> bool { + let pressed = dbg!(self.pressed.read().unwrap()); + self.hotkey.check(pressed.iter().copied()) + } + + fn clear_buffer(&mut self) { + let mut pressed = self.pressed.write().unwrap(); + pressed.clear(); + } + + pub fn start_listening(&mut self) { + self.listening = true; + } + + pub fn get_pressed(&mut self) -> bool { + let val = self.listening && self.is_pressed(); + if val { + self.clear_buffer(); + self.listening = false; + } + val + } +} From d38aa17024214ab875d54c7666f8dcbbbfb39b89 Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Tue, 5 Nov 2024 17:00:45 -0600 Subject: [PATCH 10/16] massive refactor, getting things done --- Cargo.lock | 69 ------ Cargo.toml | 7 +- src/app.rs | 245 -------------------- src/app/context.rs | 115 ++++++++++ src/app/current_image.rs | 61 +++++ src/app/mod.rs | 240 ++++++++++++++++++++ src/app/state.rs | 147 ++++++++++++ src/args.rs | 85 ++++--- src/context.rs | 420 ----------------------------------- src/{ => keyboard}/hotkey.rs | 120 ++++------ src/keyboard/mod.rs | 1 + src/lib.rs | 5 + src/main.rs | 132 +---------- src/selection/mod.rs | 8 + src/selection/modes.rs | 14 ++ src/util/mod.rs | 116 ++++++++++ 16 files changed, 814 insertions(+), 971 deletions(-) delete mode 100644 src/app.rs create mode 100644 src/app/context.rs create mode 100644 src/app/current_image.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/state.rs delete mode 100644 src/context.rs rename src/{ => keyboard}/hotkey.rs (74%) create mode 100644 src/keyboard/mod.rs create mode 100644 src/lib.rs create mode 100644 src/selection/mod.rs create mode 100644 src/selection/modes.rs create mode 100644 src/util/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b27cc8c..5fa339d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,9 +275,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "bitstream-io" @@ -485,10 +482,8 @@ dependencies = [ "chrono", "clap", "cleave-graphics", - "device_query", "glam", "image", - "keyboard-types", "pollster", "thiserror", "wgpu", @@ -689,18 +684,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "device_query" -version = "2.1.0" -dependencies = [ - "macos-accessibility-client", - "pkg-config", - "readkey", - "readmouse", - "windows 0.48.0", - "x11", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -1137,17 +1120,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.6.0", - "serde", - "unicode-segmentation", -] - [[package]] name = "khronos-egl" version = "6.0.0" @@ -1255,16 +1227,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "macos-accessibility-client" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb" -dependencies = [ - "core-foundation 0.9.4", - "core-foundation-sys", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -2033,18 +1995,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "readkey" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7677f98ca49bc9bb26e04c8abf80ba579e2cb98e8a384a0ff8128ad70670d249" - -[[package]] -name = "readmouse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be105c72a1e6a5a1198acee3d5b506a15676b74a02ecd78060042a447f408d94" - [[package]] name = "redox_syscall" version = "0.4.1" @@ -2836,15 +2786,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows" version = "0.57.0" @@ -3246,16 +3187,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index f451776..46b043b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,6 @@ thiserror = { workspace = true } cleave-graphics = { path = "cleave-graphics" } clap = { version = "4.5.20", features = ["derive"] } chrono = "0.4.38" -keyboard-types = "0.7.0" -device_query = { path = "../device_query" } -# device_query = { git = "https://github.com/exotik850/device_query", branch = "sleep_duration" } -# device_query = "2.1.0" [workspace.dependencies] anyhow = "1" @@ -33,10 +29,11 @@ arboard = "3.4.1" thiserror = "1" bytemuck = { version = "1.19.0", features = ["derive"] } glam = { version = "0.29.1", features = ["bytemuck"] } -image = "0.25.4" +image = "0.25" pollster = "0.4.0" wgpu = "23.0.0" winit = { version = "0.30.5", features = ["rwh_06"] } +winit_input_helper = { git = "https://github.com/zachdedoo13/winit_input_helper_updated.git" } xcap = "0.0.14" [profile.release] diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index abda228..0000000 --- a/src/app.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::sync::{atomic::AtomicBool, mpsc::TryRecvError}; - -use crate::{args::Args, hotkey::HotKey}; -use device_query::{DeviceEvents, DeviceEventsHandler, DeviceQuery, KeyboardCallback, Keycode}; -use winit::{ - application::ApplicationHandler, - event::{ElementState, KeyEvent, MouseButton, WindowEvent}, - event_loop::EventLoop, - keyboard::{Key, NamedKey}, -}; - -use crate::context::{AppContext, Direction, SelectionMode}; - -#[derive(Debug)] -struct KeyAction { - key: Keycode, - pressed: bool, -} - -struct Daemon { - pressed: Vec, - rx: std::sync::mpsc::Receiver, - _event_handler: DeviceEventsHandler, - hotkey: HotKey, - key_up: device_query::CallbackGuard, - key_down: device_query::CallbackGuard, -} - -impl Daemon { - fn clear_buffer(&mut self) { - for action in self.rx.iter() { - println!("{:?}", action); - if action.pressed { - self.pressed.push(action.key); - } else { - self.pressed.retain(|&x| x != action.key); - } - } - } -} - -pub struct App { - args: Option, - daemon: Option>, - context: Option, -} - -impl App { - pub fn new(args: Option) -> Self { - App { - args, - context: None, - daemon: None, - } - } - - pub fn run(mut self) -> anyhow::Result<()> { - let mut start_daemon = None; - - if let Some(args) = &self.args { - if let Err(e) = args.verify() { - eprintln!("{}", e); - std::process::exit(1); - } - - if args.monitor_list { - println!("Available monitors:"); - for monitor in xcap::Monitor::all().into_iter().flatten() { - println!("ID: {}", monitor.id()); - } - std::process::exit(0); - } - - if let Some(hotkey) = &args.daemon_hotkey { - // Wait until the hotkey is pressed - let hotkey: crate::hotkey::HotKey = hotkey.parse()?; - // crate::hotkey::wait_until_pressed(hotkey); - start_daemon = Some(hotkey); - } - - if args.delay > 0 { - std::thread::sleep(std::time::Duration::from_millis(args.delay)); - } - - if let Some(output_dir) = &args.output_dir { - std::fs::create_dir_all(output_dir)?; - } - } - let daemon = start_daemon.map(|hotkey| { - let (tx, rx) = std::sync::mpsc::channel(); - let _event_handler = - device_query::DeviceEventsHandler::new(std::time::Duration::from_millis(10)) - .expect("Could not start event loop"); - let txa = tx.clone(); - let key_down = _event_handler.on_key_down(move |key| { - let _ = txa.send(KeyAction { - key: *key, - pressed: true, - }); - }); - let key_up = _event_handler.on_key_up(move |key| { - let _ = tx.send(KeyAction { - key: *key, - pressed: false, - }); - }); - Daemon { - _event_handler, - rx, - pressed: Vec::new(), - key_up, - key_down, - hotkey, - } - }); - - // match daemon { - // Some(daemon) => loop { - // if let Ok(_) = daemon.rx.try_recv() { - // self.start_loop()?; - // if daemon.stay_running { - // continue; - // } - // break; - // } - // }, - // None => self.start_loop()?, - // } - self.daemon = daemon; - - let event_loop = EventLoop::new()?; - Ok(event_loop.run_app(&mut self)?) - } -} - -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - if self.context.is_some() { - return; - } - let mut context = AppContext::new(event_loop).expect("Could not start context"); - - if let Some(args) = &self.args { - let Some(arg_context) = context.set_args(args) else { - event_loop.exit(); - return; - }; - context = arg_context; - } - - self.context = Some(context); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - let Some(context) = &mut self.context else { - return; - }; - if let Some(daemon) = self.daemon.as_mut() { - daemon.clear_buffer(); - if !daemon.hotkey.check(daemon.pressed.iter().copied()) { - return; - } - daemon.pressed.clear(); - } - if id != context.window_id() { - return; - } - - let stay_running = self.args.as_ref().is_some_and(|d| d.stay_running()); - - match event { - WindowEvent::RedrawRequested => { - context.draw(); - } - WindowEvent::CursorMoved { position, .. } => { - context.update_mouse_position(position.x, position.y); - } - WindowEvent::KeyboardInput { - event: - KeyEvent { - state, - logical_key: key, - .. - }, - .. - } => match (state, key) { - (ElementState::Pressed, Key::Named(NamedKey::Escape)) => { - if !stay_running { - event_loop.exit(); - context.destroy(); - } - } - (ElementState::Pressed, Key::Named(NamedKey::Space)) => { - context.set_window_visibility(false); - if let Err(e) = context.save_selection(self.args.as_ref()) { - eprintln!("{}", e); - }; - if !stay_running { - event_loop.exit(); - } - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowDown)) => { - context.handle_move(Direction::Down); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowUp)) => { - context.handle_move(Direction::Up); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowLeft)) => { - context.handle_move(Direction::Left); - } - (ElementState::Pressed, Key::Named(NamedKey::ArrowRight)) => { - context.handle_move(Direction::Right); - } - (ElementState::Pressed, Key::Named(NamedKey::Shift)) => { - context.set_mode(SelectionMode::InverseResize); - } - (ElementState::Released, Key::Named(NamedKey::Shift)) => { - context.set_mode(SelectionMode::Resize); - } - (ElementState::Pressed, Key::Named(NamedKey::Control)) => { - context.set_mode(SelectionMode::Move); - } - (ElementState::Released, Key::Named(NamedKey::Control)) => { - context.set_mode(SelectionMode::Resize); - } - _ => {} - }, - WindowEvent::MouseInput { state, button, .. } => match (state, button) { - (ElementState::Pressed, MouseButton::Left) => context.start_drag(), - (ElementState::Released, MouseButton::Left) => context.end_drag(), - (_, MouseButton::Right) => context.cancel_drag(), - _ => {} - }, - WindowEvent::CloseRequested => { - event_loop.exit(); - } - _ => {} - } - } -} diff --git a/src/app/context.rs b/src/app/context.rs new file mode 100644 index 0000000..bdd05f7 --- /dev/null +++ b/src/app/context.rs @@ -0,0 +1,115 @@ + +use bytemuck::{Pod, Zeroable}; +use glam::Vec2; +use image::GenericImageView; +// use pixels::{Pixels, SurfaceTexture}; +use winit::{ + dpi::PhysicalSize, + window::{Icon, Window, WindowAttributes}, +}; + +// use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; +use cleave_graphics::prelude::*; + +#[repr(C)] +#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, Default, Debug)] +pub struct SelectionUniforms { + pub screen_size: Vec2, + pub drag_start: Vec2, + pub drag_end: Vec2, + pub selection_start: Vec2, + pub selection_end: Vec2, + pub time: f32, + pub is_dragging: u32, // 0 = None, 1 = Dragging, 2 = Selected, 3 = Both +} + +impl std::fmt::Display for SelectionUniforms { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "size: {:?}, is_dragging: {}, drag_start: {:?}, drag_end: {:?}, selection_start: {:?}, selection_end: {:?}, time: {}", + self.screen_size, self.is_dragging, self.drag_start, self.drag_end, self.selection_start, self.selection_end, self.time) + } +} + +pub struct CleaveContext { + pub graphics: Graphics, + pub total_time: f32, + last_frame: std::time::Instant, +} + +impl CleaveContext { + pub fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> anyhow::Result { + let (width, height, rgba) = crate::util::load_icon()?; + let window = event_loop.create_window( + WindowAttributes::default() + .with_inner_size(PhysicalSize::new(width, height)) + .with_title("Cleave") + .with_resizable(false) + .with_decorations(false) + .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) + .with_visible(false) + .with_window_icon(Some(Icon::from_rgba(rgba, width, height)?)), + )?; + + let graphics = Graphics::new(window, width, height); + let graphics = pollster::block_on(graphics)?; + + // let bundle = GraphicsBundle::new( + // img.clone().into(), + // &graphics.device, + // &graphics.queue, + // wgpu::PrimitiveTopology::TriangleStrip, + // graphics.config.format, + // ); + + graphics.window.set_visible(true); + + let _ = graphics + .window + .set_cursor_grab(winit::window::CursorGrabMode::Confined); + + // let surface_texture = SurfaceTexture::new(size.width, size.height, window.clone()); + // let pixels = Pixels::new(size.width, size.height, surface_texture)?; + + Ok(Self { + total_time: 0.0, + last_frame: std::time::Instant::now(), + graphics, + }) + } + + pub fn draw(&mut self, bundle: &GraphicsBundle) { + let mut pass = match self.graphics.render() { + Ok(pass) => pass, + Err(err) => { + eprintln!("Error rendering frame: {:?}", err); + return; + } + }; + bundle.draw(&mut pass); + pass.finish(); + self.graphics.request_redraw(); + } + + pub fn update(&mut self) { + let time = self.last_frame.elapsed().as_secs_f32(); + self.total_time += time; + self.last_frame = std::time::Instant::now(); + } + + pub fn window_id(&self) -> winit::window::WindowId { + self.graphics.id() + } + + pub fn destroy(&self) { + self.graphics.window.set_minimized(true); + } + + pub fn set_window_visibility(&self, val: bool) { + self.graphics.set_visible(val); + } + + pub fn size(&self) -> (f32, f32) { + let size = self.graphics.window.outer_size(); + (size.width as f32, size.height as f32) + } +} diff --git a/src/app/current_image.rs b/src/app/current_image.rs new file mode 100644 index 0000000..b46b7e6 --- /dev/null +++ b/src/app/current_image.rs @@ -0,0 +1,61 @@ +use cleave_graphics::prelude::GraphicsBundle; +use glam::Vec2; +use image::RgbaImage; + +use crate::selection::UserSelection; + +use super::context::SelectionUniforms; + +pub struct CurrentImage { + pub image: RgbaImage, + pub bundle: GraphicsBundle, +} + +impl CurrentImage { + pub fn capture_image( + monitor: Option, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> anyhow::Result { + let img = crate::util::capture_screen(monitor)?; + let bundle = GraphicsBundle::new( + img.clone().into(), + device, + queue, + wgpu::PrimitiveTopology::TriangleStrip, + wgpu::TextureFormat::Bgra8UnormSrgb, + ); + Ok(Self { image: img, bundle }) + } + + pub fn update_uniforms(&mut self, time: f32, user: &UserSelection, (w, h): (f32, f32)) { + self.bundle.uniforms.time = time; + self.bundle.uniforms.screen_size.x = w; + self.bundle.uniforms.screen_size.y = h; + + let drag = &user.drag; + let selection = &user.selection; + self.bundle.uniforms.is_dragging = match (drag, selection) { + (Some(d), Some(s)) if (d.x != 0. || d.y != 0.) && (s.x != 0. || s.y != 0.) => 3, + (Some(d), None) if (d.x != 0. || d.y != 0.) => 1, + (None, Some(s)) if (s.x != 0. || s.y != 0.) => 2, + _ => 0, + }; + if let Some(drag) = drag { + self.bundle.uniforms.drag_start = Vec2::new(drag.x, drag.y); + self.bundle.uniforms.drag_end = Vec2::new(drag.x + drag.w, drag.y + drag.h); + } else { + self.bundle.uniforms.drag_start = Vec2::ZERO; + self.bundle.uniforms.drag_end = Vec2::ZERO; + }; + + if let Some(selection) = selection { + self.bundle.uniforms.selection_start = Vec2::new(selection.x, selection.y); + self.bundle.uniforms.selection_end = + Vec2::new(selection.x + selection.w, selection.y + selection.h); + } else { + self.bundle.uniforms.selection_start = Vec2::ZERO; + self.bundle.uniforms.selection_end = Vec2::ZERO; + }; + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..ea7294c --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,240 @@ +use crate::{ + args::{Args, Verified}, + selection::modes::{Direction, SelectionMode}, +}; + +use current_image::CurrentImage; +use state::CleaveState; +use winit::{ + application::ApplicationHandler, + event::{ElementState, KeyEvent, MouseButton, WindowEvent}, + event_loop::EventLoop, + keyboard::{Key, NamedKey}, +}; + +mod context; +mod current_image; +mod state; + +use context::CleaveContext; + +pub struct App { + args: Option, + current_image: Option, + context: Option, + state: CleaveState, +} + +impl App { + pub fn new(args: Option) -> anyhow::Result { + Ok(App { + args: args.map(Args::verify).transpose()?, + context: None, + state: Default::default(), + current_image: None, + }) + } + + fn start_loop(&mut self) -> anyhow::Result<()> { + let event_loop = EventLoop::new()?; + Ok(event_loop.run_app(self)?) + } + + pub fn run(mut self) -> anyhow::Result<()> { + let Some(args) = &self.args else { + return self.start_loop(); + }; + + if args.monitor_list { + println!("Available monitors:"); + for monitor in xcap::Monitor::all().into_iter().flatten() { + println!("ID: {}", monitor.id()); + } + std::process::exit(0); + } + + if args.delay > 0 { + std::thread::sleep(std::time::Duration::from_millis(args.delay)); + } + + if let Some(output_dir) = &args.output_dir { + std::fs::create_dir_all(output_dir)?; + } + + if let Some(region) = args.region { + let img = crate::util::capture_screen(args.monitor)?; + let cropped = crate::util::crop_image(&img, Some(args), region)?; + if let Some(out) = &args.output_dir { + crate::util::save_selection(cropped, Some(args), out)?; + } else { + crate::util::save_to_clipboard(&cropped)?; + } + return Ok(()); + } + + self.start_loop() + } + + fn execute_key_command( + &mut self, + event: KeyEvent, + event_loop: &winit::event_loop::ActiveEventLoop, + ) -> bool { + let Some(context) = &mut self.context else { + return false; + }; + let stay_running = self.args.as_ref().is_some_and(|d| d.stay_running()); + let KeyEvent { + logical_key: Key::Named(key), + state: pressed, + .. + } = event + else { + return false; + }; + match (pressed, key) { + (ElementState::Pressed, NamedKey::Escape) => { + if !stay_running { + event_loop.exit(); + context.destroy(); + } + } + (ElementState::Pressed, NamedKey::Space) => { + let Some(c_img) = self.current_image.take() else { + return false; + }; + let Some(rect) = self.state.selection.selection else { + return false; + }; + let Ok(cropped) = crate::util::crop_image(&c_img.image, self.args.as_ref(), rect) + else { + eprintln!("Could not crop image"); + return false; + }; + match self.args.as_ref().and_then(|a| a.output_dir.as_ref()) { + Some(path) => { + if let Err(e) = + crate::util::save_selection(cropped, self.args.as_ref(), path) + { + eprintln!("{}", e); + }; + } + None => { + // Save to clipboard + if let Err(e) = crate::util::save_to_clipboard(&cropped) { + eprintln!("{}", e); + }; + } + } + if !stay_running { + event_loop.exit(); + } + } + (ElementState::Pressed, NamedKey::ArrowDown) => { + self.state.handle_move(Direction::Down); + } + (ElementState::Pressed, NamedKey::ArrowUp) => { + self.state.handle_move(Direction::Up); + } + (ElementState::Pressed, NamedKey::ArrowLeft) => { + self.state.handle_move(Direction::Left); + } + (ElementState::Pressed, NamedKey::ArrowRight) => { + self.state.handle_move(Direction::Right); + } + (ElementState::Pressed, NamedKey::Shift) => { + self.state.set_mode(SelectionMode::InverseResize); + } + (ElementState::Released, NamedKey::Shift | NamedKey::Control) => { + self.state.set_mode(SelectionMode::Move); + } + (ElementState::Pressed, NamedKey::Control) => { + self.state.set_mode(SelectionMode::Resize); + } + _ => {} + } + true + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.context.is_some() { + return; + } + let context = CleaveContext::new(event_loop).expect("Could not start context"); + if self + .args + .as_ref() + .is_some_and(|a| a.daemon_hotkey.is_some()) + { + self.state.start_listening(); + context.set_window_visibility(false); + } else { + let mut current_image = CurrentImage::capture_image( + self.args.as_ref().and_then(|a| a.monitor), + &context.graphics.device, + &context.graphics.queue, + ) + .expect("Could not capture image"); + let (w, h) = current_image.image.dimensions(); + let (w, h) = (w as f32, h as f32); + current_image.update_uniforms(context.total_time, &self.state.selection, (w, h)); + self.current_image = Some(current_image); + } + self.context = Some(context); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + let Some(context) = &mut self.context else { + return; + }; + + if id != context.window_id() { + return; + } + + // Check if we are in daemon mode + self.state.handle_event(&event); + if !self + .state + .get_listening(self.args.as_ref().and_then(|a| a.daemon_hotkey)) + { + return; + } + + match event { + WindowEvent::RedrawRequested => { + context.update(); + if let Some(c_img) = &mut self.current_image { + c_img.update_uniforms( + context.total_time, + &self.state.selection, + context.size(), + ); + context.draw(&c_img.bundle); + } + } + WindowEvent::KeyboardInput { event, .. } => { + if !self.execute_key_command(event, event_loop) { + return; + } + } + WindowEvent::MouseInput { state, button, .. } => match (state, button) { + (ElementState::Pressed, MouseButton::Left) => self.state.start_drag(), + (ElementState::Released, MouseButton::Left) => self.state.end_drag(), + (_, MouseButton::Right) => self.state.cancel_drag(), + _ => {} + }, + WindowEvent::CloseRequested => { + event_loop.exit(); + } + _ => {} + } + } +} diff --git a/src/app/state.rs b/src/app/state.rs new file mode 100644 index 0000000..09abc5f --- /dev/null +++ b/src/app/state.rs @@ -0,0 +1,147 @@ +use std::collections::HashSet; + +use glam::DVec2; +use wgpu::core::command::Rect; +use winit::{ + event::{KeyEvent, WindowEvent}, + keyboard::{KeyCode, ModifiersState, PhysicalKey}, +}; + +use crate::{ + keyboard::hotkey::HotKey, + selection::{ + modes::{Direction, SelectionMode}, + UserSelection, + }, +}; + +#[derive(Debug, Default)] +pub struct CleaveState { + pub selection: UserSelection, + mouse_position: DVec2, + mode: SelectionMode, + size: Option<(f32, f32)>, + mods: ModifiersState, + listening: bool, + pressed: HashSet, +} + +impl CleaveState { + pub fn start_listening(&mut self) { + self.listening = true; + } + + pub(crate) fn get_listening(&mut self, hotkey: Option) -> bool { + if !self.listening { + return true; + } + let Some(hotkey) = hotkey else { + return true; // Should never happen + }; + if self.pressed.iter().any(|k| hotkey.matches(self.mods, k)) { + self.stop_listening(); + return true; + } + false + } + + pub fn stop_listening(&mut self) { + self.listening = false; + } + + pub fn handle_event(&mut self, event: &WindowEvent) { + match event { + WindowEvent::KeyboardInput { event, .. } => { + self.handle_key(event); + } + WindowEvent::ModifiersChanged(mods) => self.mods = mods.state(), + WindowEvent::CursorMoved { position, .. } => { + self.mouse_position = DVec2::new(position.x, position.y); + if let Some(drag) = self.selection.drag.as_mut() { + drag.w = position.x as f32 - drag.x; + drag.h = position.y as f32 - drag.y; + } + } + _ => {} + } + println!("Pressed: {:?}, mods: {:?}", self.pressed, self.mods); + } + + pub fn handle_key(&mut self, event: &KeyEvent) { + if let PhysicalKey::Code(code) = event.physical_key { + if event.state.is_pressed() { + self.pressed.insert(code); + } else { + self.pressed.remove(&code); + } + } + } + + pub fn start_drag(&mut self) { + if let Some(drag) = self.selection.drag.as_mut() { + if drag.x != 0. && drag.y != 0. { + return; + } + }; + let mouse_pos = self.mouse_position.as_vec2(); + self.selection.drag = Some(Rect { + x: mouse_pos.x, + y: mouse_pos.y, + w: 0.0, + h: 0.0, + }); + } + + pub fn end_drag(&mut self) { + self.selection.selection = self.selection.drag.take(); + } + + pub fn cancel_drag(&mut self) { + self.selection.drag = None; + self.selection.selection = None; + } + + pub fn handle_move(&mut self, dir: Direction) -> Option<()> { + let (width, height) = self.size?; + + let (dx, dy) = match dir { + Direction::Up => (0.0, -1.0), + Direction::Down => (0.0, 1.0), + Direction::Left => (-1.0, 0.0), + Direction::Right => (1.0, 0.0), + }; + + let selection = self.selection.selection.as_mut()?; + + let (x_delta, y_delta) = match self.mode { + SelectionMode::Move => (dx, dy), + SelectionMode::InverseResize => (dx, dy), + SelectionMode::Resize => (0.0, 0.0), + }; + + if matches!( + self.mode, + SelectionMode::Move | SelectionMode::InverseResize + ) { + selection.x = (selection.x + x_delta).clamp(0.0, width); + selection.y = (selection.y + y_delta).clamp(0.0, height); + } + + if matches!(self.mode, SelectionMode::Move | SelectionMode::Resize) { + selection.w = (selection.w + dx).clamp(0.0, width); + selection.h = (selection.h + dy).clamp(0.0, height); + } + + Some(()) + } + + pub fn size(&mut self, size: (f32, f32)) -> &mut Self { + self.size = Some(size); + self + } + + pub fn set_mode(&mut self, mode: SelectionMode) -> &mut Self { + self.mode = mode; + self + } +} diff --git a/src/args.rs b/src/args.rs index 2376878..be4b50d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,19 +1,13 @@ use std::path::PathBuf; use image::ImageFormat; +use wgpu::core::command::Rect; -use crate::context::SelectionMode; +use crate::keyboard::hotkey::HotKey; +use crate::selection::modes::SelectionMode; -#[derive(Debug, Copy, Clone)] -pub struct Region { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, -} - -fn parse_region(s: &str) -> Result { - let coords: Vec = s +fn parse_region(s: &str) -> Result, String> { + let coords: Vec = s .split(',') .map(|s| s.parse().map_err(|_| "Invalid region format")) .collect::, _>>()?; @@ -22,11 +16,11 @@ fn parse_region(s: &str) -> Result { return Err("Region must be in format: x,y,width,height".into()); } - Ok(Region { + Ok(Rect { x: coords[0], y: coords[1], - width: coords[2], - height: coords[3], + w: coords[2], + h: coords[3], }) } @@ -83,13 +77,13 @@ pub struct Args { /// /// If not provided, the primary monitor is used #[arg(long)] - pub monitor: Option, // If not provided, the primary monitor is used + pub monitor: Option, // If not provided, the primary monitor is used /// Region to capture in the format: x,y,width,height /// /// If not provided, the entire screen is captured and the user is prompted to select a region /// If provided, the user is not prompted and the region is captured immediately #[arg(long, short='r', value_parser=parse_region)] - pub region: Option, + pub region: Option>, /// Filename for the captured image /// /// If not provided, the image is saved with a timestamp: 'cleave-YYYY-MM-DD-HH-MM-SS' @@ -141,7 +135,7 @@ pub struct Args { } impl Args { - pub fn verify(&self) -> Result<(), String> { + pub fn verify(self) -> anyhow::Result { if self.monitor_list && (self.output_dir.is_some() || self.image_format.is_some() @@ -150,33 +144,72 @@ impl Args { || self.scale.is_some() || self.daemon_hotkey.is_some()) { - return Err("Monitor list option cannot be used with other options".into()); + anyhow::bail!("Monitor list option cannot be used with other options"); } if let Some(scale) = self.scale { if scale <= 0.0 { - return Err("Scale factor must be greater than 0".into()); + anyhow::bail!("Scale factor must be greater than 0"); } } if let Some(region) = self.region { - if region.width == 0 || region.height == 0 { - return Err("Region width and height must be greater than 0".into()); + if region.w == 0. || region.h == 0. { + anyhow::bail!("Region width and height must be greater than 0"); } } if (self.image_format.is_some() || self.filename.is_some()) && self.output_dir.is_none() { - return Err( - "Output format and filename is only used when output directory is provided".into(), + anyhow::bail!( + "Output format and filename is only used when output directory is provided" ); } if self.persistent && self.daemon_hotkey.is_none() { - return Err("Persistent daemon mode can only be used with daemon hotkey".into()); + anyhow::bail!("Persistent daemon mode can only be used with daemon hotkey"); } if self.daemon_hotkey.is_some() && self.delay > 0 { - return Err("Delay cannot be used with daemon hotkey".into()); + anyhow::bail!("Delay cannot be used with daemon hotkey"); } + if let Some(hotkey) = &self.daemon_hotkey { + if hotkey.is_empty() { + anyhow::bail!("Hotkey cannot be empty"); + } + } + + let daemon_hotkey = self.daemon_hotkey.map(|s| s.parse()).transpose()?; - Ok(()) + Ok(Verified { + output_dir: self.output_dir, + image_format: self.image_format, + mode: self.mode, + monitor: self.monitor, + region: self.region, + filename: self.filename, + delay: self.delay, + monitor_list: self.monitor_list, + config_path: None, + scale: self.scale, + filter: self.filter, + daemon_hotkey, + persistent: self.persistent, + }) } +} + +pub struct Verified { + pub output_dir: Option, + pub image_format: Option, + pub mode: SelectionMode, + pub monitor: Option, + pub region: Option>, + pub filename: Option, + pub delay: u64, + pub monitor_list: bool, + pub config_path: Option, + pub scale: Option, + pub filter: Option, + pub daemon_hotkey: Option, + pub persistent: bool, +} +impl Verified { pub fn stay_running(&self) -> bool { self.daemon_hotkey.is_some() && self.persistent } diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index bf9976c..0000000 --- a/src/context.rs +++ /dev/null @@ -1,420 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::Context; -use arboard::ImageData; -use glam::{DVec2, Vec2}; -use image::{ - imageops::FilterType, GenericImageView, ImageBuffer, ImageError, ImageFormat, Rgba, RgbaImage, -}; -// use pixels::{Pixels, SurfaceTexture}; -use winit::{ - dpi::PhysicalSize, - window::{Icon, Window, WindowAttributes}, -}; - -// use crate::{graphics_bundle::GraphicsBundle, graphics_impl::Graphics}; -use cleave_graphics::prelude::*; - -use crate::args::Args; - -#[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub enum SelectionMode { - Move, // Move the selection - InverseResize, // Make the selection smaller - Resize, // Make the selection larger -} - -pub enum Direction { - Up, - Down, - Left, - Right, -} - -#[repr(C)] -#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, Default, Debug)] -pub struct SelectionUniforms { - screen_size: Vec2, - drag_start: Vec2, - drag_end: Vec2, - selection_start: Vec2, - selection_end: Vec2, - time: f32, - is_dragging: u32, // 0 = None, 1 = Dragging, 2 = Selected, 3 = Both -} - -impl std::fmt::Display for SelectionUniforms { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "size: {:?}, is_dragging: {}, drag_start: {:?}, drag_end: {:?}, selection_start: {:?}, selection_end: {:?}, time: {}", - self.screen_size, self.is_dragging, self.drag_start, self.drag_end, self.selection_start, self.selection_end, self.time) - } -} - -#[derive(Clone, Copy, Debug)] -pub struct Drag { - start: Vec2, - end: Option, -} - -#[derive(Clone, Copy, Debug)] -pub struct Selection { - start: Vec2, - end: Vec2, -} - -pub struct UserSelection { - drag: Option, - selection: Option, -} - -impl UserSelection { - fn new() -> Self { - Self { - drag: None, - selection: None, - } - } - - fn sel_coords(&self) -> Option<((u32, u32), (u32, u32))> { - let selection = self.selection.as_ref()?; - let (start_x, start_y) = (selection.start.x, selection.start.y); - let (end_x, end_y) = (selection.end.x, selection.end.y); - - let (min_x, max_x) = (start_x.min(end_x).ceil(), start_x.max(end_x).floor()); - let (min_y, max_y) = (start_y.min(end_y).ceil(), start_y.max(end_y).floor()); - Some(((min_x as u32, min_y as u32), (max_x as u32, max_y as u32))) - } - - // fn sel_dimensions(&self) -> Option<(f32, f32)> { - // let selection = self.selection.as_ref()?; - // let width = (selection.end.x - selection.start.x).abs(); - // let height = (selection.end.y - selection.start.y).abs(); - // Some((width, height)) - // } - - // fn get_ -} - -fn resize_image( - image: &RgbaImage, - scale: f32, - filter: FilterType, -) -> Result { - let new_width = (image.width() as f32 * scale).round() as u32; - let new_height = (image.height() as f32 * scale).round() as u32; - Ok(image::imageops::resize( - image, new_width, new_height, filter, - )) -} - -fn generate_output_path(dir: impl AsRef, filename: &str, format: ImageFormat) -> PathBuf { - let ext = format.extensions_str().first().copied().unwrap_or("png"); - - let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); - let filename = format!("{filename}-{}.{ext}", timestamp); - - dir.as_ref().join(filename) -} - -pub struct AppContext { - mouse_position: DVec2, - selection: UserSelection, - image: ImageBuffer, Vec>, - total_time: f32, - last_frame: std::time::Instant, - graphics: Graphics, - bundle: GraphicsBundle, - mode: SelectionMode, -} - -impl AppContext { - pub fn start_drag(&mut self) { - if let Some(drag) = self.selection.drag.as_mut() { - if drag.start != Vec2::ZERO { - return; - } - }; - self.selection.drag = Some(Drag { - start: self.mouse_position.as_vec2(), - end: Some(self.mouse_position.as_vec2()), - }); - } - - pub fn end_drag(&mut self) { - self.selection.selection = None; - if let Some(drag) = self.selection.drag.take() { - let end_pos = drag.end.unwrap_or(drag.start); // Use end if set, otherwise use start - self.selection.selection = Some(Selection { - start: drag.start, - end: end_pos, - }); - } - } - - pub fn cancel_drag(&mut self) { - self.selection.drag = None; - self.selection.selection = None; - } - - fn get_selection_data(&self, ax: u32, ay: u32, bx: u32, by: u32) -> RgbaImage { - let img = self.image.view(ax, ay, bx.abs_diff(ax), by.abs_diff(ay)); - img.to_image() - } - - fn save_to_clipboard(&self, image_data: RgbaImage) { - let mut clipboard = arboard::Clipboard::new().unwrap(); - let image_data = ImageData { - width: image_data.width() as usize, - height: image_data.height() as usize, - bytes: std::borrow::Cow::Owned(image_data.to_vec()), - }; - let _ = clipboard.set_image(image_data); - } - - pub fn save_selection(&self, args: Option<&Args>) -> anyhow::Result<()> { - let image_data = crop_image(&self.image, args, &self.selection)?; - - let Some(output) = args.and_then(|a| a.output_dir.as_deref()) else { - self.save_to_clipboard(image_data); - return Ok(()); - }; - - save_selection(image_data, args, &self.selection, output) - } - - pub fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> anyhow::Result { - let img = capture_screen()?; - let (width, height, rgba) = load_icon()?; - - let window = event_loop.create_window( - WindowAttributes::default() - .with_inner_size(PhysicalSize::new(img.width(), img.height())) - .with_title("Cleave") - .with_resizable(false) - .with_decorations(false) - .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) - .with_visible(false) - .with_window_icon(Some(Icon::from_rgba(rgba, width, height)?)), - )?; - - let graphics = Graphics::new(window, img.width(), img.height()); - let graphics = pollster::block_on(graphics)?; - - let bundle = GraphicsBundle::new( - img.clone().into(), - &graphics.device, - &graphics.queue, - wgpu::PrimitiveTopology::TriangleStrip, - graphics.config.format, - ); - - // graphics.window.set_visible(true); - - let _ = graphics - .window - .set_cursor_grab(winit::window::CursorGrabMode::Confined); - - // let surface_texture = SurfaceTexture::new(size.width, size.height, window.clone()); - // let pixels = Pixels::new(size.width, size.height, surface_texture)?; - - Ok(Self { - image: img, - bundle, - total_time: 0.0, - last_frame: std::time::Instant::now(), - selection: UserSelection::new(), - // window, - graphics, - mouse_position: DVec2::new(0.0, 0.0), - mode: SelectionMode::Resize, - }) - } - - pub fn handle_move(&mut self, dir: Direction) -> Option<()> { - let (dx, dy) = match dir { - Direction::Up => (0.0, -1.0), - Direction::Down => (0.0, 1.0), - Direction::Left => (-1.0, 0.0), - Direction::Right => (1.0, 0.0), - }; - - let selection = self.selection.selection.as_mut()?; - - match self.mode { - SelectionMode::Move => { - selection.start.x = (selection.start.x + dx).clamp(0.0, self.image.width() as f32); - selection.start.y = (selection.start.y + dy).clamp(0.0, self.image.height() as f32); - selection.end.x = (selection.end.x + dx).clamp(0.0, self.image.width() as f32); - selection.end.y = (selection.end.y + dy).clamp(0.0, self.image.height() as f32); - } - SelectionMode::Resize => { - selection.end.x = (selection.end.x + dx).clamp(0.0, self.image.width() as f32); - selection.end.y = (selection.end.y + dy).clamp(0.0, self.image.height() as f32); - } - SelectionMode::InverseResize => { - selection.start.x = (selection.start.x + dx).clamp(0.0, self.image.width() as f32); - selection.start.y = (selection.start.y + dy).clamp(0.0, self.image.height() as f32); - } - } - - Some(()) - } - - pub fn draw(&mut self) { - let time = self.last_frame.elapsed().as_secs_f32(); - self.total_time += time; - self.last_frame = std::time::Instant::now(); - - self.update_uniforms(); - self.bundle.update_buffer(&self.graphics.queue); - - let mut pass = match self.graphics.render() { - Ok(pass) => pass, - Err(err) => { - eprintln!("Error rendering frame: {:?}", err); - return; - } - }; - self.bundle.draw(&mut pass); - pass.finish(); - self.graphics.request_redraw(); - } - - fn update_uniforms(&mut self) { - self.bundle.uniforms.time = self.total_time; - self.bundle.uniforms.screen_size.x = self.image.width() as f32; - self.bundle.uniforms.screen_size.y = self.image.height() as f32; - - let drag = self.selection.drag; - let selection = self.selection.selection; - self.bundle.uniforms.is_dragging = match (drag, selection) { - (Some(d), Some(s)) if d.start != Vec2::ZERO || s.start != Vec2::ZERO => 3, - (Some(d), None) if d.start != Vec2::ZERO => 1, - (None, Some(s)) if s.start != Vec2::ZERO => 2, - _ => 0, - }; - - if let Some(drag) = drag { - self.bundle.uniforms.drag_start = drag.start; - self.bundle.uniforms.drag_end = drag.end.unwrap_or_default(); - } else { - self.bundle.uniforms.drag_start = Vec2::ZERO; - self.bundle.uniforms.drag_end = Vec2::ZERO; - }; - - if let Some(selection) = selection { - self.bundle.uniforms.selection_start = selection.start; - self.bundle.uniforms.selection_end = selection.end; - } else { - self.bundle.uniforms.selection_start = Vec2::ZERO; - self.bundle.uniforms.selection_end = Vec2::ZERO; - }; - } - - pub fn window_id(&self) -> winit::window::WindowId { - self.graphics.id() - } - - pub fn destroy(&self) { - self.graphics.window.set_minimized(true); - } - - pub fn set_window_visibility(&self, val: bool) { - self.graphics.set_visible(val); - } - - pub fn set_mode(&mut self, mode: SelectionMode) { - self.mode = mode - } - - pub fn update_mouse_position(&mut self, x: f64, y: f64) { - self.mouse_position = DVec2::new(x, y); - if let Some(drag) = self.selection.drag.as_mut() { - drag.end = Some(self.mouse_position.as_vec2()); - } - } - - pub fn set_args(mut self, args: &crate::args::Args) -> Option { - // self.bundle.uniforms.screen_size.x = args.width as f32; - // self.bundle.uniforms.screen_size.y = args.height as f32; - if let Some(region) = args.region { - self.selection.selection = Some(Selection { - start: Vec2::new(region.x as f32, region.y as f32), - end: Vec2::new( - (region.x + region.width) as f32, - (region.y + region.height) as f32, - ), - }); - if let Err(e) = self.save_selection(Some(args)) { - eprintln!("Error saving selection: {:?}", e); - }; - return None; - } - - Some(self) - } -} - -fn crop_image( - img: &RgbaImage, - args: Option<&Args>, - selection: &UserSelection, -) -> anyhow::Result { - let ((ax, ay), (bx, by)) = if let Some(region) = args.and_then(|a| a.region) { - ( - (region.x, region.y), - ((region.x + region.width), (region.y + region.height)), - ) - } else { - selection - .sel_coords() - .ok_or_else(|| anyhow::anyhow!("No selection made"))? - }; - - Ok(img.view(ax, ay, bx.abs_diff(ax), by.abs_diff(ay)).to_image()) -} - -fn save_selection( - mut image: RgbaImage, - args: Option<&Args>, - selection: &UserSelection, - save_path: impl AsRef, -) -> anyhow::Result<()> { - // Handle scaling if requested - if let Some(scale) = args.and_then(|a| a.scale) { - image = resize_image( - &image, - scale, - args.and_then(|a| a.filter).unwrap_or(FilterType::Nearest), - )?; - } - - // Generate filename and save - let format = args - .and_then(|f| f.image_format) - .unwrap_or(ImageFormat::Png); - let path = generate_output_path( - save_path, - args.and_then(|f| f.filename.as_deref()).unwrap_or("cleave"), - format, - ); - - Ok(image.save_with_format(path, format)?) -} - -fn capture_screen() -> anyhow::Result, Vec>> { - let monitor = xcap::Monitor::all()? - .into_iter() - .find(|m| m.is_primary()) - .with_context(|| "Could not get primary monitor")?; - let img = monitor.capture_image()?; - Ok(img) -} - -fn load_icon() -> Result<(u32, u32, Vec), anyhow::Error> { - let icon_bytes = include_bytes!("../icon.png"); - let rgba = image::load_from_memory(icon_bytes)?.to_rgba8(); - let (width, height) = rgba.dimensions(); - let rgba = rgba.into_raw(); - Ok((width, height, rgba)) -} diff --git a/src/hotkey.rs b/src/keyboard/hotkey.rs similarity index 74% rename from src/hotkey.rs rename to src/keyboard/hotkey.rs index d39ee8e..70de9e4 100644 --- a/src/hotkey.rs +++ b/src/keyboard/hotkey.rs @@ -1,14 +1,9 @@ -use device_query::{DeviceQuery, Keycode}; -pub use keyboard_types::{Code, Modifiers}; -use std::{borrow::Borrow, fmt::Display, hash::Hash, str::FromStr}; +use std::{borrow::Borrow, fmt::Display, str::FromStr}; -use crate::keycode_to_code; -use device_query::{DeviceEvents, DeviceState}; - -#[cfg(target_os = "macos")] -pub const CMD_OR_CTRL: Modifiers = Modifiers::SUPER; -#[cfg(not(target_os = "macos"))] -pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL; +use winit::{ + event::Modifiers, + keyboard::{KeyCode, ModifiersState}, +}; #[derive(thiserror::Error, Debug)] pub enum HotKeyParseError { @@ -23,86 +18,55 @@ pub enum HotKeyParseError { /// A keyboard shortcut that consists of an optional combination /// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and /// one key ([`Code`](crate::hotkey::Code)). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct HotKey { /// The hotkey modifiers. pub mods: Modifiers, /// The hotkey key. - pub key: Code, - /// The hotkey id. - pub id: u32, + pub key: KeyCode, } impl HotKey { /// Creates a new hotkey to define keyboard shortcuts throughout your application. /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] - pub fn new(mods: Option, key: Code) -> Self { - let mut mods = mods.unwrap_or_else(Modifiers::empty); - if mods.contains(Modifiers::META) { - mods.remove(Modifiers::META); - mods.insert(Modifiers::SUPER); - } - - Self { - mods, - key, - id: mods.bits() << 16 | key as u32, - } - } - - pub fn check(&self, codes: impl IntoIterator) -> bool { - let mut mods = Modifiers::empty(); - let mut code = None; - for key in codes { - match key { - Keycode::LShift | Keycode::RShift => mods |= Modifiers::SHIFT, - Keycode::LControl | Keycode::RControl => mods |= Modifiers::CONTROL, - Keycode::LAlt | Keycode::RAlt => mods |= Modifiers::ALT, - Keycode::LMeta | Keycode::RMeta => mods |= Modifiers::SUPER, - other => { - code = Some(other); - } - } - } - - if code.is_none() { - return false; - } - - self.matches(mods, keycode_to_code(code.unwrap())) - } - - /// Returns the id associated with this hotKey - /// which is a hash of the string represention of modifiers and key within this hotKey. - pub fn id(&self) -> u32 { - self.id + pub fn new(mods: Option, key: KeyCode) -> Self { + let mods = mods.unwrap_or_default(); + Self { mods, key } } /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. - pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { + pub fn matches( + &self, + modifiers: impl Borrow, + key: impl Borrow, + ) -> bool { // Should be a const but const bit_or doesn't work here. - let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; + let base_mods = ModifiersState::SHIFT + | ModifiersState::CONTROL + | ModifiersState::ALT + | ModifiersState::SUPER; let modifiers = modifiers.borrow(); let key = key.borrow(); - self.mods == *modifiers & base_mods && self.key == *key + dbg!((self.mods == (*modifiers & base_mods).into())) && dbg!((self.key == *key)) } /// Converts this hotkey into a string. pub fn into_string(self) -> String { let mut hotkey = String::new(); - if self.mods.contains(Modifiers::SHIFT) { - hotkey.push_str("shift+") + let state = self.mods.state(); + if state.contains(ModifiersState::SHIFT) { + hotkey.push_str("shift+"); } - if self.mods.contains(Modifiers::CONTROL) { - hotkey.push_str("control+") + if state.contains(ModifiersState::CONTROL) { + hotkey.push_str("control+"); } - if self.mods.contains(Modifiers::ALT) { - hotkey.push_str("alt+") + if state.contains(ModifiersState::ALT) { + hotkey.push_str("alt+"); } - if self.mods.contains(Modifiers::SUPER) { - hotkey.push_str("super+") + if state.contains(ModifiersState::SUPER) { + hotkey.push_str("super+"); } - hotkey.push_str(&self.key.to_string()); + hotkey.push_str(&format!("{:?}", self.key).to_lowercase()); hotkey } } @@ -142,7 +106,7 @@ impl TryFrom for HotKey { fn parse_hotkey(hotkey: &str) -> Result { let tokens = hotkey.split('+').collect::>(); - let mut mods = Modifiers::empty(); + let mut mods = ModifiersState::empty(); let mut key = None; match tokens.len() { @@ -171,24 +135,24 @@ fn parse_hotkey(hotkey: &str) -> Result { match token.to_uppercase().as_str() { "OPTION" | "ALT" => { - mods |= Modifiers::ALT; + mods |= ModifiersState::ALT; } "CONTROL" | "CTRL" => { - mods |= Modifiers::CONTROL; + mods |= ModifiersState::CONTROL; } "COMMAND" | "CMD" | "SUPER" => { - mods |= Modifiers::SUPER; + mods |= ModifiersState::SUPER; } "SHIFT" => { - mods |= Modifiers::SHIFT; + mods |= ModifiersState::SHIFT; } #[cfg(target_os = "macos")] "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { - mods |= Modifiers::SUPER; + mods |= ModifiersState::SUPER; } #[cfg(not(target_os = "macos"))] "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { - mods |= Modifiers::CONTROL; + mods |= ModifiersState::CONTROL; } _ => { key = Some(parse_key(token)?); @@ -199,13 +163,13 @@ fn parse_hotkey(hotkey: &str) -> Result { } Ok(HotKey::new( - Some(mods), + Some(mods.into()), key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, )) } -fn parse_key(key: &str) -> Result { - use Code::*; +fn parse_key(key: &str) -> Result { + use KeyCode::*; match key.to_uppercase().as_str() { "BACKQUOTE" | "`" => Ok(Backquote), "BACKSLASH" | "\\" => Ok(Backslash), @@ -306,8 +270,8 @@ fn parse_key(key: &str) -> Result { "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown), "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp), "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute), - "MEDIAPLAY" => Ok(MediaPlay), - "MEDIAPAUSE" => Ok(MediaPause), + // "MEDIAPLAY" => Ok(Media), + // "MEDIAPAUSE" => Ok(MediaPause), "MEDIAPLAYPAUSE" => Ok(MediaPlayPause), "MEDIASTOP" => Ok(MediaStop), "MEDIATRACKNEXT" => Ok(MediaTrackNext), diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs new file mode 100644 index 0000000..73459d5 --- /dev/null +++ b/src/keyboard/mod.rs @@ -0,0 +1 @@ +pub mod hotkey; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..03aa2a1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod app; +pub mod args; +pub mod keyboard; +pub mod selection; +pub mod util; diff --git a/src/main.rs b/src/main.rs index 48bce63..58b6ba1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,138 +2,14 @@ // However, when opened inside a terminal, the program should be able to output to the terminal. // #![windows_subsystem = "windows"] -use std::io::IsTerminal; - -use args::Args; use clap::Parser; -use device_query::Keycode; -use keyboard_types::Code; - -mod app; -mod args; -mod context; -mod hotkey; +use cleave::args::Args; +use std::io::IsTerminal; fn main() -> anyhow::Result<()> { - let stdout = std::io::stdout(); + let stdout = std::io::stdin(); let args = stdout.is_terminal().then(Args::parse); - let mut app = app::App::new(args); + let app = cleave::app::App::new(args)?; app.run()?; Ok(()) } - -fn keycode_to_code(keycode: Keycode) -> Code { - match keycode { - Keycode::Key0 => Code::Digit0, - Keycode::Key1 => Code::Digit1, - Keycode::Key2 => Code::Digit2, - Keycode::Key3 => Code::Digit3, - Keycode::Key4 => Code::Digit4, - Keycode::Key5 => Code::Digit5, - Keycode::Key6 => Code::Digit6, - Keycode::Key7 => Code::Digit7, - Keycode::Key8 => Code::Digit8, - Keycode::Key9 => Code::Digit9, - Keycode::A => Code::KeyA, - Keycode::B => Code::KeyB, - Keycode::C => Code::KeyC, - Keycode::D => Code::KeyD, - Keycode::E => Code::KeyE, - Keycode::F => Code::KeyF, - Keycode::G => Code::KeyG, - Keycode::H => Code::KeyH, - Keycode::I => Code::KeyI, - Keycode::J => Code::KeyJ, - Keycode::K => Code::KeyK, - Keycode::L => Code::KeyL, - Keycode::M => Code::KeyM, - Keycode::N => Code::KeyN, - Keycode::O => Code::KeyO, - Keycode::P => Code::KeyP, - Keycode::Q => Code::KeyQ, - Keycode::R => Code::KeyR, - Keycode::S => Code::KeyS, - Keycode::T => Code::KeyT, - Keycode::U => Code::KeyU, - Keycode::V => Code::KeyV, - Keycode::W => Code::KeyW, - Keycode::X => Code::KeyX, - Keycode::Y => Code::KeyY, - Keycode::Z => Code::KeyZ, - Keycode::F1 => Code::F1, - Keycode::F2 => Code::F2, - Keycode::F3 => Code::F3, - Keycode::F4 => Code::F4, - Keycode::F5 => Code::F5, - Keycode::F6 => Code::F6, - Keycode::F7 => Code::F7, - Keycode::F8 => Code::F8, - Keycode::F9 => Code::F9, - Keycode::F10 => Code::F10, - Keycode::F11 => Code::F11, - Keycode::F12 => Code::F12, - Keycode::F13 => Code::F13, - Keycode::F14 => Code::F14, - Keycode::F15 => Code::F15, - Keycode::F16 => Code::F16, - Keycode::F17 => Code::F17, - Keycode::F18 => Code::F18, - Keycode::F19 => Code::F19, - Keycode::F20 => Code::F20, - Keycode::Escape => Code::Escape, - Keycode::Space => Code::Space, - Keycode::LControl => Code::ControlLeft, - Keycode::RControl => Code::ControlRight, - Keycode::LShift => Code::ShiftLeft, - Keycode::RShift => Code::ShiftRight, - Keycode::LAlt => Code::AltLeft, - Keycode::RAlt => Code::AltRight, - Keycode::Command => Code::MetaLeft, - Keycode::LOption => Code::MetaLeft, - Keycode::ROption => Code::MetaRight, - Keycode::LMeta => Code::MetaLeft, - Keycode::RMeta => Code::MetaRight, - Keycode::Enter => Code::Enter, - Keycode::Up => Code::ArrowUp, - Keycode::Down => Code::ArrowDown, - Keycode::Left => Code::ArrowLeft, - Keycode::Right => Code::ArrowRight, - Keycode::Backspace => Code::Backspace, - Keycode::CapsLock => Code::CapsLock, - Keycode::Tab => Code::Tab, - Keycode::Home => Code::Home, - Keycode::End => Code::End, - Keycode::PageUp => Code::PageUp, - Keycode::PageDown => Code::PageDown, - Keycode::Insert => Code::Insert, - Keycode::Delete => Code::Delete, - Keycode::Numpad0 => Code::Numpad0, - Keycode::Numpad1 => Code::Numpad1, - Keycode::Numpad2 => Code::Numpad2, - Keycode::Numpad3 => Code::Numpad3, - Keycode::Numpad4 => Code::Numpad4, - Keycode::Numpad5 => Code::Numpad5, - Keycode::Numpad6 => Code::Numpad6, - Keycode::Numpad7 => Code::Numpad7, - Keycode::Numpad8 => Code::Numpad8, - Keycode::Numpad9 => Code::Numpad9, - Keycode::NumpadSubtract => Code::NumpadSubtract, - Keycode::NumpadAdd => Code::NumpadAdd, - Keycode::NumpadDivide => Code::NumpadDivide, - Keycode::NumpadMultiply => Code::NumpadMultiply, - Keycode::NumpadEquals => Code::NumpadEqual, - Keycode::NumpadEnter => Code::NumpadEnter, - Keycode::NumpadDecimal => Code::NumpadDecimal, - Keycode::Grave => Code::Backquote, - Keycode::Minus => Code::Minus, - Keycode::Equal => Code::Equal, - Keycode::LeftBracket => Code::BracketLeft, - Keycode::RightBracket => Code::BracketRight, - Keycode::BackSlash => Code::Backslash, - Keycode::Semicolon => Code::Semicolon, - Keycode::Apostrophe => Code::Quote, - Keycode::Comma => Code::Comma, - Keycode::Dot => Code::Period, - Keycode::Slash => Code::Slash, - } -} diff --git a/src/selection/mod.rs b/src/selection/mod.rs new file mode 100644 index 0000000..c70c63e --- /dev/null +++ b/src/selection/mod.rs @@ -0,0 +1,8 @@ +use wgpu::core::command::Rect; + +pub mod modes; +#[derive(Debug, Clone, Default)] +pub struct UserSelection { + pub drag: Option>, + pub selection: Option>, +} diff --git a/src/selection/modes.rs b/src/selection/modes.rs new file mode 100644 index 0000000..184b452 --- /dev/null +++ b/src/selection/modes.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)] +pub enum SelectionMode { + #[default] + Move, // Move the selection + InverseResize, // Make the selection smaller + Resize, // Make the selection larger +} + +pub enum Direction { + Up, + Down, + Left, + Right, +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..96a77ba --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,116 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Context; +use arboard::ImageData; +use image::{imageops::FilterType, GenericImageView, ImageFormat, RgbaImage}; +use wgpu::core::command::Rect; + +use crate::args::Verified; + +pub(crate) fn crop_and_save( + img: &RgbaImage, + args: Option<&Verified>, + rect: Rect, + output: impl AsRef, +) -> anyhow::Result<()> { + let img = crop_image(img, args, rect)?; + save_selection(img, args, output) +} + +pub(crate) fn crop_image( + img: &RgbaImage, + args: Option<&Verified>, + selection: Rect, +) -> anyhow::Result { + let rect = args.and_then(|a| a.region).unwrap_or(selection); + // Round this to be smaller rather than larger + let rect = Rect { + x: rect.x.ceil() as u32, + y: rect.y.ceil() as u32, + w: rect.w.floor() as u32, + h: rect.h.floor() as u32, + }; + let img = img.view(rect.x, rect.y, rect.w, rect.h); + Ok(img.to_image()) +} + +pub(crate) fn save_selection( + mut image: RgbaImage, + args: Option<&Verified>, + save_path: impl AsRef, +) -> anyhow::Result<()> { + // Handle scaling if requested + if let Some(scale) = args.and_then(|a| a.scale) { + image = resize_image( + &image, + scale, + args.and_then(|a| a.filter).unwrap_or(FilterType::Nearest), + )?; + } + + // Generate filename and save + let format = args + .and_then(|f| f.image_format) + .unwrap_or(ImageFormat::Png); + let path = generate_output_path( + save_path, + args.and_then(|f| f.filename.as_deref()).unwrap_or("cleave"), + format, + ); + + Ok(image.save_with_format(path, format)?) +} + +pub(crate) fn resize_image( + image: &RgbaImage, + scale: f32, + filter: FilterType, +) -> Result { + let new_width = (image.width() as f32 * scale).round() as u32; + let new_height = (image.height() as f32 * scale).round() as u32; + Ok(image::imageops::resize( + image, new_width, new_height, filter, + )) +} + +pub(crate) fn generate_output_path( + dir: impl AsRef, + filename: &str, + format: ImageFormat, +) -> PathBuf { + let ext = format.extensions_str().first().copied().unwrap_or("png"); + + let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); + let filename = format!("{filename}-{}.{ext}", timestamp); + + dir.as_ref().join(filename) +} + +pub(crate) fn save_to_clipboard(image_data: &RgbaImage) -> Result<(), arboard::Error> { + let mut clipboard = arboard::Clipboard::new()?; + let image_data = ImageData { + width: image_data.width() as usize, + height: image_data.height() as usize, + bytes: std::borrow::Cow::Borrowed(image_data.as_raw()), + }; + clipboard.set_image(image_data) +} + +pub(crate) fn load_icon() -> Result<(u32, u32, Vec), anyhow::Error> { + let icon_bytes = include_bytes!("../../icon.png"); + let rgba = image::load_from_memory(icon_bytes)?.to_rgba8(); + let (width, height) = rgba.dimensions(); + let rgba = rgba.into_raw(); + Ok((width, height, rgba)) +} + +pub(crate) fn capture_screen(monitor_id: Option) -> anyhow::Result { + let monitors = xcap::Monitor::all()?; + let img = monitors + .iter() + .find(|m| monitor_id.map_or(m.is_primary(), |id| m.id() == id)) + .or_else(|| monitors.iter().find(|m| m.is_primary())) + .with_context(|| "Could not select monitor")? + .capture_image()?; + Ok(img) +} From 3e5548ab7bf0af56363b145c0a0a3fbc6300d908 Mon Sep 17 00:00:00 2001 From: "kidkool850@gmail.com" Date: Wed, 6 Nov 2024 10:41:41 -0600 Subject: [PATCH 11/16] feat: Update draw method to accept optional GraphicsBundle and move input handling to state --- src/app/context.rs | 6 ++- src/app/mod.rs | 91 +++++++++++++++++++++++++++++----------------- src/app/state.rs | 15 ++++++-- 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/app/context.rs b/src/app/context.rs index bdd05f7..c2a5af2 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -77,7 +77,7 @@ impl CleaveContext { }) } - pub fn draw(&mut self, bundle: &GraphicsBundle) { + pub fn draw(&mut self, bundle: Option<&GraphicsBundle>) { let mut pass = match self.graphics.render() { Ok(pass) => pass, Err(err) => { @@ -85,7 +85,9 @@ impl CleaveContext { return; } }; - bundle.draw(&mut pass); + if let Some(bundle) = bundle { + bundle.draw(&mut pass); + } pass.finish(); self.graphics.request_redraw(); } diff --git a/src/app/mod.rs b/src/app/mod.rs index ea7294c..4a6aad9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -101,9 +101,11 @@ impl App { } (ElementState::Pressed, NamedKey::Space) => { let Some(c_img) = self.current_image.take() else { + eprintln!("No image to crop"); return false; }; let Some(rect) = self.state.selection.selection else { + eprintln!("No selection to crop"); return false; }; let Ok(cropped) = crate::util::crop_image(&c_img.image, self.args.as_ref(), rect) @@ -155,6 +157,37 @@ impl App { } true } + + fn handle_input( + &mut self, + event: &WindowEvent, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.state.handle_event(&event); + match event { + WindowEvent::KeyboardInput { event, .. } => { + self.execute_key_command(event.clone(), event_loop); + } + _ => {} + } + } + + fn capture_image(&mut self) { + let Some(context) = &self.context else { + return; + }; + let mut current_image = CurrentImage::capture_image( + self.args.as_ref().and_then(|a| a.monitor), + &context.graphics.device, + &context.graphics.queue, + ) + .expect("Could not capture image"); + let (w, h) = current_image.image.dimensions(); + let (w, h) = (w as f32, h as f32); + current_image.update_uniforms(context.total_time, &self.state.selection, (w, h)); + current_image.bundle.update_buffer(&context.graphics.queue); + self.current_image = Some(current_image); + } } impl ApplicationHandler for App { @@ -170,17 +203,11 @@ impl ApplicationHandler for App { { self.state.start_listening(); context.set_window_visibility(false); + event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); } else { - let mut current_image = CurrentImage::capture_image( - self.args.as_ref().and_then(|a| a.monitor), - &context.graphics.device, - &context.graphics.queue, - ) - .expect("Could not capture image"); - let (w, h) = current_image.image.dimensions(); - let (w, h) = (w as f32, h as f32); - current_image.update_uniforms(context.total_time, &self.state.selection, (w, h)); - self.current_image = Some(current_image); + self.context = Some(context); + self.capture_image(); + return; } self.context = Some(context); } @@ -191,46 +218,42 @@ impl ApplicationHandler for App { id: winit::window::WindowId, event: winit::event::WindowEvent, ) { - let Some(context) = &mut self.context else { - return; - }; - - if id != context.window_id() { - return; - } - + self.handle_input(&event, event_loop); // Check if we are in daemon mode - self.state.handle_event(&event); if !self .state .get_listening(self.args.as_ref().and_then(|a| a.daemon_hotkey)) { + event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); return; } - + if let Some(context) = &self.context { + if !context.graphics.is_visible().unwrap_or(true) && self.current_image.is_none() { + self.capture_image(); + } + } match event { WindowEvent::RedrawRequested => { + let Some(context) = &mut self.context else { + return; + }; + + if id != context.window_id() { + return; + } + context.update(); - if let Some(c_img) = &mut self.current_image { + let bund = self.current_image.as_mut().map(|c_img| { c_img.update_uniforms( context.total_time, &self.state.selection, context.size(), ); - context.draw(&c_img.bundle); - } - } - WindowEvent::KeyboardInput { event, .. } => { - if !self.execute_key_command(event, event_loop) { - return; - } + c_img.bundle.update_buffer(&context.graphics.queue); + &c_img.bundle + }); + context.draw(bund); } - WindowEvent::MouseInput { state, button, .. } => match (state, button) { - (ElementState::Pressed, MouseButton::Left) => self.state.start_drag(), - (ElementState::Released, MouseButton::Left) => self.state.end_drag(), - (_, MouseButton::Right) => self.state.cancel_drag(), - _ => {} - }, WindowEvent::CloseRequested => { event_loop.exit(); } diff --git a/src/app/state.rs b/src/app/state.rs index 09abc5f..ee0892b 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use glam::DVec2; use wgpu::core::command::Rect; use winit::{ - event::{KeyEvent, WindowEvent}, + event::{ElementState, KeyEvent, MouseButton, WindowEvent}, keyboard::{KeyCode, ModifiersState, PhysicalKey}, }; @@ -33,10 +33,10 @@ impl CleaveState { pub(crate) fn get_listening(&mut self, hotkey: Option) -> bool { if !self.listening { - return true; + return false; } let Some(hotkey) = hotkey else { - return true; // Should never happen + return false; }; if self.pressed.iter().any(|k| hotkey.matches(self.mods, k)) { self.stop_listening(); @@ -61,10 +61,17 @@ impl CleaveState { drag.w = position.x as f32 - drag.x; drag.h = position.y as f32 - drag.y; } + println!("Mouse position: {:?}", self.mouse_position); } + WindowEvent::MouseInput { state, button, .. } => match (state, button) { + (ElementState::Pressed, MouseButton::Left) => self.start_drag(), + (ElementState::Released, MouseButton::Left) => self.end_drag(), + (_, MouseButton::Right) => self.cancel_drag(), + _ => {} + }, _ => {} } - println!("Pressed: {:?}, mods: {:?}", self.pressed, self.mods); + // println!("Pressed: {:?}, mods: {:?}", self.pressed, self.mods); } pub fn handle_key(&mut self, event: &KeyEvent) { From 870a9f45c5ffc9a2d89a0ec066f6ad4fb7853764 Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Wed, 6 Nov 2024 17:13:19 -0600 Subject: [PATCH 12/16] attempt at using global hotkey, doesnt register when the app is out of focus --- Cargo.lock | 92 ++++++--- Cargo.toml | 1 + cleave-graphics/src/graphics_impl.rs | 4 +- src/app/context.rs | 38 ++-- src/app/current_image.rs | 5 +- src/app/mod.rs | 69 +++++-- src/app/state.rs | 58 ++---- src/args.rs | 2 +- src/keyboard/hotkey.rs | 294 --------------------------- src/keyboard/mod.rs | 1 - src/lib.rs | 1 - src/util/mod.rs | 40 ++-- 12 files changed, 178 insertions(+), 427 deletions(-) delete mode 100644 src/keyboard/hotkey.rs delete mode 100644 src/keyboard/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5fa339d..8d1d97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -142,15 +142,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arboard" @@ -275,12 +275,15 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitstream-io" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "block" @@ -375,9 +378,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.34" +version = "1.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" dependencies = [ "jobserver", "libc", @@ -483,6 +486,7 @@ dependencies = [ "clap", "cleave-graphics", "glam", + "global-hotkey", "image", "pollster", "thiserror", @@ -636,6 +640,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -853,13 +866,29 @@ dependencies = [ [[package]] name = "glam" -version = "0.29.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c9417a5dc586fc0c0cb67891170e59cc11e9dc79ba1c11ddd2c56ca3f3b90" +checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" dependencies = [ "bytemuck", ] +[[package]] +name = "global-hotkey" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d88f1be7bf4cd2e61623ce08e84be2dfa4eab458e5d632d3dab95f16c1f64" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "thiserror", + "windows-sys 0.59.0", + "x11-dl", +] + [[package]] name = "glow" version = "0.14.2" @@ -954,9 +983,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "heck" @@ -1001,9 +1030,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.4" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -1045,7 +1074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", ] [[package]] @@ -1120,6 +1149,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.6.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -1243,6 +1283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", + "rayon", ] [[package]] @@ -1966,6 +2007,7 @@ dependencies = [ "loop9", "quick-error", "rav1e", + "rayon", "rgb", ] @@ -2033,9 +2075,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ "bitflags 2.6.0", "errno", @@ -2268,18 +2310,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" dependencies = [ "proc-macro2", "quote", @@ -3275,9 +3317,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index 46b043b..8df4d34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ thiserror = { workspace = true } cleave-graphics = { path = "cleave-graphics" } clap = { version = "4.5.20", features = ["derive"] } chrono = "0.4.38" +global-hotkey = "0.6.3" [workspace.dependencies] anyhow = "1" diff --git a/cleave-graphics/src/graphics_impl.rs b/cleave-graphics/src/graphics_impl.rs index 1e4e77b..f0fd542 100644 --- a/cleave-graphics/src/graphics_impl.rs +++ b/cleave-graphics/src/graphics_impl.rs @@ -163,6 +163,7 @@ impl GraphicsPass<'_, '_, W> { pub fn finish(mut self) { drop(self.pass); let Some(encoder) = self.encoder.take() else { + eprintln!("No encoder available"); return; }; self.graphics.queue.submit(Some(encoder.finish())); @@ -179,13 +180,12 @@ fn find_config(surface: &Surface, adapter: &wgpu::Adapter, size: UVec2) -> Surfa .iter() .find(|f| f.is_srgb()) .unwrap_or(&surface_config.formats[0]); - SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: *format, width: size.x, height: size.y, - present_mode: wgpu::PresentMode::Immediate, + present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: 2, alpha_mode: surface_config.alpha_modes[0], view_formats: vec![], diff --git a/src/app/context.rs b/src/app/context.rs index c2a5af2..7bfd913 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -1,4 +1,3 @@ - use bytemuck::{Pod, Zeroable}; use glam::Vec2; use image::GenericImageView; @@ -37,8 +36,12 @@ pub struct CleaveContext { } impl CleaveContext { - pub fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> anyhow::Result { - let (width, height, rgba) = crate::util::load_icon()?; + pub fn new( + event_loop: &winit::event_loop::ActiveEventLoop, + width: u32, + height: u32, + ) -> anyhow::Result { + let (ico_width, ico_height, rgba) = crate::util::load_icon()?; let window = event_loop.create_window( WindowAttributes::default() .with_inner_size(PhysicalSize::new(width, height)) @@ -47,28 +50,20 @@ impl CleaveContext { .with_decorations(false) .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) .with_visible(false) - .with_window_icon(Some(Icon::from_rgba(rgba, width, height)?)), + .with_window_icon(Some(Icon::from_rgba(rgba, ico_width, ico_height)?)), )?; let graphics = Graphics::new(window, width, height); let graphics = pollster::block_on(graphics)?; - // let bundle = GraphicsBundle::new( - // img.clone().into(), - // &graphics.device, - // &graphics.queue, - // wgpu::PrimitiveTopology::TriangleStrip, - // graphics.config.format, - // ); - - graphics.window.set_visible(true); - - let _ = graphics + graphics .window - .set_cursor_grab(winit::window::CursorGrabMode::Confined); - - // let surface_texture = SurfaceTexture::new(size.width, size.height, window.clone()); - // let pixels = Pixels::new(size.width, size.height, surface_texture)?; + .set_cursor_grab(winit::window::CursorGrabMode::Confined) + .or_else(|_| { + graphics + .window + .set_cursor_grab(winit::window::CursorGrabMode::Locked) + })?; Ok(Self { total_time: 0.0, @@ -77,7 +72,10 @@ impl CleaveContext { }) } - pub fn draw(&mut self, bundle: Option<&GraphicsBundle>) { + pub fn draw( + &mut self, + bundle: Option<&GraphicsBundle>, + ) { let mut pass = match self.graphics.render() { Ok(pass) => pass, Err(err) => { diff --git a/src/app/current_image.rs b/src/app/current_image.rs index b46b7e6..31be4c9 100644 --- a/src/app/current_image.rs +++ b/src/app/current_image.rs @@ -16,6 +16,7 @@ impl CurrentImage { monitor: Option, device: &wgpu::Device, queue: &wgpu::Queue, + format: wgpu::TextureFormat, ) -> anyhow::Result { let img = crate::util::capture_screen(monitor)?; let bundle = GraphicsBundle::new( @@ -23,13 +24,15 @@ impl CurrentImage { device, queue, wgpu::PrimitiveTopology::TriangleStrip, - wgpu::TextureFormat::Bgra8UnormSrgb, + format, ); Ok(Self { image: img, bundle }) } pub fn update_uniforms(&mut self, time: f32, user: &UserSelection, (w, h): (f32, f32)) { self.bundle.uniforms.time = time; + + // println!("{}", self.bundle.uniforms); self.bundle.uniforms.screen_size.x = w; self.bundle.uniforms.screen_size.y = h; diff --git a/src/app/mod.rs b/src/app/mod.rs index 4a6aad9..8dd0240 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,15 +1,19 @@ +use std::sync::{Arc, Mutex}; + use crate::{ args::{Args, Verified}, selection::modes::{Direction, SelectionMode}, }; use current_image::CurrentImage; +use global_hotkey::{GlobalHotKeyEventReceiver, GlobalHotKeyManager}; use state::CleaveState; use winit::{ application::ApplicationHandler, event::{ElementState, KeyEvent, MouseButton, WindowEvent}, event_loop::EventLoop, keyboard::{Key, NamedKey}, + window::Window, }; mod context; @@ -20,9 +24,10 @@ use context::CleaveContext; pub struct App { args: Option, - current_image: Option, + current_image: Arc>>, context: Option, state: CleaveState, + _hk_manager: Option, } impl App { @@ -31,7 +36,8 @@ impl App { args: args.map(Args::verify).transpose()?, context: None, state: Default::default(), - current_image: None, + current_image: Default::default(), + _hk_manager: None, }) } @@ -72,6 +78,12 @@ impl App { return Ok(()); } + if let Some(hotkey) = self.args.as_ref().and_then(|a| a.daemon_hotkey) { + let manager = GlobalHotKeyManager::new()?; + manager.register(hotkey)?; + self._hk_manager = Some(manager); + } + self.start_loop() } @@ -94,13 +106,11 @@ impl App { }; match (pressed, key) { (ElementState::Pressed, NamedKey::Escape) => { - if !stay_running { - event_loop.exit(); - context.destroy(); - } + event_loop.exit(); + context.destroy(); } (ElementState::Pressed, NamedKey::Space) => { - let Some(c_img) = self.current_image.take() else { + let Some(c_img) = self.current_image.lock().unwrap().take() else { eprintln!("No image to crop"); return false; }; @@ -172,7 +182,7 @@ impl App { } } - fn capture_image(&mut self) { + fn capture_image(&self) { let Some(context) = &self.context else { return; }; @@ -180,28 +190,33 @@ impl App { self.args.as_ref().and_then(|a| a.monitor), &context.graphics.device, &context.graphics.queue, + context.graphics.config.format, ) .expect("Could not capture image"); let (w, h) = current_image.image.dimensions(); let (w, h) = (w as f32, h as f32); current_image.update_uniforms(context.total_time, &self.state.selection, (w, h)); current_image.bundle.update_buffer(&context.graphics.queue); - self.current_image = Some(current_image); + context.set_window_visibility(true); + *self.current_image.lock().unwrap() = Some(current_image); } } + impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { if self.context.is_some() { return; } - let context = CleaveContext::new(event_loop).expect("Could not start context"); + let size = crate::util::get_monitor(self.args.as_ref().and_then(|a| a.monitor)) + .expect("Could not find monitor!"); + let context = CleaveContext::new(event_loop, size.width(), size.height()) + .expect("Could not start context"); if self .args .as_ref() .is_some_and(|a| a.daemon_hotkey.is_some()) { - self.state.start_listening(); context.set_window_visibility(false); event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); } else { @@ -218,17 +233,26 @@ impl ApplicationHandler for App { id: winit::window::WindowId, event: winit::event::WindowEvent, ) { - self.handle_input(&event, event_loop); // Check if we are in daemon mode - if !self - .state - .get_listening(self.args.as_ref().and_then(|a| a.daemon_hotkey)) - { - event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); - return; + if self.args.as_ref().and_then(|a| a.daemon_hotkey).is_some() { + if self.current_image.lock().unwrap().is_none() { + if let Ok(ev) = global_hotkey::GlobalHotKeyEvent::receiver().try_recv() + // .is_ok() + { + println!("{:?}", ev); + event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); + return; + // self.capture_image(); + } else { + return; + } + } } + self.handle_input(&event, event_loop); if let Some(context) = &self.context { - if !context.graphics.is_visible().unwrap_or(true) && self.current_image.is_none() { + if !context.graphics.is_visible().unwrap_or(true) + && self.current_image.lock().unwrap().is_none() + { self.capture_image(); } } @@ -243,7 +267,8 @@ impl ApplicationHandler for App { } context.update(); - let bund = self.current_image.as_mut().map(|c_img| { + let mut c_img = self.current_image.lock().unwrap(); + let bund = c_img.as_mut().map(|c_img| { c_img.update_uniforms( context.total_time, &self.state.selection, @@ -251,8 +276,8 @@ impl ApplicationHandler for App { ); c_img.bundle.update_buffer(&context.graphics.queue); &c_img.bundle - }); - context.draw(bund); + }); + context.draw(bund); } WindowEvent::CloseRequested => { event_loop.exit(); diff --git a/src/app/state.rs b/src/app/state.rs index ee0892b..dd54d3e 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -7,12 +7,9 @@ use winit::{ keyboard::{KeyCode, ModifiersState, PhysicalKey}, }; -use crate::{ - keyboard::hotkey::HotKey, - selection::{ - modes::{Direction, SelectionMode}, - UserSelection, - }, +use crate::selection::{ + modes::{Direction, SelectionMode}, + UserSelection, }; #[derive(Debug, Default)] @@ -22,38 +19,14 @@ pub struct CleaveState { mode: SelectionMode, size: Option<(f32, f32)>, mods: ModifiersState, - listening: bool, - pressed: HashSet, } impl CleaveState { - pub fn start_listening(&mut self) { - self.listening = true; - } - - pub(crate) fn get_listening(&mut self, hotkey: Option) -> bool { - if !self.listening { - return false; - } - let Some(hotkey) = hotkey else { - return false; - }; - if self.pressed.iter().any(|k| hotkey.matches(self.mods, k)) { - self.stop_listening(); - return true; - } - false - } - - pub fn stop_listening(&mut self) { - self.listening = false; - } - pub fn handle_event(&mut self, event: &WindowEvent) { match event { - WindowEvent::KeyboardInput { event, .. } => { - self.handle_key(event); - } + // WindowEvent::KeyboardInput { event, .. } => { + // self.handle_key(event); + // } WindowEvent::ModifiersChanged(mods) => self.mods = mods.state(), WindowEvent::CursorMoved { position, .. } => { self.mouse_position = DVec2::new(position.x, position.y); @@ -61,7 +34,6 @@ impl CleaveState { drag.w = position.x as f32 - drag.x; drag.h = position.y as f32 - drag.y; } - println!("Mouse position: {:?}", self.mouse_position); } WindowEvent::MouseInput { state, button, .. } => match (state, button) { (ElementState::Pressed, MouseButton::Left) => self.start_drag(), @@ -74,15 +46,15 @@ impl CleaveState { // println!("Pressed: {:?}, mods: {:?}", self.pressed, self.mods); } - pub fn handle_key(&mut self, event: &KeyEvent) { - if let PhysicalKey::Code(code) = event.physical_key { - if event.state.is_pressed() { - self.pressed.insert(code); - } else { - self.pressed.remove(&code); - } - } - } + // pub fn handle_key(&mut self, event: &KeyEvent) { + // if let PhysicalKey::Code(code) = event.physical_key { + // if event.state.is_pressed() { + // self.pressed.insert(code); + // } else { + // self.pressed.remove(&code); + // } + // } + // } pub fn start_drag(&mut self) { if let Some(drag) = self.selection.drag.as_mut() { diff --git a/src/args.rs b/src/args.rs index be4b50d..d0f1b79 100644 --- a/src/args.rs +++ b/src/args.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use image::ImageFormat; use wgpu::core::command::Rect; -use crate::keyboard::hotkey::HotKey; +use global_hotkey::hotkey::HotKey; use crate::selection::modes::SelectionMode; fn parse_region(s: &str) -> Result, String> { diff --git a/src/keyboard/hotkey.rs b/src/keyboard/hotkey.rs deleted file mode 100644 index 70de9e4..0000000 --- a/src/keyboard/hotkey.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::{borrow::Borrow, fmt::Display, str::FromStr}; - -use winit::{ - event::Modifiers, - keyboard::{KeyCode, ModifiersState}, -}; - -#[derive(thiserror::Error, Debug)] -pub enum HotKeyParseError { - #[error("Couldn't recognize \"{0}\" as a valid key for hotkey, if you feel like it should be, please report this to https://github.com/tauri-apps/muda")] - UnsupportedKey(String), - #[error("Found empty token while parsing hotkey: {0}")] - EmptyToken(String), - #[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")] - InvalidFormat(String), -} - -/// A keyboard shortcut that consists of an optional combination -/// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and -/// one key ([`Code`](crate::hotkey::Code)). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct HotKey { - /// The hotkey modifiers. - pub mods: Modifiers, - /// The hotkey key. - pub key: KeyCode, -} - -impl HotKey { - /// Creates a new hotkey to define keyboard shortcuts throughout your application. - /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] - pub fn new(mods: Option, key: KeyCode) -> Self { - let mods = mods.unwrap_or_default(); - Self { mods, key } - } - - /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. - pub fn matches( - &self, - modifiers: impl Borrow, - key: impl Borrow, - ) -> bool { - // Should be a const but const bit_or doesn't work here. - let base_mods = ModifiersState::SHIFT - | ModifiersState::CONTROL - | ModifiersState::ALT - | ModifiersState::SUPER; - let modifiers = modifiers.borrow(); - let key = key.borrow(); - dbg!((self.mods == (*modifiers & base_mods).into())) && dbg!((self.key == *key)) - } - - /// Converts this hotkey into a string. - pub fn into_string(self) -> String { - let mut hotkey = String::new(); - let state = self.mods.state(); - if state.contains(ModifiersState::SHIFT) { - hotkey.push_str("shift+"); - } - if state.contains(ModifiersState::CONTROL) { - hotkey.push_str("control+"); - } - if state.contains(ModifiersState::ALT) { - hotkey.push_str("alt+"); - } - if state.contains(ModifiersState::SUPER) { - hotkey.push_str("super+"); - } - hotkey.push_str(&format!("{:?}", self.key).to_lowercase()); - hotkey - } -} - -impl Display for HotKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.into_string()) - } -} - -// HotKey::from_str is available to be backward -// compatible with tauri and it also open the option -// to generate hotkey from string -impl FromStr for HotKey { - type Err = HotKeyParseError; - fn from_str(hotkey_string: &str) -> Result { - parse_hotkey(hotkey_string) - } -} - -impl TryFrom<&str> for HotKey { - type Error = HotKeyParseError; - - fn try_from(value: &str) -> Result { - parse_hotkey(value) - } -} - -impl TryFrom for HotKey { - type Error = HotKeyParseError; - - fn try_from(value: String) -> Result { - parse_hotkey(&value) - } -} - -fn parse_hotkey(hotkey: &str) -> Result { - let tokens = hotkey.split('+').collect::>(); - - let mut mods = ModifiersState::empty(); - let mut key = None; - - match tokens.len() { - // single key hotkey - 1 => { - key = Some(parse_key(tokens[0])?); - } - // modifiers and key comobo hotkey - _ => { - for raw in tokens { - let token = raw.trim(); - - if token.is_empty() { - return Err(HotKeyParseError::EmptyToken(hotkey.to_string())); - } - - if key.is_some() { - // At this point we have parsed the modifiers and a main key, so by reaching - // this code, the function either received more than one main key or - // the hotkey is not in the right order - // examples: - // 1. "Ctrl+Shift+C+A" => only one main key should be allowd. - // 2. "Ctrl+C+Shift" => wrong order - return Err(HotKeyParseError::InvalidFormat(hotkey.to_string())); - } - - match token.to_uppercase().as_str() { - "OPTION" | "ALT" => { - mods |= ModifiersState::ALT; - } - "CONTROL" | "CTRL" => { - mods |= ModifiersState::CONTROL; - } - "COMMAND" | "CMD" | "SUPER" => { - mods |= ModifiersState::SUPER; - } - "SHIFT" => { - mods |= ModifiersState::SHIFT; - } - #[cfg(target_os = "macos")] - "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { - mods |= ModifiersState::SUPER; - } - #[cfg(not(target_os = "macos"))] - "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { - mods |= ModifiersState::CONTROL; - } - _ => { - key = Some(parse_key(token)?); - } - } - } - } - } - - Ok(HotKey::new( - Some(mods.into()), - key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, - )) -} - -fn parse_key(key: &str) -> Result { - use KeyCode::*; - match key.to_uppercase().as_str() { - "BACKQUOTE" | "`" => Ok(Backquote), - "BACKSLASH" | "\\" => Ok(Backslash), - "BRACKETLEFT" | "[" => Ok(BracketLeft), - "BRACKETRIGHT" | "]" => Ok(BracketRight), - "PAUSE" | "PAUSEBREAK" => Ok(Pause), - "COMMA" | "," => Ok(Comma), - "DIGIT0" | "0" => Ok(Digit0), - "DIGIT1" | "1" => Ok(Digit1), - "DIGIT2" | "2" => Ok(Digit2), - "DIGIT3" | "3" => Ok(Digit3), - "DIGIT4" | "4" => Ok(Digit4), - "DIGIT5" | "5" => Ok(Digit5), - "DIGIT6" | "6" => Ok(Digit6), - "DIGIT7" | "7" => Ok(Digit7), - "DIGIT8" | "8" => Ok(Digit8), - "DIGIT9" | "9" => Ok(Digit9), - "EQUAL" | "=" => Ok(Equal), - "KEYA" | "A" => Ok(KeyA), - "KEYB" | "B" => Ok(KeyB), - "KEYC" | "C" => Ok(KeyC), - "KEYD" | "D" => Ok(KeyD), - "KEYE" | "E" => Ok(KeyE), - "KEYF" | "F" => Ok(KeyF), - "KEYG" | "G" => Ok(KeyG), - "KEYH" | "H" => Ok(KeyH), - "KEYI" | "I" => Ok(KeyI), - "KEYJ" | "J" => Ok(KeyJ), - "KEYK" | "K" => Ok(KeyK), - "KEYL" | "L" => Ok(KeyL), - "KEYM" | "M" => Ok(KeyM), - "KEYN" | "N" => Ok(KeyN), - "KEYO" | "O" => Ok(KeyO), - "KEYP" | "P" => Ok(KeyP), - "KEYQ" | "Q" => Ok(KeyQ), - "KEYR" | "R" => Ok(KeyR), - "KEYS" | "S" => Ok(KeyS), - "KEYT" | "T" => Ok(KeyT), - "KEYU" | "U" => Ok(KeyU), - "KEYV" | "V" => Ok(KeyV), - "KEYW" | "W" => Ok(KeyW), - "KEYX" | "X" => Ok(KeyX), - "KEYY" | "Y" => Ok(KeyY), - "KEYZ" | "Z" => Ok(KeyZ), - "MINUS" | "-" => Ok(Minus), - "PERIOD" | "." => Ok(Period), - "QUOTE" | "'" => Ok(Quote), - "SEMICOLON" | ";" => Ok(Semicolon), - "SLASH" | "/" => Ok(Slash), - "BACKSPACE" => Ok(Backspace), - "CAPSLOCK" => Ok(CapsLock), - "ENTER" => Ok(Enter), - "SPACE" => Ok(Space), - "TAB" => Ok(Tab), - "DELETE" => Ok(Delete), - "END" => Ok(End), - "HOME" => Ok(Home), - "INSERT" => Ok(Insert), - "PAGEDOWN" => Ok(PageDown), - "PAGEUP" => Ok(PageUp), - "PRINTSCREEN" => Ok(PrintScreen), - "SCROLLLOCK" => Ok(ScrollLock), - "ARROWDOWN" | "DOWN" => Ok(ArrowDown), - "ARROWLEFT" | "LEFT" => Ok(ArrowLeft), - "ARROWRIGHT" | "RIGHT" => Ok(ArrowRight), - "ARROWUP" | "UP" => Ok(ArrowUp), - "NUMLOCK" => Ok(NumLock), - "NUMPAD0" | "NUM0" => Ok(Numpad0), - "NUMPAD1" | "NUM1" => Ok(Numpad1), - "NUMPAD2" | "NUM2" => Ok(Numpad2), - "NUMPAD3" | "NUM3" => Ok(Numpad3), - "NUMPAD4" | "NUM4" => Ok(Numpad4), - "NUMPAD5" | "NUM5" => Ok(Numpad5), - "NUMPAD6" | "NUM6" => Ok(Numpad6), - "NUMPAD7" | "NUM7" => Ok(Numpad7), - "NUMPAD8" | "NUM8" => Ok(Numpad8), - "NUMPAD9" | "NUM9" => Ok(Numpad9), - "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd), - "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal), - "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide), - "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter), - "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEqual), - "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply), - "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract), - "ESCAPE" | "ESC" => Ok(Escape), - "F1" => Ok(F1), - "F2" => Ok(F2), - "F3" => Ok(F3), - "F4" => Ok(F4), - "F5" => Ok(F5), - "F6" => Ok(F6), - "F7" => Ok(F7), - "F8" => Ok(F8), - "F9" => Ok(F9), - "F10" => Ok(F10), - "F11" => Ok(F11), - "F12" => Ok(F12), - "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown), - "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp), - "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute), - // "MEDIAPLAY" => Ok(Media), - // "MEDIAPAUSE" => Ok(MediaPause), - "MEDIAPLAYPAUSE" => Ok(MediaPlayPause), - "MEDIASTOP" => Ok(MediaStop), - "MEDIATRACKNEXT" => Ok(MediaTrackNext), - "MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious), - "F13" => Ok(F13), - "F14" => Ok(F14), - "F15" => Ok(F15), - "F16" => Ok(F16), - "F17" => Ok(F17), - "F18" => Ok(F18), - "F19" => Ok(F19), - "F20" => Ok(F20), - "F21" => Ok(F21), - "F22" => Ok(F22), - "F23" => Ok(F23), - "F24" => Ok(F24), - - _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), - } -} diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs deleted file mode 100644 index 73459d5..0000000 --- a/src/keyboard/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod hotkey; diff --git a/src/lib.rs b/src/lib.rs index 03aa2a1..716a12b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod app; pub mod args; -pub mod keyboard; pub mod selection; pub mod util; diff --git a/src/util/mod.rs b/src/util/mod.rs index 96a77ba..c8e6d44 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -7,15 +7,15 @@ use wgpu::core::command::Rect; use crate::args::Verified; -pub(crate) fn crop_and_save( - img: &RgbaImage, - args: Option<&Verified>, - rect: Rect, - output: impl AsRef, -) -> anyhow::Result<()> { - let img = crop_image(img, args, rect)?; - save_selection(img, args, output) -} +// pub(crate) fn crop_and_save( +// img: &RgbaImage, +// args: Option<&Verified>, +// rect: Rect, +// output: impl AsRef, +// ) -> anyhow::Result<()> { +// let img = crop_image(img, args, rect)?; +// save_selection(img, args, output) +// } pub(crate) fn crop_image( img: &RgbaImage, @@ -93,7 +93,10 @@ pub(crate) fn save_to_clipboard(image_data: &RgbaImage) -> Result<(), arboard::E height: image_data.height() as usize, bytes: std::borrow::Cow::Borrowed(image_data.as_raw()), }; - clipboard.set_image(image_data) + if let Err(e) = clipboard.set_image(image_data) { + eprintln!("Error setting image to clipboard: {:?}", e); + }; + Ok(()) } pub(crate) fn load_icon() -> Result<(u32, u32, Vec), anyhow::Error> { @@ -105,12 +108,15 @@ pub(crate) fn load_icon() -> Result<(u32, u32, Vec), anyhow::Error> { } pub(crate) fn capture_screen(monitor_id: Option) -> anyhow::Result { - let monitors = xcap::Monitor::all()?; - let img = monitors + get_monitor(monitor_id).and_then(|e| Ok(e.capture_image()?)) +} + +pub(crate) fn get_monitor(monitor_id: Option) -> anyhow::Result { + let mut monitors = xcap::Monitor::all()?; + let monitor = monitors .iter() - .find(|m| monitor_id.map_or(m.is_primary(), |id| m.id() == id)) - .or_else(|| monitors.iter().find(|m| m.is_primary())) - .with_context(|| "Could not select monitor")? - .capture_image()?; - Ok(img) + .position(|m| monitor_id.map_or(m.is_primary(), |id| m.id() == id)) + .or_else(|| monitors.iter().position(|m| m.is_primary())) + .with_context(|| "Could not select monitor")?; + Ok(monitors.swap_remove(monitor)) } From 6d51bb84775a219b17393b1a415d597e5f2f56a8 Mon Sep 17 00:00:00 2001 From: "kidkool850@gmail.com" Date: Thu, 7 Nov 2024 09:14:34 -0600 Subject: [PATCH 13/16] start working on daemon crate --- Cargo.lock | 115 ++++++++++--- Cargo.toml | 5 +- cleave-daemon/Cargo.toml | 10 ++ cleave-daemon/src/hotkey.rs | 296 +++++++++++++++++++++++++++++++++ cleave-daemon/src/main.rs | 68 ++++++++ cleave-daemon/src/modifiers.rs | 65 ++++++++ src/app/state.rs | 50 ++++-- 7 files changed, 564 insertions(+), 45 deletions(-) create mode 100644 cleave-daemon/Cargo.toml create mode 100644 cleave-daemon/src/hotkey.rs create mode 100644 cleave-daemon/src/main.rs create mode 100644 cleave-daemon/src/modifiers.rs diff --git a/Cargo.lock b/Cargo.lock index 8d1d97c..0e6d138 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -142,15 +142,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "arboard" @@ -281,9 +281,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.6.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" [[package]] name = "block" @@ -378,9 +378,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.36" +version = "1.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" dependencies = [ "jobserver", "libc", @@ -495,6 +495,16 @@ dependencies = [ "xcap", ] +[[package]] +name = "cleave-daemon" +version = "0.1.0" +dependencies = [ + "bitflags 2.6.0", + "clap", + "device_query", + "thiserror", +] + [[package]] name = "cleave-graphics" version = "0.1.0" @@ -697,6 +707,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "device_query" +version = "2.1.0" +dependencies = [ + "macos-accessibility-client", + "pkg-config", + "readkey", + "readmouse", + "windows 0.48.0", + "x11", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -866,9 +888,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.29.2" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" +checksum = "480c9417a5dc586fc0c0cb67891170e59cc11e9dc79ba1c11ddd2c56ca3f3b90" dependencies = [ "bytemuck", ] @@ -983,9 +1005,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -1030,9 +1052,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", "byteorder-lite", @@ -1074,7 +1096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.0", ] [[package]] @@ -1267,6 +1289,16 @@ dependencies = [ "imgref", ] +[[package]] +name = "macos-accessibility-client" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1283,7 +1315,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", - "rayon", ] [[package]] @@ -2007,7 +2038,6 @@ dependencies = [ "loop9", "quick-error", "rav1e", - "rayon", "rgb", ] @@ -2037,6 +2067,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "readkey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7677f98ca49bc9bb26e04c8abf80ba579e2cb98e8a384a0ff8128ad70670d249" + +[[package]] +name = "readmouse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be105c72a1e6a5a1198acee3d5b506a15676b74a02ecd78060042a447f408d94" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2075,9 +2117,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -2310,18 +2352,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" dependencies = [ "proc-macro2", "quote", @@ -2828,6 +2870,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.57.0" @@ -3229,6 +3280,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -3317,9 +3378,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.23" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index 8df4d34..e02f70f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["Exotik850"] [workspace] -members = ["cleave-graphics"] +members = [ "cleave-daemon","cleave-graphics"] [dependencies] @@ -19,12 +19,13 @@ pollster = { workspace = true } wgpu = { workspace = true } xcap = { workspace = true } thiserror = { workspace = true } +clap = { workspace = true } cleave-graphics = { path = "cleave-graphics" } -clap = { version = "4.5.20", features = ["derive"] } chrono = "0.4.38" global-hotkey = "0.6.3" [workspace.dependencies] +clap = { version = "4.5.20", features = ["derive"] } anyhow = "1" arboard = "3.4.1" thiserror = "1" diff --git a/cleave-daemon/Cargo.toml b/cleave-daemon/Cargo.toml new file mode 100644 index 0000000..de6630f --- /dev/null +++ b/cleave-daemon/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cleave-daemon" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { workspace = true } +thiserror = { workspace = true } +device_query = { version = "2.1.0", path = "../../device_query" } +bitflags = "2.6.0" diff --git a/cleave-daemon/src/hotkey.rs b/cleave-daemon/src/hotkey.rs new file mode 100644 index 0000000..9f7dc45 --- /dev/null +++ b/cleave-daemon/src/hotkey.rs @@ -0,0 +1,296 @@ +use std::{borrow::Borrow, fmt::Display, str::FromStr}; + +use device_query::Keycode; + +use crate::modifiers::Modifiers; + +#[derive(thiserror::Error, Debug)] +pub enum HotKeyParseError { + #[error("Couldn't recognize \"{0}\" as a valid key for hotkey, if you feel like it should be, please report this to https://github.com/tauri-apps/muda")] + UnsupportedKey(String), + #[error("Found empty token while parsing hotkey: {0}")] + EmptyToken(String), + #[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")] + InvalidFormat(String), +} + +/// A keyboard shortcut that consists of an optional combination +/// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and +/// one key ([`Code`](crate::hotkey::Code)). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HotKey { + /// The hotkey modifiers. + pub mods: Modifiers, + /// The hotkey key. + pub key: Keycode, +} + +impl HotKey { + /// Creates a new hotkey to define keyboard shortcuts throughout your application. + /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] + pub fn new(mods: Option, key: Keycode) -> Self { + let mods = mods.unwrap_or_default(); + Self { mods, key } + } + + /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. + pub fn matches( + &self, + modifiers: impl Borrow, + key: impl Borrow, + ) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = Modifiers::SHIFT + | Modifiers::CONTROL + | Modifiers::ALT + | Modifiers::SUPER; + let modifiers = modifiers.borrow(); + let key = key.borrow(); + (self.mods == (*modifiers & base_mods).into()) && (self.key == *key) + } + + /// Converts this hotkey into a string. + pub fn into_string(self) -> String { + let mut hotkey = String::new(); + let state = self.mods; + if state.contains(Modifiers::SHIFT) { + hotkey.push_str("shift+"); + } + if state.contains(Modifiers::CONTROL) { + hotkey.push_str("control+"); + } + if state.contains(Modifiers::ALT) { + hotkey.push_str("alt+"); + } + if state.contains(Modifiers::SUPER) { + hotkey.push_str("super+"); + } + hotkey.push_str(&format!("{:?}", self.key).to_lowercase()); + hotkey + } +} + +impl Display for HotKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.into_string()) + } +} + +// HotKey::from_str is available to be backward +// compatible with tauri and it also open the option +// to generate hotkey from string +impl FromStr for HotKey { + type Err = HotKeyParseError; + fn from_str(hotkey_string: &str) -> Result { + parse_hotkey(hotkey_string) + } +} + +impl TryFrom<&str> for HotKey { + type Error = HotKeyParseError; + + fn try_from(value: &str) -> Result { + parse_hotkey(value) + } +} + +impl TryFrom for HotKey { + type Error = HotKeyParseError; + + fn try_from(value: String) -> Result { + parse_hotkey(&value) + } +} + +fn parse_hotkey(hotkey: &str) -> Result { + let tokens = hotkey.split('+').collect::>(); + + let mut mods = Modifiers::empty(); + let mut key = None; + + match tokens.len() { + // single key hotkey + 1 => { + key = Some(parse_key(tokens[0])?); + } + // modifiers and key comobo hotkey + _ => { + for raw in tokens { + let token = raw.trim(); + + if token.is_empty() { + return Err(HotKeyParseError::EmptyToken(hotkey.to_string())); + } + + if key.is_some() { + // At this point we have parsed the modifiers and a main key, so by reaching + // this code, the function either received more than one main key or + // the hotkey is not in the right order + // examples: + // 1. "Ctrl+Shift+C+A" => only one main key should be allowd. + // 2. "Ctrl+C+Shift" => wrong order + return Err(HotKeyParseError::InvalidFormat(hotkey.to_string())); + } + + match token.to_uppercase().as_str() { + "OPTION" | "ALT" => { + mods |= Modifiers::ALT; + } + "CONTROL" | "CTRL" => { + mods |= Modifiers::CONTROL; + } + "COMMAND" | "CMD" | "SUPER" => { + mods |= Modifiers::SUPER; + } + "SHIFT" => { + mods |= Modifiers::SHIFT; + } + #[cfg(target_os = "macos")] + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + mods |= Modifiers::SUPER; + } + #[cfg(not(target_os = "macos"))] + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + mods |= Modifiers::CONTROL; + } + "META" => { + mods |= Modifiers::META; + } + _ => { + key = Some(parse_key(token)?); + } + } + } + } + } + + Ok(HotKey::new( + Some(mods.into()), + key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, + )) +} + +fn parse_key(key: &str) -> Result { + use Keycode::*; + match key.to_uppercase().as_str() { + "BACKQUOTE" | "`" => Ok(Grave), + "BACKSLASH" | "\\" => Ok(BackSlash), + "BRACKETLEFT" | "[" => Ok(LeftBracket), + "BRACKETRIGHT" | "]" => Ok(RightBracket), + // "PAUSE" | "PAUSEBREAK" => Ok(), + "COMMA" | "," => Ok(Comma), + "DIGIT0" | "0" => Ok(Key0), + "DIGIT1" | "1" => Ok(Key1), + "DIGIT2" | "2" => Ok(Key2), + "DIGIT3" | "3" => Ok(Key3), + "DIGIT4" | "4" => Ok(Key4), + "DIGIT5" | "5" => Ok(Key5), + "DIGIT6" | "6" => Ok(Key6), + "DIGIT7" | "7" => Ok(Key7), + "DIGIT8" | "8" => Ok(Key8), + "DIGIT9" | "9" => Ok(Key9), + "EQUAL" | "=" => Ok(Equal), + "KEYA" | "A" => Ok(A), + "KEYB" | "B" => Ok(B), + "KEYC" | "C" => Ok(C), + "KEYD" | "D" => Ok(D), + "KEYE" | "E" => Ok(E), + "KEYF" | "F" => Ok(F), + "KEYG" | "G" => Ok(G), + "KEYH" | "H" => Ok(H), + "KEYI" | "I" => Ok(I), + "KEYJ" | "J" => Ok(J), + "KEYK" | "K" => Ok(K), + "KEYL" | "L" => Ok(L), + "KEYM" | "M" => Ok(M), + "KEYN" | "N" => Ok(N), + "KEYO" | "O" => Ok(O), + "KEYP" | "P" => Ok(P), + "KEYQ" | "Q" => Ok(Q), + "KEYR" | "R" => Ok(R), + "KEYS" | "S" => Ok(S), + "KEYT" | "T" => Ok(T), + "KEYU" | "U" => Ok(U), + "KEYV" | "V" => Ok(V), + "KEYW" | "W" => Ok(W), + "KEYX" | "X" => Ok(X), + "KEYY" | "Y" => Ok(Y), + "KEYZ" | "Z" => Ok(Z), + "MINUS" | "-" => Ok(Minus), + "PERIOD" | "." => Ok(Dot), + "QUOTE" | "'" => Ok(Apostrophe), + "SEMICOLON" | ";" => Ok(Semicolon), + "SLASH" | "/" => Ok(Slash), + "BACKSPACE" => Ok(Backspace), + "CAPSLOCK" => Ok(CapsLock), + "ENTER" => Ok(Enter), + "SPACE" => Ok(Space), + "TAB" => Ok(Tab), + "DELETE" => Ok(Delete), + "END" => Ok(End), + "HOME" => Ok(Home), + "INSERT" => Ok(Insert), + "PAGEDOWN" => Ok(PageDown), + "PAGEUP" => Ok(PageUp), + // "PRINTSCREEN" => Ok(Keycode::), + // "SCROLLLOCK" => Ok(ScrollLock), + "ARROWDOWN" | "DOWN" => Ok(Down), + "ARROWLEFT" | "LEFT" => Ok(Left), + "ARROWRIGHT" | "RIGHT" => Ok(Right), + "ARROWUP" | "UP" => Ok(Up), + // "NUMLOCK" => Ok(), + "NUMPAD0" | "NUM0" => Ok(Numpad0), + "NUMPAD1" | "NUM1" => Ok(Numpad1), + "NUMPAD2" | "NUM2" => Ok(Numpad2), + "NUMPAD3" | "NUM3" => Ok(Numpad3), + "NUMPAD4" | "NUM4" => Ok(Numpad4), + "NUMPAD5" | "NUM5" => Ok(Numpad5), + "NUMPAD6" | "NUM6" => Ok(Numpad6), + "NUMPAD7" | "NUM7" => Ok(Numpad7), + "NUMPAD8" | "NUM8" => Ok(Numpad8), + "NUMPAD9" | "NUM9" => Ok(Numpad9), + "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd), + "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal), + "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide), + "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter), + "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEquals), + "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply), + "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract), + "ESCAPE" | "ESC" => Ok(Escape), + "F1" => Ok(F1), + "F2" => Ok(F2), + "F3" => Ok(F3), + "F4" => Ok(F4), + "F5" => Ok(F5), + "F6" => Ok(F6), + "F7" => Ok(F7), + "F8" => Ok(F8), + "F9" => Ok(F9), + "F10" => Ok(F10), + "F11" => Ok(F11), + "F12" => Ok(F12), + // "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(Keycode::), + // "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp), + // "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute), + // "MEDIAPLAY" => Ok(Media), + // "MEDIAPAUSE" => Ok(MediaPause), + // "MEDIAPLAYPAUSE" => Ok(MediaPlayPause), + // "MEDIASTOP" => Ok(MediaStop), + // "MEDIATRACKNEXT" => Ok(MediaTrackNext), + // "MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious), + "F13" => Ok(F13), + "F14" => Ok(F14), + "F15" => Ok(F15), + "F16" => Ok(F16), + "F17" => Ok(F17), + "F18" => Ok(F18), + "F19" => Ok(F19), + "F20" => Ok(F20), + // "F21" => Ok(F21), + // "F22" => Ok(F22), + // "F23" => Ok(F23), + // "F24" => Ok(F24), + + _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), + } +} \ No newline at end of file diff --git a/cleave-daemon/src/main.rs b/cleave-daemon/src/main.rs new file mode 100644 index 0000000..3ce32cc --- /dev/null +++ b/cleave-daemon/src/main.rs @@ -0,0 +1,68 @@ +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, + time::Duration, +}; + +use clap::Parser; +use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; +use hotkey::HotKey; +use modifiers::Modifiers; +mod hotkey; +mod modifiers; + +#[derive(clap::Parser, Debug)] +struct Args { + /// The amount of time to sleep between each event loop iteration + #[arg(short, long, default_value = "100")] + sleep: u64, + + /// The hotkey to use to start the event loop + #[arg(short = 'm', long, default_value = "Shift+X")] + hotkey: HotKey, +} + +#[derive(Debug)] +struct KeyAction { + key: Keycode, + pressed: bool, +} + +fn main() { + let args: Args = Args::parse(); + let handler = DeviceEventsHandler::new(Duration::from_millis(args.sleep)) + .expect("Could not create event loop"); + let (tx, rx) = std::sync::mpsc::channel(); + let ta = tx.clone(); + let _g1 = handler.on_key_down(move |key| { + ta.send(KeyAction { key, pressed: true }).unwrap(); + }); + let tb = tx; + let _g2 = handler.on_key_up(move |key| { + tb.send(KeyAction { + key, + pressed: false, + }) + .unwrap(); + }); + + let mut pressed = HashSet::new(); + let mut mods = Modifiers::empty(); + for event in rx.iter() { + if let Some(m) = Modifiers::from_keycode(event.key) { + if event.pressed { + mods |= m; + } else { + mods &= !m; + } + } + if event.pressed { + pressed.insert(event.key); + } else { + pressed.remove(&event.key); + } + if args.hotkey.matches(mods, event.key) && event.pressed { + print!("RUN"); + } + } +} diff --git a/cleave-daemon/src/modifiers.rs b/cleave-daemon/src/modifiers.rs new file mode 100644 index 0000000..068b7bd --- /dev/null +++ b/cleave-daemon/src/modifiers.rs @@ -0,0 +1,65 @@ +//! Modifier key data. +//! +//! Modifier keys like Shift and Control alter the character value +//! and are used in keyboard shortcuts. +//! +//! Use the constants to match for combinations of the modifier keys. + +use device_query::Keycode; + +bitflags::bitflags! { + /// Pressed modifier keys. + /// + /// Specification: + /// + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct Modifiers: u32 { + const ALT = 0x01; + const ALT_GRAPH = 0x2; + const CAPS_LOCK = 0x4; + const CONTROL = 0x8; + const FN = 0x10; + const FN_LOCK = 0x20; + const META = 0x40; + const NUM_LOCK = 0x80; + const SCROLL_LOCK = 0x100; + const SHIFT = 0x200; + const SYMBOL = 0x400; + const SYMBOL_LOCK = 0x800; + const HYPER = 0x1000; + const SUPER = 0x2000; + } +} + +impl Modifiers { + /// Return `true` if a shift key is pressed. + pub fn shift(&self) -> bool { + self.contains(Modifiers::SHIFT) + } + + /// Return `true` if a control key is pressed. + pub fn ctrl(&self) -> bool { + self.contains(Modifiers::CONTROL) + } + + /// Return `true` if an alt key is pressed. + pub fn alt(&self) -> bool { + self.contains(Modifiers::ALT) + } + + /// Return `true` if a meta key is pressed. + pub fn meta(&self) -> bool { + self.contains(Modifiers::META) + } + + pub fn from_keycode(key: Keycode) -> Option { + match key { + Keycode::LShift | Keycode::RShift => Some(Modifiers::SHIFT), + Keycode::LControl | Keycode::RControl => Some(Modifiers::CONTROL), + Keycode::LAlt | Keycode::RAlt => Some(Modifiers::ALT), + Keycode::LMeta | Keycode::RMeta => Some(Modifiers::META), + _ => None, + } + } +} \ No newline at end of file diff --git a/src/app/state.rs b/src/app/state.rs index dd54d3e..6968d34 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -24,9 +24,9 @@ pub struct CleaveState { impl CleaveState { pub fn handle_event(&mut self, event: &WindowEvent) { match event { - // WindowEvent::KeyboardInput { event, .. } => { - // self.handle_key(event); - // } + WindowEvent::KeyboardInput { event, .. } => { + self.handle_key(event); + } WindowEvent::ModifiersChanged(mods) => self.mods = mods.state(), WindowEvent::CursorMoved { position, .. } => { self.mouse_position = DVec2::new(position.x, position.y); @@ -46,15 +46,35 @@ impl CleaveState { // println!("Pressed: {:?}, mods: {:?}", self.pressed, self.mods); } - // pub fn handle_key(&mut self, event: &KeyEvent) { - // if let PhysicalKey::Code(code) = event.physical_key { - // if event.state.is_pressed() { - // self.pressed.insert(code); - // } else { - // self.pressed.remove(&code); - // } - // } - // } + pub fn handle_key(&mut self, event: &KeyEvent) { + let PhysicalKey::Code(code) = event.physical_key else { + return; + }; + match (event.state, code) { + (ElementState::Pressed, KeyCode::ArrowUp) => { + self.handle_move(Direction::Up); + } + (ElementState::Pressed, KeyCode::ArrowDown) => { + self.handle_move(Direction::Down); + } + (ElementState::Pressed, KeyCode::ArrowLeft) => { + self.handle_move(Direction::Left); + } + (ElementState::Pressed, KeyCode::ArrowRight) => { + self.handle_move(Direction::Right); + } + (ElementState::Pressed, KeyCode::ShiftLeft) => { + self.set_mode(SelectionMode::InverseResize); + } + (ElementState::Released, KeyCode::ShiftLeft | KeyCode::ControlLeft) => { + self.set_mode(SelectionMode::Move); + } + (ElementState::Pressed, KeyCode::ControlLeft) => { + self.set_mode(SelectionMode::Resize); + } + _ => {} + }; + } pub fn start_drag(&mut self) { if let Some(drag) = self.selection.drag.as_mut() { @@ -82,16 +102,14 @@ impl CleaveState { pub fn handle_move(&mut self, dir: Direction) -> Option<()> { let (width, height) = self.size?; - + let selection = self.selection.selection.as_mut()?; + let (dx, dy) = match dir { Direction::Up => (0.0, -1.0), Direction::Down => (0.0, 1.0), Direction::Left => (-1.0, 0.0), Direction::Right => (1.0, 0.0), }; - - let selection = self.selection.selection.as_mut()?; - let (x_delta, y_delta) = match self.mode { SelectionMode::Move => (dx, dy), SelectionMode::InverseResize => (dx, dy), From 05fbaca1eb9ba1c9f6f9d0cf7e062a57e733ccdf Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Thu, 7 Nov 2024 19:09:25 -0600 Subject: [PATCH 14/16] remove unused daemon file --- src/daemon.rs | 73 --------------------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 src/daemon.rs diff --git a/src/daemon.rs b/src/daemon.rs deleted file mode 100644 index 3d333cc..0000000 --- a/src/daemon.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; - -use crate::hotkey::HotKey; - -#[derive(Debug)] -pub(crate) struct KeyAction { - key: Keycode, - pressed: bool, -} - -pub(crate) struct Daemon { - pressed: Arc>>, - - // pub rx: std::sync::mpsc::Receiver, - _event_handler: DeviceEventsHandler, - _key_up: device_query::CallbackGuard, - _key_down: device_query::CallbackGuard, - hotkey: HotKey, - listening: bool, -} - -impl Daemon { - pub fn start(hotkey: HotKey) -> Self { - let _event_handler = - device_query::DeviceEventsHandler::new(std::time::Duration::from_millis(10)) - .expect("Could not start event loop"); - let pressed: Arc>> = Default::default(); - let pa: Arc<_> = pressed.clone(); - let _key_down = _event_handler.on_key_down(move |key| { - let mut pressed = pa.write().unwrap(); - pressed.push(key); - }); - let pb: Arc<_> = pressed.clone(); - let _key_up = _event_handler.on_key_up(move |key| { - let mut pressed = pb.write().unwrap(); - pressed.retain(|&k| k != key); - }); - Daemon { - _event_handler, - _key_up, - _key_down, - // rx, - pressed, - hotkey, - listening: true, - } - } - - fn is_pressed(&self) -> bool { - let pressed = dbg!(self.pressed.read().unwrap()); - self.hotkey.check(pressed.iter().copied()) - } - - fn clear_buffer(&mut self) { - let mut pressed = self.pressed.write().unwrap(); - pressed.clear(); - } - - pub fn start_listening(&mut self) { - self.listening = true; - } - - pub fn get_pressed(&mut self) -> bool { - let val = self.listening && self.is_pressed(); - if val { - self.clear_buffer(); - self.listening = false; - } - val - } -} From c22fcd2c3548f28b66f7921c4b1477c0115c6531 Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Thu, 7 Nov 2024 19:10:35 -0600 Subject: [PATCH 15/16] fmt, start implementing daemon --- cleave-daemon/Cargo.toml | 5 +++- cleave-daemon/src/hotkey.rs | 25 +++++----------- cleave-daemon/src/lib.rs | 5 ++++ cleave-daemon/src/main.rs | 51 +++++++++++++++++++++++--------- cleave-daemon/src/modifiers.rs | 53 +++++++++++++++++----------------- 5 files changed, 80 insertions(+), 59 deletions(-) create mode 100644 cleave-daemon/src/lib.rs diff --git a/cleave-daemon/Cargo.toml b/cleave-daemon/Cargo.toml index de6630f..9989152 100644 --- a/cleave-daemon/Cargo.toml +++ b/cleave-daemon/Cargo.toml @@ -6,5 +6,8 @@ edition = "2021" [dependencies] clap = { workspace = true } thiserror = { workspace = true } -device_query = { version = "2.1.0", path = "../../device_query" } +anyhow = { workspace = true } +device_query = { git = "https://github.com/exotik850/device_query", branch = "refactor" } bitflags = "2.6.0" +fd-lock = "4.0.2" +dirs = "5.0.1" diff --git a/cleave-daemon/src/hotkey.rs b/cleave-daemon/src/hotkey.rs index 9f7dc45..a873482 100644 --- a/cleave-daemon/src/hotkey.rs +++ b/cleave-daemon/src/hotkey.rs @@ -1,8 +1,7 @@ use std::{borrow::Borrow, fmt::Display, str::FromStr}; -use device_query::Keycode; - -use crate::modifiers::Modifiers; +pub use crate::modifiers::Modifiers; +pub use device_query::Keycode; #[derive(thiserror::Error, Debug)] pub enum HotKeyParseError { @@ -34,19 +33,12 @@ impl HotKey { } /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey. - pub fn matches( - &self, - modifiers: impl Borrow, - key: impl Borrow, - ) -> bool { + pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { // Should be a const but const bit_or doesn't work here. - let base_mods = Modifiers::SHIFT - | Modifiers::CONTROL - | Modifiers::ALT - | Modifiers::SUPER; + let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; let modifiers = modifiers.borrow(); let key = key.borrow(); - (self.mods == (*modifiers & base_mods).into()) && (self.key == *key) + (self.mods == (*modifiers & base_mods)) && (self.key == *key) } /// Converts this hotkey into a string. @@ -154,7 +146,7 @@ fn parse_hotkey(hotkey: &str) -> Result { mods |= Modifiers::CONTROL; } "META" => { - mods |= Modifiers::META; + mods |= Modifiers::META; } _ => { key = Some(parse_key(token)?); @@ -165,7 +157,7 @@ fn parse_hotkey(hotkey: &str) -> Result { } Ok(HotKey::new( - Some(mods.into()), + Some(mods), key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?, )) } @@ -290,7 +282,6 @@ fn parse_key(key: &str) -> Result { // "F22" => Ok(F22), // "F23" => Ok(F23), // "F24" => Ok(F24), - _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())), } -} \ No newline at end of file +} diff --git a/cleave-daemon/src/lib.rs b/cleave-daemon/src/lib.rs new file mode 100644 index 0000000..86b17d3 --- /dev/null +++ b/cleave-daemon/src/lib.rs @@ -0,0 +1,5 @@ +pub use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; +pub use hotkey::HotKey; +pub use modifiers::Modifiers; +mod hotkey; +mod modifiers; diff --git a/cleave-daemon/src/main.rs b/cleave-daemon/src/main.rs index 3ce32cc..4405d61 100644 --- a/cleave-daemon/src/main.rs +++ b/cleave-daemon/src/main.rs @@ -1,25 +1,20 @@ -use std::{ - collections::HashSet, - sync::{Arc, Mutex}, - time::Duration, -}; - use clap::Parser; -use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; -use hotkey::HotKey; -use modifiers::Modifiers; -mod hotkey; -mod modifiers; +use cleave_daemon::{DeviceEvents, DeviceEventsHandler, HotKey, Keycode, Modifiers}; +use std::{collections::HashSet, time::Duration}; #[derive(clap::Parser, Debug)] struct Args { - /// The amount of time to sleep between each event loop iteration + /// The amount of time to sleep between each event loop iteration in milliseconds #[arg(short, long, default_value = "100")] sleep: u64, /// The hotkey to use to start the event loop #[arg(short = 'm', long, default_value = "Shift+X")] hotkey: HotKey, + + /// Whether or not to stay alive after the hotkey is pressed + #[arg(short, long)] + persist: bool, } #[derive(Debug)] @@ -28,7 +23,8 @@ struct KeyAction { pressed: bool, } -fn main() { +fn main() -> anyhow::Result<()> { + let config_path = dirs::config_dir().expect("Could not find config directory"); let args: Args = Args::parse(); let handler = DeviceEventsHandler::new(Duration::from_millis(args.sleep)) .expect("Could not create event loop"); @@ -62,7 +58,34 @@ fn main() { pressed.remove(&event.key); } if args.hotkey.matches(mods, event.key) && event.pressed { - print!("RUN"); + run_cleave()?; + pressed.clear(); + mods = Modifiers::empty(); + if !args.persist { + break; + } } } + Ok(()) +} + +fn run_cleave() -> anyhow::Result<()> { + let mut cleave = std::process::Command::new("cleave"); + cleave.args(std::env::args().skip(1)); + match cleave.status() { + Ok(status) => { + if !status.success() { + anyhow::bail!("cleave exited with status: {}", status); + } + } + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + anyhow::bail!("Could not find cleave in PATH"); + } + _ => { + anyhow::bail!("Could not start cleave: {}", e); + } + }, + }; + Ok(()) } diff --git a/cleave-daemon/src/modifiers.rs b/cleave-daemon/src/modifiers.rs index 068b7bd..2c33355 100644 --- a/cleave-daemon/src/modifiers.rs +++ b/cleave-daemon/src/modifiers.rs @@ -13,7 +13,6 @@ bitflags::bitflags! { /// Specification: /// #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Modifiers: u32 { const ALT = 0x01; const ALT_GRAPH = 0x2; @@ -33,33 +32,33 @@ bitflags::bitflags! { } impl Modifiers { - /// Return `true` if a shift key is pressed. - pub fn shift(&self) -> bool { - self.contains(Modifiers::SHIFT) - } + /// Return `true` if a shift key is pressed. + pub fn shift(&self) -> bool { + self.contains(Modifiers::SHIFT) + } - /// Return `true` if a control key is pressed. - pub fn ctrl(&self) -> bool { - self.contains(Modifiers::CONTROL) - } + /// Return `true` if a control key is pressed. + pub fn ctrl(&self) -> bool { + self.contains(Modifiers::CONTROL) + } - /// Return `true` if an alt key is pressed. - pub fn alt(&self) -> bool { - self.contains(Modifiers::ALT) - } + /// Return `true` if an alt key is pressed. + pub fn alt(&self) -> bool { + self.contains(Modifiers::ALT) + } - /// Return `true` if a meta key is pressed. - pub fn meta(&self) -> bool { - self.contains(Modifiers::META) - } + /// Return `true` if a meta key is pressed. + pub fn meta(&self) -> bool { + self.contains(Modifiers::META) + } - pub fn from_keycode(key: Keycode) -> Option { - match key { - Keycode::LShift | Keycode::RShift => Some(Modifiers::SHIFT), - Keycode::LControl | Keycode::RControl => Some(Modifiers::CONTROL), - Keycode::LAlt | Keycode::RAlt => Some(Modifiers::ALT), - Keycode::LMeta | Keycode::RMeta => Some(Modifiers::META), - _ => None, - } - } -} \ No newline at end of file + pub fn from_keycode(key: Keycode) -> Option { + match key { + Keycode::LShift | Keycode::RShift => Some(Modifiers::SHIFT), + Keycode::LControl | Keycode::RControl => Some(Modifiers::CONTROL), + Keycode::LAlt | Keycode::RAlt => Some(Modifiers::ALT), + Keycode::LMeta | Keycode::RMeta => Some(Modifiers::META), + _ => None, + } + } +} From fc756f9ef2a4e9e7ceb0943a493dbe3bc3d652a7 Mon Sep 17 00:00:00 2001 From: Exotik850 Date: Thu, 7 Nov 2024 19:11:25 -0600 Subject: [PATCH 16/16] remove global_hotkey dep, start daemon correctly --- Cargo.lock | 209 +++++++++++++++++++++++++++-------------------- Cargo.toml | 15 +++- src/app/mod.rs | 74 +++-------------- src/app/state.rs | 3 +- src/args.rs | 49 +++++++---- 5 files changed, 182 insertions(+), 168 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e6d138..740d11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,7 +67,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -142,15 +142,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arboard" @@ -275,15 +275,12 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "bitstream-io" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "block" @@ -361,7 +358,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -378,9 +375,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.34" +version = "1.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" dependencies = [ "jobserver", "libc", @@ -484,12 +481,12 @@ dependencies = [ "bytemuck", "chrono", "clap", + "cleave-daemon", "cleave-graphics", "glam", - "global-hotkey", "image", "pollster", - "thiserror", + "thiserror 2.0.0", "wgpu", "winit", "xcap", @@ -499,10 +496,13 @@ dependencies = [ name = "cleave-daemon" version = "0.1.0" dependencies = [ + "anyhow", "bitflags 2.6.0", "clap", "device_query", - "thiserror", + "dirs", + "fd-lock", + "thiserror 2.0.0", ] [[package]] @@ -512,7 +512,7 @@ dependencies = [ "bytemuck", "glam", "image", - "thiserror", + "thiserror 2.0.0", "wgpu", "winit", ] @@ -650,15 +650,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -710,6 +701,7 @@ dependencies = [ [[package]] name = "device_query" version = "2.1.0" +source = "git+https://github.com/exotik850/device_query?branch=refactor#65abb2d257fe01150b1fc2c04249cd58e331a928" dependencies = [ "macos-accessibility-client", "pkg-config", @@ -719,6 +711,27 @@ dependencies = [ "x11", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -798,6 +811,17 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "fdeflate" version = "0.3.6" @@ -888,29 +912,13 @@ dependencies = [ [[package]] name = "glam" -version = "0.29.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c9417a5dc586fc0c0cb67891170e59cc11e9dc79ba1c11ddd2c56ca3f3b90" +checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" dependencies = [ "bytemuck", ] -[[package]] -name = "global-hotkey" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00d88f1be7bf4cd2e61623ce08e84be2dfa4eab458e5d632d3dab95f16c1f64" -dependencies = [ - "crossbeam-channel", - "keyboard-types", - "objc2", - "objc2-app-kit", - "once_cell", - "thiserror", - "windows-sys 0.59.0", - "x11-dl", -] - [[package]] name = "glow" version = "0.14.2" @@ -959,7 +967,7 @@ checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.68", "windows 0.58.0", ] @@ -1005,9 +1013,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "heck" @@ -1052,9 +1060,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.4" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -1096,7 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", ] [[package]] @@ -1136,7 +1144,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.68", "walkdir", "windows-sys 0.45.0", ] @@ -1171,17 +1179,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.6.0", - "serde", - "unicode-segmentation", -] - [[package]] name = "khronos-egl" version = "6.0.0" @@ -1207,9 +1204,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libdbus-sys" @@ -1222,13 +1219,12 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -1315,6 +1311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", + "rayon", ] [[package]] @@ -1380,7 +1377,7 @@ dependencies = [ "rustc-hash", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.68", "unicode-xid", ] @@ -1396,7 +1393,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1743,6 +1740,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.48" @@ -1843,9 +1846,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -2022,7 +2025,7 @@ dependencies = [ "rand_chacha", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.68", "v_frame", "wasm-bindgen", ] @@ -2038,6 +2041,7 @@ dependencies = [ "loop9", "quick-error", "rav1e", + "rayon", "rgb", ] @@ -2097,6 +2101,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.68", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -2117,9 +2132,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ "bitflags 2.6.0", "errno", @@ -2250,7 +2265,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.68", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -2352,18 +2367,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +dependencies = [ + "thiserror-impl 1.0.68", +] + +[[package]] +name = "thiserror" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.0", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972" dependencies = [ "proc-macro2", "quote", @@ -2778,7 +2813,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.68", "wgpu-hal", "wgpu-types", ] @@ -2820,7 +2855,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.68", "wasm-bindgen", "web-sys", "wgpu-types", @@ -3335,7 +3370,7 @@ dependencies = [ "log", "percent-encoding", "sysinfo", - "thiserror", + "thiserror 1.0.68", "windows 0.58.0", "xcb", ] @@ -3378,9 +3413,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index e02f70f..58474ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ authors = ["Exotik850"] [workspace] members = [ "cleave-daemon","cleave-graphics"] - [dependencies] bytemuck = { workspace = true } glam = { workspace = true } @@ -21,16 +20,16 @@ xcap = { workspace = true } thiserror = { workspace = true } clap = { workspace = true } cleave-graphics = { path = "cleave-graphics" } +cleave-daemon = { path = "cleave-daemon" } chrono = "0.4.38" -global-hotkey = "0.6.3" [workspace.dependencies] clap = { version = "4.5.20", features = ["derive"] } anyhow = "1" arboard = "3.4.1" -thiserror = "1" +thiserror = "2" bytemuck = { version = "1.19.0", features = ["derive"] } -glam = { version = "0.29.1", features = ["bytemuck"] } +glam = { version = "0.29", features = ["bytemuck"] } image = "0.25" pollster = "0.4.0" wgpu = "23.0.0" @@ -46,3 +45,11 @@ lto = "thin" rustflags = ["-C", "target-cpu=native"] [features] + +[[bin]] +name = "cleave-daemon" +path = "cleave-daemon/src/main.rs" + +[[bin]] +name = "cleave" +path = "src/main.rs" \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs index 8dd0240..395811f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,4 +1,3 @@ -use std::sync::{Arc, Mutex}; use crate::{ args::{Args, Verified}, @@ -6,14 +5,12 @@ use crate::{ }; use current_image::CurrentImage; -use global_hotkey::{GlobalHotKeyEventReceiver, GlobalHotKeyManager}; use state::CleaveState; use winit::{ application::ApplicationHandler, - event::{ElementState, KeyEvent, MouseButton, WindowEvent}, + event::{ElementState, KeyEvent, WindowEvent}, event_loop::EventLoop, keyboard::{Key, NamedKey}, - window::Window, }; mod context; @@ -24,10 +21,9 @@ use context::CleaveContext; pub struct App { args: Option, - current_image: Arc>>, context: Option, state: CleaveState, - _hk_manager: Option, + current_image: Option, } impl App { @@ -36,8 +32,7 @@ impl App { args: args.map(Args::verify).transpose()?, context: None, state: Default::default(), - current_image: Default::default(), - _hk_manager: None, + current_image: None, }) } @@ -78,12 +73,6 @@ impl App { return Ok(()); } - if let Some(hotkey) = self.args.as_ref().and_then(|a| a.daemon_hotkey) { - let manager = GlobalHotKeyManager::new()?; - manager.register(hotkey)?; - self._hk_manager = Some(manager); - } - self.start_loop() } @@ -95,7 +84,6 @@ impl App { let Some(context) = &mut self.context else { return false; }; - let stay_running = self.args.as_ref().is_some_and(|d| d.stay_running()); let KeyEvent { logical_key: Key::Named(key), state: pressed, @@ -110,7 +98,7 @@ impl App { context.destroy(); } (ElementState::Pressed, NamedKey::Space) => { - let Some(c_img) = self.current_image.lock().unwrap().take() else { + let Some(c_img) = self.current_image.take() else { eprintln!("No image to crop"); return false; }; @@ -138,9 +126,7 @@ impl App { }; } } - if !stay_running { - event_loop.exit(); - } + event_loop.exit(); } (ElementState::Pressed, NamedKey::ArrowDown) => { self.state.handle_move(Direction::Down); @@ -173,16 +159,13 @@ impl App { event: &WindowEvent, event_loop: &winit::event_loop::ActiveEventLoop, ) { - self.state.handle_event(&event); - match event { - WindowEvent::KeyboardInput { event, .. } => { - self.execute_key_command(event.clone(), event_loop); - } - _ => {} + self.state.handle_event(event); + if let WindowEvent::KeyboardInput { event, .. } = event { + self.execute_key_command(event.clone(), event_loop); } } - fn capture_image(&self) { + fn capture_image(&mut self) { let Some(context) = &self.context else { return; }; @@ -198,11 +181,10 @@ impl App { current_image.update_uniforms(context.total_time, &self.state.selection, (w, h)); current_image.bundle.update_buffer(&context.graphics.queue); context.set_window_visibility(true); - *self.current_image.lock().unwrap() = Some(current_image); + self.current_image = Some(current_image); } } - impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { if self.context.is_some() { @@ -212,19 +194,8 @@ impl ApplicationHandler for App { .expect("Could not find monitor!"); let context = CleaveContext::new(event_loop, size.width(), size.height()) .expect("Could not start context"); - if self - .args - .as_ref() - .is_some_and(|a| a.daemon_hotkey.is_some()) - { - context.set_window_visibility(false); - event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); - } else { - self.context = Some(context); - self.capture_image(); - return; - } self.context = Some(context); + self.capture_image(); } fn window_event( @@ -233,26 +204,9 @@ impl ApplicationHandler for App { id: winit::window::WindowId, event: winit::event::WindowEvent, ) { - // Check if we are in daemon mode - if self.args.as_ref().and_then(|a| a.daemon_hotkey).is_some() { - if self.current_image.lock().unwrap().is_none() { - if let Ok(ev) = global_hotkey::GlobalHotKeyEvent::receiver().try_recv() - // .is_ok() - { - println!("{:?}", ev); - event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); - return; - // self.capture_image(); - } else { - return; - } - } - } self.handle_input(&event, event_loop); if let Some(context) = &self.context { - if !context.graphics.is_visible().unwrap_or(true) - && self.current_image.lock().unwrap().is_none() - { + if !context.graphics.is_visible().unwrap_or(true) && self.current_image.is_none() { self.capture_image(); } } @@ -265,10 +219,8 @@ impl ApplicationHandler for App { if id != context.window_id() { return; } - context.update(); - let mut c_img = self.current_image.lock().unwrap(); - let bund = c_img.as_mut().map(|c_img| { + let bund = self.current_image.as_mut().map(|c_img| { c_img.update_uniforms( context.total_time, &self.state.selection, diff --git a/src/app/state.rs b/src/app/state.rs index 6968d34..6543463 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use glam::DVec2; use wgpu::core::command::Rect; @@ -103,7 +102,7 @@ impl CleaveState { pub fn handle_move(&mut self, dir: Direction) -> Option<()> { let (width, height) = self.size?; let selection = self.selection.selection.as_mut()?; - + let (dx, dy) = match dir { Direction::Up => (0.0, -1.0), Direction::Down => (0.0, 1.0), diff --git a/src/args.rs b/src/args.rs index d0f1b79..b258d83 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; +use cleave_daemon::HotKey; use image::ImageFormat; use wgpu::core::command::Rect; -use global_hotkey::hotkey::HotKey; use crate::selection::modes::SelectionMode; fn parse_region(s: &str) -> Result, String> { @@ -82,7 +82,7 @@ pub struct Args { /// /// If not provided, the entire screen is captured and the user is prompted to select a region /// If provided, the user is not prompted and the region is captured immediately - #[arg(long, short='r', value_parser=parse_region)] + #[arg(long, short='i', value_parser=parse_region)] pub region: Option>, /// Filename for the captured image /// @@ -108,7 +108,7 @@ pub struct Args { // #[arg(long, short='p')] // optimize: bool, /// Scale the captured image by a factor - #[arg(long, short = 's')] + #[arg(long, short = 'r')] pub scale: Option, /// Filter to use when scaling the image /// @@ -132,6 +132,14 @@ pub struct Args { /// Only used when daemon_hotkey is provided #[arg(long, short)] pub persistent: bool, + + /// Key Listen Sleep Duration + /// + /// If provided, the app will sleep for the specified duration in milliseconds before listening for the hotkey + /// + /// Only used when daemon_hotkey is provided + #[arg(long, short, default_value = "100")] + pub sleep: u64, } impl Args { @@ -173,7 +181,30 @@ impl Args { } } - let daemon_hotkey = self.daemon_hotkey.map(|s| s.parse()).transpose()?; + if let Some(hotkey) = self + .daemon_hotkey + .map(|s| s.parse::()) + .transpose()? + { + let mut daemon = std::process::Command::new("cleave-daemon"); + daemon.args(["--hotkey", &hotkey.to_string()]); + daemon.args(["--sleep", &self.sleep.to_string()]); + if self.persistent { + daemon.arg("--persistent"); + } + if let Err(e) = daemon.spawn() { + match e.kind() { + std::io::ErrorKind::NotFound => { + anyhow::bail!("Could not find cleave-daemon in PATH"); + } + _ => { + anyhow::bail!("Could not start cleave-daemon: {}", e); + } + } + }; + println!("Daemon started, press {} to capture the screen", hotkey); + std::process::exit(0); + } Ok(Verified { output_dir: self.output_dir, @@ -187,8 +218,6 @@ impl Args { config_path: None, scale: self.scale, filter: self.filter, - daemon_hotkey, - persistent: self.persistent, }) } } @@ -205,12 +234,4 @@ pub struct Verified { pub config_path: Option, pub scale: Option, pub filter: Option, - pub daemon_hotkey: Option, - pub persistent: bool, -} - -impl Verified { - pub fn stay_running(&self) -> bool { - self.daemon_hotkey.is_some() && self.persistent - } }