UI(system tray menu): added pause/resume menu items + replaced subscription to obsolete "runtime:subsystems" by "runtime:system/status"

This commit is contained in:
Alexandr Stelnykovych
2025-11-07 16:58:42 +02:00
parent 4d58f68fde
commit 997f95698b
5 changed files with 188 additions and 87 deletions

View File

@@ -1,4 +1,4 @@
pub mod config;
pub mod spn;
pub mod notification;
pub mod subsystem;
pub mod system_status_types;

View File

@@ -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<ModuleStatus>,
#[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;

View File

@@ -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<String>,
#[serde(rename = "Type")]
pub state_type: Option<StateType>,
#[serde(rename = "Time")]
pub time: Option<String>, // time.Time serialized by GoLang
#[serde(rename = "Data")]
pub data: Option<serde_json::Value>, // any type
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateUpdate {
#[serde(rename = "Module")]
pub module: String,
#[serde(rename = "States")]
pub states: Option<Vec<State>>,
}
#[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<StateUpdate>,
#[serde(rename = "WorstState")]
pub worst_state: Option<WorstState>,
// add more fields when needed
// ...
}

View File

@@ -217,6 +217,47 @@ impl<R: Runtime> PortmasterInterface<R> {
});
}
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() {

View File

@@ -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 {
@@ -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<String, Subsystem>, 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<String, Subsystem> = 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::<Subsystem>() {
Ok(n) => {
subsystems.insert(n.id.clone(), n);
update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
match payload.parse::<SystemStatus>() {
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) => {