From af7b978eb405b6c221f8342658fbf5466432b108 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sun, 27 Aug 2023 19:34:37 +0400 Subject: [PATCH] Implement taking a monitor screenshot --- Cargo.lock | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + README.md | 1 + src/input.rs | 19 ++++- src/niri.rs | 98 +++++++++++++++++++++++- src/tty.rs | 21 +++-- src/winit.rs | 16 +++- 7 files changed, 353 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b31621c..0f3ee8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.0.4" @@ -413,6 +419,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.0" @@ -477,6 +489,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -496,6 +517,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derivative" version = "2.2.0" @@ -517,6 +544,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +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" @@ -662,6 +710,25 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -831,6 +898,20 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -907,6 +988,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + [[package]] name = "jni-sys" version = "0.3.0" @@ -1092,6 +1179,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.8" @@ -1140,11 +1237,14 @@ dependencies = [ "anyhow", "bitflags 2.4.0", "clap", + "directories", + "image", "keyframe", "profiling", "sd-notify", "smithay", "smithay-drm-extras", + "time", "tracing", "tracing-subscriber", "tracy-client", @@ -1211,6 +1311,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1263,6 +1384,15 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "objc-sys" version = "0.2.0-beta.2" @@ -1295,13 +1425,19 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[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.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" dependencies = [ - "redox_syscall", + "redox_syscall 0.3.5", ] [[package]] @@ -1350,6 +1486,19 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.8.0" @@ -1465,6 +1614,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -1474,6 +1632,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.9.3" @@ -1639,6 +1808,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -1791,7 +1966,7 @@ checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand 2.0.0", - "redox_syscall", + "redox_syscall 0.3.5", "rustix 0.38.8", "windows-sys 0.48.0", ] @@ -1826,6 +2001,36 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -2417,7 +2622,7 @@ dependencies = [ "orbclient", "percent-encoding", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.3.5", "smithay-client-toolkit", "wasm-bindgen", "wayland-client", diff --git a/Cargo.toml b/Cargo.toml index ab04be7..5c94716 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,13 @@ edition = "2021" anyhow = { version = "1.0.72" } bitflags = "2.3.3" clap = { version = "4.3.21", features = ["derive"] } +directories = "5.0.1" +image = { version = "0.24.7", default-features = false, features = ["png"] } keyframe = { version = "1.1.1", default-features = false } profiling = "1.0.9" sd-notify = "0.4.1" smithay-drm-extras = { version = "0.1.0", path = "../smithay/smithay-drm-extras" } +time = { version = "0.3.28", features = ["formatting", "local-offset", "macros"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracy-client = { version = "0.15.2", default-features = false } diff --git a/README.md b/README.md index 13d39bd..bbe646b 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ The general system is: if a hotkey switches somewhere, then adding CtrlModR | Toggle between preset column widths | | ModF | Maximize column | | ModShiftF | Toggle full-screen on the focused window | +| ModPrtSc | Save a screenshot to `~/Pictures/Screenshots/` | | ModShiftE | Exit niri | [PaperWM]: https://github.com/paperwm/PaperWM diff --git a/src/input.rs b/src/input.rs index c542062..9ffe061 100644 --- a/src/input.rs +++ b/src/input.rs @@ -20,6 +20,7 @@ enum Action { Quit, ChangeVt(i32), Spawn(String), + Screenshot, CloseWindow, ToggleFullscreen, FocusLeft, @@ -48,6 +49,12 @@ enum Action { ToggleFullWidth, } +pub enum BackendAction { + None, + ChangeVt(i32), + Screenshot, +} + pub enum CompositorMod { Super, Alt, @@ -88,6 +95,8 @@ fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) - KEY_t => Action::Spawn("alacritty".to_owned()), KEY_d => Action::Spawn("fuzzel".to_owned()), KEY_n => Action::Spawn("nautilus".to_owned()), + // Alt + PrtSc = SysRq + KEY_Sys_Req | KEY_Print => Action::Screenshot, KEY_q => Action::CloseWindow, KEY_F => Action::ToggleFullscreen, KEY_comma => Action::ConsumeIntoColumn, @@ -126,10 +135,9 @@ fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) - impl Niri { pub fn process_input_event( &mut self, - change_vt: &mut dyn FnMut(i32), comp_mod: CompositorMod, event: InputEvent, - ) { + ) -> BackendAction { let _span = tracy_client::span!("process_input_event"); trace!("process_input_event"); @@ -167,13 +175,16 @@ impl Niri { self.stop_signal.stop() } Action::ChangeVt(vt) => { - (*change_vt)(vt); + return BackendAction::ChangeVt(vt); } Action::Spawn(command) => { if let Err(err) = Command::new(command).spawn() { warn!("error spawning alacritty: {err}"); } } + Action::Screenshot => { + return BackendAction::Screenshot; + } Action::CloseWindow => { if let Some(window) = self.monitor_set.focus() { window.toplevel().send_close(); @@ -602,6 +613,8 @@ impl Niri { } _ => {} } + + BackendAction::None } pub fn process_libinput_event(&mut self, event: &mut InputEvent) { diff --git a/src/niri.rs b/src/niri.rs index d8f53e0..b7361e6 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -2,16 +2,22 @@ use std::collections::HashMap; use std::os::unix::io::AsRawFd; use std::process::Command; use std::sync::{Arc, Mutex}; +use std::thread; use std::time::Duration; +use anyhow::Context; +use directories::UserDirs; use sd_notify::NotifyState; +use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::surface::{ render_elements_from_surface_tree, WaylandSurfaceRenderElement, }; use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; -use smithay::backend::renderer::element::{render_elements, AsRenderElements, RenderElementStates}; +use smithay::backend::renderer::element::{ + render_elements, AsRenderElements, Element, RenderElement, RenderElementStates, +}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; -use smithay::backend::renderer::{ImportAll, Renderer}; +use smithay::backend::renderer::{Bind, ExportMem, Frame, ImportAll, Offscreen, Renderer}; use smithay::desktop::utils::{ send_frames_surface_tree, surface_presentation_feedback_flags_from_states, take_presentation_feedback_surface_tree, OutputPresentationFeedback, @@ -32,7 +38,9 @@ use smithay::reexports::wayland_server::backend::{ }; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle}; -use smithay::utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale, SERIAL_COUNTER}; +use smithay::utils::{ + IsAlive, Logical, Physical, Point, Rectangle, Scale, Transform, SERIAL_COUNTER, +}; use smithay::wayland::compositor::{with_states, CompositorClientState, CompositorState}; use smithay::wayland::data_device::DataDeviceState; use smithay::wayland::output::OutputManagerState; @@ -42,6 +50,7 @@ use smithay::wayland::shell::xdg::XdgShellState; use smithay::wayland::shm::ShmState; use smithay::wayland::socket::ListeningSocketSource; use smithay::wayland::tablet_manager::TabletManagerState; +use time::OffsetDateTime; use crate::backend::Backend; use crate::dbus::mutter_service_channel::ServiceChannel; @@ -712,6 +721,89 @@ impl Niri { feedback } + + pub fn screenshot( + &mut self, + renderer: &mut GlesRenderer, + output: &Output, + ) -> anyhow::Result<()> { + let _span = tracy_client::span!("Niri::screenshot"); + + let size = output.current_mode().unwrap().size; + let output_rect = Rectangle::from_loc_and_size((0, 0), size); + let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); + let fourcc = Fourcc::Abgr8888; + + let texture: GlesTexture = renderer + .create_buffer(fourcc, buffer_size) + .context("error creating texture")?; + + let elements = self.render(renderer, output); + + renderer.bind(texture).context("error binding texture")?; + let mut frame = renderer + .render(size, Transform::Normal) + .context("error starting frame")?; + + frame + .clear([0.1, 0.1, 0.1, 1.], &[output_rect]) + .context("error clearing")?; + + for element in elements.into_iter().rev() { + let src = element.src(); + let dst = element.geometry(Scale::from(1.)); + element + .draw(&mut frame, src, dst, &[output_rect]) + .context("error drawing element")?; + } + + let sync_point = frame.finish().context("error finishing frame")?; + sync_point.wait(); + + let mapping = renderer + .copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc) + .context("error copying framebuffer")?; + let copy = renderer + .map_texture(&mapping) + .context("error mapping texture")?; + let pixels = copy.to_vec(); + + let dirs = UserDirs::new().context("error retrieving home directory")?; + let mut path = dirs.picture_dir().map(|p| p.to_owned()).unwrap_or_else(|| { + let mut dir = dirs.home_dir().to_owned(); + dir.push("Pictures"); + dir + }); + path.push("Screenshots"); + + unsafe { + // are you kidding me + time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound); + }; + + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let desc = time::macros::format_description!( + "Screenshot from [year]-[month]-[day] [hour]-[minute]-[second].png" + ); + let name = now.format(desc).context("error formatting time")?; + path.push(name); + + debug!("saving screenshot to {path:?}"); + + thread::spawn(move || { + if let Err(err) = image::save_buffer( + path, + &pixels, + size.w as u32, + size.h as u32, + image::ColorType::Rgba8, + ) { + warn!("error saving screenshot image: {err:?}"); + } + }); + + Ok(()) + } } render_elements! { diff --git a/src/tty.rs b/src/tty.rs index 528fb0b..79b9e3c 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -31,7 +31,7 @@ use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; use smithay_drm_extras::edid::EdidInfo; use crate::backend::Backend; -use crate::input::CompositorMod; +use crate::input::{BackendAction, CompositorMod}; use crate::niri::OutputRenderElements; use crate::{LoopData, Niri}; @@ -173,10 +173,21 @@ impl Tty { event_loop .insert_source(input_backend, |mut event, _, data| { let tty = data.tty.as_mut().unwrap(); - let mut change_vt = |vt| tty.change_vt(vt); - data.niri.process_libinput_event(&mut event); - data.niri - .process_input_event(&mut change_vt, CompositorMod::Super, event); + let niri = &mut data.niri; + + niri.process_libinput_event(&mut event); + match niri.process_input_event(CompositorMod::Super, event) { + BackendAction::None => (), + BackendAction::ChangeVt(vt) => tty.change_vt(vt), + BackendAction::Screenshot => { + let active = niri.monitor_set.active_output().cloned(); + if let Some(active) = active { + if let Err(err) = niri.screenshot(tty.renderer(), &active) { + warn!("error taking screenshot: {err:?}"); + } + } + } + }; }) .unwrap(); diff --git a/src/winit.rs b/src/winit.rs index c8b2920..69fad6e 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -12,7 +12,7 @@ use smithay::reexports::winit::window::WindowBuilder; use smithay::utils::Transform; use crate::backend::Backend; -use crate::input::CompositorMod; +use crate::input::{BackendAction, CompositorMod}; use crate::niri::OutputRenderElements; use crate::utils::get_monotonic_time; use crate::{LoopData, Niri}; @@ -129,6 +129,7 @@ impl Winit { } fn dispatch(&mut self, niri: &mut Niri) { + let renderer = self.backend.renderer(); let res = self .winit_event_loop .dispatch_new_events(|event| match event { @@ -145,7 +146,18 @@ impl Winit { niri.output_resized(self.output.clone()); } WinitEvent::Input(event) => { - niri.process_input_event(&mut |_| (), CompositorMod::Alt, event) + match niri.process_input_event(CompositorMod::Alt, event) { + BackendAction::None => (), + BackendAction::ChangeVt(_) => (), + BackendAction::Screenshot => { + let active = niri.monitor_set.active_output().cloned(); + if let Some(active) = active { + if let Err(err) = niri.screenshot(renderer, &active) { + warn!("error taking screenshot: {err:?}"); + } + } + } + } } WinitEvent::Focus(_) => (), WinitEvent::Refresh => niri.queue_redraw(self.output.clone()),