From 160d733054d3a67e051da790f96f89850c8a2ccd Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Sun, 19 Jan 2025 23:13:05 +0100 Subject: [PATCH] feat: support for plugin dependencies and wayvr dashboards (using unreleased api) --- Cargo.lock | 26 ++++ Cargo.toml | 1 + src/file_builders/mod.rs | 1 + src/file_builders/wayvr_dashboard_config.rs | 13 ++ src/ui/app.rs | 4 +- src/ui/plugins/mod.rs | 100 ++++++++++++++- src/ui/plugins/store.rs | 127 +++++++++++++++----- 7 files changed, 234 insertions(+), 38 deletions(-) create mode 100644 src/file_builders/wayvr_dashboard_config.rs diff --git a/Cargo.lock b/Cargo.lock index a11284f..01a7fcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,7 @@ dependencies = [ "rusb", "serde", "serde_json", + "serde_yml", "sha2", "tokio", "tracing", @@ -1635,6 +1636,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + [[package]] name = "libz-sys" version = "1.1.20" @@ -2555,6 +2566,21 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 2e10d37..1f5a0fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,4 @@ zbus = { version = "5.1.1", features = ["tokio"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } tracing = "0.1.41" tracing-appender = "0.2.3" +serde_yml = "0.0.12" diff --git a/src/file_builders/mod.rs b/src/file_builders/mod.rs index b6078ae..33c9829 100644 --- a/src/file_builders/mod.rs +++ b/src/file_builders/mod.rs @@ -1,5 +1,6 @@ pub mod active_runtime_json; pub mod monado_autorun; pub mod openvrpaths_vrpath; +pub mod wayvr_dashboard_config; pub mod wivrn_config; pub mod wivrn_encoder_presets; diff --git a/src/file_builders/wayvr_dashboard_config.rs b/src/file_builders/wayvr_dashboard_config.rs new file mode 100644 index 0000000..6e9315c --- /dev/null +++ b/src/file_builders/wayvr_dashboard_config.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WayVrDashboardConfigFragmentInner { + pub exec: String, + pub args: Option, + pub env: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WayVrDashboardConfigFragment { + pub dashboard: WayVrDashboardConfigFragmentInner, +} diff --git a/src/ui/app.rs b/src/ui/app.rs index acaac50..c3226c9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -248,7 +248,7 @@ impl App { .plugins .values() .filter_map(|cp| { - if cp.enabled && cp.plugin.validate() { + if cp.plugin.plugin_type.launches_directly() && cp.enabled && cp.plugin.validate() { if let Err(e) = cp.plugin.mark_as_executable() { error!( "failed to mark plugin {} as executable: {e}", @@ -263,7 +263,7 @@ impl App { .unwrap() .to_string_lossy() .to_string()]; - cmd_parts.extend(cp.plugin.luanch_opts.clone().unwrap_or_default()); + cmd_parts.extend(cp.plugin.args.clone().unwrap_or_default()); cmd_parts.join(" ") })) } diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs index ec8a9e3..a992ac9 100644 --- a/src/ui/plugins/mod.rs +++ b/src/ui/plugins/mod.rs @@ -4,13 +4,36 @@ mod store_detail; mod store_row_factory; use crate::{ + constants::APP_ID, downloader::{cache_file_path, download_file_async}, + file_builders::wayvr_dashboard_config::{ + WayVrDashboardConfigFragment, WayVrDashboardConfigFragmentInner, + }, paths::get_plugins_dir, - util::file_utils::mark_as_executable, + util::file_utils::{get_writer, mark_as_executable}, + xdg::XDG, }; use anyhow::bail; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{ + collections::HashMap, + fs::{create_dir_all, remove_file}, + path::PathBuf, +}; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)] +pub enum PluginType { + #[default] + Executable, + WayVrApp, + WayVrDashboard, +} + +impl PluginType { + pub fn launches_directly(&self) -> bool { + self == &Self::Executable + } +} #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)] pub struct Plugin { @@ -28,10 +51,79 @@ pub struct Plugin { /// either one of exec_url or exec_path must be provided pub exec_path: Option, /// options and arguments that should be passed to the plugin executable - pub luanch_opts: Option>, + pub args: Option>, + pub env_vars: Option>, + /// defined as a list of appids of other plugins + pub dependencies: Option>, + /// defined as a list of appids of other plugins + /// all plugins of type WayVrDashboard should conflict with each other by default + pub conflicts: Option>, + #[serde(default = "PluginType::default")] + pub plugin_type: PluginType, } impl Plugin { + fn wayvr_config_fragment_filename(&self) -> String { + format!("99-{APP_ID}-plugin.{}.yaml", self.appid) + } + + fn wayvr_conf_dir() -> PathBuf { + XDG.get_config_home().join("wlxoverlay/wayvr.conf.d") + } + + pub fn enable(&self) -> anyhow::Result<()> { + match self.plugin_type { + PluginType::Executable => {} + PluginType::WayVrApp => todo!(), + PluginType::WayVrDashboard => { + let wayvr_conf_dir = Self::wayvr_conf_dir(); + if !wayvr_conf_dir.exists() { + create_dir_all(&wayvr_conf_dir)?; + } else if wayvr_conf_dir.is_file() { + bail!("wayvr.conf.d is a file and not a directory") + } + let config_fragment = WayVrDashboardConfigFragment { + dashboard: WayVrDashboardConfigFragmentInner { + exec: self + .executable() + .ok_or(anyhow::Error::msg("executable missing"))? + .to_string_lossy() + .to_string(), + args: self.args.as_ref().map(|args| args.join(" ")), + env: self + .env_vars + .as_ref() + .map(|vars| vars.iter().map(|(k, v)| format!("{k}={v}")).collect()), + }, + }; + let writer = + get_writer(&wayvr_conf_dir.join(self.wayvr_config_fragment_filename()))?; + serde_yml::to_writer(writer, &config_fragment)?; + } + } + Ok(()) + } + + pub fn disable(&self) -> anyhow::Result<()> { + match self.plugin_type { + PluginType::Executable => {} + PluginType::WayVrApp => todo!(), + PluginType::WayVrDashboard => { + let wayvr_conf_dir = Self::wayvr_conf_dir(); + remove_file(wayvr_conf_dir.join(self.wayvr_config_fragment_filename()))?; + } + }; + Ok(()) + } + + pub fn set_enabled(&self, enabled: bool) -> anyhow::Result<()> { + if enabled { + self.enable() + } else { + self.disable() + } + } + pub fn executable(&self) -> Option { if self.exec_path.is_some() { self.exec_path.clone() @@ -72,7 +164,7 @@ impl Plugin { /// urls to manifest json files representing plugins. /// each manifest should be json and the link should always point to the latest version const MANIFESTS: [&str;2] = [ - "https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galiser.wlx-overlay-s.json", + "https://github.com/galister/wlx-overlay-s/raw/refs/heads/meta/com.github.galister.wlx-overlay-s.json", "https://gitlab.com/gabmus/envision-plugin-manifests/-/raw/main/org.stardustxr.telescope.json", ]; diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs index 58ddc26..e8fca91 100644 --- a/src/ui/plugins/store.rs +++ b/src/ui/plugins/store.rs @@ -51,7 +51,7 @@ pub enum PluginStoreMsg { DoRefresh, Install(Plugin, relm4::Sender), InstallFromDetails(Plugin), - InstallDownload(Plugin, relm4::Sender), + InstallDownload(Vec, relm4::Sender), Remove(Plugin), SetEnabled(PluginStoreSignalSource, Plugin, bool), ShowDetails(usize), @@ -308,33 +308,70 @@ impl AsyncComponent for PluginStore { } Self::Input::Install(plugin, row_sender) => { self.set_locked(true); - sender.input(Self::Input::InstallDownload(plugin, row_sender)) - } - Self::Input::InstallDownload(plugin, row_sender) => { - let mut plugin = plugin.clone(); - match plugin.exec_url.as_ref() { - Some(url) => { - let exec_path = plugin.canonical_exec_path(); - if let Err(e) = download_file_async(url, &exec_path).await { - alert( - "Download failed", - Some(&format!( - "Downloading {} {} failed:\n\n{e}", - plugin.name, - plugin - .version - .as_ref() - .unwrap_or(&"(no version)".to_string()) - )), - Some(&self.win.as_ref().unwrap().clone().upcast::()), - ); - } else { - plugin.exec_path = Some(exec_path); - sender.input(Self::Input::AddPluginToConfig(plugin.clone())); - } - } - None => { + let mut plugins = vec![plugin]; + for dep in plugins[0].dependencies.clone().unwrap_or_default() { + if let Some(dep_plugin) = self + .plugins + .iter() + .find(|plugin| plugin.appid == dep) + .cloned() + { + plugins.push(dep_plugin); + } else { + error!( + "unable to find dependency for {}: {}", + plugins[0].appid, dep + ); alert( + "Missing dependencies", + Some(&format!( + "{} depends on unknown plugin {}", + plugins[0].name, dep + )), + Some(&self.win.clone().unwrap().upcast::()), + ); + return; + } + } + sender.input(Self::Input::InstallDownload(plugins, row_sender)) + } + Self::Input::InstallDownload(plugins, row_sender) => { + for plugin in plugins { + let mut plugin = plugin.clone(); + match plugin.exec_url.as_ref() { + Some(url) => { + let exec_path = plugin.canonical_exec_path(); + if let Err(e) = download_file_async(url, &exec_path).await { + alert( + "Download failed", + Some(&format!( + "Downloading {} {} failed:\n\n{e}", + plugin.name, + plugin + .version + .as_ref() + .unwrap_or(&"(no version)".to_string()) + )), + Some( + &self.win.as_ref().unwrap().clone().upcast::(), + ), + ); + } else { + plugin.exec_path = Some(exec_path); + if let Err(e) = plugin.enable() { + error!("failed to enable plugin {}: {e}", plugin.appid); + alert( + "Failed to enable plugin", + Some(&e.to_string()), + Some(&self.win.clone().unwrap().upcast::()), + ); + return; + } + sender.input(Self::Input::AddPluginToConfig(plugin.clone())); + } + } + None => { + alert( "Download failed", Some(&format!( "Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!", @@ -343,15 +380,25 @@ impl AsyncComponent for PluginStore { ), Some(&self.win.as_ref().unwrap().clone().upcast::()) ); - } - }; - row_sender.emit(StoreRowModelMsg::Refresh(true, false)); - self.details - .emit(StoreDetailMsg::Refresh(plugin.appid, true, false)); + } + }; + row_sender.emit(StoreRowModelMsg::Refresh(true, false)); + self.details + .emit(StoreDetailMsg::Refresh(plugin.appid, true, false)); + } self.set_locked(false); } Self::Input::Remove(plugin) => { self.set_locked(true); + if let Err(e) = plugin.disable() { + error!("failed to disable plugin {}: {e}", plugin.appid); + alert( + "Failed to disable plugin", + Some(&e.to_string()), + Some(&self.win.clone().unwrap().upcast::()), + ); + return; + } if let Some(exec) = plugin.executable() { // delete executable only if it's not a custom plugin if exec.is_file() && plugin.exec_url.is_some() { @@ -387,6 +434,22 @@ impl AsyncComponent for PluginStore { } Self::Input::SetEnabled(signal_sender, plugin, enabled) => { if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) { + if let Err(e) = plugin.set_enabled(enabled) { + error!( + "failed to {} plugin {}: {e}", + if enabled { "enable" } else { "disable" }, + plugin.appid + ); + alert( + &format!( + "Failed to {} plugin", + if enabled { "enable" } else { "disable" } + ), + Some(&e.to_string()), + Some(&self.win.clone().unwrap().upcast::()), + ); + return; + } cp.enabled = enabled; if signal_sender == PluginStoreSignalSource::Detail { if let Some(row) = self