From 997f95698be608ad0ab45a7d050c5c082e5f3aaf Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Fri, 7 Nov 2025 16:58:42 +0200 Subject: [PATCH] UI(system tray menu): added pause/resume menu items + replaced subscription to obsolete "runtime:subsystems" by "runtime:system/status" --- .../tauri/src-tauri/src/portapi/models/mod.rs | 2 +- .../src-tauri/src/portapi/models/subsystem.rs | 45 ------ .../src/portapi/models/system_status_types.rs | 57 ++++++++ desktop/tauri/src-tauri/src/portmaster/mod.rs | 41 ++++++ desktop/tauri/src-tauri/src/traymenu.rs | 130 ++++++++++++------ 5 files changed, 188 insertions(+), 87 deletions(-) delete mode 100644 desktop/tauri/src-tauri/src/portapi/models/subsystem.rs create mode 100644 desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs diff --git a/desktop/tauri/src-tauri/src/portapi/models/mod.rs b/desktop/tauri/src-tauri/src/portapi/models/mod.rs index 91336dd0..08f5e555 100644 --- a/desktop/tauri/src-tauri/src/portapi/models/mod.rs +++ b/desktop/tauri/src-tauri/src/portapi/models/mod.rs @@ -1,4 +1,4 @@ pub mod config; pub mod spn; pub mod notification; -pub mod subsystem; \ No newline at end of file +pub mod system_status_types; \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portapi/models/subsystem.rs b/desktop/tauri/src-tauri/src/portapi/models/subsystem.rs deleted file mode 100644 index c8b0ea27..00000000 --- a/desktop/tauri/src-tauri/src/portapi/models/subsystem.rs +++ /dev/null @@ -1,45 +0,0 @@ -#![allow(dead_code)] -use serde::*; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct ModuleStatus { - #[serde(rename = "Name")] - pub name: String, - - #[serde(rename = "Enabled")] - pub enabled: bool, - - #[serde(rename = "Status")] - pub status: u8, - - #[serde(rename = "FailureStatus")] - pub failure_status: u8, - - #[serde(rename = "FailureID")] - pub failure_id: String, - - #[serde(rename = "FailureMsg")] - pub failure_msg: String, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct Subsystem { - #[serde(rename = "ID")] - pub id: String, - - #[serde(rename = "Name")] - pub name: String, - - #[serde(rename = "Description")] - pub description: String, - - #[serde(rename = "Modules")] - pub module_status: Vec, - - #[serde(rename = "FailureStatus")] - pub failure_status: u8, -} -pub const FAILURE_NONE: u8 = 0; -pub const FAILURE_HINT: u8 = 1; -pub const FAILURE_WARNING: u8 = 2; -pub const FAILURE_ERROR: u8 = 3; diff --git a/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs b/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs new file mode 100644 index 00000000..afb24337 --- /dev/null +++ b/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StateType { + #[serde(rename = "")] + Undefined, + #[serde(rename = "hint")] + Hint, + #[serde(rename = "warning")] + Warning, + #[serde(rename = "error")] + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + #[serde(rename = "ID")] + pub id: String, + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Message")] + pub message: Option, + #[serde(rename = "Type")] + pub state_type: Option, + #[serde(rename = "Time")] + pub time: Option, // time.Time serialized by GoLang + #[serde(rename = "Data")] + pub data: Option, // any type +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateUpdate { + #[serde(rename = "Module")] + pub module: String, + #[serde(rename = "States")] + pub states: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorstState { + #[serde(rename = "Module")] + pub module: String, + #[serde(flatten)] + pub state: State, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemStatus { + #[serde(rename = "Modules")] + pub modules: Vec, + #[serde(rename = "WorstState")] + pub worst_state: Option, + + // add more fields when needed + // ... +} \ No newline at end of file diff --git a/desktop/tauri/src-tauri/src/portmaster/mod.rs b/desktop/tauri/src-tauri/src/portmaster/mod.rs index 0941f306..19c33d7a 100644 --- a/desktop/tauri/src-tauri/src/portmaster/mod.rs +++ b/desktop/tauri/src-tauri/src/portmaster/mod.rs @@ -217,6 +217,47 @@ impl PortmasterInterface { }); } + pub fn set_resume(&self) { + tauri::async_runtime::spawn(async move { + let client = reqwest::Client::new(); + match client + .post(format!("{}control/resume", PORTMASTER_BASE_URL)) + .send() + .await + { + Ok(v) => { + debug!("resume request sent {:?}", v); + } + Err(err) => { + error!("failed to send resume request {}", err); + } + } + }); + } + + + pub fn set_pause(&self, duration_seconds: u64, spn_only: bool) { + tauri::async_runtime::spawn(async move { + let client = reqwest::Client::new(); + match client + .post(format!("{}control/pause", PORTMASTER_BASE_URL)) + .json(&serde_json::json!({ + "duration": duration_seconds, + "onlySPN": spn_only + })) + .send() + .await + { + Ok(v) => { + debug!("pause request sent {:?}", v); + } + Err(err) => { + error!("failed to send pause request {}", err); + } + } + }); + } + //// Internal functions fn start_notification_handler(&self) { if let Some(api) = self.get_api() { diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 197bdabc..106793bf 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use std::sync::atomic::AtomicBool; use std::sync::RwLock; -use std::{collections::HashMap, sync::atomic::Ordering}; +use std::{sync::atomic::Ordering}; use log::{debug, error}; use tauri::{ @@ -16,11 +16,11 @@ use crate::config; use crate::{ portapi::{ client::PortAPI, - message::ParseError, + message::{ParseError}, models::{ config::BooleanValue, spn::SPNStatus, - subsystem::{self, Subsystem}, + system_status_types::{self, SystemStatus}, }, types::{Request, Response}, }, @@ -59,6 +59,14 @@ const FORCE_SHOW_KEY: &str = "force-show"; const PM_TRAY_ICON_ID: &str = "pm_icon"; const PM_TRAY_MENU_ID: &str = "pm_tray_menu"; +const PAUSE_SPN_5_KEY: &str = "pause_spn_5"; +const PAUSE_SPN_15_KEY: &str = "pause_spn_15"; +const PAUSE_SPN_60_KEY: &str = "pause_spn_60"; +const PAUSE_PM_5_KEY: &str = "pause_pm_5"; +const PAUSE_PM_15_KEY: &str = "pause_pm_15"; +const PAUSE_PM_60_KEY: &str = "pause_pm_60"; +const RESUME_KEY: &str = "resume_all"; + // Icons fn get_theme_mode() -> dark_light::Mode { @@ -142,7 +150,7 @@ fn build_tray_menu( .enabled(false) .build(app) .unwrap(); - + // Setup SPN button let spn_button_text = match spn_status_text { "disabled" => "Enable SPN", @@ -152,6 +160,7 @@ fn build_tray_menu( .build(app) .unwrap(); + // Setup Icon theme submenu let system_theme = MenuItemBuilder::with_id(SYSTEM_THEME_KEY, "System") .build(app) .unwrap(); @@ -165,11 +174,38 @@ fn build_tray_menu( .items(&[&system_theme, &light_theme, &dark_theme]) .build()?; + + // Setup Pause/Resume menu items + let disabled_spn = spn_status_text == "disabled"; + let pause_spn_5min_item = MenuItemBuilder::with_id(PAUSE_SPN_5_KEY, "Pause SPN for 5 minutes").enabled(!disabled_spn).build(app)?; + let pause_spn_15min_item = MenuItemBuilder::with_id(PAUSE_SPN_15_KEY, "Pause SPN for 15 minutes").enabled(!disabled_spn).build(app)?; + let pause_spn_1hour_item = MenuItemBuilder::with_id(PAUSE_SPN_60_KEY, "Pause SPN for 1 hour").enabled(!disabled_spn).build(app)?; + + let pause_pm_5min_item = MenuItemBuilder::with_id(PAUSE_PM_5_KEY, "Pause for 5 minutes").build(app)?; + let pause_pm_15min_item = MenuItemBuilder::with_id(PAUSE_PM_15_KEY, "Pause for 15 minutes").build(app)?; + let pause_pm_1hour_item = MenuItemBuilder::with_id(PAUSE_PM_60_KEY, "Pause for 1 hour").build(app)?; + + let pause_menu = SubmenuBuilder::new(app, "Pause") + .items(&[ + &pause_spn_5min_item, + &pause_spn_15min_item, + &pause_spn_1hour_item, + &PredefinedMenuItem::separator(app)?, + &pause_pm_5min_item, + &pause_pm_15min_item, + &pause_pm_1hour_item, + ]) + .build()?; + + let resume_item = MenuItemBuilder::with_id(RESUME_KEY, "Resume now").build(app)?; + + /* DEV MENU let force_show_window = MenuItemBuilder::with_id(FORCE_SHOW_KEY, "Force Show UI").build(app)?; let reload_btn = MenuItemBuilder::with_id(RELOAD_KEY, "Reload User Interface").build(app)?; let developer_menu = SubmenuBuilder::new(app, "Developer") .items(&[&reload_btn, &force_show_window]) .build()?; + */ let menu = MenuBuilder::with_id(app, PM_TRAY_MENU_ID) .items(&[ @@ -177,6 +213,9 @@ fn build_tray_menu( &PredefinedMenuItem::separator(app)?, &global_status, &PredefinedMenuItem::separator(app)?, + &pause_menu, + &resume_item, + &PredefinedMenuItem::separator(app)?, &spn_status, &spn_button, &PredefinedMenuItem::separator(app)?, @@ -184,7 +223,7 @@ fn build_tray_menu( &PredefinedMenuItem::separator(app)?, &exit_ui_btn, &shutdown_btn, - &developer_menu, + //&developer_menu, ]) .build()?; @@ -250,6 +289,15 @@ pub fn setup_tray_menu( SYSTEM_THEME_KEY => update_icon_theme(app, dark_light::Mode::Default), DARK_THEME_KEY => update_icon_theme(app, dark_light::Mode::Dark), LIGHT_THEME_KEY => update_icon_theme(app, dark_light::Mode::Light), + + PAUSE_SPN_5_KEY => app.portmaster().set_pause(60*5, true), + PAUSE_SPN_15_KEY => app.portmaster().set_pause(60*15, true), + PAUSE_SPN_60_KEY => app.portmaster().set_pause(60*60, true), + PAUSE_PM_5_KEY => app.portmaster().set_pause(60*5, false), + PAUSE_PM_15_KEY => app.portmaster().set_pause(60*15, false), + PAUSE_PM_60_KEY => app.portmaster().set_pause(60*60, false), + RESUME_KEY => app.portmaster().set_resume(), + other => { error!("unknown menu event id: {}", other); } @@ -275,33 +323,29 @@ pub fn setup_tray_menu( Ok(icon) } -pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { - // iterate over the subsystems and check if there's a module failure - let failure = subsystems.values().map(|s| &s.module_status).fold( - (subsystem::FAILURE_NONE, "".to_string()), - |mut acc, s| { - for m in s { - if m.failure_status > acc.0 { - acc = (m.failure_status, m.failure_msg.clone()) - } +pub fn update_icon(icon: AppIcon, system_status: SystemStatus, spn_status: String) { + // Extract the worst state type + let worst_state_type = match system_status.worst_state { + Some(ws) => { + match ws.state.state_type { + Some(s) => s, + None => system_status_types::StateType::Undefined } - acc - }, - ); + } + None => system_status_types::StateType::Undefined + }; - let mut status = "Secured".to_owned(); - - if failure.0 != subsystem::FAILURE_NONE { - status = failure.1; - } - - let icon_color = match failure.0 { - subsystem::FAILURE_WARNING => IconColor::Yellow, - subsystem::FAILURE_ERROR => IconColor::Red, - _ => match spn_status.as_str() { - "connected" | "connecting" => IconColor::Blue, - _ => IconColor::Green, - }, + // Determine status and icon color in a single match expression + let (status, icon_color) = match worst_state_type { + system_status_types::StateType::Error => ("Insecure", IconColor::Red), + system_status_types::StateType::Warning => ("Insecure", IconColor::Yellow), + _ => { + let color = match spn_status.as_str() { + "connected" | "connecting" => IconColor::Blue, + _ => IconColor::Green, + }; + ("Secured", color) + } }; if let Ok(menu) = build_tray_menu(icon.app_handle(), status.as_ref(), spn_status.as_str()) { @@ -322,9 +366,9 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { } }; - let mut subsystem_subscription = match cli + let mut system_status_subscription = match cli .request(Request::QuerySubscribe( - "query runtime:subsystems/".to_string(), + "query runtime:system/status".to_string(), )) .await { @@ -388,12 +432,16 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { update_icon_color(&icon, IconColor::Blue); - let mut subsystems: HashMap = HashMap::new(); + let mut system_status = SystemStatus { + modules: Vec::new(), + worst_state: None, + }; + let mut spn_status: String = "".to_string(); loop { tokio::select! { - msg = subsystem_subscription.recv() => { + msg = system_status_subscription.recv() => { let msg = match msg { Some(m) => m, None => { break } @@ -407,17 +455,17 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { }; if let Some((_, payload)) = res { - match payload.parse::() { - Ok(n) => { - subsystems.insert(n.id.clone(), n); - update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + match payload.parse::() { + Ok(system_status_update) => { + system_status.clone_from(&system_status_update); + update_icon(icon.clone(), system_status.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { - error!("failed to parse subsystem: {}", err); + error!("failed to parse SystemStatus: {}", err); } _ => { - error!("unknown error when parsing notifications payload"); + error!("unknown error when parsing SystemStatus payload"); } }, } @@ -441,7 +489,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { Ok(value) => { debug!("SPN status update: {}", value.status); spn_status.clone_from(&value.status); - update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), system_status.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => {