diff --git a/desktop/angular/src/app/layout/navigation/navigation.html b/desktop/angular/src/app/layout/navigation/navigation.html
index 09b8e5f9..ae961f6c 100644
--- a/desktop/angular/src/app/layout/navigation/navigation.html
+++ b/desktop/angular/src/app/layout/navigation/navigation.html
@@ -220,10 +220,10 @@
- Paused: {{ pauseInfo }}
+ {{ pauseInfo }}
- Auto-resume at: {{ pauseInfoTillTime }}
+ Auto-resume at {{ pauseInfoTillTime }}
diff --git a/desktop/angular/src/app/layout/navigation/navigation.ts b/desktop/angular/src/app/layout/navigation/navigation.ts
index 48195e5d..1cff5acb 100644
--- a/desktop/angular/src/app/layout/navigation/navigation.ts
+++ b/desktop/angular/src/app/layout/navigation/navigation.ts
@@ -42,11 +42,11 @@ export class NavigationComponent implements OnInit {
get isPausedSPN(): boolean { return this.pauseState?.SPN===true; }
get pauseInfo(): string {
if (this.pauseState?.Interception===true && this.pauseState?.SPN===true)
- return 'Portmaster and SPN';
+ return 'Portmaster and SPN are paused';
else if (this.pauseState?.Interception===true)
- return 'Portmaster';
+ return 'Portmaster is paused';
else if (this.pauseState?.SPN===true)
- return 'SPN';
+ return 'SPN is paused';
return '';
}
get pauseInfoTillTime(): string {
diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock
index 2f5e7e06..6d1c7727 100644
--- a/desktop/tauri/src-tauri/Cargo.lock
+++ b/desktop/tauri/src-tauri/Cargo.lock
@@ -859,8 +859,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
+ "js-sys",
"num-traits",
"serde",
+ "wasm-bindgen",
"windows-link",
]
@@ -3949,6 +3951,7 @@ version = "2.0.25"
dependencies = [
"assert_matches",
"cached",
+ "chrono",
"clap_lex",
"ctor",
"dark-light",
diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml
index fb562e22..b7e9a6d6 100644
--- a/desktop/tauri/src-tauri/Cargo.toml
+++ b/desktop/tauri/src-tauri/Cargo.toml
@@ -52,6 +52,7 @@ reqwest = { version = "0.12", features = ["cookies", "json"] }
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
open = "5.1.3"
+chrono = { version = "0.4", features = ["serde"] }
dark-light = { path = "../rust-dark-light" }
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
index afb24337..c4e0a47a 100644
--- a/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs
+++ b/desktop/tauri/src-tauri/src/portapi/models/system_status_types.rs
@@ -45,7 +45,7 @@ pub struct WorstState {
pub state: State,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SystemStatus {
#[serde(rename = "Modules")]
pub modules: Vec
,
@@ -54,4 +54,28 @@ pub struct SystemStatus {
// add more fields when needed
// ...
+}
+
+// PauseInfo represents pause status data from "control:paused" state in Control module
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct PauseInfo {
+ #[serde(rename = "Interception")]
+ pub interception: bool,
+ #[serde(rename = "SPN")]
+ pub spn: bool,
+ #[serde(rename = "TillTime")]
+ pub till_time: String, // time.Time serialized as string by GoLang
+}
+
+
+impl SystemStatus {
+ pub fn get_module_state(&self, module_name: &str, state_id: &str) -> Option<&State> {
+ if let Some(module) = self.modules.iter().find(|m| m.module == module_name) {
+ if let Some(states) = &module.states {
+ return states.iter().find(|s| s.id == state_id);
+ }
+ }
+ None
+ }
+
}
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs
index 106793bf..1d7a88b7 100644
--- a/desktop/tauri/src-tauri/src/traymenu.rs
+++ b/desktop/tauri/src-tauri/src/traymenu.rs
@@ -2,6 +2,7 @@ use std::ops::Deref;
use std::sync::atomic::AtomicBool;
use std::sync::RwLock;
use std::{sync::atomic::Ordering};
+use chrono::{DateTime};
use log::{debug, error};
use tauri::{
@@ -66,6 +67,8 @@ 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";
+const PAUSE_INFO_KEY: &str = "pause_info";
+const PAUSE_INFO_TIME_KEY: &str = "pause_info_time";
// Icons
@@ -133,6 +136,7 @@ fn build_tray_menu(
app: &tauri::AppHandle,
status: &str,
spn_status_text: &str,
+ pause_info: &system_status_types::PauseInfo,
) -> core::result::Result> {
load_theme(app);
@@ -140,22 +144,46 @@ fn build_tray_menu(
let exit_ui_btn = MenuItemBuilder::with_id(EXIT_UI_KEY, "Exit UI").build(app)?;
let shutdown_btn = MenuItemBuilder::with_id(SHUTDOWN_KEY, "Shut Down Portmaster").build(app)?;
- let global_status = MenuItemBuilder::with_id(GLOBAL_STATUS_KEY, format!("Status: {}", status))
+ // Global status
+ let global_status_text = if pause_info.interception {
+ format!("Status: {} (PAUSED)", status)
+ } else {
+ format!("Status: {}", status)
+ };
+ let global_status = MenuItemBuilder::with_id(GLOBAL_STATUS_KEY, global_status_text)
.enabled(false)
.build(app)
.unwrap();
- // Setup SPN status
- let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text))
- .enabled(false)
- .build(app)
- .unwrap();
-
+ // Pause items
+ let (pause_status_item, pause_status_time_item, resume_item) = if pause_info.interception || pause_info.spn {
+ let status_text = match (pause_info.interception, pause_info.spn) {
+ (true, true) => "Portmaster and SPN are paused",
+ (true, false) => "Portmaster is paused",
+ (false, true) => "SPN is paused",
+ _ => unreachable!(), // We already checked at least one is true
+ };
+ let status_item = MenuItemBuilder::with_id(PAUSE_INFO_KEY, status_text).enabled(false).build(app)?;
+
+ let formatted_time = DateTime::parse_from_rfc3339(&pause_info.till_time)
+ .map(|dt| dt.with_timezone(&chrono::Local).format("%H:%M:%S").to_string())
+ .unwrap_or_else(|_| pause_info.till_time.clone());
+ let time_item = MenuItemBuilder::with_id(PAUSE_INFO_TIME_KEY, format!("Auto-resume at {}", formatted_time)).enabled(false).build(app)?;
+ let resume_item = MenuItemBuilder::with_id(RESUME_KEY, "Resume now").build(app)?;
+ (Some(status_item), Some(time_item), Some(resume_item))
+ } else {
+ (None, None, None)
+ };
+
// Setup SPN button
let spn_button_text = match spn_status_text {
"disabled" => "Enable SPN",
_ => "Disable SPN",
- };
+ };
+ let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text))
+ .enabled(false)
+ .build(app)
+ .unwrap();
let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, spn_button_text)
.build(app)
.unwrap();
@@ -176,10 +204,10 @@ fn build_tray_menu(
// 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 disabled_spn_pause = (spn_status_text == "disabled" && !pause_info.spn) || pause_info.interception;
+ let pause_spn_5min_item = MenuItemBuilder::with_id(PAUSE_SPN_5_KEY, "Pause SPN for 5 minutes").enabled(!disabled_spn_pause).build(app)?;
+ let pause_spn_15min_item = MenuItemBuilder::with_id(PAUSE_SPN_15_KEY, "Pause SPN for 15 minutes").enabled(!disabled_spn_pause).build(app)?;
+ let pause_spn_1hour_item = MenuItemBuilder::with_id(PAUSE_SPN_60_KEY, "Pause SPN for 1 hour").enabled(!disabled_spn_pause).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)?;
@@ -197,8 +225,6 @@ fn build_tray_menu(
])
.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)?;
@@ -206,25 +232,44 @@ fn build_tray_menu(
.items(&[&reload_btn, &force_show_window])
.build()?;
*/
+
+ // Assemble menu items
+ let s = PredefinedMenuItem::separator(app)?;
+ let mut items: Vec<&dyn tauri::menu::IsMenuItem> = Vec::new();
+
+
+ items.push(&global_status);
+ items.push(&s);
+
+ if let Some(ref pause_status_item) = pause_status_item {
+ items.push(pause_status_item);
+ }
+ if let Some(ref pause_status_time_item) = pause_status_time_item {
+ items.push(pause_status_time_item);
+ }
+ if let Some(ref resume_item) = resume_item {
+ items.push(resume_item);
+ }
+ items.push(&pause_menu);
+ items.push(&s);
+
+ items.push(&spn_status);
+ items.push(&spn_button);
+ items.push(&s);
+
+ items.push(&theme_menu);
+ items.push(&s);
+
+ items.push(&open_btn);
+ items.push(&s);
+
+ items.push(&exit_ui_btn);
+ items.push(&shutdown_btn);
+ //items.push(&developer_menu);
+
let menu = MenuBuilder::with_id(app, PM_TRAY_MENU_ID)
- .items(&[
- &open_btn,
- &PredefinedMenuItem::separator(app)?,
- &global_status,
- &PredefinedMenuItem::separator(app)?,
- &pause_menu,
- &resume_item,
- &PredefinedMenuItem::separator(app)?,
- &spn_status,
- &spn_button,
- &PredefinedMenuItem::separator(app)?,
- &theme_menu,
- &PredefinedMenuItem::separator(app)?,
- &exit_ui_btn,
- &shutdown_btn,
- //&developer_menu,
- ])
+ .items(&items)
.build()?;
return Ok(menu);
@@ -233,7 +278,7 @@ fn build_tray_menu(
pub fn setup_tray_menu(
app: &mut tauri::App,
) -> core::result::Result> {
- let menu = build_tray_menu(app.handle(), "Secured", "disabled")?;
+ let menu = build_tray_menu(app.handle(), "Secured", "disabled", &system_status_types::PauseInfo::default())?;
let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID)
.icon(Image::from_bytes(get_red_icon()).unwrap())
@@ -325,15 +370,10 @@ pub fn setup_tray_menu(
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
- }
- }
- None => system_status_types::StateType::Undefined
- };
+ let worst_state_type = system_status.worst_state
+ .as_ref()
+ .and_then(|ws| ws.state.state_type.clone())
+ .unwrap_or(system_status_types::StateType::Undefined);
// Determine status and icon color in a single match expression
let (status, icon_color) = match worst_state_type {
@@ -348,7 +388,15 @@ pub fn update_icon(icon: AppIcon, system_status: SystemStatus, spn_status: Strin
}
};
- if let Ok(menu) = build_tray_menu(icon.app_handle(), status.as_ref(), spn_status.as_str()) {
+ // Extract pause info from system status
+ let pause_info = system_status
+ .get_module_state("Control", "control:paused")
+ .and_then(|state| state.data.as_ref())
+ .and_then(|data| serde_json::from_value::(data.clone()).ok())
+ .unwrap_or_default();
+
+ // Rebuild and set the tray menu
+ if let Ok(menu) = build_tray_menu(icon.app_handle(), status, spn_status.as_str(), &pause_info) {
if let Err(err) = icon.set_menu(Some(menu)) {
error!("failed to set menu on tray icon: {}", err.to_string());
}
@@ -432,11 +480,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
update_icon_color(&icon, IconColor::Blue);
- let mut system_status = SystemStatus {
- modules: Vec::new(),
- worst_state: None,
- };
-
+ let mut system_status = SystemStatus::default();
let mut spn_status: String = "".to_string();
loop {