@@ -29,6 +29,9 @@ fn load_config() -> Config {
2929const ICON_INACTIVE : & str = "○" ; // Empty circle for idle
3030const ICON_ACTIVE : & str = "●" ; // Filled circle for active
3131
32+ // Tag to identify the "Disconnect All" menu item
33+ const DISCONNECT_ALL_TAG : isize = 9999 ;
34+
3235// Declare the MenuHandler class using objc2's define_class! macro
3336define_class ! (
3437 // SAFETY:
@@ -83,6 +86,11 @@ define_class!(
8386 fn exit_application( & self , _item: & NSMenuItem ) {
8487 exit_application_handler( ) ;
8588 }
89+
90+ #[ unsafe ( method( disconnectAll: ) ) ]
91+ fn disconnect_all( & self , _item: & NSMenuItem ) {
92+ disconnect_all_handler( ) ;
93+ }
8694 }
8795) ;
8896
@@ -205,6 +213,66 @@ fn toggle_tunnel_handler(item: &NSMenuItem) {
205213 if let Some ( status_item) = app. get_status_item ( ) {
206214 if let Some ( mtm) = objc2_foundation:: MainThreadMarker :: new ( ) {
207215 update_status_item_title ( & status_item, any_active, mtm) ;
216+
217+ // Enable/disable "Disconnect All" menu item based on active state
218+ if let Some ( menu) = status_item. menu ( mtm) {
219+ let num_items = menu. numberOfItems ( ) ;
220+ for i in 0 ..num_items {
221+ if let Some ( menu_item) = menu. itemAtIndex ( i) {
222+ if menu_item. tag ( ) == DISCONNECT_ALL_TAG {
223+ menu_item. setEnabled ( any_active) ;
224+ break ;
225+ }
226+ }
227+ }
228+ }
229+ }
230+ }
231+ }
232+ }
233+ }
234+
235+ /// Handler to disconnect all active tunnels
236+ fn disconnect_all_handler ( ) {
237+ use log:: info;
238+
239+ if let Some ( app) = GLOBAL_APP . get ( ) {
240+ // Get all active tunnel keys
241+ let active_keys: Vec < String > = {
242+ let tunnels = app. tunnel_manager . active_tunnels . lock ( ) . unwrap ( ) ;
243+ tunnels. iter ( ) . cloned ( ) . collect ( )
244+ } ;
245+
246+ if active_keys. is_empty ( ) {
247+ return ;
248+ }
249+
250+ info ! ( "Disconnecting {} tunnel(s)" , active_keys. len( ) ) ;
251+
252+ // Disconnect each tunnel
253+ for key in & active_keys {
254+ app. tunnel_manager . toggle ( key, false ) ;
255+ }
256+
257+ // Update UI
258+ if let Some ( status_item) = app. get_status_item ( ) {
259+ if let Some ( mtm) = objc2_foundation:: MainThreadMarker :: new ( ) {
260+ update_status_item_title ( & status_item, false , mtm) ;
261+
262+ if let Some ( menu) = status_item. menu ( mtm) {
263+ let num_items = menu. numberOfItems ( ) ;
264+ for i in 0 ..num_items {
265+ if let Some ( item) = menu. itemAtIndex ( i) {
266+ // Uncheck all tunnel items (have represented object, no submenu)
267+ if item. representedObject ( ) . is_some ( ) && item. submenu ( ) . is_none ( ) {
268+ item. setState ( 0 ) ;
269+ }
270+ // Disable "Disconnect All" item
271+ if item. tag ( ) == DISCONNECT_ALL_TAG {
272+ item. setEnabled ( false ) ;
273+ }
274+ }
275+ }
208276 }
209277 }
210278 }
@@ -269,6 +337,9 @@ fn update_scheduled_task_items(menu: &NSMenu) {
269337pub fn create_menu ( handler : & MenuHandler , mtm : MainThreadMarker ) -> Retained < NSMenu > {
270338 let menu = NSMenu :: new ( mtm) ;
271339
340+ // Disable auto-enable so we can manually control item enabled state
341+ menu. setAutoenablesItems ( false ) ;
342+
272343 // Set the delegate so menuNeedsUpdate gets called
273344 let delegate = ProtocolObject :: from_ref ( handler) ;
274345 menu. setDelegate ( Some ( delegate) ) ;
@@ -333,6 +404,18 @@ pub fn create_menu(handler: &MenuHandler, mtm: MainThreadMarker) -> Retained<NSM
333404 set_menu_item_target ( & config_folder_item, handler as & AnyObject ) ;
334405 menu. addItem ( & config_folder_item) ;
335406
407+ // Add "Disconnect All" item
408+ let disconnect_all_item = create_menu_item_with_action (
409+ ns_string ! ( "Disconnect All" ) ,
410+ Some ( sel ! ( disconnectAll: ) ) ,
411+ ns_string ! ( "" ) ,
412+ mtm,
413+ ) ;
414+ set_menu_item_target ( & disconnect_all_item, handler as & AnyObject ) ;
415+ disconnect_all_item. setTag ( DISCONNECT_ALL_TAG ) ;
416+ disconnect_all_item. setEnabled ( false ) ; // Disabled initially (no active tunnels)
417+ menu. addItem ( & disconnect_all_item) ;
418+
336419 // Add About item (clickable, opens About window)
337420 let about_item = create_menu_item_with_action (
338421 ns_string ! ( "About" ) ,
0 commit comments