Abstract clipboard inside plugin runtime behind backend api

This commit is contained in:
Exidex 2024-12-13 18:34:18 +01:00
parent 364beb647b
commit 37d8a218f7
No known key found for this signature in database
GPG key ID: 46D8D21671EB48FA
6 changed files with 293 additions and 267 deletions

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData};
use common::model::{EntrypointId, PhysicalKey};
use crate::model::{AdditionalSearchItem, PreferenceUserData};
use std::collections::HashMap;
pub trait BackendForPluginRuntimeApi {
async fn reload_search_index(&self, generated_commands: Vec<AdditionalSearchItem>, refresh_search_list: bool) -> anyhow::Result<()> ;
@ -19,4 +19,9 @@ pub trait BackendForPluginRuntimeApi {
async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result<HashMap<String, PreferenceUserData>>;
async fn plugin_preferences_required(&self) -> anyhow::Result<bool>;
async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result<bool>;
async fn clipboard_read(&self) -> anyhow::Result<ClipboardData>;
async fn clipboard_read_text(&self) -> anyhow::Result<Option<String>>;
async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()>;
async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()>;
async fn clipboard_clear(&self) -> anyhow::Result<()>;
}

View file

@ -24,4 +24,10 @@ pub enum PreferenceUserData {
Bool(bool),
ListOfStrings(Vec<String>),
ListOfNumbers(Vec<f64>),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ClipboardData {
pub text_data: Option<String>,
pub png_data: Option<Vec<u8>>
}

View file

@ -0,0 +1,147 @@
use anyhow::{anyhow, Context, Error};
use arboard::ImageData;
use image::RgbaImage;
use std::io::Cursor;
use std::sync::{Arc, RwLock};
use common_plugin_runtime::model::ClipboardData;
#[derive(Clone)]
pub struct Clipboard {
clipboard: Arc<RwLock<arboard::Clipboard>>,
}
impl Clipboard {
pub fn new() -> anyhow::Result<Self> {
let clipboard = arboard::Clipboard::new()
.context("error while creating clipboard")?;
Ok(Self {
clipboard: Arc::new(RwLock::new(clipboard)),
})
}
pub fn read(&self) -> anyhow::Result<ClipboardData> {
let mut clipboard = self.clipboard.write().expect("lock is poisoned");
let png_data = match clipboard.get_image() {
Ok(data) => {
let rgba_image = RgbaImage::from_raw(data.width as u32, data.height as u32, data.bytes.into());
let rgba_image = image::DynamicImage::ImageRgba8(rgba_image.unwrap());
let mut result = Cursor::new(vec![]);
rgba_image.write_to(&mut result, image::ImageFormat::Png)
.expect("should be able to convert to png");
Some(result.into_inner())
},
Err(err) => {
match err {
arboard::Error::ContentNotAvailable => None,
err @ _ => {
return Err(unknown_err_clipboard(err));
},
}
}
};
let text_data = match clipboard.get_text() {
Ok(data) => Some(data),
Err(err) => {
match err {
arboard::Error::ContentNotAvailable => None,
err @ _ => {
return Err(unknown_err_clipboard(err));
},
}
}
};
Ok(ClipboardData {
text_data,
png_data,
})
}
pub fn read_text(&self) -> anyhow::Result<Option<String>> {
let mut clipboard = self.clipboard.write().expect("lock is poisoned");
let data = match clipboard.get_text() {
Ok(data) => Some(data),
Err(err) => {
match err {
arboard::Error::ContentNotAvailable => None,
err @ _ => {
return Err(unknown_err_clipboard(err));
},
}
}
};
Ok(data)
}
pub 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 {
let cursor = Cursor::new(&png_data);
let mut reader = image::io::Reader::new(cursor);
reader.set_format(image::ImageFormat::Png);
let image = reader.decode()
.map_err(|_err| unable_to_convert_image_err())?
.into_rgba8();
let (w, h) = image.dimensions();
let image_data = ImageData {
width: w as usize,
height: h as usize,
bytes: image.into_raw().into()
};
clipboard.set_image(image_data)
.map_err(|err| unknown_err_clipboard(err))?;
}
if let Some(text_data) = data.text_data {
clipboard.set_text(text_data)
.map_err(|err| unknown_err_clipboard(err))?;
}
Ok(())
}
pub 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(())
}
pub 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")
}

View file

@ -1,308 +1,101 @@
use std::cell::RefCell;
use std::io::Cursor;
use std::rc::Rc;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use anyhow::{anyhow, Context, Error};
use arboard::ImageData;
use crate::plugins::js::BackendForPluginRuntimeApiImpl;
use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi;
use common_plugin_runtime::model::ClipboardData;
use deno_core::{op2, OpState};
use image::RgbaImage;
use serde::{Deserialize, Serialize};
use tokio::task::spawn_blocking;
use crate::plugins::js::permissions::PluginPermissionsClipboard;
use crate::plugins::js::{clipboard, PluginData};
#[derive(Clone)]
pub struct Clipboard {
clipboard: Arc<RwLock<arboard::Clipboard>>,
}
impl Clipboard {
pub fn new() -> anyhow::Result<Self> {
let clipboard = arboard::Clipboard::new()
.context("error while creating clipboard")?;
Ok(Self {
clipboard: Arc::new(RwLock::new(clipboard)),
})
}
fn read(&self) -> anyhow::Result<ClipboardData> {
let mut clipboard = self.clipboard.write().expect("lock is poisoned");
let png_data = match clipboard.get_image() {
Ok(data) => {
let rgba_image = RgbaImage::from_raw(data.width as u32, data.height as u32, data.bytes.into());
let rgba_image = image::DynamicImage::ImageRgba8(rgba_image.unwrap());
let mut result = Cursor::new(vec![]);
rgba_image.write_to(&mut result, image::ImageFormat::Png)
.expect("should be able to convert to png");
Some(result.into_inner())
},
Err(err) => {
match err {
arboard::Error::ContentNotAvailable => None,
err @ _ => {
return Err(unknown_err_clipboard(err));
},
}
}
};
let text_data = match clipboard.get_text() {
Ok(data) => Some(data),
Err(err) => {
match err {
arboard::Error::ContentNotAvailable => None,
err @ _ => {
return Err(unknown_err_clipboard(err));
},
}
}
};
Ok(ClipboardData {
text_data,
png_data,
})
}
fn read_text(&self) -> anyhow::Result<Option<String>> {
let mut clipboard = self.clipboard.write().expect("lock is poisoned");
let data = match clipboard.get_text() {
Ok(data) => Some(data),
Err(err) => {
match err {
arboard::Error::ContentNotAvailable => None,
err @ _ => {
return Err(unknown_err_clipboard(err));
},
}
}
};
Ok(data)
}
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 {
let cursor = Cursor::new(&png_data);
let mut reader = image::io::Reader::new(cursor);
reader.set_format(image::ImageFormat::Png);
let image = reader.decode()
.map_err(|_err| unable_to_convert_image_err())?
.into_rgba8();
let (w, h) = image.dimensions();
let image_data = ImageData {
width: w as usize,
height: h as usize,
bytes: image.into_raw().into()
};
clipboard.set_image(image_data)
.map_err(|err| unknown_err_clipboard(err))?;
}
if let Some(text_data) = data.text_data {
clipboard.set_text(text_data)
.map_err(|err| unknown_err_clipboard(err))?;
}
Ok(())
}
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")
}
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Serialize, Deserialize)]
struct ClipboardData {
struct JSClipboardData {
text_data: Option<String>,
png_data: Option<Vec<u8>>
}
#[op2(async)]
#[serde]
pub async fn clipboard_read(state: Rc<RefCell<OpState>>) -> anyhow::Result<ClipboardData> {
let clipboard = {
pub async fn clipboard_read(state: Rc<RefCell<OpState>>) -> anyhow::Result<JSClipboardData> {
let api = {
let state = state.borrow();
let plugin_data = state
.borrow::<PluginData>();
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::<Clipboard>()
let api = state
.borrow::<BackendForPluginRuntimeApiImpl>()
.clone();
clipboard
api
};
spawn_blocking(move || clipboard.read()).await?
let result = api.clipboard_read().await?;
Ok(JSClipboardData {
text_data: result.text_data,
png_data: result.png_data,
})
}
#[op2(async)]
#[string]
pub async fn clipboard_read_text(state: Rc<RefCell<OpState>>) -> anyhow::Result<Option<String>> {
let clipboard = {
let api = {
let state = state.borrow();
let plugin_data = state
.borrow::<PluginData>();
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::<Clipboard>()
let api = state
.borrow::<BackendForPluginRuntimeApiImpl>()
.clone();
clipboard
api
};
spawn_blocking(move || clipboard.read_text()).await?
api.clipboard_read_text().await
}
#[op2(async)]
pub async fn clipboard_write(state: Rc<RefCell<OpState>>, #[serde] data: ClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2
let clipboard = {
pub async fn clipboard_write(state: Rc<RefCell<OpState>>, #[serde] data: JSClipboardData) -> anyhow::Result<()> { // TODO deserialization broken, fix when migrating to deno's op2
let api = {
let state = state.borrow();
let plugin_data = state
.borrow::<PluginData>();
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 to clipboard, plugin id: {:?}", plugin_data.plugin_id);
let clipboard = state
.borrow::<Clipboard>()
let api = state
.borrow::<BackendForPluginRuntimeApiImpl>()
.clone();
clipboard
api
};
spawn_blocking(move || clipboard.write(data)).await?
let clipboard_data = ClipboardData {
text_data: data.text_data,
png_data: data.png_data,
};
api.clipboard_write(clipboard_data).await
}
#[op2(async)]
pub async fn clipboard_write_text(state: Rc<RefCell<OpState>>, #[string] data: String) -> anyhow::Result<()> {
let clipboard = {
let api = {
let state = state.borrow();
let plugin_data = state
.borrow::<PluginData>();
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::<Clipboard>()
let api = state
.borrow::<BackendForPluginRuntimeApiImpl>()
.clone();
clipboard
api
};
spawn_blocking(move || clipboard.write_text(data)).await?
api.clipboard_write_text(data).await
}
#[op2(async)]
pub async fn clipboard_clear(state: Rc<RefCell<OpState>>) -> anyhow::Result<()> {
let clipboard = {
let api = {
let state = state.borrow();
let plugin_data = state
.borrow::<PluginData>();
let allow = plugin_data
.permissions()
.clipboard
.contains(&PluginPermissionsClipboard::Clear);
if !allow {
return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard"));
}
tracing::debug!("Clearing clipboard, plugin id: {:?}", plugin_data.plugin_id);
let clipboard = state
.borrow::<Clipboard>()
let api = state
.borrow::<BackendForPluginRuntimeApiImpl>()
.clone();
clipboard
api
};
spawn_blocking(move || clipboard.clear()).await?
api.clipboard_clear().await
}

View file

@ -27,20 +27,22 @@ use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use tokio::task::spawn_blocking;
use tokio_util::sync::CancellationToken;
use common::dirs::Dirs;
use common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId};
use common::rpc::frontend_api::FrontendApi;
use common_plugin_runtime::backend_for_plugin_runtime_api::BackendForPluginRuntimeApi;
use common_plugin_runtime::model::{AdditionalSearchItem, PreferenceUserData};
use common_plugin_runtime::model::{AdditionalSearchItem, ClipboardData, PreferenceUserData};
use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType};
use crate::model::{IntermediateUiEvent, JsKeyboardEventOrigin, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData};
use crate::plugins::clipboard::Clipboard;
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, Clipboard};
use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text};
use crate::plugins::js::command_generators::{get_command_generator_entrypoint_ids};
use crate::plugins::js::environment::{environment_gauntlet_version, environment_is_development, environment_plugin_cache_dir, environment_plugin_data_dir};
use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn};
@ -357,7 +359,6 @@ async fn start_js_runtime(
plugin_cache_dir,
plugin_data_dir,
inline_view_entrypoint_id,
runtime_permissions,
),
frontend_api,
ComponentModel::new(component_model),
@ -365,11 +366,12 @@ async fn start_js_runtime(
icon_cache,
repository,
search_index,
clipboard,
plugin_uuid,
plugin_id
plugin_id,
runtime_permissions,
),
numbat_context,
clipboard,
),
gauntlet_esm,
];
@ -601,7 +603,6 @@ deno_core::extension!(
component_model: ComponentModel,
backend_api: BackendForPluginRuntimeApiImpl,
numbat_context: Option<NumbatContext>,
clipboard: Clipboard,
},
state = |state, options| {
state.put(options.event_receiver);
@ -610,7 +611,6 @@ deno_core::extension!(
state.put(options.component_model);
state.put(options.backend_api);
state.put(options.numbat_context);
state.put(options.clipboard);
},
);
@ -882,7 +882,6 @@ pub struct PluginData {
plugin_cache_dir: String,
plugin_data_dir: String,
inline_view_entrypoint_id: Option<String>,
permissions: PluginRuntimePermissions
}
impl PluginData {
@ -894,7 +893,6 @@ impl PluginData {
plugin_cache_dir: String,
plugin_data_dir: String,
inline_view_entrypoint_id: Option<String>,
permissions: PluginRuntimePermissions
) -> Self {
Self {
plugin_id,
@ -904,7 +902,6 @@ impl PluginData {
plugin_cache_dir,
plugin_data_dir,
inline_view_entrypoint_id,
permissions
}
}
@ -931,10 +928,6 @@ impl PluginData {
fn inline_view_entrypoint_id(&self) -> Option<String> {
self.inline_view_entrypoint_id.clone()
}
fn permissions(&self) -> &PluginRuntimePermissions {
&self.permissions
}
}
pub struct ComponentModel {
@ -974,8 +967,10 @@ pub struct BackendForPluginRuntimeApiImpl {
icon_cache: IconCache,
repository: DataDbRepository,
search_index: SearchIndex,
clipboard: Clipboard,
plugin_uuid: String,
plugin_id: PluginId,
permissions: PluginRuntimePermissions
}
impl BackendForPluginRuntimeApiImpl {
@ -983,15 +978,19 @@ impl BackendForPluginRuntimeApiImpl {
icon_cache: IconCache,
repository: DataDbRepository,
search_index: SearchIndex,
clipboard: Clipboard,
plugin_uuid: String,
plugin_id: PluginId,
permissions: PluginRuntimePermissions
) -> Self {
Self {
icon_cache,
repository,
search_index,
clipboard,
plugin_uuid,
plugin_id,
permissions
}
}
}
@ -1192,6 +1191,81 @@ impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl {
Ok(any_preferences_missing_value(preferences, preferences_user_data))
}
async fn clipboard_read(&self) -> anyhow::Result<ClipboardData> {
let allow = self
.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: {:?}", self.plugin_id);
self.clipboard.read()
}
async fn clipboard_read_text(&self) -> anyhow::Result<Option<String>> {
let allow = self
.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: {:?}", self.plugin_id);
self.clipboard.read_text()
}
async fn clipboard_write(&self, data: ClipboardData) -> anyhow::Result<()> {
let allow = self
.permissions
.clipboard
.contains(&PluginPermissionsClipboard::Write);
if !allow {
return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard"));
}
tracing::debug!("Writing to clipboard, plugin id: {:?}", self.plugin_id);
self.clipboard.write(data)
}
async fn clipboard_write_text(&self, data: String) -> anyhow::Result<()> {
let allow = self
.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: {:?}", self.plugin_id);
self.clipboard.write_text(data)
}
async fn clipboard_clear(&self) -> anyhow::Result<()> {
let allow = self
.permissions
.clipboard
.contains(&PluginPermissionsClipboard::Clear);
if !allow {
return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard"));
}
tracing::debug!("Clearing clipboard, plugin id: {:?}", self.plugin_id);
self.clipboard.clear()
}
}

View file

@ -14,11 +14,11 @@ use common::{settings_env_data_to_string, SettingsEnvData};
use utils::channel::RequestSender;
use common::dirs::Dirs;
use crate::model::{ActionShortcutKey, JsKeyboardEventOrigin};
use crate::plugins::clipboard::Clipboard;
use crate::plugins::config_reader::ConfigReader;
use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginActionShortcutKind, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginMainSearchBarPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint};
use crate::plugins::icon_cache::IconCache;
use crate::plugins::js::{start_plugin_runtime, AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData};
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;
@ -33,6 +33,7 @@ mod run_status;
mod download_status;
mod icon_cache;
pub(super) mod frecency;
mod clipboard;
static BUNDLED_PLUGINS: [(&str, Dir); 1] = [
("gauntlet", include_dir!("$CARGO_MANIFEST_DIR/../../bundled_plugins/gauntlet/dist")),