mirror of
https://github.com/project-gauntlet/gauntlet.git
synced 2025-12-23 10:35:53 +00:00
1096 lines
No EOL
40 KiB
Rust
1096 lines
No EOL
40 KiB
Rust
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::fs::File;
|
|
use std::hash::Hash;
|
|
use std::io;
|
|
use std::net::SocketAddr;
|
|
use std::path::{Path, PathBuf};
|
|
use std::pin::Pin;
|
|
use std::rc::Rc;
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{anyhow, Context};
|
|
use bytes::Bytes;
|
|
use futures::AsyncBufReadExt;
|
|
use interprocess::local_socket::{ListenerOptions, ToFsName, ToNsName};
|
|
use interprocess::local_socket::tokio::{RecvHalf, SendHalf};
|
|
use interprocess::local_socket::traits::tokio::{Listener, Stream};
|
|
use interprocess::TryClone;
|
|
use once_cell::sync::Lazy;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
|
use tokio::net::TcpStream;
|
|
use tokio::sync::Mutex;
|
|
use tokio::task::spawn_blocking;
|
|
use tokio_util::sync::CancellationToken;
|
|
use gauntlet_common::dirs::Dirs;
|
|
use gauntlet_common::model::{EntrypointId, KeyboardEventOrigin, PhysicalKey, PluginId, RootWidget, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidgetId};
|
|
use gauntlet_common::rpc::frontend_api::FrontendApi;
|
|
use gauntlet_common::settings_env_data_to_string;
|
|
use gauntlet_plugin_runtime::{recv_message, send_message, BackendForPluginRuntimeApi, JsAdditionalSearchItem, JsClipboardData, JsInit, JsKeyboardEventOrigin, JsPluginCode, JsPluginPermissions, JsPreferenceUserData, JsEvent, JsUiPropertyValue, JsRequest, JsUiRenderLocation, JsResponse, JsMessage, JsPluginPermissionsFileSystem, JsPluginPermissionsExec, JsPluginPermissionsMainSearchBar, JsMessageSide};
|
|
use crate::model::{IntermediateUiEvent};
|
|
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::run_status::RunStatusGuard;
|
|
use crate::search::{SearchIndex, SearchIndexItem, SearchIndexItemAction};
|
|
use crate::{PLUGIN_RUNTIME_ENV, SETTINGS_ENV};
|
|
use crate::plugins::image_gatherer::ImageGatherer;
|
|
|
|
pub struct PluginRuntimeData {
|
|
pub id: PluginId,
|
|
pub uuid: String,
|
|
pub name: String,
|
|
pub entrypoint_names: HashMap<EntrypointId, String>,
|
|
pub code: JsPluginCode,
|
|
pub inline_view_entrypoint_id: Option<String>,
|
|
pub permissions: PluginPermissions,
|
|
pub command_receiver: tokio::sync::broadcast::Receiver<PluginCommand>,
|
|
pub db_repository: DataDbRepository,
|
|
pub search_index: SearchIndex,
|
|
pub icon_cache: IconCache,
|
|
pub frontend_api: FrontendApi,
|
|
pub dirs: Dirs,
|
|
pub clipboard: Clipboard,
|
|
}
|
|
|
|
pub struct PluginPermissions {
|
|
pub environment: Vec<String>,
|
|
pub network: Vec<String>,
|
|
pub filesystem: JsPluginPermissionsFileSystem,
|
|
pub exec: JsPluginPermissionsExec,
|
|
pub system: Vec<String>,
|
|
pub clipboard: Vec<PluginPermissionsClipboard>,
|
|
pub main_search_bar: Vec<JsPluginPermissionsMainSearchBar>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PluginRuntimePermissions {
|
|
pub clipboard: Vec<PluginPermissionsClipboard>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
|
pub enum PluginPermissionsClipboard {
|
|
Read,
|
|
Write,
|
|
Clear
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum PluginCommand {
|
|
One {
|
|
id: PluginId,
|
|
data: OnePluginCommandData,
|
|
},
|
|
All {
|
|
data: AllPluginCommandData,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum OnePluginCommandData {
|
|
RenderView {
|
|
entrypoint_id: EntrypointId,
|
|
},
|
|
CloseView,
|
|
RunCommand {
|
|
entrypoint_id: String,
|
|
},
|
|
RunGeneratedCommand {
|
|
entrypoint_id: String,
|
|
action_index: Option<usize>
|
|
},
|
|
HandleViewEvent {
|
|
widget_id: UiWidgetId,
|
|
event_name: String,
|
|
event_arguments: Vec<UiPropertyValue>,
|
|
},
|
|
HandleKeyboardEvent {
|
|
entrypoint_id: EntrypointId,
|
|
origin: KeyboardEventOrigin,
|
|
key: PhysicalKey,
|
|
modifier_shift: bool,
|
|
modifier_control: bool,
|
|
modifier_alt: bool,
|
|
modifier_meta: bool,
|
|
},
|
|
ReloadSearchIndex,
|
|
RefreshSearchIndex,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum AllPluginCommandData {
|
|
OpenInlineView {
|
|
text: String
|
|
}
|
|
}
|
|
|
|
pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: RunStatusGuard) -> anyhow::Result<()> {
|
|
|
|
let runtime_permissions = PluginRuntimePermissions {
|
|
clipboard: data.permissions.clipboard,
|
|
};
|
|
|
|
let api = BackendForPluginRuntimeApiImpl::new(
|
|
data.icon_cache.clone(),
|
|
data.db_repository,
|
|
data.search_index,
|
|
data.clipboard,
|
|
data.frontend_api,
|
|
data.uuid.clone(),
|
|
data.id.clone(),
|
|
data.name,
|
|
data.entrypoint_names,
|
|
runtime_permissions,
|
|
);
|
|
|
|
let mut command_receiver = data.command_receiver;
|
|
let cache = data.icon_cache;
|
|
let plugin_uuid = data.uuid.clone();
|
|
let plugin_id = data.id.clone();
|
|
|
|
let plugin_id_str = plugin_id.to_string();
|
|
let dev_plugin = plugin_id_str.starts_with("file://");
|
|
|
|
let (stdout_file, stderr_file) = if dev_plugin {
|
|
let (stdout_file, stderr_file) = data.dirs.plugin_log_files(&plugin_uuid);
|
|
|
|
let stdout_file = stdout_file
|
|
.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
let stderr_file = stderr_file.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
(Some(stdout_file), Some(stderr_file))
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
let home_dir = data.dirs.home_dir();
|
|
let local_storage_dir = data.dirs.plugin_local_storage(&plugin_uuid);
|
|
let uds_socket_file = data.dirs.plugin_uds_socket(&plugin_uuid);
|
|
let plugin_cache_dir = data.dirs.plugin_cache(&plugin_uuid)?;
|
|
let plugin_data_dir = data.dirs.plugin_data(&plugin_uuid)?;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let name_str = format!("project-gauntlet-{}", plugin_uuid);
|
|
|
|
#[cfg(unix)]
|
|
let name_str = uds_socket_file.clone();
|
|
|
|
// namespaced, removed when both client and server disconnect
|
|
#[cfg(target_os = "windows")]
|
|
let name = name_str.clone().to_ns_name::<interprocess::local_socket::GenericNamespaced>()?;
|
|
|
|
// not namespaced, needs to be cleaned up manually,
|
|
// by using close-behind semantics and additionally removing it before creating a new runtime
|
|
#[cfg(unix)]
|
|
let name = {
|
|
let uds_socket_file = uds_socket_file.clone();
|
|
|
|
// manually remove in case of unexpected situation where removing after connection did not work properly
|
|
let _ = std::fs::remove_file(&uds_socket_file);
|
|
|
|
std::fs::create_dir_all(&uds_socket_file.parent().unwrap())?;
|
|
|
|
uds_socket_file.to_fs_name::<interprocess::os::unix::local_socket::FilesystemUdSocket>()?
|
|
};
|
|
|
|
let opts = ListenerOptions::new().name(name);
|
|
|
|
let listener = opts.create_tokio()?;
|
|
|
|
let home_dir = home_dir
|
|
.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
let local_storage_dir = local_storage_dir
|
|
.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
let uds_socket_file = uds_socket_file
|
|
.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
let plugin_cache_dir = plugin_cache_dir
|
|
.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
let plugin_data_dir = plugin_data_dir
|
|
.to_str()
|
|
.context("non-uft8 paths are not supported")?
|
|
.to_string();
|
|
|
|
let permissions = JsPluginPermissions {
|
|
environment: data.permissions.environment,
|
|
network: data.permissions.network,
|
|
filesystem: data.permissions.filesystem,
|
|
exec: data.permissions.exec,
|
|
system: data.permissions.system,
|
|
main_search_bar: data.permissions.main_search_bar,
|
|
};
|
|
|
|
let init = JsInit {
|
|
plugin_id: plugin_id.clone(),
|
|
plugin_uuid: plugin_uuid.clone(),
|
|
code: data.code,
|
|
permissions,
|
|
inline_view_entrypoint_id: data.inline_view_entrypoint_id,
|
|
dev_plugin,
|
|
home_dir,
|
|
local_storage_dir,
|
|
plugin_cache_dir,
|
|
plugin_data_dir,
|
|
stdout_file,
|
|
stderr_file,
|
|
};
|
|
|
|
let current_exe = std::env::current_exe()
|
|
.context("unable to get current_exe")?;
|
|
|
|
#[cfg(not(feature = "scenario_runner"))]
|
|
std::process::Command::new(current_exe)
|
|
.env(PLUGIN_RUNTIME_ENV, name_str)
|
|
.spawn()
|
|
.context("start plugin runtime process")?;
|
|
|
|
// use only for debugging and scenario_runner, only works if only one plugin is enabled
|
|
#[cfg(feature = "scenario_runner")]
|
|
std::thread::spawn(move || {
|
|
gauntlet_plugin_runtime::run_plugin_runtime(name_str.to_str().unwrap().to_string())
|
|
});
|
|
|
|
let conn = listener.accept().await?;
|
|
|
|
#[cfg(unix)]
|
|
let _ = std::fs::remove_file(&uds_socket_file);
|
|
|
|
let (mut recver, mut sender) = conn.split();
|
|
|
|
send_message(JsMessageSide::Backend, &mut sender, init).await?;
|
|
|
|
let sender = Mutex::new(sender);
|
|
|
|
tokio::select! {
|
|
_ = run_status_guard.stopped() => {
|
|
let mut sender = sender.lock().await;
|
|
|
|
tracing::info!("Requesting plugin runtime to stop...");
|
|
|
|
send_message(JsMessageSide::Backend, &mut sender, JsMessage::Stop).await?;
|
|
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
}
|
|
result @ _ = {
|
|
tokio::task::unconstrained(async {
|
|
loop {
|
|
|
|
if let Err(err) = event_loop(&mut command_receiver, &sender, plugin_id.clone()).await {
|
|
tracing::error!("Event loop faced an error {:?}", err);
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
} => {
|
|
tracing::error!("Event loop has unexpectedly stopped {:?}", plugin_id)
|
|
}
|
|
result @ _ = {
|
|
tokio::task::unconstrained(async {
|
|
loop {
|
|
if let Err(err) = request_loop(&mut recver, &sender, &api).await {
|
|
tracing::error!("Request loop faced an error {:?}", err);
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
} => {
|
|
tracing::error!("Request loop has unexpectedly stopped {:?}", plugin_id)
|
|
}
|
|
}
|
|
|
|
drop((recver, sender));
|
|
|
|
drop(run_status_guard);
|
|
|
|
if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) {
|
|
tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err)
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn event_loop(command_receiver: &mut tokio::sync::broadcast::Receiver<PluginCommand>, send: &Mutex<SendHalf>, plugin_id: PluginId) -> anyhow::Result<()> {
|
|
let command = command_receiver.recv().await?;
|
|
|
|
let event = match command {
|
|
PluginCommand::One { id, data } => {
|
|
if id != plugin_id {
|
|
None
|
|
} else {
|
|
match data {
|
|
OnePluginCommandData::RenderView { entrypoint_id } => {
|
|
Some(IntermediateUiEvent::OpenView {
|
|
entrypoint_id,
|
|
})
|
|
}
|
|
OnePluginCommandData::CloseView => {
|
|
Some(IntermediateUiEvent::CloseView)
|
|
}
|
|
OnePluginCommandData::RunCommand { entrypoint_id } => {
|
|
Some(IntermediateUiEvent::RunCommand {
|
|
entrypoint_id,
|
|
})
|
|
}
|
|
OnePluginCommandData::RunGeneratedCommand { entrypoint_id, action_index } => {
|
|
Some(IntermediateUiEvent::RunGeneratedCommand {
|
|
entrypoint_id,
|
|
action_index
|
|
})
|
|
}
|
|
OnePluginCommandData::HandleViewEvent { widget_id, event_name, event_arguments } => {
|
|
Some(IntermediateUiEvent::HandleViewEvent {
|
|
widget_id,
|
|
event_name,
|
|
event_arguments,
|
|
})
|
|
}
|
|
OnePluginCommandData::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => {
|
|
Some(IntermediateUiEvent::HandleKeyboardEvent {
|
|
entrypoint_id,
|
|
origin,
|
|
key,
|
|
modifier_shift,
|
|
modifier_control,
|
|
modifier_alt,
|
|
modifier_meta
|
|
})
|
|
}
|
|
OnePluginCommandData::ReloadSearchIndex => {
|
|
Some(IntermediateUiEvent::ReloadSearchIndex)
|
|
}
|
|
OnePluginCommandData::RefreshSearchIndex => {
|
|
Some(IntermediateUiEvent::RefreshSearchIndex)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
PluginCommand::All { data } => {
|
|
match data {
|
|
AllPluginCommandData::OpenInlineView { text } => {
|
|
Some(IntermediateUiEvent::OpenInlineView { text })
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
if let Some(event) = event {
|
|
let mut send = send.lock().await;
|
|
|
|
send_message(JsMessageSide::Backend, &mut send, JsMessage::Event(from_intermediate_to_js_event(event))).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
|
|
async fn request_loop(recv: &mut RecvHalf, send: &Mutex<SendHalf>, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result<()> {
|
|
match recv_message::<JsRequest>(JsMessageSide::Backend, recv).await {
|
|
Err(e) => {
|
|
Err(anyhow!("Unable to handle message: {:?}", e))
|
|
}
|
|
Ok(message) => {
|
|
tracing::trace!("Handling request message: {:?}", message);
|
|
|
|
match handle_message(message, api).await {
|
|
Ok(response) => {
|
|
let mut send = send.lock().await;
|
|
|
|
tracing::trace!("Sending request response: {:?}", response);
|
|
|
|
send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Ok(response))).await?;
|
|
|
|
Ok(())
|
|
}
|
|
Err(err) => {
|
|
let mut send = send.lock().await;
|
|
|
|
let err = format!("{:?}", err);
|
|
|
|
send_message(JsMessageSide::Backend, &mut send, JsMessage::Response(Err(err))).await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_message(message: JsRequest, api: &BackendForPluginRuntimeApiImpl) -> anyhow::Result<JsResponse> {
|
|
match message {
|
|
JsRequest::Render { entrypoint_id, render_location, top_level_view, container } => {
|
|
let render_location = match render_location {
|
|
JsUiRenderLocation::InlineView => UiRenderLocation::InlineView,
|
|
JsUiRenderLocation::View => UiRenderLocation::View
|
|
};
|
|
|
|
api.ui_render(entrypoint_id, render_location, top_level_view, container).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ClearInlineView => {
|
|
api.ui_clear_inline_view().await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ShowPluginErrorView { entrypoint_id, render_location } => {
|
|
let render_location = match render_location {
|
|
JsUiRenderLocation::InlineView => UiRenderLocation::InlineView,
|
|
JsUiRenderLocation::View => UiRenderLocation::View
|
|
};
|
|
|
|
api.ui_show_plugin_error_view(entrypoint_id, render_location).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ShowPreferenceRequiredView { entrypoint_id, plugin_preferences_required, entrypoint_preferences_required } => {
|
|
api.ui_show_preferences_required_view(entrypoint_id, plugin_preferences_required, entrypoint_preferences_required).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ShowHud { display } => {
|
|
api.ui_show_hud(display).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::UpdateLoadingBar { entrypoint_id, show } => {
|
|
api.ui_update_loading_bar(entrypoint_id, show).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ReloadSearchIndex { generated_commands, refresh_search_list } => {
|
|
api.reload_search_index(generated_commands, refresh_search_list).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::GetAssetData { path } => {
|
|
let data = api.get_asset_data(&path).await?;
|
|
|
|
Ok(JsResponse::AssetData {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::GetCommandGeneratorEntrypointIds => {
|
|
let data = api.get_command_generator_entrypoint_ids().await?;
|
|
|
|
Ok(JsResponse::CommandGeneratorEntrypointIds {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::GetPluginPreferences => {
|
|
let data = api.get_plugin_preferences().await?;
|
|
|
|
Ok(JsResponse::PluginPreferences {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::GetEntrypointPreferences { entrypoint_id } => {
|
|
let data = api.get_entrypoint_preferences(entrypoint_id).await?;
|
|
|
|
Ok(JsResponse::EntrypointPreferences {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::PluginPreferencesRequired => {
|
|
let data = api.plugin_preferences_required().await?;
|
|
|
|
Ok(JsResponse::PluginPreferencesRequired {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::EntrypointPreferencesRequired { entrypoint_id } => {
|
|
let data = api.entrypoint_preferences_required(entrypoint_id).await?;
|
|
|
|
Ok(JsResponse::EntrypointPreferencesRequired {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::ClipboardRead => {
|
|
let data = api.clipboard_read().await?;
|
|
|
|
Ok(JsResponse::ClipboardRead {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::ClipboardReadText => {
|
|
let data = api.clipboard_read_text().await?;
|
|
|
|
Ok(JsResponse::ClipboardReadText {
|
|
data
|
|
})
|
|
}
|
|
JsRequest::ClipboardWrite { data } => {
|
|
api.clipboard_write(data).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ClipboardWriteText { data } => {
|
|
api.clipboard_write_text(data).await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::ClipboardClear => {
|
|
api.clipboard_clear().await?;
|
|
|
|
Ok(JsResponse::Nothing)
|
|
}
|
|
JsRequest::GetActionIdForShortcut { entrypoint_id, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => {
|
|
let data = api.ui_get_action_id_for_shortcut(
|
|
entrypoint_id,
|
|
key,
|
|
modifier_shift,
|
|
modifier_control,
|
|
modifier_alt,
|
|
modifier_meta
|
|
).await?;
|
|
|
|
Ok(JsResponse::ActionIdForShortcut {
|
|
data
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsEvent {
|
|
match event {
|
|
IntermediateUiEvent::OpenView { entrypoint_id } => JsEvent::OpenView {
|
|
entrypoint_id: entrypoint_id.to_string(),
|
|
},
|
|
IntermediateUiEvent::CloseView => JsEvent::CloseView,
|
|
IntermediateUiEvent::RunCommand { entrypoint_id } => JsEvent::RunCommand {
|
|
entrypoint_id
|
|
},
|
|
IntermediateUiEvent::RunGeneratedCommand { entrypoint_id, action_index } => JsEvent::RunGeneratedCommand {
|
|
entrypoint_id,
|
|
action_index,
|
|
},
|
|
IntermediateUiEvent::HandleViewEvent { widget_id, event_name, event_arguments } => {
|
|
let event_arguments = event_arguments.into_iter()
|
|
.map(|arg| match arg {
|
|
UiPropertyValue::String(value) => JsUiPropertyValue::String { value },
|
|
UiPropertyValue::Number(value) => JsUiPropertyValue::Number { value },
|
|
UiPropertyValue::Bool(value) => JsUiPropertyValue::Bool { value },
|
|
UiPropertyValue::Undefined => JsUiPropertyValue::Undefined,
|
|
UiPropertyValue::Array(_) | UiPropertyValue::Bytes(_) | UiPropertyValue::Object(_) => {
|
|
todo!()
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
JsEvent::ViewEvent {
|
|
widget_id,
|
|
event_name,
|
|
event_arguments,
|
|
}
|
|
}
|
|
IntermediateUiEvent::HandleKeyboardEvent { entrypoint_id, origin, key, modifier_shift, modifier_control, modifier_alt, modifier_meta } => {
|
|
JsEvent::KeyboardEvent {
|
|
entrypoint_id: entrypoint_id.to_string(),
|
|
origin: match origin {
|
|
KeyboardEventOrigin::MainView => JsKeyboardEventOrigin::MainView,
|
|
KeyboardEventOrigin::PluginView => JsKeyboardEventOrigin::PluginView,
|
|
},
|
|
key: key.to_value(),
|
|
modifier_shift,
|
|
modifier_control,
|
|
modifier_alt,
|
|
modifier_meta
|
|
}
|
|
}
|
|
IntermediateUiEvent::OpenInlineView { text } => JsEvent::OpenInlineView { text },
|
|
IntermediateUiEvent::ReloadSearchIndex => JsEvent::ReloadSearchIndex,
|
|
IntermediateUiEvent::RefreshSearchIndex => JsEvent::RefreshSearchIndex,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct BackendForPluginRuntimeApiImpl {
|
|
icon_cache: IconCache,
|
|
repository: DataDbRepository,
|
|
search_index: SearchIndex,
|
|
clipboard: Clipboard,
|
|
frontend_api: FrontendApi,
|
|
plugin_uuid: String,
|
|
plugin_id: PluginId,
|
|
plugin_name: String,
|
|
entrypoint_names: HashMap<EntrypointId, String>,
|
|
permissions: PluginRuntimePermissions
|
|
}
|
|
|
|
impl BackendForPluginRuntimeApiImpl {
|
|
fn new(
|
|
icon_cache: IconCache,
|
|
repository: DataDbRepository,
|
|
search_index: SearchIndex,
|
|
clipboard: Clipboard,
|
|
frontend_api: FrontendApi,
|
|
plugin_uuid: String,
|
|
plugin_id: PluginId,
|
|
plugin_name: String,
|
|
entrypoint_names: HashMap<EntrypointId, String>,
|
|
permissions: PluginRuntimePermissions
|
|
) -> Self {
|
|
Self {
|
|
icon_cache,
|
|
repository,
|
|
search_index,
|
|
clipboard,
|
|
frontend_api,
|
|
plugin_uuid,
|
|
plugin_id,
|
|
plugin_name,
|
|
entrypoint_names,
|
|
permissions
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BackendForPluginRuntimeApi for BackendForPluginRuntimeApiImpl {
|
|
async fn reload_search_index(&self, generated_commands: Vec<JsAdditionalSearchItem>, refresh_search_list: bool) -> anyhow::Result<()> {
|
|
self.icon_cache.clear_plugin_icon_cache_dir(&self.plugin_uuid)
|
|
.context("error when clearing up icon cache before recreating it")?;
|
|
|
|
let DbReadPlugin { name, .. } = self.repository.get_plugin_by_id(&self.plugin_id.to_string())
|
|
.await
|
|
.context("error when getting plugin by id")?;
|
|
|
|
let entrypoints = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string())
|
|
.await
|
|
.context("error when getting entrypoints by plugin id")?;
|
|
|
|
let frecency_map = self.repository.get_frecency_for_plugin(&self.plugin_id.to_string())
|
|
.await
|
|
.context("error when getting frecency for plugin")?;
|
|
|
|
let mut shortcuts = HashMap::new();
|
|
|
|
for DbReadPluginEntrypoint { id, .. } in &entrypoints {
|
|
let entrypoint_shortcuts = self.repository.action_shortcuts(&self.plugin_id.to_string(), id).await?;
|
|
shortcuts.insert(id.clone(), entrypoint_shortcuts);
|
|
}
|
|
|
|
let mut plugins_search_items = generated_commands.into_iter()
|
|
.map(|item| {
|
|
let entrypoint_icon_path = match item.entrypoint_icon {
|
|
None => None,
|
|
Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &item.entrypoint_uuid, &data)?),
|
|
};
|
|
|
|
let entrypoint_frecency = frecency_map.get(&item.entrypoint_id).cloned().unwrap_or(0.0);
|
|
|
|
let shortcuts = shortcuts
|
|
.get(&item.generator_entrypoint_id);
|
|
|
|
let entrypoint_actions = item.entrypoint_actions.iter()
|
|
.map(|action| {
|
|
let shortcut = match (shortcuts, &action.id) {
|
|
(Some(shortcuts), Some(id)) => {
|
|
shortcuts.get(id).cloned()
|
|
}
|
|
_ => None
|
|
};
|
|
|
|
SearchIndexItemAction {
|
|
label: action.label.clone(),
|
|
shortcut,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(SearchIndexItem {
|
|
entrypoint_type: SearchResultEntrypointType::GeneratedCommand,
|
|
entrypoint_id: EntrypointId::from_string(item.entrypoint_id),
|
|
entrypoint_name: item.entrypoint_name,
|
|
entrypoint_icon_path,
|
|
entrypoint_frecency,
|
|
entrypoint_actions,
|
|
})
|
|
})
|
|
.collect::<anyhow::Result<Vec<_>>>()?;
|
|
|
|
let mut icon_asset_data = HashMap::new();
|
|
|
|
for entrypoint in &entrypoints {
|
|
if let Some(path_to_asset) = &entrypoint.icon_path {
|
|
let result = self.repository.get_asset_data(&self.plugin_id.to_string(), path_to_asset)
|
|
.await;
|
|
|
|
if let Ok(data) = result {
|
|
icon_asset_data.insert((entrypoint.id.clone(), path_to_asset.clone()), data);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut builtin_search_items = entrypoints.into_iter()
|
|
.filter(|entrypoint| entrypoint.enabled)
|
|
.map(|entrypoint| {
|
|
let entrypoint_type = db_entrypoint_from_str(&entrypoint.entrypoint_type);
|
|
let entrypoint_id = entrypoint.id.to_string();
|
|
|
|
let entrypoint_frecency = frecency_map.get(&entrypoint_id).cloned().unwrap_or(0.0);
|
|
|
|
let entrypoint_icon_path = match entrypoint.icon_path {
|
|
None => None,
|
|
Some(path_to_asset) => {
|
|
match icon_asset_data.get(&(entrypoint.id, path_to_asset)) {
|
|
None => None,
|
|
Some(data) => Some(self.icon_cache.save_entrypoint_icon_to_cache(&self.plugin_uuid, &entrypoint.uuid, data)?)
|
|
}
|
|
},
|
|
};
|
|
|
|
let entrypoint_id = EntrypointId::from_string(entrypoint_id);
|
|
|
|
match &entrypoint_type {
|
|
DbPluginEntrypointType::Command => {
|
|
Ok(Some(SearchIndexItem {
|
|
entrypoint_type: SearchResultEntrypointType::Command,
|
|
entrypoint_name: entrypoint.name,
|
|
entrypoint_id,
|
|
entrypoint_icon_path,
|
|
entrypoint_frecency,
|
|
entrypoint_actions: vec![],
|
|
}))
|
|
},
|
|
DbPluginEntrypointType::View => {
|
|
Ok(Some(SearchIndexItem {
|
|
entrypoint_type: SearchResultEntrypointType::View,
|
|
entrypoint_name: entrypoint.name,
|
|
entrypoint_id,
|
|
entrypoint_icon_path,
|
|
entrypoint_frecency,
|
|
entrypoint_actions: vec![],
|
|
}))
|
|
},
|
|
DbPluginEntrypointType::CommandGenerator | DbPluginEntrypointType::InlineView => {
|
|
Ok(None)
|
|
}
|
|
}
|
|
})
|
|
.collect::<anyhow::Result<Vec<_>>>()?
|
|
.into_iter()
|
|
.flat_map(|item| item)
|
|
.collect::<Vec<_>>();
|
|
|
|
plugins_search_items.append(&mut builtin_search_items);
|
|
|
|
self.search_index.save_for_plugin(self.plugin_id.clone(), name, plugins_search_items, refresh_search_list)
|
|
.context("error when updating search index")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_asset_data(&self, path: &str) -> anyhow::Result<Vec<u8>> {
|
|
let data = self.repository.get_asset_data(&self.plugin_id.to_string(), &path)
|
|
.await?;
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
async fn get_command_generator_entrypoint_ids(&self) -> anyhow::Result<Vec<String>> {
|
|
let result = self.repository.get_entrypoints_by_plugin_id(&self.plugin_id.to_string()).await?
|
|
.into_iter()
|
|
.filter(|entrypoint| entrypoint.enabled)
|
|
.filter(|entrypoint| matches!(db_entrypoint_from_str(&entrypoint.entrypoint_type), DbPluginEntrypointType::CommandGenerator))
|
|
.map(|entrypoint| entrypoint.id)
|
|
.collect::<Vec<_>>();
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn get_plugin_preferences(&self) -> anyhow::Result<HashMap<String, JsPreferenceUserData>> {
|
|
let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository
|
|
.get_plugin_by_id(&self.plugin_id.to_string())
|
|
.await?;
|
|
|
|
Ok(preferences_to_js(preferences, preferences_user_data))
|
|
}
|
|
|
|
async fn get_entrypoint_preferences(&self, entrypoint_id: EntrypointId) -> anyhow::Result<HashMap<String, JsPreferenceUserData>> {
|
|
let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository
|
|
.get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string())
|
|
.await?;
|
|
|
|
Ok(preferences_to_js(preferences, preferences_user_data))
|
|
}
|
|
|
|
async fn plugin_preferences_required(&self) -> anyhow::Result<bool> {
|
|
let DbReadPlugin { preferences, preferences_user_data, .. } = self.repository
|
|
.get_plugin_by_id(&self.plugin_id.to_string()).await?;
|
|
|
|
Ok(any_preferences_missing_value(preferences, preferences_user_data))
|
|
}
|
|
|
|
async fn entrypoint_preferences_required(&self, entrypoint_id: EntrypointId) -> anyhow::Result<bool> {
|
|
let DbReadPluginEntrypoint { preferences, preferences_user_data, .. } = self.repository
|
|
.get_entrypoint_by_id(&self.plugin_id.to_string(), &entrypoint_id.to_string()).await?;
|
|
|
|
Ok(any_preferences_missing_value(preferences, preferences_user_data))
|
|
}
|
|
|
|
async fn clipboard_read(&self) -> anyhow::Result<JsClipboardData> {
|
|
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: JsClipboardData) -> 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()
|
|
}
|
|
|
|
async fn ui_update_loading_bar(&self, entrypoint_id: EntrypointId, show: bool) -> anyhow::Result<()> {
|
|
self.frontend_api.update_loading_bar(self.plugin_id.clone(), entrypoint_id, show).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn ui_show_hud(&self, display: String) -> anyhow::Result<()> {
|
|
self.frontend_api.show_hud(display).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn ui_get_action_id_for_shortcut(
|
|
&self,
|
|
entrypoint_id: EntrypointId,
|
|
key: String,
|
|
modifier_shift: bool,
|
|
modifier_control: bool,
|
|
modifier_alt: bool,
|
|
modifier_meta: bool
|
|
) -> anyhow::Result<Option<String>> {
|
|
let result = self.repository.get_action_id_for_shortcut(
|
|
&self.plugin_id.to_string(),
|
|
&entrypoint_id.to_string(),
|
|
PhysicalKey::from_value(key),
|
|
modifier_shift,
|
|
modifier_control,
|
|
modifier_alt,
|
|
modifier_meta
|
|
).await?;
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn ui_render(
|
|
&self,
|
|
entrypoint_id: EntrypointId,
|
|
render_location: UiRenderLocation,
|
|
top_level_view: bool,
|
|
container: RootWidget,
|
|
) -> anyhow::Result<()> {
|
|
|
|
let entrypoint_name = self.entrypoint_names
|
|
.get(&entrypoint_id)
|
|
.expect("entrypoint name for id should always exist")
|
|
.to_string();
|
|
|
|
let images = ImageGatherer::run_gatherer(&self, &container).await?;
|
|
|
|
self.frontend_api.replace_view(
|
|
self.plugin_id.clone(),
|
|
self.plugin_name.clone(),
|
|
entrypoint_id,
|
|
entrypoint_name,
|
|
render_location,
|
|
top_level_view,
|
|
container,
|
|
images
|
|
).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn ui_show_plugin_error_view(
|
|
&self,
|
|
entrypoint_id: EntrypointId,
|
|
render_location: UiRenderLocation
|
|
) -> anyhow::Result<()> {
|
|
self.frontend_api.show_plugin_error_view(
|
|
self.plugin_id.clone(),
|
|
entrypoint_id,
|
|
render_location
|
|
).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn ui_show_preferences_required_view(
|
|
&self,
|
|
entrypoint_id: EntrypointId,
|
|
plugin_preferences_required: bool,
|
|
entrypoint_preferences_required: bool
|
|
) -> anyhow::Result<()> {
|
|
|
|
self.frontend_api.show_preference_required_view(
|
|
self.plugin_id.clone(),
|
|
entrypoint_id,
|
|
plugin_preferences_required,
|
|
entrypoint_preferences_required
|
|
).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn ui_clear_inline_view(&self) -> anyhow::Result<()> {
|
|
self.frontend_api.clear_inline_view(self.plugin_id.clone()).await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
|
|
fn preferences_to_js(
|
|
preferences: HashMap<String, DbPluginPreference>,
|
|
mut preferences_user_data: HashMap<String, DbPluginPreferenceUserData>
|
|
) -> HashMap<String, JsPreferenceUserData> {
|
|
preferences.into_iter()
|
|
.map(|(name, preference)| {
|
|
let user_data = match preferences_user_data.remove(&name) {
|
|
None => match preference {
|
|
DbPluginPreference::Number { default, .. } => JsPreferenceUserData::Number(default.expect("at this point preference should always have value")),
|
|
DbPluginPreference::String { default, .. } => JsPreferenceUserData::String(default.expect("at this point preference should always have value")),
|
|
DbPluginPreference::Enum { default, .. } => JsPreferenceUserData::String(default.expect("at this point preference should always have value")),
|
|
DbPluginPreference::Bool { default, .. } => JsPreferenceUserData::Bool(default.expect("at this point preference should always have value")),
|
|
DbPluginPreference::ListOfStrings { default, .. } => JsPreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")),
|
|
DbPluginPreference::ListOfNumbers { default, .. } => JsPreferenceUserData::ListOfNumbers(default.expect("at this point preference should always have value")),
|
|
DbPluginPreference::ListOfEnums { default, .. } => JsPreferenceUserData::ListOfStrings(default.expect("at this point preference should always have value")),
|
|
}
|
|
Some(user_data) => match user_data {
|
|
DbPluginPreferenceUserData::Number { value } => JsPreferenceUserData::Number(value.expect("at this point preference should always have value")),
|
|
DbPluginPreferenceUserData::String { value } => JsPreferenceUserData::String(value.expect("at this point preference should always have value")),
|
|
DbPluginPreferenceUserData::Enum { value } => JsPreferenceUserData::String(value.expect("at this point preference should always have value")),
|
|
DbPluginPreferenceUserData::Bool { value } => JsPreferenceUserData::Bool(value.expect("at this point preference should always have value")),
|
|
DbPluginPreferenceUserData::ListOfStrings { value } => JsPreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")),
|
|
DbPluginPreferenceUserData::ListOfNumbers { value } => JsPreferenceUserData::ListOfNumbers(value.expect("at this point preference should always have value")),
|
|
DbPluginPreferenceUserData::ListOfEnums { value } => JsPreferenceUserData::ListOfStrings(value.expect("at this point preference should always have value")),
|
|
}
|
|
};
|
|
|
|
(name, user_data)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn any_preferences_missing_value(preferences: HashMap<String, DbPluginPreference>, preferences_user_data: HashMap<String, DbPluginPreferenceUserData>) -> bool {
|
|
for (name, preference) in preferences {
|
|
match preferences_user_data.get(&name) {
|
|
None => {
|
|
let no_default = match preference {
|
|
DbPluginPreference::Number { default, .. } => default.is_none(),
|
|
DbPluginPreference::String { default, .. } => default.is_none(),
|
|
DbPluginPreference::Enum { default, .. } => default.is_none(),
|
|
DbPluginPreference::Bool { default, .. } => default.is_none(),
|
|
DbPluginPreference::ListOfStrings { default, .. } => default.is_none(),
|
|
DbPluginPreference::ListOfNumbers { default, .. } => default.is_none(),
|
|
DbPluginPreference::ListOfEnums { default, .. } => default.is_none(),
|
|
};
|
|
|
|
if no_default {
|
|
return true
|
|
}
|
|
}
|
|
Some(preference) => {
|
|
let no_value = match preference {
|
|
DbPluginPreferenceUserData::Number { value } => value.is_none(),
|
|
DbPluginPreferenceUserData::String { value } => value.is_none(),
|
|
DbPluginPreferenceUserData::Enum { value } => value.is_none(),
|
|
DbPluginPreferenceUserData::Bool { value } => value.is_none(),
|
|
DbPluginPreferenceUserData::ListOfStrings { value } => value.is_none(),
|
|
DbPluginPreferenceUserData::ListOfNumbers { value } => value.is_none(),
|
|
DbPluginPreferenceUserData::ListOfEnums { value } => value.is_none(),
|
|
};
|
|
|
|
if no_value {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
} |