mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-07-31 13:18:46 +00:00
feat: support for plugin dependencies and wayvr dashboards (using unreleased api)
This commit is contained in:
parent
eda2105566
commit
160d733054
7 changed files with 234 additions and 38 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
13
src/file_builders/wayvr_dashboard_config.rs
Normal file
13
src/file_builders/wayvr_dashboard_config.rs
Normal 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,
|
||||||
|
}
|
|
@ -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(" ")
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue