From 7bd8a5f58304f3a7e68331e2f1e94d114575c8fb Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:43:46 +0200 Subject: [PATCH] Rework clipboard, fixes writeText() not working on KDE --- rust/server/src/plugins/js/clipboard.rs | 270 +++++++++++++++--------- rust/server/src/plugins/js/mod.rs | 14 +- rust/server/src/plugins/mod.rs | 9 +- 3 files changed, 189 insertions(+), 104 deletions(-) diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index 350f31c..4fa4155 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -1,52 +1,34 @@ use std::cell::RefCell; use std::io::Cursor; use std::rc::Rc; -use anyhow::anyhow; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use anyhow::{anyhow, Context, Error}; use arboard::ImageData; use deno_core::{op, OpState}; use image::RgbaImage; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; use crate::plugins::js::permissions::PluginPermissionsClipboard; -use crate::plugins::js::PluginData; +use crate::plugins::js::{clipboard, PluginData}; -fn unknown_err_clipboard(err: arboard::Error) -> anyhow::Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) +#[derive(Clone)] +pub struct Clipboard { + clipboard: Arc>, } -fn unknown_err_image(err: image::ImageError) -> anyhow::Error { - anyhow!("UNKNOWN_ERROR: {:?}", err) -} +impl Clipboard { + pub fn new() -> anyhow::Result { + let clipboard = arboard::Clipboard::new() + .context("error while creating clipboard")?; -fn unable_to_convert_image_err() -> anyhow::Error { - anyhow!("UNABLE_TO_CONVERT_IMAGE") -} - -#[derive(Debug, Serialize, Deserialize)] -struct ClipboardData { - text_data: Option, - png_data: Option> -} - -#[op] -async fn clipboard_read(state: Rc>) -> anyhow::Result { - { - let state = state.borrow(); - - let allow = state - .borrow::() - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } + Ok(Self { + clipboard: Arc::new(RwLock::new(clipboard)), + }) } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + fn read(&self) -> anyhow::Result { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); let png_data = match clipboard.get_image() { Ok(data) => { @@ -86,29 +68,10 @@ async fn clipboard_read(state: Rc>) -> anyhow::Result>) -> anyhow::Result> { - { - let state = state.borrow(); - - let allow = state - .borrow::() - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Read); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); - } } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + fn read_text(&self) -> anyhow::Result> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); let data = match clipboard.get_text() { Ok(data) => Some(data), @@ -123,28 +86,10 @@ async fn clipboard_read_text(state: Rc>) -> anyhow::Result>, data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 - { - let state = state.borrow(); - - let allow = state - .borrow::() - .permissions() - .clipboard - .contains(&PluginPermissionsClipboard::Write); - - if !allow { - return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); - } } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + fn write(&self, data: ClipboardData) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); if let Some(png_data) = data.png_data { @@ -175,16 +120,113 @@ async fn clipboard_write(state: Rc>, data: ClipboardData) -> an } Ok(()) - }).await? + } + + fn write_text(&self, data: String) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + clipboard.set_text(data) + .map_err(|err| unknown_err_clipboard(err))?; + + Ok(()) + } + + fn clear(&self) -> anyhow::Result<()> { + let mut clipboard = self.clipboard.write().expect("lock is poisoned"); + + clipboard.clear() + .map_err(|err| unknown_err_clipboard(err))?; + + Ok(()) + } +} + +fn unknown_err_clipboard(err: arboard::Error) -> Error { + anyhow!("UNKNOWN_ERROR: {:?}", err) +} + +fn unknown_err_image(err: image::ImageError) -> Error { + anyhow!("UNKNOWN_ERROR: {:?}", err) +} + +fn unable_to_convert_image_err() -> Error { + anyhow!("UNABLE_TO_CONVERT_IMAGE") +} + +#[derive(Debug, Serialize, Deserialize)] +struct ClipboardData { + text_data: Option, + png_data: Option> } #[op] -async fn clipboard_write_text(state: Rc>, data: String) -> anyhow::Result<()> { - { +async fn clipboard_read(state: Rc>) -> anyhow::Result { + let clipboard = { let state = state.borrow(); - let allow = state - .borrow::() + let plugin_data = state + .borrow::(); + + let allow = plugin_data + .permissions() + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading from clipboard, plugin id: {:?}", plugin_data.plugin_id); + + let clipboard = state + .borrow::() + .clone(); + + clipboard + }; + + spawn_blocking(move || clipboard.read()).await? +} + + +#[op] +async fn clipboard_read_text(state: Rc>) -> anyhow::Result> { + let clipboard = { + let state = state.borrow(); + + let plugin_data = state + .borrow::(); + + let allow = plugin_data + .permissions() + .clipboard + .contains(&PluginPermissionsClipboard::Read); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); + } + + tracing::debug!("Reading text from clipboard, plugin id: {:?}", plugin_data.plugin_id); + + let clipboard = state + .borrow::() + .clone(); + + clipboard + }; + + spawn_blocking(move || clipboard.read_text()).await? +} + +#[op] +async fn clipboard_write(state: Rc>, data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2 + let clipboard = { + let state = state.borrow(); + + let plugin_data = state + .borrow::(); + + let allow = plugin_data .permissions() .clipboard .contains(&PluginPermissionsClipboard::Write); @@ -192,26 +234,57 @@ async fn clipboard_write_text(state: Rc>, data: String) -> anyh if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); } - } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + tracing::debug!("Writing to clipboard, plugin id: {:?}", plugin_data.plugin_id); - clipboard.set_text(data) - .map_err(|err| unknown_err_clipboard(err))?; + let clipboard = state + .borrow::() + .clone(); - Ok(()) - }).await? + clipboard + }; + + spawn_blocking(move || clipboard.write(data)).await? +} + +#[op] +async fn clipboard_write_text(state: Rc>, data: String) -> anyhow::Result<()> { + let clipboard = { + let state = state.borrow(); + + let plugin_data = state + .borrow::(); + + let allow = plugin_data + .permissions() + .clipboard + .contains(&PluginPermissionsClipboard::Write); + + if !allow { + return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); + } + + tracing::debug!("Writing text to clipboard, plugin id: {:?}", plugin_data.plugin_id); + + let clipboard = state + .borrow::() + .clone(); + + clipboard + }; + + spawn_blocking(move || clipboard.write_text(data)).await? } #[op] async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { - { + let clipboard = { let state = state.borrow(); - let allow = state - .borrow::() + let plugin_data = state + .borrow::(); + + let allow = plugin_data .permissions() .clipboard .contains(&PluginPermissionsClipboard::Clear); @@ -219,14 +292,15 @@ async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { if !allow { return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); } - } - spawn_blocking(|| { - let mut clipboard = arboard::Clipboard::new() - .map_err(|err| unknown_err_clipboard(err))?; + tracing::debug!("Clearing clipboard, plugin id: {:?}", plugin_data.plugin_id); - clipboard.clear()?; + let clipboard = state + .borrow::() + .clone(); - Ok(()) - }).await? + clipboard + }; + + spawn_blocking(move || clipboard.clear()).await? } diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 126eb91..1aa246b 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -37,7 +37,7 @@ use crate::plugins::applications::{get_apps, DesktopEntry}; use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; -use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; +use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text, Clipboard}; use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; @@ -57,7 +57,7 @@ mod assets; mod preferences; mod search; mod command_generators; -mod clipboard; +pub mod clipboard; pub mod permissions; pub struct PluginRuntimeData { @@ -74,6 +74,7 @@ pub struct PluginRuntimeData { pub icon_cache: IconCache, pub frontend_api: FrontendApi, pub dirs: Dirs, + pub clipboard: Clipboard, } pub struct PluginCode { @@ -252,7 +253,8 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run data.db_repository, data.search_index, data.icon_cache, - data.dirs + data.dirs, + data.clipboard ).await }) } => { @@ -294,6 +296,7 @@ async fn start_js_runtime( search_index: SearchIndex, icon_cache: IconCache, dirs: Dirs, + clipboard: Clipboard, ) -> anyhow::Result<()> { let dev_plugin = plugin_id.to_string().starts_with("file://"); @@ -353,7 +356,8 @@ async fn start_js_runtime( repository, search_index, icon_cache, - numbat_context + numbat_context, + clipboard, )], // maybe_inspector_server: Some(inspector_server.clone()), // should_wait_for_inspector_session: true, @@ -543,6 +547,7 @@ deno_core::extension!( search_index: SearchIndex, icon_cache: IconCache, numbat_context: Option, + clipboard: Clipboard, }, state = |state, options| { state.put(options.event_receiver); @@ -553,6 +558,7 @@ deno_core::extension!( state.put(options.search_index); state.put(options.icon_cache); state.put(options.numbat_context); + state.put(options.clipboard); }, ); diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 64a5242..b2d77eb 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -21,6 +21,7 @@ use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_st use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::clipboard::Clipboard; use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; @@ -53,7 +54,8 @@ pub struct ApplicationManager { frontend_api: FrontendApi, global_hotkey_manager: GlobalHotKeyManager, current_hotkey: Mutex>, - dirs: Dirs + dirs: Dirs, + clipboard: Clipboard, } impl ApplicationManager { @@ -67,6 +69,7 @@ impl ApplicationManager { let run_status_holder = RunStatusHolder::new(); let search_index = SearchIndex::create_index(frontend_api.clone())?; let global_hotkey_manager = GlobalHotKeyManager::new()?; + let clipboard = Clipboard::new()?; let (command_broadcaster, _) = tokio::sync::broadcast::channel::(100); @@ -82,6 +85,7 @@ impl ApplicationManager { icon_cache, frontend_api, global_hotkey_manager, + clipboard, current_hotkey: Mutex::new(None), dirs }; @@ -564,7 +568,8 @@ impl ApplicationManager { search_index: self.search_index.clone(), icon_cache: self.icon_cache.clone(), frontend_api: self.frontend_api.clone(), - dirs: self.dirs.clone() + dirs: self.dirs.clone(), + clipboard: self.clipboard.clone(), }; self.start_plugin_runtime(data);