diff --git a/Cargo.lock b/Cargo.lock index 2664b09..d8f99d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,27 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.59.0", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -1226,6 +1247,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1900,11 +1927,13 @@ dependencies = [ name = "imagey" version = "0.1.0" dependencies = [ + "arboard", "dirs", "iced", "image", "jxl-oxide", "rfd", + "size", "zip", ] @@ -3064,6 +3093,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -3130,6 +3169,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -3787,6 +3837,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" + [[package]] name = "skrifa" version = "0.37.0" @@ -4246,6 +4302,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + [[package]] name = "ttf-parser" version = "0.25.1" @@ -5109,6 +5176,24 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index 9015f09..382a20f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "AGPL-3.0-or-later" [dependencies] dirs = "6.0.0" rfd = "0.17.2" +size = "0.5.0" zip = "8.6.0" [dependencies.iced] @@ -21,3 +22,7 @@ features = [ "avif-native" ] version = "0.12.6" # I really wish the image crate was named better features = [ "image" ] + +[dependencies.arboard] +version = "3.6.1" +features = [ "wayland-data-control" ] diff --git a/src/main.rs b/src/main.rs index ba75666..2c16d88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,18 @@ use std::{ borrow::Cow, env, ffi::OsStr, + os::unix::fs::MetadataExt, path::{Path, PathBuf}, }; +use arboard::Clipboard; use iced::{ Alignment, Color, Element, Length, Renderer, Subscription, Task, Theme, event, keyboard::{self, Key, key}, theme, widget, window, }; use image::{EncodableLayout, RgbaImage, imageops}; +use size::Size; mod kra; mod utils; @@ -24,7 +27,7 @@ enum Message { Event(iced::Event), } -#[derive(Clone, Debug, Default)] +#[derive(Default)] struct State { image: Option, image_display: Option, @@ -33,6 +36,8 @@ struct State { error: Option, info: Option, infobar_shown: bool, + + clipboard: Option, } impl State { fn new() -> (Self, Task) { @@ -48,9 +53,7 @@ impl State { (state, Task::none()) } fn view(&self) -> Element<'_, Message, Theme, Renderer> { - let mut main = Vec::new(); - - main.push(self.image_display.as_ref().map_or_else( + let main = self.image_display.as_ref().map_or_else( || { widget::container(widget::text(include_str!("usage.txt"))) .height(Length::Fill) @@ -68,16 +71,27 @@ impl State { .height(Length::Fill) .into() }, - )); + ); + let mut bar = Vec::new(); if let Some(error) = self.error.as_ref() { - main.push(widget::text(error).style(widget::text::danger).into()); + bar.push( + widget::container(widget::text(error).style(widget::text::danger)) + .align_x(Alignment::Start) + .width(Length::Fill) + .into(), + ); } if self.infobar_shown && self.info.is_some() { - main.push(widget::text(self.title()).into()); + bar.push( + widget::container(widget::text(self.title())) + .align_x(Alignment::End) + .width(Length::Fill) + .into(), + ); } - widget::column(main).into() + widget::column([main, widget::row(bar).into()]).into() } fn update(&mut self, message: Message) -> Task { match message { @@ -155,6 +169,52 @@ impl State { Key::Character("q") => return window::latest().and_then(window::close), + Key::Character("y") | Key::Character("c") => { + if let Some(image) = self.image.as_ref() { + match match self.clipboard.take() { + Some(c) => Ok(c), + None => Clipboard::new(), + } { + Ok(mut clipboard) => { + self.error = clipboard + .set_image(utils::arboard_from_rgbaimage(image)) + .map_err(|e| e.to_string()) + .err(); + self.clipboard = Some(clipboard); + } + Err(e) => self.error = Some(e.to_string()), + } + } else { + self.error = Some("no image to yank".into()); + } + } + Key::Character("p") | Key::Character("v") => { + match match self.clipboard.take() { + Some(c) => Ok(c), + None => Clipboard::new(), + } { + Ok(mut clipboard) => { + match clipboard.get_image().map(utils::rgbaimage_from_arboard) { + Ok(image) => { + self.error = None; + self.clipboard = Some(clipboard); + + self.info = + Some(format!("{}x{}", image.width(), image.height(),)); + if image.dimensions() < (100, 100) { + self.image_filter = widget::image::FilterMethod::Nearest; + } + self.image = Some(image); + return self.allocate_image(); + } + Err(e) => self.error = Some(e.to_string()), + }; + self.clipboard = Some(clipboard); + } + Err(e) => self.error = Some(e.to_string()), + } + } + // ignore unused keys _ => {} }, @@ -179,10 +239,17 @@ impl State { fn load_image(&mut self, path: impl AsRef) -> Result, String> { utils::load_image(&path).map(|image| { self.info = Some(format!( - "{} {}x{}", + "{} {} {}x{}", path.as_ref() .file_name() .map_or(Cow::Borrowed("[no file]"), OsStr::to_string_lossy), + path.as_ref().metadata().map_or_else( + |e| { + eprintln!("failed to read metadata: {e}"); + Cow::Borrowed("[no size]") + }, + |m| Cow::Owned(Size::from_bytes(m.size()).to_string()) + ), image.width(), image.height(), )); diff --git a/src/utils.rs b/src/utils.rs index fc1841d..474c3b3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; use iced::widget; use image::{DynamicImage, ImageDecoder, ImageReader, ImageResult, RgbaImage}; @@ -52,3 +55,19 @@ pub const fn string_from_filter_type(f: widget::image::FilterMethod) -> &'static widget::image::FilterMethod::Nearest => "nearest neighbor", } } + +pub fn arboard_from_rgbaimage<'a>(image: &'a RgbaImage) -> arboard::ImageData<'a> { + arboard::ImageData { + width: image.width() as usize, + height: image.height() as usize, + bytes: Cow::Borrowed(image.as_ref()), + } +} +pub fn rgbaimage_from_arboard(image: arboard::ImageData) -> RgbaImage { + RgbaImage::from_raw( + image.width as u32, + image.height as u32, + image.bytes.into_owned(), + ) + .unwrap() +}