feat: support for plugin dependencies and wayvr dashboards (using unreleased api)
Some checks are pending
/ cargo-fmtcheck (push) Waiting to run
/ cargo-clippy (push) Waiting to run
/ cargo-test (push) Waiting to run
/ appimage (push) Waiting to run

This commit is contained in:
Gabriele Musco 2025-01-19 23:13:05 +01:00
commit 160d733054
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
7 changed files with 234 additions and 38 deletions

26
Cargo.lock generated
View file

@ -583,6 +583,7 @@ dependencies = [
"rusb", "rusb",
"serde", "serde",
"serde_json", "serde_json",
"serde_yml",
"sha2", "sha2",
"tokio", "tokio",
"tracing", "tracing",
@ -1635,6 +1636,16 @@ dependencies = [
"vcpkg", "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]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.20" version = "1.1.20"
@ -2555,6 +2566,21 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"

View file

@ -43,3 +43,4 @@ zbus = { version = "5.1.1", features = ["tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
serde_yml = "0.0.12"

View file

@ -1,5 +1,6 @@
pub mod active_runtime_json; pub mod active_runtime_json;
pub mod monado_autorun; pub mod monado_autorun;
pub mod openvrpaths_vrpath; pub mod openvrpaths_vrpath;
pub mod wayvr_dashboard_config;
pub mod wivrn_config; pub mod wivrn_config;
pub mod wivrn_encoder_presets; pub mod wivrn_encoder_presets;

View file

@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WayVrDashboardConfigFragmentInner {
pub exec: String,
pub args: Option<String>,
pub env: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WayVrDashboardConfigFragment {
pub dashboard: WayVrDashboardConfigFragmentInner,
}

View file

@ -248,7 +248,7 @@ impl App {
.plugins .plugins
.values() .values()
.filter_map(|cp| { .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() { if let Err(e) = cp.plugin.mark_as_executable() {
error!( error!(
"failed to mark plugin {} as executable: {e}", "failed to mark plugin {} as executable: {e}",
@ -263,7 +263,7 @@ impl App {
.unwrap() .unwrap()
.to_string_lossy() .to_string_lossy()
.to_string()]; .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(" ") cmd_parts.join(" ")
})) }))
} }

View file

@ -4,13 +4,36 @@ mod store_detail;
mod store_row_factory; mod store_row_factory;
use crate::{ use crate::{
constants::APP_ID,
downloader::{cache_file_path, download_file_async}, downloader::{cache_file_path, download_file_async},
file_builders::wayvr_dashboard_config::{
WayVrDashboardConfigFragment, WayVrDashboardConfigFragmentInner,
},
paths::get_plugins_dir, paths::get_plugins_dir,
util::file_utils::mark_as_executable, util::file_utils::{get_writer, mark_as_executable},
xdg::XDG,
}; };
use anyhow::bail; use anyhow::bail;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
pub struct Plugin { pub struct Plugin {
@ -28,10 +51,79 @@ pub struct Plugin {
/// either one of exec_url or exec_path must be provided /// either one of exec_url or exec_path must be provided
pub exec_path: Option<PathBuf>, pub exec_path: Option<PathBuf>,
/// options and arguments that should be passed to the plugin executable /// options and arguments that should be passed to the plugin executable
pub luanch_opts: Option<Vec<String>>, pub args: Option<Vec<String>>,
pub env_vars: Option<HashMap<String, String>>,
/// defined as a list of appids of other plugins
pub dependencies: Option<Vec<String>>,
/// defined as a list of appids of other plugins
/// all plugins of type WayVrDashboard should conflict with each other by default
pub conflicts: Option<Vec<String>>,
#[serde(default = "PluginType::default")]
pub plugin_type: PluginType,
} }
impl Plugin { 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<PathBuf> { pub fn executable(&self) -> Option<PathBuf> {
if self.exec_path.is_some() { if self.exec_path.is_some() {
self.exec_path.clone() self.exec_path.clone()
@ -72,7 +164,7 @@ impl Plugin {
/// urls to manifest json files representing plugins. /// urls to manifest json files representing plugins.
/// each manifest should be json and the link should always point to the latest version /// each manifest should be json and the link should always point to the latest version
const MANIFESTS: [&str;2] = [ 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", "https://gitlab.com/gabmus/envision-plugin-manifests/-/raw/main/org.stardustxr.telescope.json",
]; ];

View file

@ -51,7 +51,7 @@ pub enum PluginStoreMsg {
DoRefresh, DoRefresh,
Install(Plugin, relm4::Sender<StoreRowModelMsg>), Install(Plugin, relm4::Sender<StoreRowModelMsg>),
InstallFromDetails(Plugin), InstallFromDetails(Plugin),
InstallDownload(Plugin, relm4::Sender<StoreRowModelMsg>), InstallDownload(Vec<Plugin>, relm4::Sender<StoreRowModelMsg>),
Remove(Plugin), Remove(Plugin),
SetEnabled(PluginStoreSignalSource, Plugin, bool), SetEnabled(PluginStoreSignalSource, Plugin, bool),
ShowDetails(usize), ShowDetails(usize),
@ -308,33 +308,70 @@ impl AsyncComponent for PluginStore {
} }
Self::Input::Install(plugin, row_sender) => { Self::Input::Install(plugin, row_sender) => {
self.set_locked(true); self.set_locked(true);
sender.input(Self::Input::InstallDownload(plugin, row_sender)) let mut plugins = vec![plugin];
} for dep in plugins[0].dependencies.clone().unwrap_or_default() {
Self::Input::InstallDownload(plugin, row_sender) => { if let Some(dep_plugin) = self
let mut plugin = plugin.clone(); .plugins
match plugin.exec_url.as_ref() { .iter()
Some(url) => { .find(|plugin| plugin.appid == dep)
let exec_path = plugin.canonical_exec_path(); .cloned()
if let Err(e) = download_file_async(url, &exec_path).await { {
alert( plugins.push(dep_plugin);
"Download failed", } else {
Some(&format!( error!(
"Downloading {} {} failed:\n\n{e}", "unable to find dependency for {}: {}",
plugin.name, plugins[0].appid, dep
plugin );
.version
.as_ref()
.unwrap_or(&"(no version)".to_string())
)),
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
);
} else {
plugin.exec_path = Some(exec_path);
sender.input(Self::Input::AddPluginToConfig(plugin.clone()));
}
}
None => {
alert( alert(
"Missing dependencies",
Some(&format!(
"{} depends on unknown plugin {}",
plugins[0].name, dep
)),
Some(&self.win.clone().unwrap().upcast::<gtk::Window>()),
);
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::<gtk::Window>(),
),
);
} 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::<gtk::Window>()),
);
return;
}
sender.input(Self::Input::AddPluginToConfig(plugin.clone()));
}
}
None => {
alert(
"Download failed", "Download failed",
Some(&format!( Some(&format!(
"Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!", "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::<gtk::Window>()) Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>())
); );
} }
}; };
row_sender.emit(StoreRowModelMsg::Refresh(true, false)); row_sender.emit(StoreRowModelMsg::Refresh(true, false));
self.details self.details
.emit(StoreDetailMsg::Refresh(plugin.appid, true, false)); .emit(StoreDetailMsg::Refresh(plugin.appid, true, false));
}
self.set_locked(false); self.set_locked(false);
} }
Self::Input::Remove(plugin) => { Self::Input::Remove(plugin) => {
self.set_locked(true); 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::<gtk::Window>()),
);
return;
}
if let Some(exec) = plugin.executable() { if let Some(exec) = plugin.executable() {
// delete executable only if it's not a custom plugin // delete executable only if it's not a custom plugin
if exec.is_file() && plugin.exec_url.is_some() { if exec.is_file() && plugin.exec_url.is_some() {
@ -387,6 +434,22 @@ impl AsyncComponent for PluginStore {
} }
Self::Input::SetEnabled(signal_sender, plugin, enabled) => { Self::Input::SetEnabled(signal_sender, plugin, enabled) => {
if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) { 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::<gtk::Window>()),
);
return;
}
cp.enabled = enabled; cp.enabled = enabled;
if signal_sender == PluginStoreSignalSource::Detail { if signal_sender == PluginStoreSignalSource::Detail {
if let Some(row) = self if let Some(row) = self