From a9016c85c6f2516263c05fdad4d99dfa1b36ff17 Mon Sep 17 00:00:00 2001
From: Stefano Novelli
Date: Fri, 22 Aug 2025 12:05:32 +0200
Subject: [PATCH] feat: add 'Hide Icon on Close' setting and related
functionality
---
src-tauri/Cargo.lock | 56 +++++++++++++++++++
src-tauri/Cargo.toml | 2 +
src-tauri/src/lib.rs | 92 +++++++++++++++++++++++++++++++-
src/index.html | 9 ++++
src/managers/settings-manager.js | 65 +++++++++++++++++++++-
src/utils/theme-loader.js | 2 +-
6 files changed, 222 insertions(+), 4 deletions(-)
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 7e905d1..6290e50 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -301,6 +301,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -517,6 +523,36 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "cocoa"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "cocoa-foundation",
+ "core-foundation 0.9.4",
+ "core-graphics 0.23.2",
+ "foreign-types 0.5.0",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "libc",
+ "objc",
+]
+
[[package]]
name = "combine"
version = "4.6.7"
@@ -2185,6 +2221,15 @@ dependencies = [
"time",
]
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "markup5ever"
version = "0.14.1"
@@ -2408,6 +2453,15 @@ dependencies = [
"syn 2.0.104",
]
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -3053,10 +3107,12 @@ version = "0.3.10"
dependencies = [
"base64 0.21.7",
"chrono",
+ "cocoa",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"dotenv",
"libc",
+ "objc",
"serde",
"serde_json",
"tauri",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index d58cba0..bbed9d3 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -51,4 +51,6 @@ base64 = "0.21"
core-graphics = "0.23"
core-foundation = "0.9"
libc = "0.2"
+cocoa = "0.25"
+objc = "0.2"
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 8481916..5ea6b19 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -87,6 +87,8 @@ struct AppSettings {
autostart: bool,
#[serde(default = "default_analytics_enabled")]
analytics_enabled: bool,
+ #[serde(default)]
+ hide_icon_on_close: bool,
}
#[derive(Serialize, Deserialize, Clone)]
@@ -174,6 +176,7 @@ impl Default for AppSettings {
advanced: AdvancedSettings::default(),
autostart: false, // default to disabled
analytics_enabled: true, // default to enabled
+ hide_icon_on_close: false, // default to disabled
}
}
}
@@ -612,6 +615,22 @@ async fn update_tray_icon(
#[tauri::command]
async fn show_window(app: AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
+ // Check if hide_icon_on_close is enabled to restore dock visibility
+ match load_settings(app.clone()).await {
+ Ok(settings) => {
+ if settings.hide_icon_on_close {
+ // Restore dock visibility when showing window
+ #[cfg(target_os = "macos")]
+ {
+ let _ = set_dock_visibility(app.clone(), true).await;
+ }
+ }
+ }
+ Err(_) => {
+ // Ignore error, just proceed with showing window
+ }
+ }
+
window
.show()
.map_err(|e| format!("Failed to show window: {}", e))?;
@@ -929,7 +948,8 @@ pub fn run() {
save_session_tags,
add_session_tag,
write_excel_file,
- start_oauth_server
+ start_oauth_server,
+ set_dock_visibility
])
.setup(|app| {
// Track app started event (if enabled)
@@ -1023,9 +1043,42 @@ pub fn run() {
.build(app)?;
if let Some(window) = app.get_webview_window("main") {
+ let app_handle_for_close = app.handle().clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
+ // Always prevent close
api.prevent_close();
+
+ // Check if we should hide the app icon
+ let app_handle_clone = app_handle_for_close.clone();
+ tauri::async_runtime::spawn(async move {
+ match load_settings(app_handle_clone.clone()).await {
+ Ok(settings) => {
+ if settings.hide_icon_on_close {
+ // Hide the window and set app as dock hidden
+ if let Some(window) = app_handle_clone.get_webview_window("main") {
+ let _ = window.hide();
+ // Use macOS specific API to hide from dock
+ #[cfg(target_os = "macos")]
+ {
+ let _ = set_dock_visibility(app_handle_clone.clone(), false).await;
+ }
+ }
+ } else {
+ // Just hide the window without hiding from dock
+ if let Some(window) = app_handle_clone.get_webview_window("main") {
+ let _ = window.hide();
+ }
+ }
+ }
+ Err(_) => {
+ // Default behavior: just hide the window
+ if let Some(window) = app_handle_clone.get_webview_window("main") {
+ let _ = window.hide();
+ }
+ }
+ }
+ });
}
});
}
@@ -1283,3 +1336,40 @@ async fn start_oauth_server(window: tauri::Window) -> Result {
})
.map_err(|err| err.to_string())
}
+
+#[tauri::command]
+async fn set_dock_visibility(app: AppHandle, visible: bool) -> Result<(), String> {
+ #[cfg(target_os = "macos")]
+ {
+ app.run_on_main_thread(move || {
+ set_dock_visibility_native(visible);
+ })
+ .map_err(|e| format!("Failed to run on main thread: {}", e))?;
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ return Err("Dock visibility is only supported on macOS".to_string());
+ }
+
+ Ok(())
+}
+
+#[cfg(target_os = "macos")]
+fn set_dock_visibility_native(visible: bool) {
+ use cocoa::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy};
+ use cocoa::base::nil;
+
+ unsafe {
+ let app = NSApp();
+ if app != nil {
+ let policy = if visible {
+ NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular
+ } else {
+ NSApplicationActivationPolicy::NSApplicationActivationPolicyAccessory
+ };
+
+ app.setActivationPolicy_(policy);
+ }
+ }
+}
diff --git a/src/index.html b/src/index.html
index 23a8dd2..de838bb 100644
--- a/src/index.html
+++ b/src/index.html
@@ -797,6 +797,15 @@ System Integration
start
minimized to the system tray.
+
+
+
+
Hide the app icon from the dock when closing the window with X. The app will continue running in the system tray.
+
diff --git a/src/managers/settings-manager.js b/src/managers/settings-manager.js
index c2c4444..5098782 100644
--- a/src/managers/settings-manager.js
+++ b/src/managers/settings-manager.js
@@ -83,7 +83,8 @@ export class SettingsManager {
appearance: { ...defaultSettings.appearance, ...loadedSettings.appearance },
advanced: { ...defaultSettings.advanced, ...loadedSettings.advanced },
autostart: loadedSettings.autostart !== undefined ? loadedSettings.autostart : defaultSettings.autostart,
- analytics_enabled: loadedSettings.analytics_enabled !== undefined ? loadedSettings.analytics_enabled : defaultSettings.analytics_enabled
+ analytics_enabled: loadedSettings.analytics_enabled !== undefined ? loadedSettings.analytics_enabled : defaultSettings.analytics_enabled,
+ hide_icon_on_close: loadedSettings.hide_icon_on_close !== undefined ? loadedSettings.hide_icon_on_close : defaultSettings.hide_icon_on_close
};
}
@@ -118,7 +119,8 @@ export class SettingsManager {
debug_mode: false // Debug mode with 3-second timers
},
autostart: false, // default to disabled
- analytics_enabled: true // Analytics enabled by default
+ analytics_enabled: true, // Analytics enabled by default
+ hide_icon_on_close: false // Hide icon on close disabled by default
};
}
@@ -198,6 +200,9 @@ export class SettingsManager {
// Populate analytics setting
this.loadAnalyticsSetting();
+
+ // Populate hide icon on close setting
+ this.loadHideIconOnCloseSetting();
}
setupEventListeners() {
@@ -860,6 +865,62 @@ export class SettingsManager {
}
}
+ async loadHideIconOnCloseSetting() {
+ try {
+ // Get current hide icon on close setting from our stored settings
+ const hideIconOnClose = this.settings.hide_icon_on_close;
+
+ const checkbox = document.getElementById('hide-icon-on-close');
+ if (checkbox) {
+ checkbox.checked = hideIconOnClose;
+
+ // Setup event listener for the hide icon on close checkbox
+ checkbox.addEventListener('change', async (e) => {
+ await this.toggleHideIconOnClose(e.target.checked);
+ });
+ }
+ } catch (error) {
+ console.error('Failed to load hide icon on close setting:', error);
+ // Default to disabled if we can't check the status
+ const checkbox = document.getElementById('hide-icon-on-close');
+ if (checkbox) {
+ checkbox.checked = false;
+ checkbox.addEventListener('change', async (e) => {
+ await this.toggleHideIconOnClose(e.target.checked);
+ });
+ }
+ }
+ }
+
+ async toggleHideIconOnClose(enabled) {
+ try {
+ // Update our settings
+ this.settings.hide_icon_on_close = enabled;
+
+ // Show user feedback
+ if (enabled) {
+ console.log('Hide icon on close enabled');
+ NotificationUtils.showNotificationPing('✓ Hide icon on close enabled - App will hide from dock when closed', 'success');
+ } else {
+ console.log('Hide icon on close disabled');
+ NotificationUtils.showNotificationPing('✓ Hide icon on close disabled - App will remain visible in dock', 'success');
+ }
+
+ // Schedule auto-save to persist the setting
+ this.scheduleAutoSave();
+
+ } catch (error) {
+ console.error('Failed to toggle hide icon on close:', error);
+ NotificationUtils.showNotificationPing('❌ Failed to toggle hide icon on close: ' + error, 'error');
+
+ // Revert the checkbox state on error
+ const checkbox = document.getElementById('hide-icon-on-close');
+ if (checkbox) {
+ checkbox.checked = !enabled;
+ }
+ }
+ }
+
// Theme management functions
async applyTheme(theme) {
const html = document.documentElement;
diff --git a/src/utils/theme-loader.js b/src/utils/theme-loader.js
index 14e3dfa..41d1a3f 100644
--- a/src/utils/theme-loader.js
+++ b/src/utils/theme-loader.js
@@ -39,7 +39,7 @@ class ThemeLoader {
// that gets updated by the build process or manually maintained
// This could be enhanced to use a build-time script that generates this list
- const knownThemes = [
+ const knownThemes = [
'espresso.css',
'pipboy.css',
'pommodore64.css'