From 9202bd4d099d7f6fa93b3aa723d82d36d63195e9 Mon Sep 17 00:00:00 2001 From: ruffsl Date: Sun, 11 Jan 2026 01:08:37 -0600 Subject: [PATCH 1/6] wip: stage implement graceful shutdown for system tray and enhance operation management --- src/tray/app.rs | 66 +++++++++++++++++++++++++++++++++++++++---------- src/tray/mod.rs | 47 +++++++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 20 deletions(-) diff --git a/src/tray/app.rs b/src/tray/app.rs index 8690913..04525e4 100644 --- a/src/tray/app.rs +++ b/src/tray/app.rs @@ -9,6 +9,7 @@ use log::{error, info}; use notify_rust::Notification; use parking_lot::Mutex; use std::error::Error; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, mpsc}; use std::thread; @@ -137,6 +138,9 @@ pub struct CtrlAssistTray { state: Arc>, /// Store shutdown sender for signaling shutdown_tx: Option>, + pub tray_handle: Option>, + /// Shutdown signal for main loop + pub shutdown_signal: Option>, } impl CtrlAssistTray { @@ -149,9 +153,27 @@ impl CtrlAssistTray { Ok(Self { state: Arc::new(Mutex::new(state)), shutdown_tx: None, + tray_handle: None, + shutdown_signal: None, }) } + fn shutdown_tray(&mut self) { + // Stop any running operation first + self.stop_operation(); + + // Signal the main loop to exit + if let Some(signal) = &self.shutdown_signal { + signal.store(true, std::sync::atomic::Ordering::SeqCst); + } + + // Shutdown the tray service itself + if let Some(handle) = &self.tray_handle { + // Request shutdown - this will break the event loop + let _ = handle.shutdown(); + } + } + fn send_notification(summary: &str, body: &str) { let summary = summary.to_string(); let body = body.to_string(); @@ -307,7 +329,7 @@ impl CtrlAssistTray { } } - fn stop_operation(&mut self) { + pub fn stop_operation(&mut self) { let mut state = self.state.lock(); if state.status == OperationStatus::Stopped { @@ -321,26 +343,31 @@ impl CtrlAssistTray { let _ = tx.send(()); } + // Clear virtual paths before joining threads state.virtual_device_paths.clear(); + // Join the operation thread BEFORE clearing the handle if let Some(handle) = state.operation_handle.take() { - // Drop lock to avoid deadlock during join if thread tries to lock state while stopping + // CRITICAL: Drop lock before joining to avoid deadlock drop(state); - let _ = handle.join(); + + match handle.join() { + Ok(_) => info!("Operation thread joined successfully"), + Err(e) => error!("Failed to join operation thread: {:?}", e), + } + + // Re-acquire lock after join state = self.state.lock(); } state.status = OperationStatus::Stopped; state.shutdown_signal = None; - - // Clear runtime settings state.mux.runtime_settings = None; state.demux.runtime_settings = None; - info!("Operation stopped"); + info!("Operation stopped completely"); Self::send_notification("CtrlAssist", "Operation stopped"); } - fn refresh_controllers(&self) { let mut state = self.state.lock(); if let Ok(gilrs) = crate::utils::gilrs::new_gilrs() { @@ -368,6 +395,17 @@ impl CtrlAssistTray { } } +impl Drop for CtrlAssistTray { + fn drop(&mut self) { + // Ensure operation is stopped on drop + // This will invoke ScopedDeviceHider::drop + if self.state.lock().status == OperationStatus::Running { + eprintln!("Warning: CtrlAssistTray dropped while operation running, forcing stop"); + self.stop_operation(); + } + } +} + impl Tray for CtrlAssistTray { const MENU_ON_ACTIVATE: bool = true; @@ -640,12 +678,14 @@ impl Tray for CtrlAssistTray { }, action: |t| t.start_operation()), simple_item!(label: "Stop", icon: "media-playback-stop", enabled: is_running, action: |t| t.stop_operation()), MenuItem::Separator, - simple_item!(label: "Exit", icon: "application-exit", enabled: true, action: |t| { - t.stop_operation(); - // Graceful termination is preferred, but for tray apps exit(0) is often required - // to break out of the native event loop immediately. - std::process::exit(0); - }), + simple_item!( + label: "Exit", + icon: "application-exit", + enabled: true, + action: |t| { + t.shutdown_tray(); + } + ), ]); items } diff --git a/src/tray/mod.rs b/src/tray/mod.rs index b655971..af2b05d 100644 --- a/src/tray/mod.rs +++ b/src/tray/mod.rs @@ -9,32 +9,65 @@ pub use app::CtrlAssistTray; use ashpd::is_sandboxed; use ksni::TrayMethods; use std::error::Error; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; pub async fn run_tray() -> Result<(), Box> { let tray = CtrlAssistTray::new()?; - // Use ashpd for robust sandbox detection let is_sandboxed = is_sandboxed().await; - let handle_result = if is_sandboxed { + let handle = if is_sandboxed { tray.disable_dbus_name(true) .spawn() .map_err(|e| format!("Failed to spawn tray (sandbox workaround): {}", e)) - .await + .await? } else { tray.spawn() .map_err(|e| format!("Failed to spawn tray: {}", e)) - .await + .await? }; - handle_result?; + // Set up Ctrl+C handler + let shutdown_signal = Arc::new(AtomicBool::new(false)); + let shutdown_signal_ctrlc = Arc::clone(&shutdown_signal); + + ctrlc::set_handler(move || { + println!("\nShutting down gracefully..."); + shutdown_signal_ctrlc.store(true, Ordering::SeqCst); + })?; + + // Store handle and shutdown signal in tray + let handle_clone = handle.clone(); + let shutdown_signal_clone = Arc::clone(&shutdown_signal); + handle + .update(|tray: &mut CtrlAssistTray| { + tray.tray_handle = Some(handle_clone); + tray.shutdown_signal = Some(shutdown_signal_clone); + }) + .await; println!("CtrlAssist system tray started"); println!("Configure and control the mux from your system tray"); println!("Press Ctrl+C to exit"); - // Run forever - std::thread::park(); + // Wait for shutdown signal (from either Ctrl+C or Exit button) + while !shutdown_signal.load(Ordering::SeqCst) { + tokio::time::sleep(Duration::from_millis(100)).await; + } + + // Perform cleanup + println!("Stopping operations..."); + handle + .update(|tray: &mut CtrlAssistTray| { + tray.stop_operation(); + }) + .await; + + println!("Shutting down tray..."); + handle.shutdown().await; + println!("Cleanup complete, exiting."); Ok(()) } From d5e1d088b0087fdabe7fe7c1fcbef400aece4bbb Mon Sep 17 00:00:00 2001 From: ruffsl Date: Sun, 11 Jan 2026 01:10:47 -0600 Subject: [PATCH 2/6] refactor: update shutdown handling in CtrlAssistTray for improved async operation --- src/tray/app.rs | 25 ++++++++++++------------- src/tray/mod.rs | 32 +++++++++++++++++++------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/tray/app.rs b/src/tray/app.rs index 04525e4..5cc29f8 100644 --- a/src/tray/app.rs +++ b/src/tray/app.rs @@ -9,7 +9,6 @@ use log::{error, info}; use notify_rust::Notification; use parking_lot::Mutex; use std::error::Error; -use std::sync::atomic::AtomicBool; use std::sync::{Arc, mpsc}; use std::thread; @@ -136,11 +135,11 @@ macro_rules! enum_menu { pub struct CtrlAssistTray { state: Arc>, - /// Store shutdown sender for signaling - shutdown_tx: Option>, + /// Store shutdown sender for tray main loop (async channel) + pub shutdown_tx: Option>, pub tray_handle: Option>, - /// Shutdown signal for main loop - pub shutdown_signal: Option>, + /// Store shutdown sender for operation threads (mpsc) + op_shutdown_tx: Option>, } impl CtrlAssistTray { @@ -154,7 +153,7 @@ impl CtrlAssistTray { state: Arc::new(Mutex::new(state)), shutdown_tx: None, tray_handle: None, - shutdown_signal: None, + op_shutdown_tx: None, }) } @@ -162,15 +161,15 @@ impl CtrlAssistTray { // Stop any running operation first self.stop_operation(); - // Signal the main loop to exit - if let Some(signal) = &self.shutdown_signal { - signal.store(true, std::sync::atomic::Ordering::SeqCst); + // Signal the main loop to exit via async channel + if let Some(shutdown) = self.shutdown_tx.take() { + shutdown(); } // Shutdown the tray service itself if let Some(handle) = &self.tray_handle { // Request shutdown - this will break the event loop - let _ = handle.shutdown(); + std::mem::drop(handle.shutdown()); } } @@ -252,7 +251,7 @@ impl CtrlAssistTray { // Use a channel for shutdown signaling let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); - self.shutdown_tx = Some(shutdown_tx); + self.op_shutdown_tx = Some(shutdown_tx); let state_arc = Arc::clone(&self.state); let handle = thread::spawn(move || match start_mux_with_state(config, state_arc) { @@ -305,7 +304,7 @@ impl CtrlAssistTray { // Use a channel for shutdown signaling let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); - self.shutdown_tx = Some(shutdown_tx); + self.op_shutdown_tx = Some(shutdown_tx); let state_arc = Arc::clone(&self.state); let handle = thread::spawn(move || match start_demux_with_state(config, state_arc) { @@ -339,7 +338,7 @@ impl CtrlAssistTray { info!("Stopping operation"); // Signal shutdown via channel - if let Some(tx) = self.shutdown_tx.take() { + if let Some(tx) = self.op_shutdown_tx.take() { let _ = tx.send(()); } diff --git a/src/tray/mod.rs b/src/tray/mod.rs index af2b05d..1a4e24b 100644 --- a/src/tray/mod.rs +++ b/src/tray/mod.rs @@ -9,9 +9,8 @@ pub use app::CtrlAssistTray; use ashpd::is_sandboxed; use ksni::TrayMethods; use std::error::Error; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; +use std::sync::{Arc, Mutex}; +use tokio::sync::oneshot; pub async fn run_tray() -> Result<(), Box> { let tray = CtrlAssistTray::new()?; @@ -29,22 +28,31 @@ pub async fn run_tray() -> Result<(), Box> { .await? }; - // Set up Ctrl+C handler - let shutdown_signal = Arc::new(AtomicBool::new(false)); - let shutdown_signal_ctrlc = Arc::clone(&shutdown_signal); + // Set up async shutdown channel (shared for Ctrl+C and tray Exit) + let (tx, shutdown_rx) = oneshot::channel::<()>(); + let shutdown_tx: Arc>>> = + Arc::new(Mutex::new(Some(tx))); + // Set up Ctrl+C handler + let shutdown_tx_ctrlc = Arc::clone(&shutdown_tx); ctrlc::set_handler(move || { println!("\nShutting down gracefully..."); - shutdown_signal_ctrlc.store(true, Ordering::SeqCst); + if let Some(tx) = shutdown_tx_ctrlc.lock().unwrap().take() { + let _ = tx.send(()); + } })?; - // Store handle and shutdown signal in tray + // Store handle and shutdown sender in tray let handle_clone = handle.clone(); - let shutdown_signal_clone = Arc::clone(&shutdown_signal); + let shutdown_tx_tray = Arc::clone(&shutdown_tx); handle .update(|tray: &mut CtrlAssistTray| { tray.tray_handle = Some(handle_clone); - tray.shutdown_signal = Some(shutdown_signal_clone); + tray.shutdown_tx = Some(Box::new(move || { + if let Some(tx) = shutdown_tx_tray.lock().unwrap().take() { + let _ = tx.send(()); + } + })); }) .await; @@ -53,9 +61,7 @@ pub async fn run_tray() -> Result<(), Box> { println!("Press Ctrl+C to exit"); // Wait for shutdown signal (from either Ctrl+C or Exit button) - while !shutdown_signal.load(Ordering::SeqCst) { - tokio::time::sleep(Duration::from_millis(100)).await; - } + let _ = shutdown_rx.await; // Perform cleanup println!("Stopping operations..."); From 627b3b2f85b8cec52783d7383a98e29d55bdb1ba Mon Sep 17 00:00:00 2001 From: ruffsl Date: Sun, 11 Jan 2026 13:19:36 -0600 Subject: [PATCH 3/6] fix: idiomatic async shutdown with tokio::watch, no polling Refactor tray shutdown to use a single tokio::sync::watch channel for unified async signaling between the tray and Ctrl+C handler. Remove all polling, atomic flags, and boxed closures for shutdown. Tray and CLI shutdown now both trigger the same event-driven cleanup path. Retain mpsc for operation thread signaling, keeping thread code simple and idiomatic. No sleeping or spin loops; all shutdown is event-driven and efficient. Improves maintainability, clarity, and Rust idiomaticity for hybrid async/threaded code. --- Cargo.toml | 2 +- src/tray/app.rs | 50 ++++++++++++++++++++++------------------------ src/tray/mod.rs | 53 +++++++++++++++++++++---------------------------- 3 files changed, 48 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2db11eb..f806491 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ libc = "0.2.180" log = "0.4.29" udev = "0.9.3" uuid = "1.19.0" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] } # New dependencies for tray ashpd = "0.12.0" diff --git a/src/tray/app.rs b/src/tray/app.rs index 5cc29f8..dadaad3 100644 --- a/src/tray/app.rs +++ b/src/tray/app.rs @@ -11,6 +11,7 @@ use parking_lot::Mutex; use std::error::Error; use std::sync::{Arc, mpsc}; use std::thread; +use tokio::sync::watch; use super::config::TrayConfig; use super::state::{OperationMode, OperationStatus, TrayState}; @@ -135,42 +136,38 @@ macro_rules! enum_menu { pub struct CtrlAssistTray { state: Arc>, - /// Store shutdown sender for tray main loop (async channel) - pub shutdown_tx: Option>, - pub tray_handle: Option>, - /// Store shutdown sender for operation threads (mpsc) + /// Broadcast channel to signal shutdown to all listeners + shutdown_tx: watch::Sender, + /// Synchronous channel for signaling operation threads op_shutdown_tx: Option>, } impl CtrlAssistTray { - pub fn new() -> Result> { + pub fn new() -> Result<(Self, watch::Receiver), Box> { let gilrs = crate::utils::gilrs::new_gilrs().map_err(|e| format!("Failed to init Gilrs: {}", e))?; let config = TrayConfig::load(); let state = TrayState::new(&gilrs, config); - Ok(Self { - state: Arc::new(Mutex::new(state)), - shutdown_tx: None, - tray_handle: None, - op_shutdown_tx: None, - }) + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + Ok(( + Self { + state: Arc::new(Mutex::new(state)), + shutdown_tx, + op_shutdown_tx: None, + }, + shutdown_rx, + )) } - fn shutdown_tray(&mut self) { + /// Signal shutdown to all listeners + pub fn shutdown(&self) { // Stop any running operation first self.stop_operation(); - // Signal the main loop to exit via async channel - if let Some(shutdown) = self.shutdown_tx.take() { - shutdown(); - } - - // Shutdown the tray service itself - if let Some(handle) = &self.tray_handle { - // Request shutdown - this will break the event loop - std::mem::drop(handle.shutdown()); - } + // Signal shutdown + let _ = self.shutdown_tx.send(true); } fn send_notification(summary: &str, body: &str) { @@ -328,7 +325,7 @@ impl CtrlAssistTray { } } - pub fn stop_operation(&mut self) { + pub fn stop_operation(&self) { let mut state = self.state.lock(); if state.status == OperationStatus::Stopped { @@ -338,7 +335,7 @@ impl CtrlAssistTray { info!("Stopping operation"); // Signal shutdown via channel - if let Some(tx) = self.op_shutdown_tx.take() { + if let Some(tx) = self.op_shutdown_tx.as_ref() { let _ = tx.send(()); } @@ -367,6 +364,7 @@ impl CtrlAssistTray { info!("Operation stopped completely"); Self::send_notification("CtrlAssist", "Operation stopped"); } + fn refresh_controllers(&self) { let mut state = self.state.lock(); if let Ok(gilrs) = crate::utils::gilrs::new_gilrs() { @@ -399,7 +397,7 @@ impl Drop for CtrlAssistTray { // Ensure operation is stopped on drop // This will invoke ScopedDeviceHider::drop if self.state.lock().status == OperationStatus::Running { - eprintln!("Warning: CtrlAssistTray dropped while operation running, forcing stop"); + error!("CtrlAssistTray dropped while operation running, forcing stop"); self.stop_operation(); } } @@ -682,7 +680,7 @@ impl Tray for CtrlAssistTray { icon: "application-exit", enabled: true, action: |t| { - t.shutdown_tray(); + t.shutdown(); } ), ]); diff --git a/src/tray/mod.rs b/src/tray/mod.rs index 1a4e24b..3d4dd8f 100644 --- a/src/tray/mod.rs +++ b/src/tray/mod.rs @@ -9,11 +9,10 @@ pub use app::CtrlAssistTray; use ashpd::is_sandboxed; use ksni::TrayMethods; use std::error::Error; -use std::sync::{Arc, Mutex}; -use tokio::sync::oneshot; +use tokio::sync::watch; pub async fn run_tray() -> Result<(), Box> { - let tray = CtrlAssistTray::new()?; + let (tray, mut shutdown_rx) = CtrlAssistTray::new()?; let is_sandboxed = is_sandboxed().await; @@ -28,40 +27,34 @@ pub async fn run_tray() -> Result<(), Box> { .await? }; - // Set up async shutdown channel (shared for Ctrl+C and tray Exit) - let (tx, shutdown_rx) = oneshot::channel::<()>(); - let shutdown_tx: Arc>>> = - Arc::new(Mutex::new(Some(tx))); + // Create a separate shutdown channel for Ctrl+C + let (ctrlc_tx, mut ctrlc_rx) = watch::channel(false); - // Set up Ctrl+C handler - let shutdown_tx_ctrlc = Arc::clone(&shutdown_tx); - ctrlc::set_handler(move || { + tokio::spawn(async move { + // Wait for Ctrl+C signal + tokio::signal::ctrl_c() + .await + .expect("Failed to listen for Ctrl+C"); println!("\nShutting down gracefully..."); - if let Some(tx) = shutdown_tx_ctrlc.lock().unwrap().take() { - let _ = tx.send(()); - } - })?; - - // Store handle and shutdown sender in tray - let handle_clone = handle.clone(); - let shutdown_tx_tray = Arc::clone(&shutdown_tx); - handle - .update(|tray: &mut CtrlAssistTray| { - tray.tray_handle = Some(handle_clone); - tray.shutdown_tx = Some(Box::new(move || { - if let Some(tx) = shutdown_tx_tray.lock().unwrap().take() { - let _ = tx.send(()); - } - })); - }) - .await; + let _ = ctrlc_tx.send(true); + }); println!("CtrlAssist system tray started"); println!("Configure and control the mux from your system tray"); println!("Press Ctrl+C to exit"); - // Wait for shutdown signal (from either Ctrl+C or Exit button) - let _ = shutdown_rx.await; + // Wait for either shutdown signal or Ctrl+C + tokio::select! { + _ = shutdown_rx.changed() => { + // Exit button was clicked + } + _ = ctrlc_rx.changed() => { + // Ctrl+C was pressed, trigger shutdown through the tray + handle.update(|tray: &mut CtrlAssistTray| { + tray.shutdown(); + }).await; + } + } // Perform cleanup println!("Stopping operations..."); From eee0bd306996c42e3c88355b6fb99754be100bd4 Mon Sep 17 00:00:00 2001 From: Ruffin Date: Sun, 11 Jan 2026 13:41:11 -0600 Subject: [PATCH 4/6] Update src/tray/mod.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tray/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/tray/mod.rs b/src/tray/mod.rs index 3d4dd8f..2d47351 100644 --- a/src/tray/mod.rs +++ b/src/tray/mod.rs @@ -32,11 +32,15 @@ pub async fn run_tray() -> Result<(), Box> { tokio::spawn(async move { // Wait for Ctrl+C signal - tokio::signal::ctrl_c() - .await - .expect("Failed to listen for Ctrl+C"); - println!("\nShutting down gracefully..."); - let _ = ctrlc_tx.send(true); + match tokio::signal::ctrl_c().await { + Ok(()) => { + println!("\nShutting down gracefully..."); + let _ = ctrlc_tx.send(true); + } + Err(e) => { + eprintln!("Failed to listen for Ctrl+C: {}", e); + } + } }); println!("CtrlAssist system tray started"); From 8963a76938754289bc3728ed69e7ca3a19a69999 Mon Sep 17 00:00:00 2001 From: ruffsl Date: Sun, 11 Jan 2026 14:08:54 -0600 Subject: [PATCH 5/6] refactor: inline shutdown and cleanup logic in tokio::select! - Move shutdown and cleanup actions directly into the `tokio::select!` arms for clarity and simplicity. - Each shutdown path (Exit button or Ctrl+C) is now handled in-place, making the code easier to read and maintain. - No redundant cleanup; shutdown logic is linear and idiomatic. --- src/tray/mod.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/tray/mod.rs b/src/tray/mod.rs index 2d47351..7e5517b 100644 --- a/src/tray/mod.rs +++ b/src/tray/mod.rs @@ -34,7 +34,6 @@ pub async fn run_tray() -> Result<(), Box> { // Wait for Ctrl+C signal match tokio::signal::ctrl_c().await { Ok(()) => { - println!("\nShutting down gracefully..."); let _ = ctrlc_tx.send(true); } Err(e) => { @@ -51,24 +50,19 @@ pub async fn run_tray() -> Result<(), Box> { tokio::select! { _ = shutdown_rx.changed() => { // Exit button was clicked + handle.update(|tray: &mut CtrlAssistTray| { + tray.stop_operation(); + }).await; } _ = ctrlc_rx.changed() => { - // Ctrl+C was pressed, trigger shutdown through the tray + // Ctrl+C was pressed handle.update(|tray: &mut CtrlAssistTray| { tray.shutdown(); }).await; } } - // Perform cleanup - println!("Stopping operations..."); - handle - .update(|tray: &mut CtrlAssistTray| { - tray.stop_operation(); - }) - .await; - - println!("Shutting down tray..."); + println!("\nShutting down tray..."); handle.shutdown().await; println!("Cleanup complete, exiting."); From 2a14652a839860f2e4dbdba7eab5f14895971b7d Mon Sep 17 00:00:00 2001 From: ruffsl Date: Sun, 11 Jan 2026 14:26:42 -0600 Subject: [PATCH 6/6] refactor: simplify Ctrl+C signal handling in run_tray function --- src/tray/mod.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/tray/mod.rs b/src/tray/mod.rs index 7e5517b..f72b1fd 100644 --- a/src/tray/mod.rs +++ b/src/tray/mod.rs @@ -32,13 +32,8 @@ pub async fn run_tray() -> Result<(), Box> { tokio::spawn(async move { // Wait for Ctrl+C signal - match tokio::signal::ctrl_c().await { - Ok(()) => { - let _ = ctrlc_tx.send(true); - } - Err(e) => { - eprintln!("Failed to listen for Ctrl+C: {}", e); - } + if tokio::signal::ctrl_c().await.is_ok() { + let _ = ctrlc_tx.send(true); } }); @@ -49,13 +44,10 @@ pub async fn run_tray() -> Result<(), Box> { // Wait for either shutdown signal or Ctrl+C tokio::select! { _ = shutdown_rx.changed() => { - // Exit button was clicked - handle.update(|tray: &mut CtrlAssistTray| { - tray.stop_operation(); - }).await; + // Exit button clicked, tray handled shutdown } _ = ctrlc_rx.changed() => { - // Ctrl+C was pressed + // Ctrl+C pressed, handle shutdown here handle.update(|tray: &mut CtrlAssistTray| { tray.shutdown(); }).await;