Skip to content

Commit 7808a67

Browse files
authored
Merge pull request #3 from MishaKav/feature/disconnect-all-menu-item
Add Disconnect All menu item
2 parents a9e5b6d + b76dea5 commit 7808a67

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["core", "app-macos", "app-linux", "app-windows"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "1.6.0"
6+
version = "1.7.0"
77
edition = "2024"
88

99
[profile.release]

RELEASE_NOTES.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Release Notes - Something in the Background
22

3+
## v1.7.0
4+
5+
**Release Date:** December 17, 2025
6+
7+
### 🔌 Disconnect All Menu Item
8+
9+
- Added "Disconnect All" menu item to disconnect all active tunnels at once
10+
- Item appears between "Open Config Folder" and "About"
11+
- Grayed out when no tunnels are connected, enabled when at least one tunnel is active
12+
313
## v1.6.0
414

515
**Release Date:** December 3, 2025

app-macos/src/menu.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ fn load_config() -> Config {
2929
const ICON_INACTIVE: &str = "○"; // Empty circle for idle
3030
const 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
3336
define_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) {
269337
pub 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

Comments
 (0)