mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-19 19:14:53 +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",
|
||||
"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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
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
|
||||
.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(" ")
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
];
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue