UI(system tray menu): added pause/resume menu items + replaced subscription to obsolete "runtime:subsystems" by "runtime:system/status"
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod spn;
|
||||
pub mod notification;
|
||||
pub mod subsystem;
|
||||
pub mod system_status_types;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
// ...
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<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) => {
|
||||
|
||||
Reference in New Issue
Block a user