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
parent eda2105566
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",
"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"

View file

@ -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"

View file

@ -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;

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
.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(" ")
}))
}

View file

@ -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<PathBuf>,
/// 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 {
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> {
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",
];

View file

@ -51,7 +51,7 @@ pub enum PluginStoreMsg {
DoRefresh,
Install(Plugin, relm4::Sender<StoreRowModelMsg>),
InstallFromDetails(Plugin),
InstallDownload(Plugin, relm4::Sender<StoreRowModelMsg>),
InstallDownload(Vec<Plugin>, relm4::Sender<StoreRowModelMsg>),
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::<gtk::Window>()),
);
} 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::<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",
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::<gtk::Window>())
);
}
};
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::<gtk::Window>()),
);
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::<gtk::Window>()),
);
return;
}
cp.enabled = enabled;
if signal_sender == PluginStoreSignalSource::Detail {
if let Some(row) = self