├── .github ├── dependabot.yml └── workflows │ ├── rust.yml │ └── typos.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── _typos.toml ├── archlinux └── PKGBUILD ├── images ├── haruhi.jpg └── notify.png ├── libharuhishot ├── Cargo.toml ├── README.md └── src │ ├── convert.rs │ ├── haruhierror.rs │ ├── lib.rs │ ├── overlay.rs │ ├── screenshot.rs │ ├── state.rs │ └── utils.rs ├── meson.build ├── meson_options.txt ├── misc ├── haruhi_failed.png ├── haruhi_succeeded.png ├── haruhishot.desktop ├── haruhishot.svg ├── haruhishot_source.svg └── haruhishot_symbolic.svg ├── scdoc └── haruishot.1.scd └── src ├── clapargs.rs └── main.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | # docs 7 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 8 | version: 2 9 | updates: 10 | - package-ecosystem: "cargo" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | # We release on Tuesdays and open dependabot PRs will rebase after the 15 | # version bump and thus consume unnecessary workers during release, thus 16 | # let's open new ones on Wednesday 17 | day: "wednesday" 18 | ignore: 19 | - dependency-name: "*" 20 | update-types: ["version-update:semver-patch"] 21 | groups: 22 | # Only update polars as a whole as there are many subcrates that need to 23 | # be updated at once. We explicitly depend on some of them, so batch their 24 | # updates to not take up dependabot PR slots with dysfunctional PRs 25 | polars: 26 | patterns: 27 | - "polars" 28 | - "polars-*" 29 | - package-ecosystem: "github-actions" 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | day: "wednesday" 34 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | container: 17 | image: archlinux:latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: clippy rustfmt 23 | - name: Install wayland dependencies 24 | run: | 25 | pacman -Syu --noconfirm wayland base-devel mesa pango cairo 26 | - name: Build 27 | run: cargo build --verbose 28 | - name: Run tests 29 | run: cargo test --verbose 30 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yamllint disable rule:line-length 3 | name: check_typos 4 | 5 | on: # yamllint disable-line rule:truthy 6 | push: 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: typos-action 19 | uses: crate-ci/typos@v1.32.0 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | version = "0.5.0-rc1" 3 | edition = "2024" 4 | license = "MIT" 5 | description = "impl screencopy for wayland" 6 | authors = ["Decodertalkers "] 7 | homepage = "https://github.com/Decodetalkers/haruhishot" 8 | documentation = "https://docs.rs/libharuhishot/" 9 | keywords = ["wayland"] 10 | 11 | [package] 12 | name = "haruhishot" 13 | version.workspace = true 14 | edition.workspace = true 15 | license.workspace = true 16 | description = "haruhishot" 17 | authors.workspace = true 18 | homepage.workspace = true 19 | keywords.workspace = true 20 | 21 | [workspace] 22 | members = [".", "libharuhishot"] 23 | 24 | [workspace.dependencies] 25 | image = { version = "0.25", default-features = false, features = [ 26 | "jpeg", 27 | "png", 28 | "pnm", 29 | ] } 30 | wayland-client = "0.31" 31 | tracing-subscriber = "0.3.19" 32 | tracing = "0.1.41" 33 | wayland-protocols = { version = "0.32.6", default-features = false, features = [ 34 | "unstable", 35 | "client", 36 | "staging", 37 | ] } 38 | thiserror = "2.0.11" 39 | 40 | nix = { version = "0.30.0", features = ["fs", "mman"] } 41 | memmap2 = "0.9.5" 42 | 43 | [dependencies] 44 | libharuhishot = { path = "libharuhishot", version = "0.5.0-rc1" } 45 | 46 | image.workspace = true 47 | memmap2.workspace = true 48 | 49 | tracing-subscriber.workspace = true 50 | tracing.workspace = true 51 | clap = { version = "4.5.30", features = ["derive", "color"] } 52 | 53 | dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } 54 | 55 | wayland-client.workspace = true 56 | thiserror.workspace = true 57 | notify-rust = { version = "4.11.5", features = ["images"] } 58 | libwaysip = "0.4.0" 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2022 Decodetalkers and Haruhi Suzumiya 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # haruhishot 2 | 3 | It is a screenshot tool for wlroots based compositors such as sway and river written in Rust, with wayland-rs 4 | 5 | ![haruhis](./images/haruhi.jpg) 6 | 7 | ## How to build 8 | 9 | dependiences: `wayland` , `wlroots` , `rust` and `meson` 10 | 11 | You can just run `cargo run` 12 | 13 | If you want to package it , you can use `meson.build` 14 | 15 | ```bash 16 | meson setup \ 17 | -Dprefix=/usr \ 18 | -Dbuildtype=release \ 19 | build 20 | ninja -C build 21 | ``` 22 | 23 | ## Installation 24 | 25 | [![Packaging status](https://repology.org/badge/vertical-allrepos/haruhishot.svg)](https://repology.org/project/haruhishot/versions) 26 | 27 | ## Thanks to wayshot 28 | 29 | ## Use example 30 | 31 | Pick with Region 32 | 33 | ``` 34 | haruhishot -S --stdout | wl-copy 35 | ``` 36 | 37 | or 38 | 39 | ``` 40 | haruhishot --slurp --stdout | wl-copy 41 | ``` 42 | 43 | Get Lists 44 | 45 | ``` 46 | haruhishot -L 47 | ``` 48 | 49 | or 50 | ``` 51 | haruhishot --list-outputs 52 | ``` 53 | 54 | Shot one screen 55 | 56 | ``` 57 | haruhishot -O DP-2 --stdout > test.png 58 | ``` 59 | 60 | or 61 | 62 | ``` 63 | haruhishot --output DP-2 --stdout > test.png 64 | ``` 65 | 66 | Get Color 67 | 68 | ``` 69 | haruhishot -C 70 | ``` 71 | 72 | or 73 | 74 | ``` 75 | haruhishot --color 76 | ``` 77 | 78 | ## Features 79 | 80 | ### Notify Message 81 | 82 | ![notify](./images/notify.png) 83 | 84 | ## TODO 85 | 86 | * I want to add a slint fontend 87 | * ~~Real Fullscreen shot~~ 88 | * In the code of wayshot, it seems need to make change if meet some format, but it works well on my computer, so.. 89 | 90 | ## Thanks to the help of developers in Smithay 91 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["**/*.svg"] 3 | -------------------------------------------------------------------------------- /archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=haruhishot 2 | pkgver=0.3.10 3 | pkgrel=1.0 4 | pkgdesc='One day Haruhi Suzumiya made a wlr screenshot tool' 5 | arch=('x86_64' 'aarch64') 6 | url='https://github.com/Decodetalkers/haruhishot' 7 | license=('MIT') 8 | depends=('wayland' 'qt5-base') 9 | makedepends=('git' 'ninja' 'meson' 'rust' 'wayland-protocols' 'libxkbcommon') 10 | source=('source.tar.gz') 11 | sha512sums=('SKIP') 12 | 13 | build() { 14 | meson setup \ 15 | -Dprefix=/usr \ 16 | -Dbuildtype=release \ 17 | -Denable-notify=true \ 18 | -Denable-gui=true \ 19 | -Denable-swayipc=true \ 20 | -Ddesktop-entry=true \ 21 | build 22 | ninja -C build 23 | } 24 | 25 | package() { 26 | DESTDIR="$pkgdir" ninja -C build install 27 | } 28 | -------------------------------------------------------------------------------- /images/haruhi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decodetalkers/haruhishot/3fb6f169898c58f2c064019113ad35c34c775e6d/images/haruhi.jpg -------------------------------------------------------------------------------- /images/notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decodetalkers/haruhishot/3fb6f169898c58f2c064019113ad35c34c775e6d/images/notify.png -------------------------------------------------------------------------------- /libharuhishot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libharuhishot" 3 | version.workspace = true 4 | edition.workspace = true 5 | license.workspace = true 6 | description = "impl screencopy for wayland" 7 | authors.workspace = true 8 | homepage.workspace = true 9 | documentation = "https://docs.rs/libharuhishot/" 10 | keywords = ["wayland"] 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | wayland-protocols = { version = "0.32.6", default-features = false, features = [ 17 | "unstable", 18 | "client", 19 | "staging" 20 | ] } 21 | #wayland-protocols = { version = "=0.30.0-beta.13", features = ["client", "unstable"] } 22 | 23 | 24 | wayland-protocols-wlr = { version = "0.3.6", default-features = false, features = [ 25 | "client", 26 | ] } 27 | wayland-client.workspace = true 28 | #wayland-client = "=0.30.0-beta.13" 29 | 30 | nix = { version = "0.30.0", features = ["fs", "mman"] } 31 | 32 | memmap2 = "0.9.5" 33 | 34 | # in the feature 35 | tracing = "0.1.41" 36 | 37 | thiserror = "2.0.11" 38 | 39 | image.workspace = true 40 | -------------------------------------------------------------------------------- /libharuhishot/README.md: -------------------------------------------------------------------------------- 1 | # libharuhishot 2 | 3 | libharuhishot, it is used for wlr-screencopy, split it because I want to help with wayshot, but 4 | I also learn a lot. I like my program very much, because it makes me feel alive. Wayshot is a 5 | good program, please help them. 6 | 7 | The lib is simple enough to use, you can take the haruhishot for example, simple usage is like 8 | 9 | ```rust 10 | use libharuhishot::HaruhiShotState; 11 | fn main() { 12 | let mut state = HaruhiShotState::init().unwrap(); 13 | let outputs = state.outputs(); 14 | let output = outputs[0].clone(); 15 | let image_info = state.shot_single_output(output).unwrap(); 16 | } 17 | 18 | ``` 19 | Then you will get a [FrameInfo], There is a mmap , you can get data there 20 | -------------------------------------------------------------------------------- /libharuhishot/src/convert.rs: -------------------------------------------------------------------------------- 1 | use image::ColorType; 2 | use wayland_client::protocol::wl_shm; 3 | 4 | pub trait Convert { 5 | /// Convert raw image data into output type, return said type 6 | fn convert_inplace(&self, data: &mut [u8]) -> ColorType; 7 | } 8 | 9 | #[derive(Default)] 10 | struct ConvertBGR10; 11 | 12 | #[derive(Default)] 13 | struct ConvertNone; 14 | 15 | #[derive(Default)] 16 | struct ConvertRGB8; 17 | 18 | #[derive(Default)] 19 | struct ConvertBGR888; 20 | 21 | const SHIFT10BITS_1: u32 = 20; 22 | const SHIFT10BITS_2: u32 = 10; 23 | 24 | /// Creates format converter based of input format, return None if conversion 25 | /// isn't possible. Conversion is happening inplace. 26 | pub fn create_converter(format: wl_shm::Format) -> Option> { 27 | match format { 28 | wl_shm::Format::Xbgr8888 | wl_shm::Format::Abgr8888 => Some(Box::::default()), 29 | wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888 => Some(Box::::default()), 30 | wl_shm::Format::Xbgr2101010 | wl_shm::Format::Abgr2101010 => { 31 | Some(Box::::default()) 32 | } 33 | wl_shm::Format::Bgr888 => Some(Box::::default()), 34 | _ => None, 35 | } 36 | } 37 | 38 | impl Convert for ConvertNone { 39 | fn convert_inplace(&self, _data: &mut [u8]) -> ColorType { 40 | ColorType::Rgba8 41 | } 42 | } 43 | 44 | impl Convert for ConvertRGB8 { 45 | fn convert_inplace(&self, data: &mut [u8]) -> ColorType { 46 | for chunk in data.chunks_exact_mut(4) { 47 | chunk.swap(0, 2); 48 | } 49 | ColorType::Rgba8 50 | } 51 | } 52 | 53 | /// Simple conversion from 10 to 8 bits for one channel 54 | fn convert10_to_8(color: u32) -> u8 { 55 | ((color >> 2) & 255) as u8 56 | } 57 | 58 | impl Convert for ConvertBGR10 { 59 | fn convert_inplace(&self, data: &mut [u8]) -> ColorType { 60 | for chunk in data.chunks_exact_mut(4) { 61 | let pixel = ((chunk[3] as u32) << 24) 62 | | ((chunk[2] as u32) << 16) 63 | | ((chunk[1] as u32) << 8) 64 | | chunk[0] as u32; 65 | let r = convert10_to_8(pixel >> SHIFT10BITS_1); 66 | let g = convert10_to_8(pixel >> SHIFT10BITS_2); 67 | let b = convert10_to_8(pixel); 68 | chunk[0] = b; 69 | chunk[1] = g; 70 | chunk[2] = r; 71 | chunk[3] = 255; 72 | } 73 | ColorType::Rgba8 74 | } 75 | } 76 | 77 | impl Convert for ConvertBGR888 { 78 | fn convert_inplace(&self, _data: &mut [u8]) -> ColorType { 79 | ColorType::Rgb8 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /libharuhishot/src/haruhierror.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use thiserror::Error; 3 | use wayland_client::{ 4 | ConnectError, DispatchError, 5 | globals::{BindError, GlobalError}, 6 | }; 7 | 8 | /// This describe the error happens during screenshot 9 | #[derive(Error, Debug)] 10 | pub enum HaruhiError { 11 | #[error("Init Failed connection")] 12 | InitFailedConnection(#[from] ConnectError), 13 | #[error("Init Failed Global")] 14 | InitFailedGlobal(#[from] GlobalError), 15 | #[error("Dispatch Error")] 16 | DispatchError(#[from] DispatchError), 17 | #[error("Error during queue")] 18 | BindError(#[from] BindError), 19 | #[error("Error in write image in shm")] 20 | ShmError(#[from] io::Error), 21 | #[error("Not Support format")] 22 | NotSupportFormat, 23 | #[error("Capture Failed")] 24 | CaptureFailed(String), 25 | } 26 | -------------------------------------------------------------------------------- /libharuhishot/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod convert; 2 | mod haruhierror; 3 | mod overlay; 4 | mod screenshot; 5 | mod state; 6 | mod utils; 7 | 8 | pub use screenshot::{AreaSelectCallback, CaptureOption, ImageInfo, ImageViewInfo}; 9 | pub use state::*; 10 | pub use utils::*; 11 | 12 | pub use image::ColorType; 13 | 14 | pub use haruhierror::HaruhiError as Error; 15 | 16 | /// for user to read the state, report some object 17 | pub mod reexport { 18 | pub mod wl_output { 19 | /// rexport wl_output Transform 20 | pub use wayland_client::protocol::wl_output::Transform; 21 | pub use wayland_client::protocol::wl_output::WlOutput; 22 | } 23 | pub mod wl_shm { 24 | /// reexport wl_shm Format 25 | pub use wayland_client::protocol::wl_shm::Format; 26 | } 27 | /// rexport wl_output Transform 28 | pub use wl_output::Transform; 29 | /// reexport wl_shm Format 30 | pub use wl_shm::Format; 31 | 32 | pub mod ext_foreign_toplevel_handle_v1 { 33 | pub use wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libharuhishot/src/overlay.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use wayland_client::{Connection, QueueHandle, delegate_noop, protocol::wl_output::WlOutput}; 4 | use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::{ 5 | self, ZwlrLayerSurfaceV1, 6 | }; 7 | 8 | use wayland_client::protocol::{ 9 | wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_shm::WlShm, wl_shm_pool::WlShmPool, 10 | wl_surface::WlSurface, 11 | }; 12 | 13 | use wayland_protocols::wp::viewporter::client::{ 14 | wp_viewport::WpViewport, wp_viewporter::WpViewporter, 15 | }; 16 | 17 | use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1; 18 | 19 | #[derive(Debug)] 20 | pub(crate) struct LayerShellState { 21 | pub configured_outputs: HashSet, 22 | } 23 | 24 | impl LayerShellState { 25 | pub(crate) fn new() -> Self { 26 | Self { 27 | configured_outputs: HashSet::new(), 28 | } 29 | } 30 | } 31 | 32 | delegate_noop!(LayerShellState: ignore WlCompositor); 33 | delegate_noop!(LayerShellState: ignore WlShm); 34 | delegate_noop!(LayerShellState: ignore WlShmPool); 35 | delegate_noop!(LayerShellState: ignore WlBuffer); 36 | delegate_noop!(LayerShellState: ignore ZwlrLayerShellV1); 37 | delegate_noop!(LayerShellState: ignore WlSurface); 38 | delegate_noop!(LayerShellState: ignore WpViewport); 39 | delegate_noop!(LayerShellState: ignore WpViewporter); 40 | 41 | impl wayland_client::Dispatch for LayerShellState { 42 | // No need to instrument here, span from lib.rs is automatically used. 43 | fn event( 44 | state: &mut Self, 45 | proxy: &ZwlrLayerSurfaceV1, 46 | event: ::Event, 47 | data: &WlOutput, 48 | _conn: &Connection, 49 | _qhandle: &QueueHandle, 50 | ) { 51 | match event { 52 | zwlr_layer_surface_v1::Event::Configure { 53 | serial, 54 | width: _, 55 | height: _, 56 | } => { 57 | tracing::debug!("Acking configure"); 58 | state.configured_outputs.insert(data.clone()); 59 | 60 | proxy.ack_configure(serial); 61 | tracing::trace!("Acked configure"); 62 | } 63 | zwlr_layer_surface_v1::Event::Closed => { 64 | tracing::debug!("Closed") 65 | } 66 | _ => {} 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /libharuhishot/src/screenshot.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::Deref, 3 | os::fd::OwnedFd, 4 | sync::{Arc, RwLock}, 5 | }; 6 | 7 | use crate::{ 8 | HaruhiShotState, WlOutputInfo, 9 | haruhierror::HaruhiError, 10 | overlay::LayerShellState, 11 | state::{CaptureInfo, CaptureState, FrameInfo}, 12 | utils::{Position, Region, Size}, 13 | }; 14 | use image::ColorType; 15 | use memmap2::MmapMut; 16 | use tracing::debug; 17 | use wayland_client::{ 18 | EventQueue, WEnum, 19 | protocol::{ 20 | wl_buffer::WlBuffer, 21 | wl_compositor::WlCompositor, 22 | wl_output::{self, WlOutput}, 23 | wl_shm, 24 | wl_surface::WlSurface, 25 | }, 26 | }; 27 | use wayland_protocols::{ 28 | ext::image_copy_capture::v1::client::{ 29 | ext_image_copy_capture_frame_v1::FailureReason, ext_image_copy_capture_manager_v1::Options, 30 | }, 31 | wp::viewporter::client::wp_viewporter::WpViewporter, 32 | }; 33 | 34 | use wayland_protocols_wlr::layer_shell::v1::client::{ 35 | zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1}, 36 | zwlr_layer_surface_v1::{Anchor, ZwlrLayerSurfaceV1}, 37 | }; 38 | 39 | use std::os::fd::{AsFd, AsRawFd}; 40 | use std::{ 41 | fs::File, 42 | time::{SystemTime, UNIX_EPOCH}, 43 | }; 44 | 45 | use nix::{ 46 | fcntl, 47 | sys::{memfd, mman, stat}, 48 | unistd, 49 | }; 50 | 51 | /// capture_output_frame. 52 | fn create_shm_fd() -> std::io::Result { 53 | // Only try memfd on linux and freebsd. 54 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 55 | loop { 56 | // Create a file that closes on successful execution and seal it's operations. 57 | match memfd::memfd_create( 58 | c"wayshot", 59 | memfd::MFdFlags::MFD_CLOEXEC | memfd::MFdFlags::MFD_ALLOW_SEALING, 60 | ) { 61 | Ok(fd) => { 62 | // This is only an optimization, so ignore errors. 63 | // F_SEAL_SRHINK = File cannot be reduced in size. 64 | // F_SEAL_SEAL = Prevent further calls to fcntl(). 65 | let _ = fcntl::fcntl( 66 | fd.as_fd(), 67 | fcntl::F_ADD_SEALS( 68 | fcntl::SealFlag::F_SEAL_SHRINK | fcntl::SealFlag::F_SEAL_SEAL, 69 | ), 70 | ); 71 | return Ok(fd); 72 | } 73 | Err(nix::errno::Errno::EINTR) => continue, 74 | Err(nix::errno::Errno::ENOSYS) => break, 75 | Err(errno) => return Err(std::io::Error::from(errno)), 76 | } 77 | } 78 | 79 | // Fallback to using shm_open. 80 | let sys_time = SystemTime::now(); 81 | let mut mem_file_handle = format!( 82 | "/wayshot-{}", 83 | sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() 84 | ); 85 | loop { 86 | match mman::shm_open( 87 | // O_CREAT = Create file if does not exist. 88 | // O_EXCL = Error if create and file exists. 89 | // O_RDWR = Open for reading and writing. 90 | // O_CLOEXEC = Close on successful execution. 91 | // S_IRUSR = Set user read permission bit . 92 | // S_IWUSR = Set user write permission bit. 93 | mem_file_handle.as_str(), 94 | fcntl::OFlag::O_CREAT 95 | | fcntl::OFlag::O_EXCL 96 | | fcntl::OFlag::O_RDWR 97 | | fcntl::OFlag::O_CLOEXEC, 98 | stat::Mode::S_IRUSR | stat::Mode::S_IWUSR, 99 | ) { 100 | Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) { 101 | Ok(_) => return Ok(fd), 102 | Err(errno) => match unistd::close(fd.as_raw_fd()) { 103 | Ok(_) => return Err(std::io::Error::from(errno)), 104 | Err(errno) => return Err(std::io::Error::from(errno)), 105 | }, 106 | }, 107 | Err(nix::errno::Errno::EEXIST) => { 108 | // If a file with that handle exists then change the handle 109 | mem_file_handle = format!( 110 | "/wayshot-{}", 111 | sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() 112 | ); 113 | continue; 114 | } 115 | Err(nix::errno::Errno::EINTR) => continue, 116 | Err(errno) => return Err(std::io::Error::from(errno)), 117 | } 118 | } 119 | } 120 | 121 | /// The data of the image, for the whole screen 122 | #[derive(Debug, Clone)] 123 | pub struct ImageInfo { 124 | pub data: Vec, 125 | pub width: u32, 126 | pub height: u32, 127 | pub color_type: ColorType, 128 | } 129 | 130 | #[allow(unused)] 131 | #[derive(Debug, Clone)] 132 | struct CaptureOutputData { 133 | output: WlOutput, 134 | buffer: WlBuffer, 135 | real_width: u32, 136 | real_height: u32, 137 | width: u32, 138 | height: u32, 139 | frame_bytes: u32, 140 | stride: u32, 141 | transform: wl_output::Transform, 142 | frame_format: wl_shm::Format, 143 | screen_position: Position, 144 | } 145 | 146 | /// Image view means what part to use 147 | /// When use the project, every time you will get a picture of the full screen, 148 | /// and when you do area screenshot, This lib will also provide you with the view of the selected 149 | /// part 150 | #[derive(Debug, Clone)] 151 | pub struct ImageViewInfo { 152 | pub info: ImageInfo, 153 | pub region: Region, 154 | } 155 | 156 | /// Describe the capture option 157 | /// Now this library provide two options 158 | /// [CaptureOption::PaintCursors] and [CaptureOption::None] 159 | /// It decides whether cursor will be shown 160 | #[derive(Debug, Clone, Copy)] 161 | pub enum CaptureOption { 162 | PaintCursors, 163 | None, 164 | } 165 | 166 | impl From for Options { 167 | fn from(val: CaptureOption) -> Self { 168 | match val { 169 | CaptureOption::None => Options::empty(), 170 | CaptureOption::PaintCursors => Options::PaintCursors, 171 | } 172 | } 173 | } 174 | 175 | pub trait AreaSelectCallback { 176 | fn slurp(self, state: &HaruhiShotState) -> Result; 177 | } 178 | 179 | impl AreaSelectCallback for F 180 | where 181 | F: Fn(&HaruhiShotState) -> Result, 182 | { 183 | fn slurp(self, state: &HaruhiShotState) -> Result { 184 | self(state) 185 | } 186 | } 187 | impl AreaSelectCallback for Region { 188 | fn slurp(self, _state: &HaruhiShotState) -> Result { 189 | Ok(self) 190 | } 191 | } 192 | impl HaruhiShotState { 193 | fn capture_output_inner( 194 | &mut self, 195 | WlOutputInfo { 196 | output, 197 | logical_size: 198 | Size { 199 | width: real_width, 200 | height: real_height, 201 | }, 202 | position: screen_position, 203 | .. 204 | }: WlOutputInfo, 205 | option: CaptureOption, 206 | fd: T, 207 | file: Option<&File>, 208 | ) -> Result { 209 | let mut event_queue = self.take_event_queue(); 210 | let img_manager = self.output_image_manager(); 211 | let capture_manager = self.image_copy_capture_manager(); 212 | let qh = self.qhandle(); 213 | 214 | let source = img_manager.create_source(&output, qh, ()); 215 | let info = Arc::new(RwLock::new(FrameInfo::default())); 216 | let session = capture_manager.create_session(&source, option.into(), qh, info.clone()); 217 | 218 | let capture_info = CaptureInfo::new(); 219 | let frame = session.create_frame(qh, capture_info.clone()); 220 | event_queue.blocking_dispatch(self).unwrap(); 221 | let qh = self.qhandle(); 222 | 223 | let shm = self.shm(); 224 | let info = info.read().unwrap(); 225 | 226 | let Size { width, height } = info.size(); 227 | let WEnum::Value(frame_format) = info.format() else { 228 | return Err(HaruhiError::NotSupportFormat); 229 | }; 230 | if !matches!( 231 | frame_format, 232 | wl_shm::Format::Xbgr2101010 233 | | wl_shm::Format::Abgr2101010 234 | | wl_shm::Format::Argb8888 235 | | wl_shm::Format::Xrgb8888 236 | | wl_shm::Format::Xbgr8888 237 | ) { 238 | return Err(HaruhiError::NotSupportFormat); 239 | } 240 | let frame_bytes = 4 * height * width; 241 | let mem_fd = fd.as_fd(); 242 | 243 | if let Some(file) = file { 244 | file.set_len(frame_bytes as u64).unwrap(); 245 | } 246 | 247 | let stride = 4 * width; 248 | 249 | let shm_pool = shm.create_pool(mem_fd, (width * height * 4) as i32, qh, ()); 250 | let buffer = shm_pool.create_buffer( 251 | 0, 252 | width as i32, 253 | height as i32, 254 | stride as i32, 255 | frame_format, 256 | qh, 257 | (), 258 | ); 259 | frame.attach_buffer(&buffer); 260 | frame.capture(); 261 | 262 | let transform; 263 | loop { 264 | event_queue.blocking_dispatch(self)?; 265 | let info = capture_info.read().unwrap(); 266 | match info.state() { 267 | CaptureState::Succeeded => { 268 | transform = info.transform(); 269 | break; 270 | } 271 | CaptureState::Failed(info) => match info { 272 | WEnum::Value(reason) => match reason { 273 | FailureReason::Stopped => { 274 | return Err(HaruhiError::CaptureFailed("Stopped".to_owned())); 275 | } 276 | 277 | FailureReason::BufferConstraints => { 278 | return Err(HaruhiError::CaptureFailed("BufferConstraints".to_owned())); 279 | } 280 | FailureReason::Unknown | _ => { 281 | return Err(HaruhiError::CaptureFailed("Unknown".to_owned())); 282 | } 283 | }, 284 | WEnum::Unknown(code) => { 285 | return Err(HaruhiError::CaptureFailed(format!( 286 | "Unknown reason, code : {code}" 287 | ))); 288 | } 289 | }, 290 | CaptureState::Pending => {} 291 | } 292 | } 293 | 294 | self.reset_event_queue(event_queue); 295 | 296 | Ok(CaptureOutputData { 297 | output, 298 | buffer, 299 | width, 300 | height, 301 | frame_bytes, 302 | stride, 303 | frame_format, 304 | real_width: real_width as u32, 305 | real_height: real_height as u32, 306 | transform, 307 | screen_position, 308 | }) 309 | } 310 | 311 | pub fn capture_single_output_with_fd( 312 | &mut self, 313 | option: CaptureOption, 314 | output: WlOutputInfo, 315 | file: F, 316 | ) -> Result<(), HaruhiError> { 317 | self.capture_output_inner(output, option, file.as_fd(), None)?; 318 | Ok(()) 319 | } 320 | 321 | /// Capture a single output 322 | pub fn capture_single_output( 323 | &mut self, 324 | option: CaptureOption, 325 | output: WlOutputInfo, 326 | ) -> Result { 327 | let mem_fd = create_shm_fd().unwrap(); 328 | let mem_file = File::from(mem_fd); 329 | let CaptureOutputData { 330 | width, 331 | height, 332 | frame_format, 333 | .. 334 | } = self.capture_output_inner(output, option, mem_file.as_fd(), Some(&mem_file))?; 335 | 336 | let mut frame_mmap = unsafe { MmapMut::map_mut(&mem_file).unwrap() }; 337 | 338 | let converter = crate::convert::create_converter(frame_format).unwrap(); 339 | let color_type = converter.convert_inplace(&mut frame_mmap); 340 | 341 | Ok(ImageInfo { 342 | data: frame_mmap.deref().into(), 343 | width, 344 | height, 345 | color_type, 346 | }) 347 | } 348 | 349 | /// capture with a area region 350 | pub fn capture_area( 351 | &mut self, 352 | option: CaptureOption, 353 | callback: F, 354 | ) -> Result 355 | where 356 | F: AreaSelectCallback, 357 | { 358 | let outputs = self.outputs().clone(); 359 | 360 | let mut data_list = vec![]; 361 | for data in outputs.into_iter() { 362 | let mem_fd = create_shm_fd().unwrap(); 363 | let mem_file = File::from(mem_fd); 364 | let data = 365 | self.capture_output_inner(data, option, mem_file.as_fd(), Some(&mem_file))?; 366 | data_list.push(AreaShotInfo { data, mem_file }) 367 | } 368 | 369 | let mut state = LayerShellState::new(); 370 | let mut event_queue: EventQueue = self.connection().new_event_queue(); 371 | let globals = self.globals(); 372 | let qh = event_queue.handle(); 373 | let compositor = globals.bind::(&qh, 3..=3, ())?; 374 | let layer_shell = globals.bind::(&qh, 1..=1, ())?; 375 | let viewporter = globals.bind::(&qh, 1..=1, ())?; 376 | let mut layer_shell_surfaces: Vec<(WlSurface, ZwlrLayerSurfaceV1)> = 377 | Vec::with_capacity(data_list.len()); 378 | for AreaShotInfo { data, .. } in data_list.iter() { 379 | let CaptureOutputData { 380 | output, 381 | buffer, 382 | real_width, 383 | real_height, 384 | transform, 385 | .. 386 | } = data; 387 | let surface = compositor.create_surface(&qh, ()); 388 | 389 | let layer_surface = layer_shell.get_layer_surface( 390 | &surface, 391 | Some(output), 392 | Layer::Top, 393 | "wayshot".to_string(), 394 | &qh, 395 | output.clone(), 396 | ); 397 | 398 | layer_surface.set_exclusive_zone(-1); 399 | layer_surface.set_anchor(Anchor::all()); 400 | layer_surface.set_margin(0, 0, 0, 0); 401 | 402 | debug!("Committing surface creation changes."); 403 | surface.commit(); 404 | 405 | debug!("Waiting for layer surface to be configured."); 406 | while !state.configured_outputs.contains(output) { 407 | event_queue.blocking_dispatch(&mut state)?; 408 | } 409 | 410 | surface.set_buffer_transform(*transform); 411 | // surface.set_buffer_scale(output_info.scale()); 412 | surface.attach(Some(buffer), 0, 0); 413 | 414 | let viewport = viewporter.get_viewport(&surface, &qh, ()); 415 | viewport.set_destination(*real_width as i32, *real_height as i32); 416 | 417 | debug!("Committing surface with attached buffer."); 418 | surface.commit(); 419 | layer_shell_surfaces.push((surface, layer_surface)); 420 | event_queue.blocking_dispatch(&mut state)?; 421 | } 422 | 423 | let region_re = callback.slurp(self); 424 | 425 | debug!("Unmapping and destroying layer shell surfaces."); 426 | for (surface, layer_shell_surface) in layer_shell_surfaces.iter() { 427 | surface.attach(None, 0, 0); 428 | surface.commit(); //unmap surface by committing a null buffer 429 | layer_shell_surface.destroy(); 430 | } 431 | event_queue.roundtrip(&mut state)?; 432 | let region = region_re?; 433 | 434 | let shotdata = data_list 435 | .iter() 436 | .find(|data| data.in_this_screen(region)) 437 | .ok_or(HaruhiError::CaptureFailed("not in region".to_owned()))?; 438 | let area = shotdata.clip_area(region).expect("should have"); 439 | let mut frame_mmap = unsafe { MmapMut::map_mut(&shotdata.mem_file).unwrap() }; 440 | 441 | let converter = crate::convert::create_converter(shotdata.data.frame_format).unwrap(); 442 | let color_type = converter.convert_inplace(&mut frame_mmap); 443 | 444 | Ok(ImageViewInfo { 445 | info: ImageInfo { 446 | data: frame_mmap.deref().into(), 447 | width: shotdata.data.width, 448 | height: shotdata.data.height, 449 | color_type, 450 | }, 451 | region: area, 452 | }) 453 | } 454 | } 455 | 456 | struct AreaShotInfo { 457 | data: CaptureOutputData, 458 | mem_file: File, 459 | } 460 | 461 | impl AreaShotInfo { 462 | fn in_this_screen( 463 | &self, 464 | Region { 465 | position: point, .. 466 | }: Region, 467 | ) -> bool { 468 | let CaptureOutputData { 469 | real_width, 470 | real_height, 471 | screen_position: Position { x, y }, 472 | .. 473 | } = self.data; 474 | if point.y < y 475 | || point.x < x 476 | || point.x > x + real_width as i32 477 | || point.y > y + real_height as i32 478 | { 479 | return false; 480 | } 481 | true 482 | } 483 | fn clip_area(&self, region: Region) -> Option { 484 | if !self.in_this_screen(region) { 485 | return None; 486 | } 487 | let CaptureOutputData { 488 | real_width, 489 | real_height, 490 | width, 491 | height, 492 | screen_position, 493 | .. 494 | } = self.data; 495 | let Region { 496 | position: point, 497 | size, 498 | } = region; 499 | let relative_point = point - screen_position; 500 | let position = Position { 501 | x: (relative_point.x as f64 * width as f64 / real_width as f64) as i32, 502 | y: (relative_point.y as f64 * height as f64 / real_height as f64) as i32, 503 | }; 504 | 505 | Some(Region { 506 | position, 507 | size: Size { 508 | width: (size.width as f64 * width as f64 / real_width as f64) as i32, 509 | height: (size.height as f64 * height as f64 / real_height as f64) as i32, 510 | }, 511 | }) 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /libharuhishot/src/state.rs: -------------------------------------------------------------------------------- 1 | use wayland_client::{EventQueue, WEnum}; 2 | use wayland_protocols::ext::image_copy_capture::v1::client::{ 3 | ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1, FailureReason}, 4 | ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1, 5 | ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1}, 6 | }; 7 | 8 | use wayland_protocols::ext::image_capture_source::v1::client::{ 9 | ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1, 10 | ext_image_capture_source_v1::ExtImageCaptureSourceV1, 11 | ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1, 12 | }; 13 | 14 | use wayland_protocols::ext::foreign_toplevel_list::v1::client::{ 15 | ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, 16 | ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1}, 17 | }; 18 | 19 | use wayland_client::{ 20 | Connection, Dispatch, Proxy, QueueHandle, delegate_noop, event_created_child, 21 | globals::{GlobalList, GlobalListContents, registry_queue_init}, 22 | protocol::{ 23 | wl_buffer::WlBuffer, 24 | wl_output::{self, WlOutput}, 25 | wl_registry, 26 | wl_shm::{Format, WlShm}, 27 | wl_shm_pool::WlShmPool, 28 | }, 29 | }; 30 | 31 | use wayland_protocols::xdg::xdg_output::zv1::client::{ 32 | zxdg_output_manager_v1::ZxdgOutputManagerV1, 33 | zxdg_output_v1::{self, ZxdgOutputV1}, 34 | }; 35 | 36 | use std::sync::{Arc, OnceLock, RwLock}; 37 | 38 | use crate::haruhierror::HaruhiError; 39 | use crate::utils::*; 40 | 41 | /// This main state of HaruhiShot, We use it to do screen copy 42 | #[derive(Debug, Default)] 43 | pub struct HaruhiShotState { 44 | toplevels: Vec, 45 | output_infos: Vec, 46 | img_copy_manager: OnceLock, 47 | output_image_manager: OnceLock, 48 | shm: OnceLock, 49 | qh: OnceLock>, 50 | event_queue: Option>, 51 | conn: OnceLock, 52 | globals: OnceLock, 53 | } 54 | 55 | impl HaruhiShotState { 56 | pub(crate) fn image_copy_capture_manager(&self) -> &ExtImageCopyCaptureManagerV1 { 57 | self.img_copy_manager.get().expect("Should init") 58 | } 59 | pub(crate) fn output_image_manager(&self) -> &ExtOutputImageCaptureSourceManagerV1 { 60 | self.output_image_manager.get().expect("Should init") 61 | } 62 | pub(crate) fn qhandle(&self) -> &QueueHandle { 63 | self.qh.get().expect("Should init") 64 | } 65 | 66 | pub(crate) fn take_event_queue(&mut self) -> EventQueue { 67 | self.event_queue.take().expect("control your self") 68 | } 69 | 70 | pub(crate) fn reset_event_queue(&mut self, event_queue: EventQueue) { 71 | self.event_queue = Some(event_queue); 72 | } 73 | 74 | pub(crate) fn shm(&self) -> &WlShm { 75 | self.shm.get().expect("Should init") 76 | } 77 | 78 | /// get all outputs and their info 79 | pub fn outputs(&self) -> &Vec { 80 | &self.output_infos 81 | } 82 | 83 | pub fn connection(&self) -> &Connection { 84 | self.conn.get().expect("should init") 85 | } 86 | 87 | pub fn globals(&self) -> &GlobalList { 88 | self.globals.get().expect("should init") 89 | } 90 | } 91 | 92 | impl HaruhiShotState { 93 | /// print the displays' info 94 | pub fn print_displays_info(&self) { 95 | for WlOutputInfo { 96 | size: Size { width, height }, 97 | logical_size: 98 | Size { 99 | width: logical_width, 100 | height: logical_height, 101 | }, 102 | position: Position { x, y }, 103 | name, 104 | description, 105 | scale, 106 | .. 107 | } in self.outputs() 108 | { 109 | println!("{name}, {description}"); 110 | println!(" Size: {width},{height}"); 111 | println!(" LogicSize: {logical_width}, {logical_height}"); 112 | println!(" Position: {x}, {y}"); 113 | println!(" Scale: {scale}"); 114 | } 115 | } 116 | 117 | pub fn init() -> Result { 118 | let conn = Connection::connect_to_env()?; 119 | 120 | let (globals, mut event_queue) = registry_queue_init::(&conn)?; // We just need the 121 | let display = conn.display(); 122 | 123 | let mut state = HaruhiShotState::default(); 124 | 125 | let qh = event_queue.handle(); 126 | 127 | let _registry = display.get_registry(&qh, ()); 128 | event_queue.blocking_dispatch(&mut state)?; 129 | let image_manager = globals.bind::(&qh, 1..=1, ())?; 130 | let output_image_manager = 131 | globals.bind::(&qh, 1..=1, ())?; 132 | let shm = globals.bind::(&qh, 1..=2, ())?; 133 | globals.bind::(&qh, 1..=1, ())?; 134 | let the_xdg_output_manager = globals.bind::(&qh, 3..=3, ())?; 135 | 136 | for output in state.output_infos.iter_mut() { 137 | let xdg_the_output = the_xdg_output_manager.get_xdg_output(&output.output, &qh, ()); 138 | output.xdg_output.set(xdg_the_output).unwrap(); 139 | } 140 | 141 | event_queue.blocking_dispatch(&mut state)?; 142 | 143 | state.img_copy_manager.set(image_manager).unwrap(); 144 | state 145 | .output_image_manager 146 | .set(output_image_manager) 147 | .unwrap(); 148 | state.qh.set(qh).unwrap(); 149 | state.shm.set(shm).unwrap(); 150 | state.globals.set(globals).unwrap(); 151 | state.conn.set(conn).unwrap(); 152 | state.event_queue = Some(event_queue); 153 | Ok(state) 154 | } 155 | } 156 | 157 | delegate_noop!(HaruhiShotState: ignore ExtImageCaptureSourceV1); 158 | delegate_noop!(HaruhiShotState: ignore ExtOutputImageCaptureSourceManagerV1); 159 | delegate_noop!(HaruhiShotState: ignore ExtForeignToplevelImageCaptureSourceManagerV1); 160 | delegate_noop!(HaruhiShotState: ignore WlShm); 161 | delegate_noop!(HaruhiShotState: ignore ZxdgOutputManagerV1); 162 | delegate_noop!(HaruhiShotState: ignore ExtImageCopyCaptureManagerV1); 163 | delegate_noop!(HaruhiShotState: ignore WlBuffer); 164 | delegate_noop!(HaruhiShotState: ignore WlShmPool); 165 | 166 | #[derive(Debug, Default)] 167 | pub(crate) struct FrameInfo { 168 | buffer_size: OnceLock>, 169 | shm_format: OnceLock>, 170 | } 171 | 172 | impl FrameInfo { 173 | pub(crate) fn size(&self) -> Size { 174 | self.buffer_size.get().cloned().expect("not inited") 175 | } 176 | 177 | pub(crate) fn format(&self) -> WEnum { 178 | self.shm_format.get().cloned().expect("Not inited") 179 | } 180 | } 181 | 182 | impl Dispatch>> for HaruhiShotState { 183 | fn event( 184 | _state: &mut Self, 185 | _proxy: &ExtImageCopyCaptureSessionV1, 186 | event: ::Event, 187 | data: &Arc>, 188 | _conn: &Connection, 189 | _qhandle: &wayland_client::QueueHandle, 190 | ) { 191 | let frame_info = data.write().unwrap(); 192 | match event { 193 | ext_image_copy_capture_session_v1::Event::BufferSize { width, height } => { 194 | frame_info 195 | .buffer_size 196 | .set(Size { width, height }) 197 | .expect("should set only once"); 198 | } 199 | ext_image_copy_capture_session_v1::Event::ShmFormat { format } => { 200 | frame_info 201 | .shm_format 202 | .set(format) 203 | .expect("should set only once"); 204 | } 205 | ext_image_copy_capture_session_v1::Event::Done => {} 206 | _ => {} 207 | } 208 | } 209 | } 210 | 211 | #[derive(Debug, Clone, Copy)] 212 | pub(crate) enum CaptureState { 213 | Failed(WEnum), 214 | Succeeded, 215 | Pending, 216 | } 217 | 218 | pub(crate) struct CaptureInfo { 219 | transform: wl_output::Transform, 220 | state: CaptureState, 221 | } 222 | 223 | impl CaptureInfo { 224 | pub(crate) fn new() -> Arc> { 225 | Arc::new(RwLock::new(Self { 226 | transform: wl_output::Transform::Normal, 227 | state: CaptureState::Pending, 228 | })) 229 | } 230 | 231 | pub(crate) fn transform(&self) -> wl_output::Transform { 232 | self.transform 233 | } 234 | pub(crate) fn state(&self) -> CaptureState { 235 | self.state 236 | } 237 | } 238 | 239 | impl Dispatch>> for HaruhiShotState { 240 | fn event( 241 | _state: &mut Self, 242 | _proxy: &ExtImageCopyCaptureFrameV1, 243 | event: ::Event, 244 | data: &Arc>, 245 | _conn: &Connection, 246 | _qhandle: &wayland_client::QueueHandle, 247 | ) { 248 | let mut data = data.write().unwrap(); 249 | match event { 250 | ext_image_copy_capture_frame_v1::Event::Ready => { 251 | data.state = CaptureState::Succeeded; 252 | } 253 | ext_image_copy_capture_frame_v1::Event::Failed { reason } => { 254 | data.state = CaptureState::Failed(reason) 255 | } 256 | ext_image_copy_capture_frame_v1::Event::Transform { 257 | transform: WEnum::Value(transform), 258 | } => { 259 | data.transform = transform; 260 | } 261 | _ => {} 262 | } 263 | } 264 | } 265 | 266 | impl Dispatch for HaruhiShotState { 267 | fn event( 268 | state: &mut Self, 269 | proxy: &wl_registry::WlRegistry, 270 | event: ::Event, 271 | _data: &(), 272 | _conn: &wayland_client::Connection, 273 | qh: &wayland_client::QueueHandle, 274 | ) { 275 | if let wl_registry::Event::Global { 276 | name, 277 | interface, 278 | version, 279 | } = event 280 | { 281 | if interface == WlOutput::interface().name { 282 | state 283 | .output_infos 284 | .push(WlOutputInfo::new(proxy.bind(name, version, qh, ()))); 285 | } 286 | } 287 | } 288 | } 289 | 290 | impl Dispatch for HaruhiShotState { 291 | fn event( 292 | state: &mut Self, 293 | proxy: &ZxdgOutputV1, 294 | event: ::Event, 295 | _data: &(), 296 | _conn: &Connection, 297 | _qhandle: &wayland_client::QueueHandle, 298 | ) { 299 | let Some(data) = 300 | state 301 | .output_infos 302 | .iter_mut() 303 | .find(|WlOutputInfo { xdg_output, .. }| { 304 | xdg_output.get().expect("we need to init here") == proxy 305 | }) 306 | else { 307 | return; 308 | }; 309 | 310 | match event { 311 | zxdg_output_v1::Event::LogicalPosition { x, y } => data.position = Position { x, y }, 312 | zxdg_output_v1::Event::LogicalSize { width, height } => { 313 | data.logical_size = Size { width, height }; 314 | } 315 | zxdg_output_v1::Event::Description { description } => { 316 | data.description = description; 317 | } 318 | _ => {} 319 | } 320 | } 321 | } 322 | 323 | impl Dispatch for HaruhiShotState { 324 | fn event( 325 | state: &mut Self, 326 | proxy: &WlOutput, 327 | event: ::Event, 328 | _data: &(), 329 | _conn: &wayland_client::Connection, 330 | _qhandle: &wayland_client::QueueHandle, 331 | ) { 332 | let Some(data) = state 333 | .output_infos 334 | .iter_mut() 335 | .find(|WlOutputInfo { output, .. }| output == proxy) 336 | else { 337 | return; 338 | }; 339 | match event { 340 | wl_output::Event::Name { name } => { 341 | data.name = name; 342 | } 343 | wl_output::Event::Scale { factor } => { 344 | data.scale = factor; 345 | } 346 | wl_output::Event::Mode { width, height, .. } => { 347 | data.size = Size { width, height }; 348 | } 349 | wl_output::Event::Geometry { 350 | transform: WEnum::Value(transform), 351 | .. 352 | } => { 353 | data.transform = transform; 354 | } 355 | _ => {} 356 | } 357 | } 358 | } 359 | impl Dispatch for HaruhiShotState { 360 | fn event( 361 | state: &mut Self, 362 | _proxy: &ExtForeignToplevelListV1, 363 | event: ::Event, 364 | _data: &(), 365 | _conn: &Connection, 366 | _qhandle: &wayland_client::QueueHandle, 367 | ) { 368 | if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event { 369 | state.toplevels.push(TopLevel::new(toplevel)); 370 | } 371 | } 372 | event_created_child!(HaruhiShotState, ExtForeignToplevelHandleV1, [ 373 | ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ()) 374 | ]); 375 | } 376 | impl Dispatch for HaruhiShotState { 377 | fn event( 378 | state: &mut Self, 379 | toplevel: &ExtForeignToplevelHandleV1, 380 | event: ::Event, 381 | _data: &(), 382 | _conn: &Connection, 383 | _qhandle: &wayland_client::QueueHandle, 384 | ) { 385 | let ext_foreign_toplevel_handle_v1::Event::Title { title } = event else { 386 | return; 387 | }; 388 | let Some(current_info) = state 389 | .toplevels 390 | .iter_mut() 391 | .find(|my_toplevel| my_toplevel.handle == *toplevel) 392 | else { 393 | return; 394 | }; 395 | current_info.title = title; 396 | } 397 | } 398 | 399 | impl Dispatch for HaruhiShotState { 400 | fn event( 401 | _state: &mut Self, 402 | _proxy: &wl_registry::WlRegistry, 403 | _event: ::Event, 404 | _data: &GlobalListContents, 405 | _conn: &Connection, 406 | _qh: &wayland_client::QueueHandle, 407 | ) { 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /libharuhishot/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Sub, sync::OnceLock}; 2 | 3 | use wayland_client::protocol::wl_output::{self, WlOutput}; 4 | use wayland_protocols::{ 5 | ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, 6 | xdg::xdg_output::zv1::client::zxdg_output_v1::ZxdgOutputV1, 7 | }; 8 | 9 | /// Describe the size 10 | #[derive(Debug, Default, Clone, Copy)] 11 | pub struct Size 12 | where 13 | T: Default, 14 | { 15 | pub width: T, 16 | pub height: T, 17 | } 18 | 19 | /// Describe the position 20 | #[derive(Debug, Default, Clone, Copy)] 21 | pub struct Position 22 | where 23 | T: Default, 24 | { 25 | pub x: T, 26 | pub y: T, 27 | } 28 | 29 | impl Sub for Position 30 | where 31 | T: Sub + Default, 32 | { 33 | type Output = Self; 34 | fn sub(self, rhs: Self) -> Self::Output { 35 | Self { 36 | x: self.x - rhs.x, 37 | y: self.y - rhs.y, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Debug, Clone, Copy)] 43 | pub struct Region { 44 | pub position: Position, 45 | pub size: Size, 46 | } 47 | 48 | /// contain the output and their messages 49 | #[derive(Debug, Clone)] 50 | pub struct WlOutputInfo { 51 | pub(crate) output: WlOutput, 52 | pub(crate) size: Size, 53 | pub(crate) logical_size: Size, 54 | pub(crate) position: Position, 55 | pub(crate) name: String, 56 | pub(crate) description: String, 57 | pub(crate) xdg_output: OnceLock, 58 | pub(crate) transform: wl_output::Transform, 59 | pub(crate) scale: i32, 60 | } 61 | 62 | impl WlOutputInfo { 63 | /// The name of the output or maybe the screen? 64 | pub fn name(&self) -> &str { 65 | &self.name 66 | } 67 | 68 | /// get the description 69 | pub fn description(&self) -> &str { 70 | &self.description 71 | } 72 | /// get the wl_output 73 | pub fn wl_output(&self) -> &WlOutput { 74 | &self.output 75 | } 76 | pub(crate) fn new(output: WlOutput) -> Self { 77 | Self { 78 | output, 79 | position: Position::default(), 80 | size: Size::default(), 81 | logical_size: Size::default(), 82 | name: "".to_owned(), 83 | description: "".to_owned(), 84 | xdg_output: OnceLock::new(), 85 | transform: wl_output::Transform::Normal, 86 | scale: 1, 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, Clone)] 92 | pub struct TopLevel { 93 | pub(crate) handle: ExtForeignToplevelHandleV1, 94 | pub(crate) title: String, 95 | } 96 | 97 | impl TopLevel { 98 | pub(crate) fn new(handle: ExtForeignToplevelHandleV1) -> Self { 99 | Self { 100 | handle, 101 | title: "".to_string(), 102 | } 103 | } 104 | 105 | pub fn title(&self) -> &str { 106 | &self.title 107 | } 108 | 109 | pub fn handle(&self) -> &ExtForeignToplevelHandleV1 { 110 | &self.handle 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('haruhishot', 'rust', version: '0.5.0-rc1', meson_version: '>= 0.60') 2 | 3 | dependency('wayland-client') 4 | 5 | cargo = find_program('cargo', version: '>= 1.80') 6 | 7 | find_program('rustc', version: '>= 1.80') 8 | 9 | command = [cargo, 'build'] 10 | 11 | targetdir = 'debug' 12 | 13 | if not get_option('debug') 14 | command += '--release' 15 | targetdir = 'release' 16 | endif 17 | 18 | command += [ 19 | '&&', 20 | 'cp', 21 | meson.global_source_root() / 'target' / targetdir / meson.project_name(), 22 | '@OUTPUT@', 23 | ] 24 | 25 | prefix = get_option('prefix') 26 | bindir = prefix / get_option('bindir') 27 | datadir = prefix / get_option('datadir') 28 | icondir = datadir / 'pixmaps' 29 | 30 | custom_target( 31 | meson.project_name(), 32 | output: meson.project_name(), 33 | build_by_default: true, 34 | install: true, 35 | install_dir: bindir, 36 | console: true, 37 | command: command, 38 | ) 39 | 40 | install_data('misc/haruhi_failed.png', install_dir: icondir) 41 | install_data('misc/haruhi_succeeded.png', install_dir: icondir) 42 | 43 | if get_option('man-pages') 44 | manpage = get_option('mandir') 45 | scdoc = dependency('scdoc', version: '>= 1.9.7', native: true) 46 | 47 | if scdoc.found() 48 | custom_target( 49 | 'haruhishot.1', 50 | input: 'scdoc/haruishot.1.scd', 51 | output: 'haruhishot.1', 52 | command: scdoc.get_variable('scdoc'), 53 | feed: true, 54 | capture: true, 55 | install: true, 56 | install_dir: prefix / manpage / 'man1', 57 | ) 58 | endif 59 | endif 60 | 61 | # Install desktop entry 62 | if get_option('desktop-entry') 63 | install_data('misc/haruhishot.desktop', install_dir: datadir / 'applications') 64 | install_data( 65 | 'misc/haruhishot.svg', 66 | install_dir: datadir / 'icons' / 'hicolor' / 'scalable' / 'apps', 67 | ) 68 | endif 69 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('man-pages', type: 'boolean', value: true, description: 'Generate and install man pages') 2 | option('desktop-entry', type: 'boolean', value: true, description: 'Install desktop entry') 3 | -------------------------------------------------------------------------------- /misc/haruhi_failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decodetalkers/haruhishot/3fb6f169898c58f2c064019113ad35c34c775e6d/misc/haruhi_failed.png -------------------------------------------------------------------------------- /misc/haruhi_succeeded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decodetalkers/haruhishot/3fb6f169898c58f2c064019113ad35c34c775e6d/misc/haruhi_succeeded.png -------------------------------------------------------------------------------- /misc/haruhishot.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=0.2.9 4 | Name=haruhishot 5 | Comment=wlr screenshot for CLI and GUI 6 | Exec=haruhishot --gui 7 | Icon=haruhishot 8 | Terminal=false 9 | Categories=Utilities;Screenshot;Qt; 10 | Actions=Global 11 | 12 | [Desktop Action Global] 13 | Name=Global Screenshot Directly 14 | Exec=haruhishot --global 15 | -------------------------------------------------------------------------------- /misc/haruhishot.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 68 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 111 | 118 | 122 | 126 | 130 | 134 | 138 | 142 | 151 | 160 | 168 | 172 | 173 | -------------------------------------------------------------------------------- /misc/haruhishot_source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | Adwaita Icon Template 28 | 30 | 38 | 42 | 46 | 50 | 54 | 58 | 62 | 63 | 71 | 74 | 78 | 82 | 86 | 90 | 94 | 98 | 102 | 106 | 107 | 115 | 116 | 167 | 177 | 184 | 191 | 198 | 205 | 212 | 219 | 226 | 233 | 246 | 253 | 260 | 267 | 274 | 281 | 288 | 295 | 302 | 309 | 316 | 317 | 319 | 320 | 322 | image/svg+xml 323 | 325 | 326 | 327 | GNOME Design Team 328 | 329 | 330 | 331 | 333 | Adwaita Icon Template 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 361 | 363 | 365 | 367 | 369 | 371 | 373 | 374 | 375 | 376 | 382 | 388 | 396 | 404 | 405 | 411 | 419 | 427 | Hicolor 439 | Symbolic 451 | 452 | 457 | 462 | 466 | 471 | 475 | 480 | 484 | 493 | 502 | 510 | 514 | 523 | 529 | 536 | 544 | 549 | 550 | 599 | 600 | 601 | -------------------------------------------------------------------------------- /misc/haruhishot_symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 38 | 45 | 52 | 53 | -------------------------------------------------------------------------------- /scdoc/haruishot.1.scd: -------------------------------------------------------------------------------- 1 | haruhishot(1) 2 | 3 | # NAME 4 | 5 | haruhishot - screenshot program for wlroots 6 | 7 | # DESCRIPTION 8 | 9 | Screenshot for wlroots 10 | 11 | # COMMANDS 12 | 13 | *list_outputs (--list-outputs) (-L)* 14 | Get the Display Information 15 | 16 | *slurp (--slurp) (-S)* [--stdout] 17 | Take screenshot for a center rigon 18 | 19 | Examples: 20 | ``` 21 | haruhishot -S 22 | ``` 23 | Use "--stdout: will print the image to console, you can copy it to clipboard 24 | Examples: 25 | ``` 26 | haruhishot -S --stdout | wl-copy 27 | ``` 28 | 29 | *global* [--stdout] 30 | Take screenshot for all there screen, this will combine all screens together 31 | "--stdout" is the same 32 | 33 | *gui* 34 | This will open a qt fontend for you to take screenshot 35 | 36 | *output (--output) (-O)* [--stdout] 37 | Choose screen to takescreen. There is always screen name after `-O`, you can 38 | get it with `swaymsg` or `list_outputs` option. If you do not give a variable 39 | to it, it will open a cli menu for you to select 40 | 41 | "--stdout" is the same as above 42 | 43 | Examples: 44 | ``` 45 | haruhishot -O 46 | haruhishot -O eDP-1 47 | ``` 48 | 49 | *color (--color) (-C)* 50 | Get color 51 | 52 | Examples: 53 | ``` 54 | haruhishot -C (slurp -p) 55 | ``` 56 | -------------------------------------------------------------------------------- /src/clapargs.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, arg}; 2 | 3 | #[derive(Debug, Parser, PartialEq, Eq)] 4 | #[command( 5 | name = "haruhishot", 6 | about="One day Haruhi Suzumiya made a wlr screenshot tool", 7 | long_about = None, 8 | version, 9 | )] 10 | pub enum HaruhiCli { 11 | #[command( 12 | long_flag = "list-outputs", 13 | short_flag = 'L', 14 | about = "list all outputs" 15 | )] 16 | ListOutputs, 17 | #[command(long_flag = "output", short_flag = 'O', about = "choose output")] 18 | Output { 19 | #[arg(required = false)] 20 | output: Option, 21 | #[arg(value_name = "stdout", long)] 22 | stdout: bool, 23 | #[arg(value_name = "pointer", long, default_value = "false")] 24 | cursor: bool, 25 | }, 26 | #[command(long_flag = "slurp", short_flag = 'S', about = "area select")] 27 | Slurp { 28 | #[arg(value_name = "stdout", long)] 29 | stdout: bool, 30 | #[arg(value_name = "pointer", long, default_value = "false")] 31 | cursor: bool, 32 | }, 33 | #[command(long_flag = "color", short_flag = 'C', about = "get color")] 34 | Color 35 | } 36 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod clapargs; 2 | 3 | use clap::Parser; 4 | use dialoguer::FuzzySelect; 5 | use dialoguer::theme::ColorfulTheme; 6 | use image::codecs::png::PngEncoder; 7 | use image::{GenericImageView, ImageEncoder, ImageError}; 8 | pub use libharuhishot::HaruhiShotState; 9 | use libharuhishot::{CaptureOption, ImageInfo, ImageViewInfo, Position, Region, Size}; 10 | 11 | use std::io::{BufWriter, Write, stdout}; 12 | use std::{env, fs, path::PathBuf}; 13 | 14 | use std::sync::LazyLock; 15 | 16 | use clapargs::HaruhiCli; 17 | 18 | const TMP: &str = "/tmp"; 19 | 20 | pub const SUCCEED_IMAGE: &str = "haruhi_succeeded"; 21 | pub const FAILED_IMAGE: &str = "haruhi_failed"; 22 | pub const TIMEOUT: i32 = 10000; 23 | 24 | pub static SAVEPATH: LazyLock = LazyLock::new(|| { 25 | let Ok(home) = env::var("HOME") else { 26 | return PathBuf::from(TMP); 27 | }; 28 | let targetpath = PathBuf::from(home).join("Pictures").join("haruhishot"); 29 | if !targetpath.exists() && fs::create_dir_all(&targetpath).is_err() { 30 | return PathBuf::from(TMP); 31 | } 32 | targetpath 33 | }); 34 | 35 | fn random_file_path() -> PathBuf { 36 | let file_name = format!( 37 | "{}-haruhui.png", 38 | std::time::SystemTime::now() 39 | .duration_since(std::time::UNIX_EPOCH) 40 | .unwrap() 41 | .as_secs() 42 | ); 43 | SAVEPATH.join(file_name) 44 | } 45 | 46 | #[derive(Debug, thiserror::Error)] 47 | enum HaruhiImageWriteError { 48 | #[error("Image Error")] 49 | ImageError(#[from] ImageError), 50 | #[error("file created failed")] 51 | FileCreatedFailed(#[from] std::io::Error), 52 | #[error("FuzzySelect Failed")] 53 | FuzzySelectFailed(#[from] dialoguer::Error), 54 | #[error("Output not exist")] 55 | OutputNotExist, 56 | #[error("Wayland shot error")] 57 | WaylandError(#[from] libharuhishot::Error), 58 | } 59 | 60 | #[derive(Debug, Clone)] 61 | enum HaruhiShotResult { 62 | StdoutSucceeded, 63 | SaveToFile(PathBuf), 64 | ColorSucceeded, 65 | } 66 | 67 | trait ToCaptureOption { 68 | fn to_capture_option(self) -> CaptureOption; 69 | } 70 | 71 | impl ToCaptureOption for bool { 72 | fn to_capture_option(self) -> CaptureOption { 73 | if self { 74 | CaptureOption::PaintCursors 75 | } else { 76 | CaptureOption::None 77 | } 78 | } 79 | } 80 | 81 | fn capture_output( 82 | state: &mut HaruhiShotState, 83 | output: Option, 84 | use_stdout: bool, 85 | pointer: bool, 86 | ) -> Result { 87 | let outputs = state.outputs(); 88 | let names: Vec<&str> = outputs.iter().map(|info| info.name()).collect(); 89 | 90 | let selection = match output { 91 | Some(name) => names 92 | .iter() 93 | .position(|tname| *tname == name) 94 | .ok_or(HaruhiImageWriteError::OutputNotExist)?, 95 | None => FuzzySelect::with_theme(&ColorfulTheme::default()) 96 | .with_prompt("Choose Screen") 97 | .default(0) 98 | .items(&names) 99 | .interact()?, 100 | }; 101 | 102 | let output = outputs[selection].clone(); 103 | let image_info = state.capture_single_output(pointer.to_capture_option(), output)?; 104 | 105 | write_to_image(image_info, use_stdout) 106 | } 107 | 108 | fn capture_area( 109 | state: &mut HaruhiShotState, 110 | use_stdout: bool, 111 | pointer: bool, 112 | ) -> Result { 113 | let ImageViewInfo { 114 | info: 115 | ImageInfo { 116 | data, 117 | width: img_width, 118 | height: img_height, 119 | color_type, 120 | }, 121 | region: 122 | Region { 123 | position: Position { x, y }, 124 | size: Size { width, height }, 125 | }, 126 | } = state.capture_area(pointer.to_capture_option(), |w_conn: &HaruhiShotState| { 127 | let info = libwaysip::get_area( 128 | Some(libwaysip::WaysipConnection { 129 | connection: w_conn.connection(), 130 | globals: w_conn.globals(), 131 | }), 132 | libwaysip::SelectionType::Area, 133 | ) 134 | .map_err(|e| libharuhishot::Error::CaptureFailed(e.to_string()))? 135 | .ok_or(libharuhishot::Error::CaptureFailed( 136 | "Failed to capture the area".to_string(), 137 | ))?; 138 | waysip_to_region(info.size(), info.left_top_point()) 139 | })?; 140 | 141 | let mut buff = std::io::Cursor::new(Vec::new()); 142 | PngEncoder::new(&mut buff).write_image(&data, img_width, img_height, color_type.into())?; 143 | let img = image::load_from_memory_with_format(buff.get_ref(), image::ImageFormat::Png).unwrap(); 144 | let clipimage = img.view(x as u32, y as u32, width as u32, height as u32); 145 | if use_stdout { 146 | let mut buff = std::io::Cursor::new(Vec::new()); 147 | clipimage 148 | .to_image() 149 | .write_to(&mut buff, image::ImageFormat::Png)?; 150 | let content = buff.get_ref(); 151 | let stdout = stdout(); 152 | let mut writer = BufWriter::new(stdout.lock()); 153 | writer.write_all(content)?; 154 | Ok(HaruhiShotResult::StdoutSucceeded) 155 | } else { 156 | let file = random_file_path(); 157 | clipimage.to_image().save(&file)?; 158 | Ok(HaruhiShotResult::SaveToFile(file)) 159 | } 160 | } 161 | fn get_color(state: &mut HaruhiShotState) -> Result { 162 | let ImageViewInfo { 163 | info: 164 | ImageInfo { 165 | data, 166 | width: img_width, 167 | height: img_height, 168 | color_type, 169 | }, 170 | region: 171 | Region { 172 | position: Position { x, y }, 173 | size: Size { width, height }, 174 | }, 175 | } = state.capture_area(CaptureOption::None, |w_conn: &HaruhiShotState| { 176 | let info = libwaysip::get_area( 177 | Some(libwaysip::WaysipConnection { 178 | connection: w_conn.connection(), 179 | globals: w_conn.globals(), 180 | }), 181 | libwaysip::SelectionType::Point, 182 | ) 183 | .map_err(|e| libharuhishot::Error::CaptureFailed(e.to_string()))? 184 | .ok_or(libharuhishot::Error::CaptureFailed( 185 | "Failed to capture the area".to_string(), 186 | ))?; 187 | waysip_to_region(info.size(), info.left_top_point()) 188 | })?; 189 | 190 | let mut buff = std::io::Cursor::new(Vec::new()); 191 | PngEncoder::new(&mut buff).write_image(&data, img_width, img_height, color_type.into())?; 192 | let img = image::load_from_memory_with_format(buff.get_ref(), image::ImageFormat::Png).unwrap(); 193 | 194 | let clipimage = img.view(x as u32, y as u32, width as u32, height as u32); 195 | let pixel = clipimage.get_pixel(0, 0); 196 | println!( 197 | "RGB: R:{}, G:{}, B:{}, A:{}", 198 | pixel.0[0], pixel.0[1], pixel.0[2], pixel[3] 199 | ); 200 | println!( 201 | "16hex: #{:02x}{:02x}{:02x}{:02x}", 202 | pixel.0[0], pixel.0[1], pixel.0[2], pixel[3] 203 | ); 204 | Ok(HaruhiShotResult::ColorSucceeded) 205 | } 206 | 207 | fn notify_result(shot_result: Result) { 208 | use notify_rust::Notification; 209 | match shot_result { 210 | Ok(HaruhiShotResult::StdoutSucceeded) => { 211 | let _ = Notification::new() 212 | .summary("Screenshot Succeed") 213 | .body("Screenshot Succeed") 214 | .icon(SUCCEED_IMAGE) 215 | .timeout(TIMEOUT) 216 | .show(); 217 | } 218 | Ok(HaruhiShotResult::SaveToFile(file)) => { 219 | let file_name = file.to_string_lossy().to_string(); 220 | let _ = Notification::new() 221 | .summary("File Saved SUcceed") 222 | .body(format!("File Saved to {file:?}").as_str()) 223 | .icon(&file_name) 224 | .timeout(TIMEOUT) 225 | .show(); 226 | } 227 | Ok(HaruhiShotResult::ColorSucceeded) => {} 228 | Err(e) => { 229 | let _ = Notification::new() 230 | .summary("File Saved Failed") 231 | .body(&e.to_string()) 232 | .icon(FAILED_IMAGE) 233 | .timeout(TIMEOUT) 234 | .show(); 235 | } 236 | } 237 | } 238 | 239 | pub fn waysip_to_region( 240 | size: libwaysip::Size, 241 | point: libwaysip::Position, 242 | ) -> Result { 243 | let size: Size = Size { 244 | width: size.width, 245 | height: size.height, 246 | }; 247 | let position: Position = Position { 248 | x: point.x, 249 | y: point.y, 250 | }; 251 | 252 | Ok(Region { position, size }) 253 | } 254 | 255 | fn main() { 256 | tracing_subscriber::fmt() 257 | .with_writer(std::io::stderr) 258 | .init(); 259 | let args = HaruhiCli::parse(); 260 | let mut state = 261 | HaruhiShotState::init().expect("Your wm needs to support Image Copy Capture protocol"); 262 | 263 | match args { 264 | HaruhiCli::ListOutputs => { 265 | state.print_displays_info(); 266 | } 267 | HaruhiCli::Output { 268 | output, 269 | stdout, 270 | cursor: pointer, 271 | } => notify_result(capture_output(&mut state, output, stdout, pointer)), 272 | HaruhiCli::Slurp { 273 | stdout, 274 | cursor: pointer, 275 | } => { 276 | notify_result(capture_area(&mut state, stdout, pointer)); 277 | } 278 | HaruhiCli::Color => { 279 | notify_result(get_color(&mut state)); 280 | } 281 | } 282 | } 283 | 284 | fn write_to_image( 285 | image_info: ImageInfo, 286 | use_stdout: bool, 287 | ) -> Result { 288 | if use_stdout { 289 | write_to_stdout(image_info) 290 | } else { 291 | write_to_file(image_info) 292 | } 293 | } 294 | 295 | fn write_to_stdout( 296 | ImageInfo { 297 | data, 298 | width, 299 | height, 300 | color_type, 301 | }: ImageInfo, 302 | ) -> Result { 303 | let stdout = stdout(); 304 | let mut writer = BufWriter::new(stdout.lock()); 305 | PngEncoder::new(&mut writer).write_image(&data, width, height, color_type.into())?; 306 | Ok(HaruhiShotResult::StdoutSucceeded) 307 | } 308 | 309 | fn write_to_file( 310 | ImageInfo { 311 | data, 312 | width, 313 | height, 314 | color_type, 315 | }: ImageInfo, 316 | ) -> Result { 317 | let file = random_file_path(); 318 | let mut writer = 319 | std::fs::File::create(&file).map_err(HaruhiImageWriteError::FileCreatedFailed)?; 320 | 321 | PngEncoder::new(&mut writer).write_image(&data, width, height, color_type.into())?; 322 | Ok(HaruhiShotResult::SaveToFile(file)) 323 | } 324 | --------------------------------------------------------------------------------