diff --git a/Cargo.lock b/Cargo.lock index f7844fd..c770512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,19 +380,43 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "0.6.13" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2", ] @@ -470,21 +494,23 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.2.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" +checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" dependencies = [ "clap", + "lazy_static 1.4.0", "structopt-derive", ] [[package]] name = "structopt-derive" -version = "0.2.18" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107" +checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" dependencies = [ "heck", + "proc-macro-error", "proc-macro2", "quote", "syn", @@ -492,9 +518,9 @@ dependencies = [ [[package]] name = "syn" -version = "0.15.44" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a" dependencies = [ "proc-macro2", "quote", @@ -577,9 +603,9 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "unreachable" @@ -596,6 +622,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "void" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index 96e935d..1da5d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,6 @@ anyhow = "1.0.36" image = "0.23.12" log = "0.4.8" stderrlog = "0.4.3" -structopt = "0.2" xcb-util = { version = "0.3.0", features = ["image", "cursor"] } xcb = { version = "0.9" } +structopt = "0.3.21" diff --git a/src/gui.rs b/src/gui.rs index 04e885a..78297ca 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -2,8 +2,12 @@ use std::path::Path; use std::ptr; use anyhow::Result; -use image::ColorType; -use xcb::{base::Connection, xproto, Screen}; +use image::{Bgra, ColorType, DynamicImage, ImageBuffer}; +use xcb::{ + base::Connection, + xproto::{self, Rectangle}, + Screen, +}; use xcb_util::{ffi::image::*, image as xcb_image}; pub struct Gui { @@ -48,7 +52,7 @@ impl Gui { Ok(ScreenCapture { image }) } - pub fn interactive_select(&self, image: &ScreenCapture) -> Result<()> { + pub fn interactive_select(&self, image: &ScreenCapture) -> Result { let id = self.conn.generate_id(); let screen = self.get_default_screen(); let root_window = screen.root(); @@ -67,58 +71,168 @@ impl Gui { xcb::WINDOW_CLASS_INPUT_OUTPUT as u16, screen.root_visual(), &[ - // (xcb::CW_OVERRIDE_REDIRECT, 1), - (xcb::CW_BACK_PIXEL, screen.white_pixel()), - (xcb::CW_EVENT_MASK, u32::MAX), + (xcb::CW_OVERRIDE_REDIRECT, 1), + // (xcb::CW_BACK_PIXEL, screen.white_pixel()), + // (xcb::CW_EVENT_MASK, u32::MAX), ], - ).request_check()?; + ) + .request_check()?; xproto::map_window(&self.conn, id).request_check()?; self.conn.flush(); + let wm_state = xproto::intern_atom(&self.conn, true, "_NET_WM_STATE") + .get_reply()? + .atom(); + let wm_fullscreen = xproto::intern_atom(&self.conn, true, "_NET_WM_STATE_FULLSCREEN") + .get_reply()? + .atom(); + xproto::change_property( + &self.conn, + xcb::PROP_MODE_REPLACE as u8, + id, + wm_state, + 4, + 32, + &[wm_fullscreen], + ); + + info!("Setting input focus..."); xproto::set_input_focus( &self.conn, xcb::INPUT_FOCUS_POINTER_ROOT as u8, id, xcb::CURRENT_TIME, - ); - xproto::grab_pointer( + ) + .request_check()?; + + info!("Grabbing keyboard..."); + let result = xproto::grab_keyboard( &self.conn, - true, + false, id, - u16::MAX, + xcb::CURRENT_TIME, + xcb::GRAB_MODE_ASYNC as u8, + xcb::GRAB_MODE_ASYNC as u8, + ) + .get_reply()?; + println!("result: {:?}", result.status()); + + info!("Grabbing pointer..."); + let result = xproto::grab_pointer( + &self.conn, + false, + id, + (xcb::EVENT_MASK_BUTTON_PRESS + | xcb::EVENT_MASK_BUTTON_RELEASE + | xcb::EVENT_MASK_POINTER_MOTION) as u16, xcb::GRAB_MODE_ASYNC as u8, xcb::GRAB_MODE_ASYNC as u8, xcb::NONE, xcb::NONE, xcb::CURRENT_TIME, - ); + ) + .get_reply()?; + println!("result: {:?}", result.status()); let gc = self.conn.generate_id(); - xproto::create_gc(&self.conn, gc, id, &[]); + xproto::create_gc( + &self.conn, + gc, + id, + &[ + (xcb::GC_FOREGROUND, screen.white_pixel()), + (xcb::GC_BACKGROUND, screen.white_pixel()), + (xcb::GC_LINE_WIDTH, 3), + ], + ); - // drag start - let mut dragging = false; - let mut dx = -1.0f64; - let mut dy = -1.0f64; + #[derive(Default)] + struct State { + dragging: bool, + dx: i16, + dy: i16, - while let Some(evt) = self.conn.wait_for_event() { - xcb_image::put(&self.conn, id, gc, &image.image, 0, 0); + mx: i16, + my: i16, + } - match evt.response_type() { - xcb::EXPOSE => { - println!("EXPOSE"); - } - xcb::BUTTON_PRESS => { - println!("PRESSED BUTTON"); - } - v => { - println!("value: {:?}", v); - } + impl State { + pub fn rect(&self) -> Rectangle { + let tlx = self.dx.min(self.mx); + let tly = self.dy.min(self.my); + + let brx = self.dx.max(self.mx); + let bry = self.dy.max(self.my); + + let rw = (brx - tlx) as u16; + let rh = (bry - tly) as u16; + + Rectangle::new(tlx, tly, rw, rh) } } - todo!(); + let mut state = State::default(); + + let redraw = |state: &State| { + xcb_image::put(&self.conn, id, gc, &image.image, 0, 0); + + if state.dragging { + let rect = state.rect(); + xproto::poly_rectangle(&self.conn, id, gc, &[rect]); + } + + self.conn.flush(); + }; + + redraw(&state); + + while let Some(evt) = self.conn.wait_for_event() { + match evt.response_type() { + xcb::KEY_RELEASE => { + let key_evt = unsafe { xcb::cast_event::(&evt) }; + if key_evt.detail() == 9 { + break; + } + } + xcb::KEY_PRESS => {} + xcb::BUTTON_PRESS => { + let button_evt = unsafe { xcb::cast_event::(&evt) }; + info!("pressed {:?}", button_evt.detail()); + state.dx = button_evt.root_x(); + state.dy = button_evt.root_y(); + + // left mouse button + if button_evt.detail() == 1 { + state.dragging = true; + } + } + xcb::BUTTON_RELEASE => { + let button_evt = unsafe { xcb::cast_event::(&evt) }; + info!("released {:?}", button_evt.detail()); + + // left mouse button + if state.dragging && button_evt.detail() == 1 { + break; + } + } + xcb::MOTION_NOTIFY => { + let motion_evt = unsafe { xcb::cast_event::(&evt) }; + state.mx = motion_evt.root_x(); + state.my = motion_evt.root_y(); + } + v => { + println!("event type: {:?}", v); + } + } + + redraw(&state); + } + + info!("Loop exited, cleaning up..."); + xproto::ungrab_keyboard(&self.conn, xcb::CURRENT_TIME).request_check()?; + + Ok(state.rect()) } } @@ -128,14 +242,30 @@ pub struct ScreenCapture { impl ScreenCapture { pub fn save_to(&self, to: impl AsRef) -> Result<()> { + let section = Rectangle::new(0, 0, self.image.width(), self.image.height()); + self.save_cropped_to(to, section) + } + + pub fn save_cropped_to(&self, to: impl AsRef, section: Rectangle) -> Result<()> { let to = to.as_ref(); - image::save_buffer( - to, - self.image.data(), + let image = ImageBuffer::, _>::from_raw( self.image.width() as u32, self.image.height() as u32, - ColorType::Bgra8, - )?; + self.image.data().to_vec(), + ) + .unwrap(); + let mut image = DynamicImage::ImageBgra8(image); + + // TODO: compare the rectangles to see if we can skip the crop + // crop the image + let image = image.crop( + section.x() as u32, + section.y() as u32, + section.width() as u32, + section.height() as u32, + ); + + image.save(to)?; Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 8a95ef1..13179ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,70 @@ #[macro_use] extern crate log; +#[macro_use] +extern crate anyhow; mod gui; +use std::path::PathBuf; + use anyhow::Result; +use structopt::StructOpt; use crate::gui::Gui; fn main() -> Result<()> { - stderrlog::new().module(module_path!()).init().unwrap(); + let opt = Options::from_args(); + + stderrlog::new() + .module(module_path!()) + .verbosity(5) + .init() + .unwrap(); let gui = Gui::new()?; let capture = gui.capture_entire_screen()?; - capture.save_to("shiet.jpg")?; - gui.interactive_select(&capture)?; + match opt.region { + Region::Fullscreen => { + capture.save_to(&opt.outfile)?; + } + Region::Selection => { + let rectangle = gui.interactive_select(&capture)?; + capture.save_cropped_to(&opt.outfile, rectangle); + } + _ => todo!("TODO"), + } Ok(()) } + +/// Options for screenshot +#[derive(StructOpt)] +pub struct Options { + /// The region to select (fullscreen | window | select) + #[structopt(parse(try_from_str = Region::from_str))] + pub region: Region, + + /// The file to save the screenshot to + #[structopt(short = "o", long = "out", parse(from_os_str))] + pub outfile: PathBuf, +} + +/// A region option +#[allow(missing_docs)] +pub enum Region { + Fullscreen, + ActiveWindow, + Selection, +} + +impl Region { + pub fn from_str(x: &str) -> Result { + match x { + "fullscreen" => Ok(Region::Fullscreen), + "window" => Ok(Region::ActiveWindow), + "select" | "selection" => Ok(Region::Selection), + _ => bail!("expected {fullscreen|window|selection}"), + } + } +}