diff --git a/Cargo.lock b/Cargo.lock index 48b642b..cc4258e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,6 +1091,7 @@ dependencies = [ "test-log", "tokio", "trash", + "url", "vergen", "xdg", "xdg-mime", diff --git a/Cargo.toml b/Cargo.toml index b2839b2..d7d0110 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tokio = { version = "1" } trash = "3.2.0" xdg = { version = "2.5.2", optional = true } xdg-mime = "0.3" +url = "2.5" # Internationalization i18n-embed = { version = "0.14", features = [ "fluent-system", diff --git a/src/app.rs b/src/app.rs index 6d86430..7da62a7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use cosmic::{ widget::scrollable, window, Alignment, Event, Length, }, + iced_runtime::clipboard, style, theme, widget::{ self, @@ -32,6 +33,7 @@ use std::{ }; use crate::{ + clipboard::ClipboardContents, config::{AppTheme, Config, IconSizes, TabConfig, CONFIG_VERSION}, fl, home_dir, key_bind::key_binds, @@ -787,8 +789,20 @@ impl Application for App { return self.update_config(); } } - Message::Copy(_entity_opt) => { - log::warn!("TODO: COPY"); + Message::Copy(entity_opt) => { + let mut paths = Vec::new(); + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let Some(tab) = self.tab_model.data_mut::(entity) { + if let Some(ref items) = tab.items_opt() { + for item in items.iter() { + if item.selected { + paths.push(item.path.clone()); + } + } + } + } + let contents = ClipboardContents::new(&paths); + return clipboard::write_data(contents); } Message::Cut(_entity_opt) => { log::warn!("TODO: CUT"); diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..94d0ea6 --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,74 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::iced::clipboard::mime::AsMimeTypes; +use std::{borrow::Cow, path::Path}; +use url::Url; + +pub struct ClipboardContents { + pub available: Cow<'static, [String]>, + pub text_plain: Cow<'static, [u8]>, + pub text_uri_list: Cow<'static, [u8]>, +} + +impl ClipboardContents { + pub fn new>(paths: &[P]) -> Self { + let available = vec!["text/plain".to_string(), "text/uri-list".to_string()]; + let mut text_plain = String::new(); + let mut text_uri_list = String::new(); + //TODO: do we have to use \r\n? + let newline = "\r\n"; + for path in paths.iter() { + let path = path.as_ref(); + + match path.to_str() { + Some(path_str) => { + if !text_plain.is_empty() { + text_plain.push_str(newline); + } + + //TOOD: what if the path contains a newline? + text_plain.push_str(path_str); + } + None => { + log::warn!( + "{:?} is not valid UTF-8, not adding to text/plain clipboard", + path + ); + } + } + + match Url::from_file_path(path) { + Ok(url) => { + text_uri_list.push_str(&url.to_string()); + text_uri_list.push_str(newline); + } + Err(err) => { + log::warn!( + "{:?} cannot be turned into a URL, not adding to text/uri-list clipboard: {}", + path, err + ); + } + } + } + Self { + available: Cow::from(available), + text_plain: Cow::from(text_plain.into_bytes()), + text_uri_list: Cow::from(text_uri_list.into_bytes()), + } + } +} + +impl AsMimeTypes for ClipboardContents { + fn available(&self) -> Cow<'static, [String]> { + self.available.clone() + } + + fn as_bytes(&self, mime_type: &str) -> Option> { + match mime_type { + "text/plain" => Some(self.text_plain.clone()), + "text/uri-list" => Some(self.text_uri_list.clone()), + _ => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a830d5f..9b612de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use std::{path::PathBuf, process}; use app::{App, Flags}; mod app; +mod clipboard; use config::{Config, CONFIG_VERSION}; mod config; pub mod dialog;