From 2f8caafac2194c31fc0848e31be4d53073365d96 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Sun, 18 Aug 2024 11:39:08 +0200 Subject: [PATCH 1/7] feat!: plugin store --- src/paths.rs | 4 + src/ui/app.rs | 21 ++ src/ui/main_view.rs | 3 +- src/ui/mod.rs | 1 + src/ui/plugins/mod.rs | 44 ++++ src/ui/plugins/store.rs | 327 ++++++++++++++++++++++++++++ src/ui/plugins/store_detail.rs | 274 +++++++++++++++++++++++ src/ui/plugins/store_row_factory.rs | 178 +++++++++++++++ 8 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 src/ui/plugins/mod.rs create mode 100644 src/ui/plugins/store.rs create mode 100644 src/ui/plugins/store_detail.rs create mode 100644 src/ui/plugins/store_row_factory.rs diff --git a/src/paths.rs b/src/paths.rs index 06b7575..f714964 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -87,3 +87,7 @@ pub fn get_steamvr_bin_dir_path() -> PathBuf { XDG.get_data_home() .join("Steam/steamapps/common/SteamVR/bin/linux64") } + +pub fn get_plugins_dir() -> PathBuf { + get_data_dir().join("plugins") +} diff --git a/src/ui/app.rs b/src/ui/app.rs index 2b3f94a..486bb0e 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -11,6 +11,7 @@ use super::{ }, libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow}, main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg}, + plugins::store::{PluginStore, PluginStoreMsg}, util::{copiable_code_snippet, copy_text, open_with_default_handler}, wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg}, }; @@ -89,6 +90,7 @@ pub struct App { vkinfo: Option, inhibit_fail_notif: Option, + pluginstore: Option>, } #[derive(Debug)] @@ -120,6 +122,7 @@ pub enum Msg { StartProber, OnProberExit(bool), WivrnCheckPairMode, + OpenPluginStore, NoOp, } @@ -791,6 +794,11 @@ impl AsyncComponent for App { } } } + Msg::OpenPluginStore => { + let pluginstore = PluginStore::builder().launch(()).detach(); + pluginstore.sender().emit(PluginStoreMsg::Present); + self.pluginstore = Some(pluginstore); + } } } @@ -892,6 +900,17 @@ impl AsyncComponent for App { } ) ); + stateless_action!( + actions, + PluginStoreAction, + clone!( + #[strong] + sender, + move |_| { + sender.input(Msg::OpenPluginStore); + } + ) + ); // this bypasses the macro because I need the underlying gio action // to enable/disable it in update() let configure_wivrn_action = { @@ -974,6 +993,7 @@ impl AsyncComponent for App { openxr_prober_worker: None, xrservice_ready: false, inhibit_fail_notif: None, + pluginstore: None, }; let widgets = view_output!(); @@ -1046,6 +1066,7 @@ new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofile new_stateless_action!(pub QuitAction, AppActionGroup, "quit"); new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool); new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn"); +new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store"); new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata"); new_stateless_action!(pub DebugOpenPrefixAction, AppActionGroup, "debugopenprefix"); diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 28ec87a..388ff4b 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -2,7 +2,7 @@ use super::{ alert::alert, app::{ AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigureWivrnAction, - DebugViewToggleAction, + DebugViewToggleAction, PluginStoreAction, }, devices_box::{DevicesBox, DevicesBoxMsg}, install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg}, @@ -148,6 +148,7 @@ impl AsyncComponent for MainView { menu! { app_menu: { section! { + "Plugin _store" => PluginStoreAction, // value inside action is ignored "_Debug View" => DebugViewToggleAction, "_Build Profile" => BuildProfileAction, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2a5dd01..f68333c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -13,6 +13,7 @@ mod libsurvive_setup_window; mod macros; mod main_view; mod openhmd_calibration_box; +pub mod plugins; mod preference_rows; mod profile_editor; mod steam_launch_options_box; diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs new file mode 100644 index 0000000..d402ece --- /dev/null +++ b/src/ui/plugins/mod.rs @@ -0,0 +1,44 @@ +pub mod store; +mod store_detail; +mod store_row_factory; + +use crate::paths::get_plugins_dir; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Plugin { + pub appid: String, + pub name: String, + pub icon_url: Option, + pub version: String, + pub short_description: Option, + pub description: Option, + pub hompage_url: String, + pub screenshots: Vec, + pub exec_url: String, +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.appid == other.appid + && self.name == other.name + && self.version == other.version + && self.icon_url == other.icon_url + && self.short_description == other.short_description + && self.description == other.description + && self.hompage_url == other.hompage_url + && self.screenshots == other.screenshots + && self.exec_url == other.exec_url + } +} + +impl Plugin { + pub fn exec_path(&self) -> PathBuf { + get_plugins_dir().join(format!("{}.AppImage", self.appid)) + } + + pub fn is_installed(&self) -> bool { + self.exec_path().exists() + } +} diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs new file mode 100644 index 0000000..5433e65 --- /dev/null +++ b/src/ui/plugins/store.rs @@ -0,0 +1,327 @@ +use super::{ + store_detail::{StoreDetail, StoreDetailMsg, StoreDetailOutMsg}, + store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg}, + Plugin, +}; +use crate::{downloader::download_file_async, ui::alert::alert}; +use adw::prelude::*; +use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; +use std::fs::remove_file; + +#[tracker::track] +pub struct PluginStore { + #[tracker::do_not_track] + win: Option, + #[tracker::do_not_track] + plugin_rows: Option>, + #[tracker::do_not_track] + details: AsyncController, + #[tracker::do_not_track] + main_stack: Option, + refreshing: bool, + locked: bool, + plugins: Vec, +} + +#[derive(Debug)] +pub enum PluginStoreMsg { + Present, + Refresh, + Install(Plugin, relm4::Sender), + InstallFromDetails(Plugin), + InstallDownload(Plugin, relm4::Sender), + RemoveFromDetails(Plugin), + Remove(Plugin, relm4::Sender), + Enable(String), + Disable(String), + ShowDetails(usize), + ShowPluginList, +} + +#[derive(Debug)] +pub enum PluginStoreOutMsg {} + +#[relm4::component(pub async)] +impl AsyncComponent for PluginStore { + type Init = (); + type Input = PluginStoreMsg; + type Output = PluginStoreOutMsg; + type CommandOutput = (); + + view! { + #[name(win)] + adw::Window { + set_title: Some("Plugin Store"), + #[name(main_stack)] + gtk::Stack { + add_child = &adw::ToolbarView { + set_top_bar_style: adw::ToolbarStyle::Flat, + add_top_bar: headerbar = &adw::HeaderBar { + pack_end: refreshbtn = >k::Button { + set_icon_name: "view-refresh-symbolic", + set_tooltip_text: Some("Refresh"), + #[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"] + set_sensitive: !(model.refreshing || model.locked), + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Refresh); + } + } + }, + #[wrap(Some)] + set_content: inner = >k::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + set_vexpand: true, + gtk::Stack { + set_hexpand: true, + set_vexpand: true, + add_child = >k::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + set_hexpand: true, + set_vexpand: true, + adw::Clamp { + #[name(listbox)] + gtk::ListBox { + #[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"] + set_sensitive: !(model.refreshing || model.locked), + add_css_class: "boxed-list", + set_valign: gtk::Align::Start, + set_margin_all: 12, + set_selection_mode: gtk::SelectionMode::None, + connect_row_activated[sender] => move |_, row| { + sender.input( + Self::Input::ShowDetails( + row.index() as usize + ) + ); + }, + } + } + } -> { + set_name: "pluginlist" + }, + add_child = >k::Spinner { + set_hexpand: true, + set_vexpand: true, + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + #[track = "model.changed(PluginStore::refreshing())"] + set_spinning: model.refreshing, + } -> { + set_name: "spinner" + }, + add_child = &adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "No Plugins Found", + set_description: Some("Make sure you're connected to the internet and refresh"), + set_icon_name: Some("application-x-addon-symbolic"), + } -> { + set_name: "emptystate" + }, + #[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::plugins())"] + set_visible_child_name: if model.refreshing { + "spinner" + } else if model.plugins.is_empty() { + "emptystate" + } else { + "pluginlist" + }, + }, + } + } -> { + set_name: "listview" + }, + add_child = &adw::Bin { + #[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"] + set_sensitive: !(model.refreshing || model.locked), + set_child: Some(details_view), + } -> { + set_name: "detailsview", + }, + set_visible_child_name: "listview", + } + } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _root: &Self::Root, + ) { + self.reset(); + + match message { + Self::Input::Present => { + self.win.as_ref().unwrap().present(); + sender.input(Self::Input::Refresh); + } + Self::Input::Refresh => { + self.set_refreshing(true); + // TODO: populate from web + self.set_plugins(vec![ + Plugin { + appid: "com.github.galiser.wlx-overlay-s".into(), + name: "WLX Overlay S".into(), + version: "0.4.4".into(), + hompage_url: "https://github.com/galister/wlx-overlay-s".into(), + icon_url: Some("https://github.com/galister/wlx-overlay-s/raw/main/wlx-overlay-s.svg".into()), + screenshots: vec![ + "https://github.com/galister/wlx-overlay-s/raw/guide/wlx-s.png".into(), + ], + description: Some("A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XSOverlay.\n\nWlxOverlay-S allows you to access your desktop screens while in VR.\n\nIn comparison to similar overlays, WlxOverlay-S aims to run alongside VR games and experiences while having as little performance impact as possible. The UI appearance and rendering techniques are kept as simple and efficient as possible, while still allowing a high degree of customizability.".into()), + short_description: Some("Access your Wayland/X11 desktop".into()), + exec_url: "https://github.com/galister/wlx-overlay-s/releases/download/v0.4.4/WlxOverlay-S-v0.4.4-x86_64.AppImage".into() + }, + ]); + { + let mut guard = self.plugin_rows.as_mut().unwrap().guard(); + guard.clear(); + self.plugins.iter().for_each(|plugin| { + guard.push_back(StoreRowModelInit { + plugin: plugin.clone(), + }); + }); + } + self.set_refreshing(false); + } + Self::Input::InstallFromDetails(plugin) => { + if let Some(row) = self + .plugin_rows + .as_mut() + .unwrap() + .guard() + .iter() + .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) + .flatten() + { + sender.input(Self::Input::Install(plugin, row.input_sender.clone())) + } else { + eprintln!("could not find corresponding listbox row!") + } + } + // TODO: merge implementation with install + Self::Input::RemoveFromDetails(plugin) => { + if let Some(row) = self + .plugin_rows + .as_mut() + .unwrap() + .guard() + .iter() + .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) + .flatten() + { + sender.input(Self::Input::Remove(plugin, row.input_sender.clone())) + } else { + eprintln!("could not find corresponding listbox row!") + } + } + Self::Input::Install(plugin, row_sender) => { + self.set_locked(true); + sender.input(Self::Input::InstallDownload(plugin, row_sender)) + } + Self::Input::InstallDownload(plugin, row_sender) => { + if let Err(e) = download_file_async(&plugin.exec_url, &plugin.exec_path()).await { + alert( + "Download failed", + Some(&format!( + "Downloading {} {} failed:\n\n{e}", + plugin.name, plugin.version + )), + Some(&self.win.as_ref().unwrap().clone().upcast::()), + ); + } + row_sender.emit(StoreRowModelMsg::Refresh); + self.details.emit(StoreDetailMsg::Refresh); + self.set_locked(false); + } + Self::Input::Remove(plugin, row_sender) => { + self.set_locked(true); + let exec = plugin.exec_path(); + if exec.is_file() { + if let Err(e) = remove_file(&exec) { + alert( + "Failed removing plugin", + Some(&format!( + "Could not remove plugin executable {}:\n\n{e}", + exec.to_string_lossy() + )), + Some(&self.win.as_ref().unwrap().clone().upcast::()), + ); + } + } + row_sender.emit(StoreRowModelMsg::Refresh); + self.details.emit(StoreDetailMsg::Refresh); + self.set_locked(false); + } + Self::Input::Enable(_) => todo!(), + Self::Input::Disable(_) => todo!(), + // we use index here because it's the listbox not the row that can + // send this signal, so I don't directly have the plugin object + Self::Input::ShowDetails(index) => { + if let Some(plugin) = self.plugins.get(index) { + self.details + .sender() + .emit(StoreDetailMsg::SetPlugin(plugin.clone())); + self.main_stack + .as_ref() + .unwrap() + .set_visible_child_name("detailsview"); + } else { + eprintln!("plugins list index out of range!") + } + } + Self::Input::ShowPluginList => { + self.main_stack + .as_ref() + .unwrap() + .set_visible_child_name("listview"); + } + } + } + + async fn init( + _init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let mut model = Self { + tracker: 0, + refreshing: false, + locked: false, + win: None, + plugins: Vec::default(), + plugin_rows: None, + details: StoreDetail::builder() + .launch(()) + .forward(sender.input_sender(), move |msg| match msg { + StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList, + StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin), + StoreDetailOutMsg::Remove(plugin) => Self::Input::RemoveFromDetails(plugin), + }), + main_stack: None, + }; + + let details_view = model.details.widget(); + + let widgets = view_output!(); + + model.win = Some(widgets.win.clone()); + model.plugin_rows = Some( + AsyncFactoryVecDeque::builder() + .launch(widgets.listbox.clone()) + .forward(sender.input_sender(), move |msg| match msg { + StoreRowModelOutMsg::Install(appid, row_sender) => { + Self::Input::Install(appid, row_sender) + } + StoreRowModelOutMsg::Remove(appid, row_sender) => { + Self::Input::Remove(appid, row_sender) + } + }), + ); + model.main_stack = Some(widgets.main_stack.clone()); + + AsyncComponentParts { model, widgets } + } +} diff --git a/src/ui/plugins/store_detail.rs b/src/ui/plugins/store_detail.rs new file mode 100644 index 0000000..b5fd8e5 --- /dev/null +++ b/src/ui/plugins/store_detail.rs @@ -0,0 +1,274 @@ +use super::Plugin; +use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG}; +use adw::prelude::*; +use relm4::prelude::*; + +#[tracker::track] +pub struct StoreDetail { + plugin: Option, + #[tracker::do_not_track] + carousel: Option, + #[tracker::do_not_track] + icon: Option, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum StoreDetailMsg { + SetPlugin(Plugin), + SetIcon, + SetScreenshots, + Refresh, + Install, + Remove, +} + +#[derive(Debug)] +pub enum StoreDetailOutMsg { + Install(Plugin), + Remove(Plugin), + GoBack, +} + +#[relm4::component(pub async)] +impl AsyncComponent for StoreDetail { + type Init = (); + type Input = StoreDetailMsg; + type Output = StoreDetailOutMsg; + type CommandOutput = (); + + view! { + adw::ToolbarView { + set_top_bar_style: adw::ToolbarStyle::Flat, + add_top_bar: headerbar = &adw::HeaderBar { + #[wrap(Some)] + set_title_widget: title = &adw::WindowTitle { + #[track = "model.changed(Self::plugin())"] + set_title: model + .plugin + .as_ref() + .map(|p| p.name.as_str()) + .unwrap_or_default(), + }, + pack_start: backbtn = >k::Button { + set_icon_name: "go-previous-symbolic", + set_tooltip_text: Some("Back"), + connect_clicked[sender] => move |_| { + sender.output(Self::Output::GoBack).expect(SENDER_IO_ERR_MSG); + } + }, + }, + #[wrap(Some)] + set_content: inner = >k::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + set_hexpand: true, + set_vexpand: true, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + set_vexpand: true, + set_margin_top: 12, + set_margin_bottom: 48, + set_margin_start: 12, + set_margin_end: 12, + set_spacing: 24, + adw::Clamp { // icon, name, buttons + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + set_vexpand: false, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + #[name(icon)] + gtk::Image { + set_icon_name: Some("image-missing-symbolic"), + set_pixel_size: 96, + }, + gtk::Label { + add_css_class: "title-2", + set_hexpand: true, + set_xalign: 0.0, + #[track = "model.changed(Self::plugin())"] + set_text: model + .plugin + .as_ref() + .map(|p| p.name.as_str()) + .unwrap_or_default(), + set_ellipsize: gtk::pango::EllipsizeMode::None, + set_wrap: true, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_spacing: 6, + gtk::Button { + #[track = "model.changed(Self::plugin())"] + set_visible: !model + .plugin + .as_ref() + .is_some_and(|p| p.is_installed()), + set_label: "Install", + add_css_class: "suggested-action", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Install); + } + }, + gtk::Button { + #[track = "model.changed(Self::plugin())"] + set_visible: model + .plugin + .as_ref() + .is_some_and(|p| p.is_installed()), + set_label: "Remove", + add_css_class: "destructive-action", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Remove); + } + }, + gtk::Switch { + #[track = "self.changed(Self::plugin())"] + set_visible: model.plugin.as_ref() + .is_some_and(|p| p.is_installed()), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + }, + } + } + }, + }, + gtk::Box { // screenshots + set_orientation: gtk::Orientation::Vertical, + set_spacing: 12, + #[name(carousel)] + adw::Carousel { + set_allow_mouse_drag: true, + set_allow_scroll_wheel: false, + set_spacing: 24, + }, + adw::CarouselIndicatorDots { + set_carousel: Some(&carousel), + }, + }, + adw::Clamp { // description + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + gtk::Label { + set_xalign: 0.0, + #[track = "model.changed(Self::plugin())"] + set_text: model + .plugin + .as_ref() + .and_then(|p| p + .description + .as_deref() + ).unwrap_or(""), + set_ellipsize: gtk::pango::EllipsizeMode::None, + set_wrap: true, + set_justify: gtk::Justification::Fill, + }, + }, + }, + } + } + } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _root: &Self::Root, + ) { + self.reset(); + + match message { + Self::Input::SetPlugin(p) => { + self.set_plugin(Some(p)); + sender.input(Self::Input::SetIcon); + sender.input(Self::Input::SetScreenshots); + } + Self::Input::SetIcon => { + if let Some(plugin) = self.plugin.as_ref() { + if let Some(url) = plugin.icon_url.as_ref() { + match cache_file(url, None).await { + Ok(dest) => { + self.icon.as_ref().unwrap().set_from_file(Some(dest)); + } + Err(e) => { + eprintln!("Failed downloading icon '{url}': {e}"); + } + }; + } + } + } + Self::Input::SetScreenshots => { + let carousel = self.carousel.as_ref().unwrap().clone(); + while let Some(child) = carousel.first_child() { + carousel.remove(&child); + } + if let Some(plugin) = self.plugin.as_ref() { + carousel + .parent() + .unwrap() + .set_visible(!plugin.screenshots.is_empty()); + for url in plugin.screenshots.iter() { + match cache_file(url, None).await { + Ok(dest) => { + let pic = gtk::Picture::builder() + .height_request(300) + .css_classes(["card"]) + .overflow(gtk::Overflow::Hidden) + .valign(gtk::Align::Center) + .build(); + pic.set_filename(Some(dest)); + let clamp = adw::Clamp::builder().child(&pic).build(); + carousel.append(&clamp); + } + Err(e) => { + eprintln!("Failed downloading screenshot '{url}': {e}"); + } + }; + } + } + } + Self::Input::Refresh => { + self.mark_all_changed(); + } + Self::Input::Install => { + if let Some(plugin) = self.plugin.as_ref() { + sender + .output(Self::Output::Install(plugin.clone())) + .expect(SENDER_IO_ERR_MSG); + } + } + Self::Input::Remove => { + if let Some(plugin) = self.plugin.as_ref() { + sender + .output(Self::Output::Remove(plugin.clone())) + .expect(SENDER_IO_ERR_MSG); + } + } + } + } + + async fn init( + _init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let mut model = Self { + tracker: 0, + plugin: None, + carousel: None, + icon: None, + }; + let widgets = view_output!(); + + model.carousel = Some(widgets.carousel.clone()); + model.icon = Some(widgets.icon.clone()); + + AsyncComponentParts { model, widgets } + } +} diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs new file mode 100644 index 0000000..fdf6e66 --- /dev/null +++ b/src/ui/plugins/store_row_factory.rs @@ -0,0 +1,178 @@ +use super::Plugin; +use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG}; +use gtk::prelude::*; +use relm4::{ + factory::AsyncFactoryComponent, prelude::DynamicIndex, AsyncFactorySender, RelmWidgetExt, +}; + +#[derive(Debug)] +#[tracker::track] +pub struct StoreRowModel { + #[no_eq] + pub plugin: Plugin, + #[tracker::do_not_track] + icon: Option, + #[tracker::do_not_track] + pub input_sender: relm4::Sender, +} + +#[derive(Debug)] +pub struct StoreRowModelInit { + pub plugin: Plugin, +} + +#[derive(Debug)] +pub enum StoreRowModelMsg { + LoadIcon, + Refresh, +} + +#[derive(Debug)] +pub enum StoreRowModelOutMsg { + Install(Plugin, relm4::Sender), + Remove(Plugin, relm4::Sender), +} + +#[relm4::factory(async pub)] +impl AsyncFactoryComponent for StoreRowModel { + type Init = StoreRowModelInit; + type Input = StoreRowModelMsg; + type Output = StoreRowModelOutMsg; + type CommandOutput = (); + type ParentWidget = gtk::ListBox; + + view! { + root = gtk::ListBoxRow { + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + set_vexpand: false, + set_spacing: 12, + set_margin_all: 12, + #[name(icon)] + gtk::Image { + set_icon_name: Some("image-missing-symbolic"), + set_icon_size: gtk::IconSize::Large, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 6, + set_hexpand: true, + set_vexpand: true, + gtk::Label { + add_css_class: "title-3", + set_hexpand: true, + set_xalign: 0.0, + set_text: &self.plugin.name, + set_ellipsize: gtk::pango::EllipsizeMode::None, + set_wrap: true, + }, + gtk::Label { + add_css_class: "dim-label", + set_hexpand: true, + set_xalign: 0.0, + set_text: self.plugin.short_description + .as_deref() + .unwrap_or(""), + set_ellipsize: gtk::pango::EllipsizeMode::None, + set_wrap: true, + }, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 6, + set_vexpand: true, + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + gtk::Button { + #[track = "self.changed(StoreRowModel::plugin())"] + set_visible: !self.plugin.is_installed(), + set_icon_name: "folder-download-symbolic", + add_css_class: "suggested-action", + set_tooltip_text: Some("Install"), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender, plugin] => move |_| { + sender + .output(Self::Output::Install( + plugin.clone(), + sender.input_sender().clone() + )) + .expect(SENDER_IO_ERR_MSG); + } + }, + gtk::Button { + #[track = "self.changed(StoreRowModel::plugin())"] + set_visible: self.plugin.is_installed(), + set_icon_name: "app-remove-symbolic", + add_css_class: "destructive-action", + set_tooltip_text: Some("Remove"), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender, plugin] => move |_| { + sender + .output(Self::Output::Remove( + plugin.clone(), + sender.input_sender().clone() + )) + .expect(SENDER_IO_ERR_MSG); + } + }, + gtk::Switch { + #[track = "self.changed(StoreRowModel::plugin())"] + set_visible: self.plugin.is_installed(), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + }, + }, + } + } + } + + async fn update(&mut self, message: Self::Input, _sender: AsyncFactorySender) { + self.reset(); + + match message { + Self::Input::LoadIcon => { + if let Some(url) = self.plugin.icon_url.as_ref() { + match cache_file(url, None).await { + Ok(dest) => { + self.icon.as_ref().unwrap().set_from_file(Some(dest)); + } + Err(e) => { + eprintln!("Failed downloading icon '{url}': {e}"); + } + }; + } + } + Self::Input::Refresh => self.mark_all_changed(), + } + } + + async fn init_model( + init: Self::Init, + _index: &DynamicIndex, + sender: AsyncFactorySender, + ) -> Self { + Self { + tracker: 0, + plugin: init.plugin, + icon: None, + input_sender: sender.input_sender().clone(), + } + } + + fn init_widgets( + &mut self, + _index: &DynamicIndex, + root: Self::Root, + _returned_widget: &::ReturnedWidget, + sender: AsyncFactorySender, + ) -> Self::Widgets { + let plugin = self.plugin.clone(); // for use in a signal handler + let widgets = view_output!(); + self.icon = Some(widgets.icon.clone()); + sender.input(Self::Input::LoadIcon); + widgets + } +} From 781e2f4fd24ceaea07d2e85037f5797506f5f54d Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Mon, 19 Aug 2024 19:22:29 +0200 Subject: [PATCH 2/7] feat: can enable and disable plugins --- src/config.rs | 26 +++++++- src/ui/app.rs | 23 +++++-- src/ui/plugins/store.rs | 100 +++++++++++++++++++++++----- src/ui/plugins/store_detail.rs | 31 +++++++-- src/ui/plugins/store_row_factory.rs | 27 +++++++- 5 files changed, 178 insertions(+), 29 deletions(-) diff --git a/src/config.rs b/src/config.rs index a35dd93..f0c1aa2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,15 +7,36 @@ use crate::{ lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile, survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile, }, + ui::plugins::Plugin, util::file_utils::get_writer, }; use serde::{de::Error, Deserialize, Serialize}; use std::{ + collections::HashMap, fs::File, io::BufReader, path::{Path, PathBuf}, }; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginConfig { + pub appid: String, + pub version: String, + pub enabled: bool, + pub exec_path: PathBuf, +} + +impl From<&Plugin> for PluginConfig { + fn from(p: &Plugin) -> Self { + Self { + appid: p.appid.clone(), + version: p.version.clone(), + enabled: true, + exec_path: p.exec_path(), + } + } +} + const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400]; const fn default_win_size() -> [i32; 2] { @@ -29,6 +50,8 @@ pub struct Config { pub user_profiles: Vec, #[serde(default = "default_win_size")] pub win_size: [i32; 2], + #[serde(default)] + pub plugins: HashMap, } impl Default for Config { @@ -37,8 +60,9 @@ impl Default for Config { // TODO: using an empty string here is ugly selected_profile_uuid: "".to_string(), debug_view_enabled: false, - user_profiles: vec![], + user_profiles: Vec::default(), win_size: DEFAULT_WIN_SIZE, + plugins: HashMap::default(), } } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 486bb0e..afe2869 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -11,7 +11,7 @@ use super::{ }, libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow}, main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg}, - plugins::store::{PluginStore, PluginStoreMsg}, + plugins::store::{PluginStore, PluginStoreInit, PluginStoreMsg, PluginStoreOutMsg}, util::{copiable_code_snippet, copy_text, open_with_default_handler}, wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg}, }; @@ -22,7 +22,7 @@ use crate::{ build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs, build_wivrn::get_build_wivrn_jobs, }, - config::Config, + config::{Config, PluginConfig}, constants::APP_NAME, depcheck::common::dep_pkexec, file_builders::{ @@ -57,7 +57,11 @@ use relm4::{ new_action_group, new_stateful_action, new_stateless_action, prelude::*, }; -use std::{collections::VecDeque, fs::remove_file, time::Duration}; +use std::{ + collections::{HashMap, VecDeque}, + fs::remove_file, + time::Duration, +}; use tracing::error; pub struct App { @@ -123,6 +127,7 @@ pub enum Msg { OnProberExit(bool), WivrnCheckPairMode, OpenPluginStore, + UpdateConfigPlugins(HashMap), NoOp, } @@ -795,10 +800,20 @@ impl AsyncComponent for App { } } Msg::OpenPluginStore => { - let pluginstore = PluginStore::builder().launch(()).detach(); + let pluginstore = PluginStore::builder() + .launch(PluginStoreInit { + config_plugins: self.config.plugins.clone(), + }) + .forward(sender.input_sender(), move |msg| match msg { + PluginStoreOutMsg::UpdateConfigPlugins(cp) => Msg::UpdateConfigPlugins(cp), + }); pluginstore.sender().emit(PluginStoreMsg::Present); self.pluginstore = Some(pluginstore); } + Msg::UpdateConfigPlugins(cp) => { + self.config.plugins = cp; + self.config.save(); + } } } diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs index 5433e65..36bb3d3 100644 --- a/src/ui/plugins/store.rs +++ b/src/ui/plugins/store.rs @@ -3,10 +3,14 @@ use super::{ store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg}, Plugin, }; -use crate::{downloader::download_file_async, ui::alert::alert}; +use crate::{ + config::PluginConfig, + downloader::download_file_async, + ui::{alert::alert, SENDER_IO_ERR_MSG}, +}; use adw::prelude::*; use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; -use std::fs::remove_file; +use std::{collections::HashMap, fs::remove_file}; #[tracker::track] pub struct PluginStore { @@ -18,6 +22,8 @@ pub struct PluginStore { details: AsyncController, #[tracker::do_not_track] main_stack: Option, + #[tracker::do_not_track] + config_plugins: HashMap, refreshing: bool, locked: bool, plugins: Vec, @@ -32,18 +38,24 @@ pub enum PluginStoreMsg { InstallDownload(Plugin, relm4::Sender), RemoveFromDetails(Plugin), Remove(Plugin, relm4::Sender), - Enable(String), - Disable(String), + SetEnabled(Plugin, bool), ShowDetails(usize), ShowPluginList, } #[derive(Debug)] -pub enum PluginStoreOutMsg {} +pub struct PluginStoreInit { + pub config_plugins: HashMap, +} + +#[derive(Debug)] +pub enum PluginStoreOutMsg { + UpdateConfigPlugins(HashMap), +} #[relm4::component(pub async)] impl AsyncComponent for PluginStore { - type Init = (); + type Init = PluginStoreInit; type Input = PluginStoreMsg; type Output = PluginStoreOutMsg; type CommandOutput = (); @@ -181,6 +193,10 @@ impl AsyncComponent for PluginStore { self.plugins.iter().for_each(|plugin| { guard.push_back(StoreRowModelInit { plugin: plugin.clone(), + enabled: self + .config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.enabled), }); }); } @@ -231,9 +247,17 @@ impl AsyncComponent for PluginStore { )), Some(&self.win.as_ref().unwrap().clone().upcast::()), ); + } else { + self.config_plugins + .insert(plugin.appid.clone(), PluginConfig::from(&plugin)); + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); } - row_sender.emit(StoreRowModelMsg::Refresh); - self.details.emit(StoreDetailMsg::Refresh); + row_sender.emit(StoreRowModelMsg::Refresh(true)); + self.details.emit(StoreDetailMsg::Refresh(true)); self.set_locked(false); } Self::Input::Remove(plugin, row_sender) => { @@ -249,21 +273,58 @@ impl AsyncComponent for PluginStore { )), Some(&self.win.as_ref().unwrap().clone().upcast::()), ); + } else { + self.config_plugins.remove(&plugin.appid); + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); } } - row_sender.emit(StoreRowModelMsg::Refresh); - self.details.emit(StoreDetailMsg::Refresh); + row_sender.emit(StoreRowModelMsg::Refresh(false)); + self.details.emit(StoreDetailMsg::Refresh(false)); self.set_locked(false); } - Self::Input::Enable(_) => todo!(), - Self::Input::Disable(_) => todo!(), + Self::Input::SetEnabled(plugin, enabled) => { + if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) { + cp.enabled = enabled; + if let Some(row) = self + .plugin_rows + .as_mut() + .unwrap() + .guard() + .iter() + .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) + .flatten() + { + row.input_sender.emit(StoreRowModelMsg::Refresh(enabled)); + } else { + eprintln!("could not find corresponding listbox row!") + } + self.details.emit(StoreDetailMsg::Refresh(enabled)); + } else { + eprintln!( + "failed to set plugin {} enabled: could not find in hashmap", + plugin.appid + ) + } + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); + } // we use index here because it's the listbox not the row that can // send this signal, so I don't directly have the plugin object Self::Input::ShowDetails(index) => { if let Some(plugin) = self.plugins.get(index) { - self.details - .sender() - .emit(StoreDetailMsg::SetPlugin(plugin.clone())); + self.details.sender().emit(StoreDetailMsg::SetPlugin( + plugin.clone(), + self.config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.enabled), + )); self.main_stack .as_ref() .unwrap() @@ -282,7 +343,7 @@ impl AsyncComponent for PluginStore { } async fn init( - _init: Self::Init, + init: Self::Init, root: Self::Root, sender: AsyncComponentSender, ) -> AsyncComponentParts { @@ -299,7 +360,11 @@ impl AsyncComponent for PluginStore { StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList, StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin), StoreDetailOutMsg::Remove(plugin) => Self::Input::RemoveFromDetails(plugin), + StoreDetailOutMsg::SetEnabled(plugin, enabled) => { + Self::Input::SetEnabled(plugin, enabled) + } }), + config_plugins: init.config_plugins, main_stack: None, }; @@ -318,6 +383,9 @@ impl AsyncComponent for PluginStore { StoreRowModelOutMsg::Remove(appid, row_sender) => { Self::Input::Remove(appid, row_sender) } + StoreRowModelOutMsg::SetEnabled(plugin, enabled) => { + Self::Input::SetEnabled(plugin, enabled) + } }), ); model.main_stack = Some(widgets.main_stack.clone()); diff --git a/src/ui/plugins/store_detail.rs b/src/ui/plugins/store_detail.rs index b5fd8e5..077423b 100644 --- a/src/ui/plugins/store_detail.rs +++ b/src/ui/plugins/store_detail.rs @@ -6,6 +6,7 @@ use relm4::prelude::*; #[tracker::track] pub struct StoreDetail { plugin: Option, + enabled: bool, #[tracker::do_not_track] carousel: Option, #[tracker::do_not_track] @@ -15,12 +16,13 @@ pub struct StoreDetail { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum StoreDetailMsg { - SetPlugin(Plugin), + SetPlugin(Plugin, bool), SetIcon, SetScreenshots, - Refresh, + Refresh(bool), Install, Remove, + SetEnabled(bool), } #[derive(Debug)] @@ -28,6 +30,7 @@ pub enum StoreDetailOutMsg { Install(Plugin), Remove(Plugin), GoBack, + SetEnabled(Plugin, bool), } #[relm4::component(pub async)] @@ -128,11 +131,18 @@ impl AsyncComponent for StoreDetail { } }, gtk::Switch { - #[track = "self.changed(Self::plugin())"] + #[track = "model.changed(Self::plugin())"] set_visible: model.plugin.as_ref() .is_some_and(|p| p.is_installed()), + #[track = "model.changed(Self::enabled())"] + set_active: model.enabled, + set_tooltip_text: Some("Plugin enabled"), set_valign: gtk::Align::Center, set_halign: gtk::Align::Center, + connect_state_set[sender] => move |_, state| { + sender.input(Self::Input::SetEnabled(state)); + gtk::glib::Propagation::Proceed + } }, } } @@ -184,8 +194,9 @@ impl AsyncComponent for StoreDetail { self.reset(); match message { - Self::Input::SetPlugin(p) => { + Self::Input::SetPlugin(p, enabled) => { self.set_plugin(Some(p)); + self.set_enabled(enabled); sender.input(Self::Input::SetIcon); sender.input(Self::Input::SetScreenshots); } @@ -233,8 +244,9 @@ impl AsyncComponent for StoreDetail { } } } - Self::Input::Refresh => { + Self::Input::Refresh(enabled) => { self.mark_all_changed(); + self.set_enabled(enabled); } Self::Input::Install => { if let Some(plugin) = self.plugin.as_ref() { @@ -250,6 +262,14 @@ impl AsyncComponent for StoreDetail { .expect(SENDER_IO_ERR_MSG); } } + Self::Input::SetEnabled(enabled) => { + self.set_enabled(enabled); + if let Some(plugin) = self.plugin.as_ref() { + sender + .output(Self::Output::SetEnabled(plugin.clone(), enabled)) + .expect(SENDER_IO_ERR_MSG); + } + } } } @@ -261,6 +281,7 @@ impl AsyncComponent for StoreDetail { let mut model = Self { tracker: 0, plugin: None, + enabled: false, carousel: None, icon: None, }; diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs index fdf6e66..38fc4ac 100644 --- a/src/ui/plugins/store_row_factory.rs +++ b/src/ui/plugins/store_row_factory.rs @@ -14,23 +14,27 @@ pub struct StoreRowModel { icon: Option, #[tracker::do_not_track] pub input_sender: relm4::Sender, + pub enabled: bool, } #[derive(Debug)] pub struct StoreRowModelInit { pub plugin: Plugin, + pub enabled: bool, } #[derive(Debug)] pub enum StoreRowModelMsg { LoadIcon, - Refresh, + Refresh(bool), + SetEnabled(bool), } #[derive(Debug)] pub enum StoreRowModelOutMsg { Install(Plugin, relm4::Sender), Remove(Plugin, relm4::Sender), + SetEnabled(Plugin, bool), } #[relm4::factory(async pub)] @@ -121,15 +125,22 @@ impl AsyncFactoryComponent for StoreRowModel { gtk::Switch { #[track = "self.changed(StoreRowModel::plugin())"] set_visible: self.plugin.is_installed(), + #[track = "self.changed(StoreRowModel::enabled())"] + set_active: self.enabled, set_valign: gtk::Align::Center, set_halign: gtk::Align::Center, + set_tooltip_text: Some("Plugin enabled"), + connect_state_set[sender] => move |_, state| { + sender.input(Self::Input::SetEnabled(state)); + gtk::glib::Propagation::Proceed + } }, }, } } } - async fn update(&mut self, message: Self::Input, _sender: AsyncFactorySender) { + async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender) { self.reset(); match message { @@ -145,7 +156,16 @@ impl AsyncFactoryComponent for StoreRowModel { }; } } - Self::Input::Refresh => self.mark_all_changed(), + Self::Input::SetEnabled(state) => { + self.set_enabled(state); + sender + .output(Self::Output::SetEnabled(self.plugin.clone(), state)) + .expect(SENDER_IO_ERR_MSG); + } + Self::Input::Refresh(enabled) => { + self.mark_all_changed(); + self.set_enabled(enabled) + } } } @@ -157,6 +177,7 @@ impl AsyncFactoryComponent for StoreRowModel { Self { tracker: 0, plugin: init.plugin, + enabled: init.enabled, icon: None, input_sender: sender.input_sender().clone(), } From 01a61bc842930d02b0b8531d8db7dee934b0f132 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 20 Aug 2024 00:15:25 +0200 Subject: [PATCH 3/7] feat: plugins start on service launch --- src/ui/app.rs | 65 ++++++++++++++++++++++++++++++++---------- src/ui/plugins/mod.rs | 6 +++- src/util/file_utils.rs | 12 ++++++++ 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index afe2869..ec33efd 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -43,7 +43,8 @@ use crate::{ restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile, }, util::file_utils::{ - setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip, + mark_as_executable, setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, + verify_cap_sys_nice_eip, }, vulkaninfo::VulkanInfo, wivrn_dbus, @@ -80,6 +81,7 @@ pub struct App { config: Config, xrservice_worker: Option, autostart_worker: Option, + plugins_worker: Option, restart_xrservice: bool, build_worker: Option, profiles: Vec, @@ -102,6 +104,7 @@ pub enum Msg { OnServiceLog(Vec), OnServiceExit(i32), OnAutostartExit(i32), + OnPluginsExit(i32), OnBuildLog(Vec), OnBuildExit(i32), ClockTicking, @@ -257,6 +260,39 @@ impl App { autostart_worker.start(); self.autostart_worker = Some(autostart_worker); } + + let plugins_cmd = self + .config + .plugins + .values() + .filter_map(|cp| { + if cp.enabled { + if let Err(e) = mark_as_executable(&cp.exec_path) { + eprintln!("Failed to mark plugin {} as executable: {e}", cp.appid); + None + } else { + Some(format!("'{}'", cp.exec_path.to_string_lossy())) + } + } else { + None + } + }) + .collect::>() + .join(" & "); + if !plugins_cmd.is_empty() { + let mut jobs = VecDeque::new(); + jobs.push_back(WorkerJob::new_cmd( + Some(prof.environment.clone()), + "sh".into(), + Some(vec!["-c".into(), plugins_cmd]), + )); + let plugins_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg { + JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows), + JobWorkerOut::Exit(code) => Msg::OnPluginsExit(code), + }); + plugins_worker.start(); + self.plugins_worker = Some(plugins_worker); + } } pub fn restore_openxr_openvr_files(&self) { @@ -285,27 +321,20 @@ impl App { } pub fn shutdown_xrservice(&mut self) { - if let Some(worker) = self.autostart_worker.as_ref() { - worker.stop(); + if let Some(w) = self.autostart_worker.as_ref() { + w.stop(); + } + if let Some(w) = self.plugins_worker.as_ref() { + w.stop(); } - self.xrservice_ready = false; if let Some(w) = self.openxr_prober_worker.as_ref() { w.stop(); // this can cause threads to remain hanging... self.openxr_prober_worker = None; } - self.set_inhibit_session(false); - if let Some(worker) = self.xrservice_worker.as_ref() { - worker.stop(); + if let Some(w) = self.xrservice_worker.as_ref() { + w.stop(); } - self.libmonado = None; - self.main_view - .sender() - .emit(MainViewMsg::XRServiceActiveChanged(false, None, false)); - self.debug_view - .sender() - .emit(DebugViewMsg::XRServiceActiveChanged(false)); - self.xr_devices = vec![]; } } @@ -371,6 +400,8 @@ impl AsyncComponent for App { } } Msg::OnServiceExit(code) => { + self.set_inhibit_session(false); + self.xrservice_ready = false; self.restore_openxr_openvr_files(); self.main_view .sender() @@ -378,6 +409,8 @@ impl AsyncComponent for App { self.debug_view .sender() .emit(DebugViewMsg::XRServiceActiveChanged(false)); + self.libmonado = None; + self.xr_devices = vec![]; if code != 0 && code != 15 { // 15 is SIGTERM sender.input(Msg::OnServiceLog(vec![format!( @@ -393,6 +426,7 @@ impl AsyncComponent for App { } } Msg::OnAutostartExit(_) => self.autostart_worker = None, + Msg::OnPluginsExit(_) => self.plugins_worker = None, Msg::ClockTicking => { self.main_view.sender().emit(MainViewMsg::ClockTicking); let xrservice_worker_is_alive = self @@ -998,6 +1032,7 @@ impl AsyncComponent for App { profiles, xrservice_worker: None, autostart_worker: None, + plugins_worker: None, build_worker: None, xr_devices: vec![], restart_xrservice: false, diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs index d402ece..ea5d935 100644 --- a/src/ui/plugins/mod.rs +++ b/src/ui/plugins/mod.rs @@ -2,7 +2,7 @@ pub mod store; mod store_detail; mod store_row_factory; -use crate::paths::get_plugins_dir; +use crate::{paths::get_plugins_dir, util::file_utils::mark_as_executable}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -41,4 +41,8 @@ impl Plugin { pub fn is_installed(&self) -> bool { self.exec_path().exists() } + + pub fn mark_as_executable(&self) -> anyhow::Result<()> { + mark_as_executable(&self.exec_path()) + } } diff --git a/src/util/file_utils.rs b/src/util/file_utils.rs index 936aacf..ff56926 100644 --- a/src/util/file_utils.rs +++ b/src/util/file_utils.rs @@ -7,6 +7,7 @@ use nix::{ use std::{ fs::{self, copy, create_dir_all, remove_dir_all, File, OpenOptions}, io::{BufReader, BufWriter}, + os::unix::fs::PermissionsExt, path::Path, }; use tracing::{debug, error}; @@ -151,6 +152,17 @@ pub fn mount_has_nosuid(path: &Path) -> Result { } } +pub fn mark_as_executable(path: &Path) -> anyhow::Result<()> { + if !path.is_file() { + bail!("Path '{}' is not a file", path.to_string_lossy()) + } else { + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(perms.mode() | 0o111); + fs::set_permissions(path, perms)?; + Ok(()) + } +} + #[cfg(test)] mod tests { use super::mount_has_nosuid; From e8922203e898d64d54cf5b75c2042aa48a60ed14 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Wed, 21 Aug 2024 07:53:59 +0200 Subject: [PATCH 4/7] feat: can update plugins; show plugins that are installed but no longer available --- src/config.rs | 12 ++++-- src/ui/app.rs | 5 ++- src/ui/plugins/mod.rs | 16 +------- src/ui/plugins/store.rs | 44 +++++++++++++++++---- src/ui/plugins/store_detail.rs | 26 ++++++++++-- src/ui/plugins/store_row_factory.rs | 61 +++++++++++++++++++++-------- 6 files changed, 115 insertions(+), 49 deletions(-) diff --git a/src/config.rs b/src/config.rs index f0c1aa2..5a2e29e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,8 +20,7 @@ use std::{ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PluginConfig { - pub appid: String, - pub version: String, + pub plugin: Plugin, pub enabled: bool, pub exec_path: PathBuf, } @@ -29,14 +28,19 @@ pub struct PluginConfig { impl From<&Plugin> for PluginConfig { fn from(p: &Plugin) -> Self { Self { - appid: p.appid.clone(), - version: p.version.clone(), + plugin: p.clone(), enabled: true, exec_path: p.exec_path(), } } } +impl From<&PluginConfig> for Plugin { + fn from(cp: &PluginConfig) -> Self { + cp.plugin.clone() + } +} + const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400]; const fn default_win_size() -> [i32; 2] { diff --git a/src/ui/app.rs b/src/ui/app.rs index ec33efd..5185554 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -268,7 +268,10 @@ impl App { .filter_map(|cp| { if cp.enabled { if let Err(e) = mark_as_executable(&cp.exec_path) { - eprintln!("Failed to mark plugin {} as executable: {e}", cp.appid); + eprintln!( + "Failed to mark plugin {} as executable: {e}", + cp.plugin.appid + ); None } else { Some(format!("'{}'", cp.exec_path.to_string_lossy())) diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs index ea5d935..3a88848 100644 --- a/src/ui/plugins/mod.rs +++ b/src/ui/plugins/mod.rs @@ -6,7 +6,7 @@ use crate::{paths::get_plugins_dir, util::file_utils::mark_as_executable}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct Plugin { pub appid: String, pub name: String, @@ -19,20 +19,6 @@ pub struct Plugin { pub exec_url: String, } -impl PartialEq for Plugin { - fn eq(&self, other: &Self) -> bool { - self.appid == other.appid - && self.name == other.name - && self.version == other.version - && self.icon_url == other.icon_url - && self.short_description == other.short_description - && self.description == other.description - && self.hompage_url == other.hompage_url - && self.screenshots == other.screenshots - && self.exec_url == other.exec_url - } -} - impl Plugin { pub fn exec_path(&self) -> PathBuf { get_plugins_dir().join(format!("{}.AppImage", self.appid)) diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs index 36bb3d3..c8221c8 100644 --- a/src/ui/plugins/store.rs +++ b/src/ui/plugins/store.rs @@ -172,7 +172,7 @@ impl AsyncComponent for PluginStore { Self::Input::Refresh => { self.set_refreshing(true); // TODO: populate from web - self.set_plugins(vec![ + let mut plugins = vec![ Plugin { appid: "com.github.galiser.wlx-overlay-s".into(), name: "WLX Overlay S".into(), @@ -186,7 +186,22 @@ impl AsyncComponent for PluginStore { short_description: Some("Access your Wayland/X11 desktop".into()), exec_url: "https://github.com/galister/wlx-overlay-s/releases/download/v0.4.4/WlxOverlay-S-v0.4.4-x86_64.AppImage".into() }, - ]); + ]; + { + let appids_from_web = plugins + .iter() + .map(|p| p.appid.clone()) + .collect::>(); + // add all plugins that are in config but not retrieved + plugins.extend(self.config_plugins.values().filter_map(|cp| { + if appids_from_web.contains(&cp.plugin.appid) { + None + } else { + Some(Plugin::from(cp)) + } + })); + } + self.set_plugins(plugins); { let mut guard = self.plugin_rows.as_mut().unwrap().guard(); guard.clear(); @@ -197,6 +212,10 @@ impl AsyncComponent for PluginStore { .config_plugins .get(&plugin.appid) .is_some_and(|cp| cp.enabled), + needs_update: self + .config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.plugin.version != plugin.version), }); }); } @@ -256,8 +275,8 @@ impl AsyncComponent for PluginStore { )) .expect(SENDER_IO_ERR_MSG); } - row_sender.emit(StoreRowModelMsg::Refresh(true)); - self.details.emit(StoreDetailMsg::Refresh(true)); + row_sender.emit(StoreRowModelMsg::Refresh(true, false)); + self.details.emit(StoreDetailMsg::Refresh(true, false)); self.set_locked(false); } Self::Input::Remove(plugin, row_sender) => { @@ -282,8 +301,8 @@ impl AsyncComponent for PluginStore { .expect(SENDER_IO_ERR_MSG); } } - row_sender.emit(StoreRowModelMsg::Refresh(false)); - self.details.emit(StoreDetailMsg::Refresh(false)); + row_sender.emit(StoreRowModelMsg::Refresh(false, false)); + self.details.emit(StoreDetailMsg::Refresh(false, false)); self.set_locked(false); } Self::Input::SetEnabled(plugin, enabled) => { @@ -298,11 +317,17 @@ impl AsyncComponent for PluginStore { .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) .flatten() { - row.input_sender.emit(StoreRowModelMsg::Refresh(enabled)); + row.input_sender.emit(StoreRowModelMsg::Refresh( + enabled, + cp.plugin.version != plugin.version, + )); } else { eprintln!("could not find corresponding listbox row!") } - self.details.emit(StoreDetailMsg::Refresh(enabled)); + self.details.emit(StoreDetailMsg::Refresh( + enabled, + cp.plugin.version != plugin.version, + )); } else { eprintln!( "failed to set plugin {} enabled: could not find in hashmap", @@ -324,6 +349,9 @@ impl AsyncComponent for PluginStore { self.config_plugins .get(&plugin.appid) .is_some_and(|cp| cp.enabled), + self.config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.plugin.version != plugin.version), )); self.main_stack .as_ref() diff --git a/src/ui/plugins/store_detail.rs b/src/ui/plugins/store_detail.rs index 077423b..1897a22 100644 --- a/src/ui/plugins/store_detail.rs +++ b/src/ui/plugins/store_detail.rs @@ -11,15 +11,16 @@ pub struct StoreDetail { carousel: Option, #[tracker::do_not_track] icon: Option, + needs_update: bool, } #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum StoreDetailMsg { - SetPlugin(Plugin, bool), + SetPlugin(Plugin, bool, bool), SetIcon, SetScreenshots, - Refresh(bool), + Refresh(bool, bool), Install, Remove, SetEnabled(bool), @@ -130,6 +131,20 @@ impl AsyncComponent for StoreDetail { sender.input(Self::Input::Remove); } }, + gtk::Button { + #[track = "model.changed(Self::plugin()) || model.changed(Self::needs_update())"] + set_visible: model + .plugin + .as_ref() + .is_some_and(|p| p.is_installed()) && model.needs_update, + add_css_class: "suggested-action", + set_label: "Update", + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Install); + } + }, gtk::Switch { #[track = "model.changed(Self::plugin())"] set_visible: model.plugin.as_ref() @@ -194,9 +209,10 @@ impl AsyncComponent for StoreDetail { self.reset(); match message { - Self::Input::SetPlugin(p, enabled) => { + Self::Input::SetPlugin(p, enabled, needs_update) => { self.set_plugin(Some(p)); self.set_enabled(enabled); + self.set_needs_update(needs_update); sender.input(Self::Input::SetIcon); sender.input(Self::Input::SetScreenshots); } @@ -244,9 +260,10 @@ impl AsyncComponent for StoreDetail { } } } - Self::Input::Refresh(enabled) => { + Self::Input::Refresh(enabled, needs_update) => { self.mark_all_changed(); self.set_enabled(enabled); + self.set_needs_update(needs_update); } Self::Input::Install => { if let Some(plugin) = self.plugin.as_ref() { @@ -284,6 +301,7 @@ impl AsyncComponent for StoreDetail { enabled: false, carousel: None, icon: None, + needs_update: false, }; let widgets = view_output!(); diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs index 38fc4ac..84e1e2b 100644 --- a/src/ui/plugins/store_row_factory.rs +++ b/src/ui/plugins/store_row_factory.rs @@ -15,18 +15,20 @@ pub struct StoreRowModel { #[tracker::do_not_track] pub input_sender: relm4::Sender, pub enabled: bool, + pub needs_update: bool, } #[derive(Debug)] pub struct StoreRowModelInit { pub plugin: Plugin, pub enabled: bool, + pub needs_update: bool, } #[derive(Debug)] pub enum StoreRowModelMsg { LoadIcon, - Refresh(bool), + Refresh(bool, bool), SetEnabled(bool), } @@ -105,22 +107,45 @@ impl AsyncFactoryComponent for StoreRowModel { .expect(SENDER_IO_ERR_MSG); } }, - gtk::Button { - #[track = "self.changed(StoreRowModel::plugin())"] - set_visible: self.plugin.is_installed(), - set_icon_name: "app-remove-symbolic", - add_css_class: "destructive-action", - set_tooltip_text: Some("Remove"), + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 6, set_valign: gtk::Align::Center, set_halign: gtk::Align::Center, - connect_clicked[sender, plugin] => move |_| { - sender - .output(Self::Output::Remove( - plugin.clone(), - sender.input_sender().clone() - )) - .expect(SENDER_IO_ERR_MSG); - } + gtk::Button { + #[track = "self.changed(StoreRowModel::plugin())"] + set_visible: self.plugin.is_installed(), + set_icon_name: "app-remove-symbolic", + add_css_class: "destructive-action", + set_tooltip_text: Some("Remove"), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender, plugin] => move |_| { + sender + .output(Self::Output::Remove( + plugin.clone(), + sender.input_sender().clone() + )) + .expect(SENDER_IO_ERR_MSG); + } + }, + gtk::Button { + #[track = "self.changed(StoreRowModel::plugin()) || self.changed(StoreRowModel::needs_update())"] + set_visible: self.plugin.is_installed() && self.needs_update, + set_icon_name: "view-refresh-symbolic", + add_css_class: "suggested-action", + set_tooltip_text: Some("Update"), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender, plugin] => move |_| { + sender + .output(Self::Output::Install( + plugin.clone(), + sender.input_sender().clone() + )) + .expect(SENDER_IO_ERR_MSG); + } + }, }, gtk::Switch { #[track = "self.changed(StoreRowModel::plugin())"] @@ -162,9 +187,10 @@ impl AsyncFactoryComponent for StoreRowModel { .output(Self::Output::SetEnabled(self.plugin.clone(), state)) .expect(SENDER_IO_ERR_MSG); } - Self::Input::Refresh(enabled) => { + Self::Input::Refresh(enabled, needs_update) => { self.mark_all_changed(); - self.set_enabled(enabled) + self.set_enabled(enabled); + self.set_needs_update(needs_update); } } } @@ -180,6 +206,7 @@ impl AsyncFactoryComponent for StoreRowModel { enabled: init.enabled, icon: None, input_sender: sender.input_sender().clone(), + needs_update: init.needs_update, } } From 4a7ba9c0c21403200d5e9ebe567b16bf341bcc8f Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Sun, 29 Dec 2024 18:37:06 +0100 Subject: [PATCH 5/7] fix: uninstall app icon --- src/ui/plugins/store_row_factory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs index 84e1e2b..a2acbd2 100644 --- a/src/ui/plugins/store_row_factory.rs +++ b/src/ui/plugins/store_row_factory.rs @@ -115,7 +115,7 @@ impl AsyncFactoryComponent for StoreRowModel { gtk::Button { #[track = "self.changed(StoreRowModel::plugin())"] set_visible: self.plugin.is_installed(), - set_icon_name: "app-remove-symbolic", + set_icon_name: "user-trash-symbolic", add_css_class: "destructive-action", set_tooltip_text: Some("Remove"), set_valign: gtk::Align::Center, From b6d8b0e6c8625f7c18c4e9a81ef0640e3630d813 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Mon, 30 Dec 2024 14:28:07 +0100 Subject: [PATCH 6/7] feat: add custom plugins --- src/config.rs | 2 - src/ui/app.rs | 21 +- src/ui/main_view.rs | 2 +- src/ui/plugins/add_custom_plugin_win.rs | 169 ++++++++++++++ src/ui/plugins/mod.rs | 45 +++- src/ui/plugins/store.rs | 292 +++++++++++++++--------- src/ui/plugins/store_detail.rs | 30 ++- src/ui/plugins/store_row_factory.rs | 9 +- src/ui/preference_rows.rs | 67 +++++- 9 files changed, 491 insertions(+), 146 deletions(-) create mode 100644 src/ui/plugins/add_custom_plugin_win.rs diff --git a/src/config.rs b/src/config.rs index 5a2e29e..18cd864 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,7 +22,6 @@ use std::{ pub struct PluginConfig { pub plugin: Plugin, pub enabled: bool, - pub exec_path: PathBuf, } impl From<&Plugin> for PluginConfig { @@ -30,7 +29,6 @@ impl From<&Plugin> for PluginConfig { Self { plugin: p.clone(), enabled: true, - exec_path: p.exec_path(), } } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 5185554..5e3cca4 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -266,15 +266,20 @@ impl App { .plugins .values() .filter_map(|cp| { - if cp.enabled { - if let Err(e) = mark_as_executable(&cp.exec_path) { - eprintln!( - "Failed to mark plugin {} as executable: {e}", - cp.plugin.appid - ); - None + if cp.enabled && cp.plugin.validate() { + if let Some(exec) = cp.plugin.executable() { + if let Err(e) = mark_as_executable(&exec) { + error!( + "failed to mark plugin {} as executable: {e}", + cp.plugin.appid + ); + None + } else { + Some(format!("'{}'", exec.to_string_lossy())) + } } else { - Some(format!("'{}'", cp.exec_path.to_string_lossy())) + error!("no executable for plugin {}", cp.plugin.appid); + None } } else { None diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 388ff4b..8765a32 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -148,7 +148,7 @@ impl AsyncComponent for MainView { menu! { app_menu: { section! { - "Plugin _store" => PluginStoreAction, + "Plugin_s" => PluginStoreAction, // value inside action is ignored "_Debug View" => DebugViewToggleAction, "_Build Profile" => BuildProfileAction, diff --git a/src/ui/plugins/add_custom_plugin_win.rs b/src/ui/plugins/add_custom_plugin_win.rs new file mode 100644 index 0000000..c06f0c8 --- /dev/null +++ b/src/ui/plugins/add_custom_plugin_win.rs @@ -0,0 +1,169 @@ +use std::path::PathBuf; + +use crate::{ + constants::APP_ID, + ui::{ + preference_rows::{entry_row, file_row}, + SENDER_IO_ERR_MSG, + }, +}; + +use super::Plugin; +use adw::prelude::*; +use gtk::glib::clone; +use relm4::prelude::*; + +#[tracker::track] +pub struct AddCustomPluginWin { + #[tracker::do_not_track] + parent: gtk::Window, + #[tracker::do_not_track] + win: Option, + /// this is true when enough fields are populated, allowing the creation + /// of the plugin object to add + can_add: bool, + #[tracker::do_not_track] + plugin: Plugin, +} + +#[derive(Debug)] +pub enum AddCustomPluginWinMsg { + Present, + Close, + OnNameChange(String), + OnExecPathChange(Option), + Add, +} + +#[derive(Debug)] +pub enum AddCustomPluginWinOutMsg { + Add(Plugin), +} + +#[derive(Debug)] +pub struct AddCustomPluginWinInit { + pub parent: gtk::Window, +} + +#[relm4::component(pub)] +impl SimpleComponent for AddCustomPluginWin { + type Init = AddCustomPluginWinInit; + type Input = AddCustomPluginWinMsg; + type Output = AddCustomPluginWinOutMsg; + + view! { + #[name(win)] + adw::Dialog { + set_can_close: true, + #[wrap(Some)] + set_child: inner = &adw::ToolbarView { + set_top_bar_style: adw::ToolbarStyle::Flat, + set_bottom_bar_style: adw::ToolbarStyle::Flat, + set_vexpand: true, + set_hexpand: true, + add_top_bar: top_bar = &adw::HeaderBar { + set_show_end_title_buttons: false, + set_show_start_title_buttons: false, + pack_start: cancel_btn = >k::Button { + set_label: "Cancel", + add_css_class: "destructive-action", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Close) + }, + }, + pack_end: add_btn = >k::Button { + set_label: "Add", + add_css_class: "suggested-action", + #[track = "model.changed(AddCustomPluginWin::can_add())"] + set_sensitive: model.can_add, + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Add) + }, + }, + #[wrap(Some)] + set_title_widget: title_label = &adw::WindowTitle { + set_title: "Add Custom Plugin", + }, + }, + #[wrap(Some)] + set_content: content = &adw::PreferencesPage { + set_hexpand: true, + set_vexpand: true, + add: grp = &adw::PreferencesGroup { + add: &entry_row( + "Plugin Name", + "", + clone!( + #[strong] sender, + move |row| sender.input(Self::Input::OnNameChange(row.text().to_string())) + ) + ), + add: &file_row( + "Plugin Executable", + None, + None, + Some(model.parent.clone()), + clone!( + #[strong] sender, + move |path_s| sender.input(Self::Input::OnExecPathChange(path_s)) + ) + ) + }, + }, + }, + } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender) { + self.reset(); + + match message { + Self::Input::Present => self.win.as_ref().unwrap().present(Some(&self.parent)), + Self::Input::Close => { + self.win.as_ref().unwrap().close(); + } + Self::Input::Add => { + if self.plugin.validate() { + sender + .output(Self::Output::Add(self.plugin.clone())) + .expect(SENDER_IO_ERR_MSG); + self.win.as_ref().unwrap().close(); + } + } + Self::Input::OnNameChange(name) => { + self.plugin.appid = if !name.is_empty() { + format!("{APP_ID}.customPlugin.{name}") + } else { + String::default() + }; + self.plugin.name = name; + self.set_can_add(self.plugin.validate()); + } + Self::Input::OnExecPathChange(ep) => { + self.plugin.exec_path = ep.map(PathBuf::from); + self.set_can_add(self.plugin.validate()); + } + } + } + + fn init( + init: Self::Init, + root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let mut model = Self { + tracker: 0, + win: None, + parent: init.parent, + can_add: false, + plugin: Plugin { + short_description: Some("Custom Plugin".into()), + ..Default::default() + }, + }; + let widgets = view_output!(); + model.win = Some(widgets.win.clone()); + + ComponentParts { model, widgets } + } +} diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs index 3a88848..0a29bf2 100644 --- a/src/ui/plugins/mod.rs +++ b/src/ui/plugins/mod.rs @@ -1,34 +1,63 @@ +pub mod add_custom_plugin_win; pub mod store; mod store_detail; mod store_row_factory; use crate::{paths::get_plugins_dir, util::file_utils::mark_as_executable}; +use anyhow::bail; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)] pub struct Plugin { pub appid: String, pub name: String, pub icon_url: Option, - pub version: String, + pub version: Option, pub short_description: Option, pub description: Option, - pub hompage_url: String, + pub hompage_url: Option, pub screenshots: Vec, - pub exec_url: String, + /// either one of exec_url or exec_path must be provided + pub exec_url: Option, + /// either one of exec_url or exec_path must be provided + pub exec_path: Option, } impl Plugin { - pub fn exec_path(&self) -> PathBuf { - get_plugins_dir().join(format!("{}.AppImage", self.appid)) + pub fn executable(&self) -> Option { + if self.exec_path.is_some() { + self.exec_path.clone() + } else { + let canonical = self.canonical_exec_path(); + if canonical.is_file() { + Some(canonical) + } else { + None + } + } + } + + pub fn canonical_exec_path(&self) -> PathBuf { + get_plugins_dir().join(&self.appid) } pub fn is_installed(&self) -> bool { - self.exec_path().exists() + self.executable().as_ref().is_some_and(|p| p.is_file()) } pub fn mark_as_executable(&self) -> anyhow::Result<()> { - mark_as_executable(&self.exec_path()) + if let Some(p) = self.executable().as_ref() { + mark_as_executable(p) + } else { + bail!("no exec_path for plugin") + } + } + + /// validate if the plugin can be displayed correctly and run + pub fn validate(&self) -> bool { + !self.appid.is_empty() + && !self.name.is_empty() + && self.executable().as_ref().is_some_and(|p| p.is_file()) } } diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs index c8221c8..95bbf42 100644 --- a/src/ui/plugins/store.rs +++ b/src/ui/plugins/store.rs @@ -1,4 +1,7 @@ use super::{ + add_custom_plugin_win::{ + AddCustomPluginWin, AddCustomPluginWinInit, AddCustomPluginWinMsg, AddCustomPluginWinOutMsg, + }, store_detail::{StoreDetail, StoreDetailMsg, StoreDetailOutMsg}, store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg}, Plugin, @@ -11,6 +14,7 @@ use crate::{ use adw::prelude::*; use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; use std::{collections::HashMap, fs::remove_file}; +use tracing::{debug, error}; #[tracker::track] pub struct PluginStore { @@ -27,6 +31,14 @@ pub struct PluginStore { refreshing: bool, locked: bool, plugins: Vec, + #[tracker::do_not_track] + add_custom_plugin_win: Option>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PluginStoreSignalSource { + Row, + Detail, } #[derive(Debug)] @@ -36,11 +48,13 @@ pub enum PluginStoreMsg { Install(Plugin, relm4::Sender), InstallFromDetails(Plugin), InstallDownload(Plugin, relm4::Sender), - RemoveFromDetails(Plugin), - Remove(Plugin, relm4::Sender), - SetEnabled(Plugin, bool), + Remove(Plugin), + SetEnabled(PluginStoreSignalSource, Plugin, bool), ShowDetails(usize), ShowPluginList, + PresentAddCustomPluginWin, + AddPluginToConfig(Plugin), + AddCustomPlugin(Plugin), } #[derive(Debug)] @@ -53,6 +67,26 @@ pub enum PluginStoreOutMsg { UpdateConfigPlugins(HashMap), } +impl PluginStore { + fn refresh_plugin_rows(&mut self) { + let mut guard = self.plugin_rows.as_mut().unwrap().guard(); + guard.clear(); + self.plugins.iter().for_each(|plugin| { + guard.push_back(StoreRowModelInit { + plugin: plugin.clone(), + enabled: self + .config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.enabled), + needs_update: self + .config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.plugin.version != plugin.version), + }); + }); + } +} + #[relm4::component(pub async)] impl AsyncComponent for PluginStore { type Init = PluginStoreInit; @@ -63,12 +97,21 @@ impl AsyncComponent for PluginStore { view! { #[name(win)] adw::Window { - set_title: Some("Plugin Store"), + set_title: Some("Plugins"), #[name(main_stack)] gtk::Stack { add_child = &adw::ToolbarView { set_top_bar_style: adw::ToolbarStyle::Flat, add_top_bar: headerbar = &adw::HeaderBar { + pack_start: add_custom_plugin_btn = >k::Button { + set_icon_name: "list-add-symbolic", + set_tooltip_text: Some("Add custom plugin"), + #[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"] + set_sensitive: !(model.refreshing || model.locked), + connect_clicked[sender] => move |_| { + sender.input(Self::Input::PresentAddCustomPluginWin) + }, + }, pack_end: refreshbtn = >k::Button { set_icon_name: "view-refresh-symbolic", set_tooltip_text: Some("Refresh"), @@ -76,8 +119,8 @@ impl AsyncComponent for PluginStore { set_sensitive: !(model.refreshing || model.locked), connect_clicked[sender] => move |_| { sender.input(Self::Input::Refresh); - } - } + }, + }, }, #[wrap(Some)] set_content: inner = >k::Box { @@ -169,6 +212,40 @@ impl AsyncComponent for PluginStore { self.win.as_ref().unwrap().present(); sender.input(Self::Input::Refresh); } + Self::Input::AddCustomPlugin(plugin) => { + if self.config_plugins.contains_key(&plugin.appid) { + alert( + "Failed to add custom plugin", + Some("A plugin with the same name already exists"), + Some(&self.win.as_ref().unwrap().clone().upcast::()), + ); + return; + } + sender.input(Self::Input::AddPluginToConfig(plugin)); + sender.input(Self::Input::Refresh); + } + Self::Input::AddPluginToConfig(plugin) => { + self.config_plugins + .insert(plugin.appid.clone(), PluginConfig::from(&plugin)); + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); + } + Self::Input::PresentAddCustomPluginWin => { + let add_win = AddCustomPluginWin::builder() + .launch(AddCustomPluginWinInit { + parent: self.win.as_ref().unwrap().clone().upcast(), + }) + .forward(sender.input_sender(), |msg| match msg { + AddCustomPluginWinOutMsg::Add(plugin) => { + Self::Input::AddCustomPlugin(plugin) + } + }); + add_win.sender().emit(AddCustomPluginWinMsg::Present); + self.add_custom_plugin_win = Some(add_win); + } Self::Input::Refresh => { self.set_refreshing(true); // TODO: populate from web @@ -176,15 +253,16 @@ impl AsyncComponent for PluginStore { Plugin { appid: "com.github.galiser.wlx-overlay-s".into(), name: "WLX Overlay S".into(), - version: "0.4.4".into(), - hompage_url: "https://github.com/galister/wlx-overlay-s".into(), + version: Some("0.6.0".into()), + hompage_url: Some("https://github.com/galister/wlx-overlay-s".into()), icon_url: Some("https://github.com/galister/wlx-overlay-s/raw/main/wlx-overlay-s.svg".into()), screenshots: vec![ "https://github.com/galister/wlx-overlay-s/raw/guide/wlx-s.png".into(), ], description: Some("A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XSOverlay.\n\nWlxOverlay-S allows you to access your desktop screens while in VR.\n\nIn comparison to similar overlays, WlxOverlay-S aims to run alongside VR games and experiences while having as little performance impact as possible. The UI appearance and rendering techniques are kept as simple and efficient as possible, while still allowing a high degree of customizability.".into()), short_description: Some("Access your Wayland/X11 desktop".into()), - exec_url: "https://github.com/galister/wlx-overlay-s/releases/download/v0.4.4/WlxOverlay-S-v0.4.4-x86_64.AppImage".into() + exec_url: Some("https://github.com/galister/wlx-overlay-s/releases/download/v0.6/WlxOverlay-S-v0.6-x86_64.AppImage".into()), + exec_path: None, }, ]; { @@ -202,23 +280,7 @@ impl AsyncComponent for PluginStore { })); } self.set_plugins(plugins); - { - let mut guard = self.plugin_rows.as_mut().unwrap().guard(); - guard.clear(); - self.plugins.iter().for_each(|plugin| { - guard.push_back(StoreRowModelInit { - plugin: plugin.clone(), - enabled: self - .config_plugins - .get(&plugin.appid) - .is_some_and(|cp| cp.enabled), - needs_update: self - .config_plugins - .get(&plugin.appid) - .is_some_and(|cp| cp.plugin.version != plugin.version), - }); - }); - } + self.refresh_plugin_rows(); self.set_refreshing(false); } Self::Input::InstallFromDetails(plugin) => { @@ -233,23 +295,7 @@ impl AsyncComponent for PluginStore { { sender.input(Self::Input::Install(plugin, row.input_sender.clone())) } else { - eprintln!("could not find corresponding listbox row!") - } - } - // TODO: merge implementation with install - Self::Input::RemoveFromDetails(plugin) => { - if let Some(row) = self - .plugin_rows - .as_mut() - .unwrap() - .guard() - .iter() - .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) - .flatten() - { - sender.input(Self::Input::Remove(plugin, row.input_sender.clone())) - } else { - eprintln!("could not find corresponding listbox row!") + error!("could not find corresponding listbox row") } } Self::Input::Install(plugin, row_sender) => { @@ -257,79 +303,110 @@ impl AsyncComponent for PluginStore { sender.input(Self::Input::InstallDownload(plugin, row_sender)) } Self::Input::InstallDownload(plugin, row_sender) => { - if let Err(e) = download_file_async(&plugin.exec_url, &plugin.exec_path()).await { - alert( - "Download failed", - Some(&format!( - "Downloading {} {} failed:\n\n{e}", - plugin.name, plugin.version - )), - Some(&self.win.as_ref().unwrap().clone().upcast::()), - ); - } else { - self.config_plugins - .insert(plugin.appid.clone(), PluginConfig::from(&plugin)); - sender - .output(Self::Output::UpdateConfigPlugins( - self.config_plugins.clone(), - )) - .expect(SENDER_IO_ERR_MSG); - } + 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 => { + alert( + "Download failed", + Some(&format!( + "Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!", + plugin.name, + plugin.version.as_ref().unwrap_or(&"(no version)".to_string())) + ), + Some(&self.win.as_ref().unwrap().clone().upcast::()) + ); + } + }; row_sender.emit(StoreRowModelMsg::Refresh(true, false)); - self.details.emit(StoreDetailMsg::Refresh(true, false)); + self.details + .emit(StoreDetailMsg::Refresh(plugin.appid, true, false)); self.set_locked(false); } - Self::Input::Remove(plugin, row_sender) => { + Self::Input::Remove(plugin) => { self.set_locked(true); - let exec = plugin.exec_path(); - if exec.is_file() { - if let Err(e) = remove_file(&exec) { - alert( - "Failed removing plugin", - Some(&format!( - "Could not remove plugin executable {}:\n\n{e}", - exec.to_string_lossy() - )), - Some(&self.win.as_ref().unwrap().clone().upcast::()), - ); - } else { - self.config_plugins.remove(&plugin.appid); - sender - .output(Self::Output::UpdateConfigPlugins( - self.config_plugins.clone(), - )) - .expect(SENDER_IO_ERR_MSG); + 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() { + if let Err(e) = remove_file(&exec) { + alert( + "Failed removing plugin", + Some(&format!( + "Could not remove plugin executable {}:\n\n{e}", + exec.to_string_lossy() + )), + Some(&self.win.as_ref().unwrap().clone().upcast::()), + ); + } } } - row_sender.emit(StoreRowModelMsg::Refresh(false, false)); - self.details.emit(StoreDetailMsg::Refresh(false, false)); + self.config_plugins.remove(&plugin.appid); + self.set_plugins( + self.plugins + .clone() + .into_iter() + .filter(|p| !(p.appid == plugin.appid && p.exec_url.is_none())) + .collect(), + ); + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); + self.refresh_plugin_rows(); + self.details + .emit(StoreDetailMsg::Refresh(plugin.appid, false, false)); self.set_locked(false); } - Self::Input::SetEnabled(plugin, enabled) => { + Self::Input::SetEnabled(signal_sender, plugin, enabled) => { if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) { cp.enabled = enabled; - if let Some(row) = self - .plugin_rows - .as_mut() - .unwrap() - .guard() - .iter() - .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) - .flatten() - { - row.input_sender.emit(StoreRowModelMsg::Refresh( + if signal_sender == PluginStoreSignalSource::Detail { + if let Some(row) = self + .plugin_rows + .as_mut() + .unwrap() + .guard() + .iter() + .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) + .flatten() + { + row.input_sender.emit(StoreRowModelMsg::Refresh( + enabled, + cp.plugin.version != plugin.version, + )); + } else { + error!("could not find corresponding listbox row") + } + } + if signal_sender == PluginStoreSignalSource::Row { + self.details.emit(StoreDetailMsg::Refresh( + plugin.appid, enabled, cp.plugin.version != plugin.version, )); - } else { - eprintln!("could not find corresponding listbox row!") } - self.details.emit(StoreDetailMsg::Refresh( - enabled, - cp.plugin.version != plugin.version, - )); } else { - eprintln!( + debug!( "failed to set plugin {} enabled: could not find in hashmap", plugin.appid ) @@ -358,7 +435,7 @@ impl AsyncComponent for PluginStore { .unwrap() .set_visible_child_name("detailsview"); } else { - eprintln!("plugins list index out of range!") + error!("plugins list index out of range!") } } Self::Input::ShowPluginList => { @@ -387,13 +464,14 @@ impl AsyncComponent for PluginStore { .forward(sender.input_sender(), move |msg| match msg { StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList, StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin), - StoreDetailOutMsg::Remove(plugin) => Self::Input::RemoveFromDetails(plugin), + StoreDetailOutMsg::Remove(plugin) => Self::Input::Remove(plugin), StoreDetailOutMsg::SetEnabled(plugin, enabled) => { - Self::Input::SetEnabled(plugin, enabled) + Self::Input::SetEnabled(PluginStoreSignalSource::Detail, plugin, enabled) } }), config_plugins: init.config_plugins, main_stack: None, + add_custom_plugin_win: None, }; let details_view = model.details.widget(); @@ -408,11 +486,9 @@ impl AsyncComponent for PluginStore { StoreRowModelOutMsg::Install(appid, row_sender) => { Self::Input::Install(appid, row_sender) } - StoreRowModelOutMsg::Remove(appid, row_sender) => { - Self::Input::Remove(appid, row_sender) - } + StoreRowModelOutMsg::Remove(appid) => Self::Input::Remove(appid), StoreRowModelOutMsg::SetEnabled(plugin, enabled) => { - Self::Input::SetEnabled(plugin, enabled) + Self::Input::SetEnabled(PluginStoreSignalSource::Row, plugin, enabled) } }), ); diff --git a/src/ui/plugins/store_detail.rs b/src/ui/plugins/store_detail.rs index 1897a22..5a1b12c 100644 --- a/src/ui/plugins/store_detail.rs +++ b/src/ui/plugins/store_detail.rs @@ -2,6 +2,7 @@ use super::Plugin; use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG}; use adw::prelude::*; use relm4::prelude::*; +use tracing::warn; #[tracker::track] pub struct StoreDetail { @@ -20,7 +21,7 @@ pub enum StoreDetailMsg { SetPlugin(Plugin, bool, bool), SetIcon, SetScreenshots, - Refresh(bool, bool), + Refresh(String, bool, bool), Install, Remove, SetEnabled(bool), @@ -86,7 +87,8 @@ impl AsyncComponent for StoreDetail { set_hexpand: true, #[name(icon)] gtk::Image { - set_icon_name: Some("image-missing-symbolic"), + set_icon_name: Some("application-x-addon-symbolic"), + set_margin_end: 12, set_pixel_size: 96, }, gtk::Label { @@ -224,9 +226,14 @@ impl AsyncComponent for StoreDetail { self.icon.as_ref().unwrap().set_from_file(Some(dest)); } Err(e) => { - eprintln!("Failed downloading icon '{url}': {e}"); + warn!("Failed downloading icon '{url}': {e}"); } }; + } else { + self.icon + .as_ref() + .unwrap() + .set_icon_name(Some("application-x-addon-symbolic")); } } } @@ -254,16 +261,18 @@ impl AsyncComponent for StoreDetail { carousel.append(&clamp); } Err(e) => { - eprintln!("Failed downloading screenshot '{url}': {e}"); + warn!("failed downloading screenshot '{url}': {e}"); } }; } } } - Self::Input::Refresh(enabled, needs_update) => { - self.mark_all_changed(); - self.set_enabled(enabled); - self.set_needs_update(needs_update); + Self::Input::Refresh(appid, enabled, needs_update) => { + if self.plugin.as_ref().is_some_and(|p| p.appid == appid) { + self.mark_all_changed(); + self.set_enabled(enabled); + self.set_needs_update(needs_update); + } } Self::Input::Install => { if let Some(plugin) = self.plugin.as_ref() { @@ -277,6 +286,11 @@ impl AsyncComponent for StoreDetail { sender .output(Self::Output::Remove(plugin.clone())) .expect(SENDER_IO_ERR_MSG); + if plugin.exec_url.is_none() { + sender + .output(Self::Output::GoBack) + .expect(SENDER_IO_ERR_MSG); + } } } Self::Input::SetEnabled(enabled) => { diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs index a2acbd2..3ea2629 100644 --- a/src/ui/plugins/store_row_factory.rs +++ b/src/ui/plugins/store_row_factory.rs @@ -4,6 +4,7 @@ use gtk::prelude::*; use relm4::{ factory::AsyncFactoryComponent, prelude::DynamicIndex, AsyncFactorySender, RelmWidgetExt, }; +use tracing::error; #[derive(Debug)] #[tracker::track] @@ -28,6 +29,7 @@ pub struct StoreRowModelInit { #[derive(Debug)] pub enum StoreRowModelMsg { LoadIcon, + /// params: enabled, needs_update Refresh(bool, bool), SetEnabled(bool), } @@ -35,7 +37,7 @@ pub enum StoreRowModelMsg { #[derive(Debug)] pub enum StoreRowModelOutMsg { Install(Plugin, relm4::Sender), - Remove(Plugin, relm4::Sender), + Remove(Plugin), SetEnabled(Plugin, bool), } @@ -57,7 +59,7 @@ impl AsyncFactoryComponent for StoreRowModel { set_margin_all: 12, #[name(icon)] gtk::Image { - set_icon_name: Some("image-missing-symbolic"), + set_icon_name: Some("application-x-addon-symbolic"), set_icon_size: gtk::IconSize::Large, }, gtk::Box { @@ -124,7 +126,6 @@ impl AsyncFactoryComponent for StoreRowModel { sender .output(Self::Output::Remove( plugin.clone(), - sender.input_sender().clone() )) .expect(SENDER_IO_ERR_MSG); } @@ -176,7 +177,7 @@ impl AsyncFactoryComponent for StoreRowModel { self.icon.as_ref().unwrap().set_from_file(Some(dest)); } Err(e) => { - eprintln!("Failed downloading icon '{url}': {e}"); + error!("failed downloading icon '{url}': {e}"); } }; } diff --git a/src/ui/preference_rows.rs b/src/ui/preference_rows.rs index 5159ec9..4502ebf 100644 --- a/src/ui/preference_rows.rs +++ b/src/ui/preference_rows.rs @@ -156,13 +156,12 @@ pub fn spin_row( row } -pub fn path_row) + 'static + Clone>( +fn filedialog_row_base) + 'static + Clone>( title: &str, description: Option<&str>, value: Option, - root_win: Option, cb: F, -) -> adw::ActionRow { +) -> (adw::ActionRow, gtk::Label) { let row = adw::ActionRow::builder() .title(title) .subtitle_lines(0) @@ -174,14 +173,14 @@ pub fn path_row) + 'static + Clone>( row.set_subtitle(d); } - let path_label = >k::Label::builder() + let path_label = gtk::Label::builder() .label(match value.as_ref() { None => "(None)", Some(p) => p.as_str(), }) .wrap(true) .build(); - row.add_suffix(path_label); + row.add_suffix(&path_label); let clear_btn = gtk::Button::builder() .icon_name("edit-clear-symbolic") @@ -200,6 +199,60 @@ pub fn path_row) + 'static + Clone>( cb(None) } )); + (row, path_label) +} + +pub fn file_row) + 'static + Clone>( + title: &str, + description: Option<&str>, + value: Option, + root_win: Option, + cb: F, +) -> adw::ActionRow { + let (row, path_label) = filedialog_row_base(title, description, value, cb.clone()); + + let filedialog = gtk::FileDialog::builder() + .modal(true) + .title(format!("Select {}", title)) + .build(); + + row.connect_activated(clone!( + #[weak] + path_label, + move |_| { + filedialog.open( + root_win.as_ref(), + gio::Cancellable::NONE, + clone!( + #[weak] + path_label, + #[strong] + cb, + move |res| { + if let Ok(file) = res { + if let Some(path) = file.path() { + let path_s = path.to_string_lossy().to_string(); + path_label.set_text(&path_s); + cb(Some(path_s)) + } + } + } + ), + ) + } + )); + + row +} + +pub fn path_row) + 'static + Clone>( + title: &str, + description: Option<&str>, + value: Option, + root_win: Option, + cb: F, +) -> adw::ActionRow { + let (row, path_label) = filedialog_row_base(title, description, value, cb.clone()); let filedialog = gtk::FileDialog::builder() .modal(true) .title(format!("Select Path for {}", title)) @@ -220,8 +273,8 @@ pub fn path_row) + 'static + Clone>( move |res| { if let Ok(file) = res { if let Some(path) = file.path() { - let path_s = path.to_str().unwrap().to_string(); - path_label.set_text(path_s.as_str()); + let path_s = path.to_string_lossy().to_string(); + path_label.set_text(&path_s); cb(Some(path_s)) } } From bd0efe2bcb937cb4345b5378f0abbf2f25f91f83 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Mon, 30 Dec 2024 14:39:11 +0100 Subject: [PATCH 7/7] feat: remove autostart as plugin system makes it redundant --- src/profile.rs | 3 --- src/ui/app.rs | 47 ++++++++++------------------------------ src/ui/plugins/mod.rs | 2 +- src/ui/profile_editor.rs | 8 ------- 4 files changed, 12 insertions(+), 48 deletions(-) diff --git a/src/profile.rs b/src/profile.rs index d67a7ac..4a637fe 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -363,7 +363,6 @@ pub struct Profile { pub lighthouse_driver: LighthouseDriver, #[serde(default = "String::default")] pub xrservice_launch_options: String, - pub autostart_command: Option, #[serde(default)] pub skip_dependency_check: bool, } @@ -422,7 +421,6 @@ impl Default for Profile { lighthouse_driver: LighthouseDriver::default(), xrservice_launch_options: String::default(), uuid, - autostart_command: None, skip_dependency_check: false, } } @@ -543,7 +541,6 @@ impl Profile { mercury_enabled: self.features.mercury_enabled, }, environment: self.environment.clone(), - autostart_command: self.autostart_command.clone(), pull_on_build: self.pull_on_build, lighthouse_driver: self.lighthouse_driver, opencomposite_repo: self.opencomposite_repo.clone(), diff --git a/src/ui/app.rs b/src/ui/app.rs index 5e3cca4..5948153 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -43,8 +43,7 @@ use crate::{ restore_runtime_entrypoint, set_runtime_entrypoint_launch_opts_from_profile, }, util::file_utils::{ - mark_as_executable, setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, - verify_cap_sys_nice_eip, + setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip, }, vulkaninfo::VulkanInfo, wivrn_dbus, @@ -80,7 +79,6 @@ pub struct App { config: Config, xrservice_worker: Option, - autostart_worker: Option, plugins_worker: Option, restart_xrservice: bool, build_worker: Option, @@ -103,7 +101,6 @@ pub struct App { pub enum Msg { OnServiceLog(Vec), OnServiceExit(i32), - OnAutostartExit(i32), OnPluginsExit(i32), OnBuildLog(Vec), OnBuildExit(i32), @@ -246,40 +243,23 @@ impl App { pub fn run_autostart(&mut self, sender: AsyncComponentSender) { let prof = self.get_selected_profile(); - if let Some(autostart_cmd) = &prof.autostart_command { - let mut jobs = VecDeque::new(); - jobs.push_back(WorkerJob::new_cmd( - Some(prof.environment.clone()), - "sh".into(), - Some(vec!["-c".into(), autostart_cmd.clone()]), - )); - let autostart_worker = JobWorker::new(jobs, sender.input_sender(), |msg| match msg { - JobWorkerOut::Log(rows) => Msg::OnServiceLog(rows), - JobWorkerOut::Exit(code) => Msg::OnAutostartExit(code), - }); - autostart_worker.start(); - self.autostart_worker = Some(autostart_worker); - } - let plugins_cmd = self .config .plugins .values() .filter_map(|cp| { if cp.enabled && cp.plugin.validate() { - if let Some(exec) = cp.plugin.executable() { - if let Err(e) = mark_as_executable(&exec) { - error!( - "failed to mark plugin {} as executable: {e}", - cp.plugin.appid - ); - None - } else { - Some(format!("'{}'", exec.to_string_lossy())) - } - } else { - error!("no executable for plugin {}", cp.plugin.appid); + if let Err(e) = cp.plugin.mark_as_executable() { + error!( + "failed to mark plugin {} as executable: {e}", + cp.plugin.appid + ); None + } else { + Some(format!( + "'{}'", + cp.plugin.executable().unwrap().to_string_lossy() + )) } } else { None @@ -329,9 +309,6 @@ impl App { } pub fn shutdown_xrservice(&mut self) { - if let Some(w) = self.autostart_worker.as_ref() { - w.stop(); - } if let Some(w) = self.plugins_worker.as_ref() { w.stop(); } @@ -433,7 +410,6 @@ impl AsyncComponent for App { self.start_xrservice(sender, false); } } - Msg::OnAutostartExit(_) => self.autostart_worker = None, Msg::OnPluginsExit(_) => self.plugins_worker = None, Msg::ClockTicking => { self.main_view.sender().emit(MainViewMsg::ClockTicking); @@ -1039,7 +1015,6 @@ impl AsyncComponent for App { config, profiles, xrservice_worker: None, - autostart_worker: None, plugins_worker: None, build_worker: None, xr_devices: vec![], diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs index 0a29bf2..11732df 100644 --- a/src/ui/plugins/mod.rs +++ b/src/ui/plugins/mod.rs @@ -50,7 +50,7 @@ impl Plugin { if let Some(p) = self.executable().as_ref() { mark_as_executable(p) } else { - bail!("no exec_path for plugin") + bail!("no executable found for plugin") } } diff --git a/src/ui/profile_editor.rs b/src/ui/profile_editor.rs index 353dff0..99f7b7b 100644 --- a/src/ui/profile_editor.rs +++ b/src/ui/profile_editor.rs @@ -130,14 +130,6 @@ impl SimpleComponent for ProfileEditor { prof.borrow_mut().prefix = n_path.unwrap_or_default().into(); }), ), - add: &entry_row("Autostart Command", - model.profile.borrow().autostart_command.as_ref().unwrap_or(&String::default()), - clone!(#[strong] prof, move |row| { - let txt = row.text().trim().to_string(); - prof.borrow_mut().autostart_command = - if txt.is_empty() {None} else {Some(txt)}; - }) - ), add: &switch_row("Dependency Check", Some("Warning: disabling dependency checks may result in build failures"), !model.profile.borrow().skip_dependency_check,