diff --git a/app/src/root_view.rs b/app/src/root_view.rs index 7cc47ce52..1602a1ab1 100644 --- a/app/src/root_view.rs +++ b/app/src/root_view.rs @@ -760,6 +760,7 @@ fn open_from_restored(arg: &OpenFromRestoredArg, ctx: &mut AppContext) { if let Some(app_state) = &arg.app_state { maybe_register_global_window_shortcuts(global_resource_handles.clone(), ctx); + KeysSettings::as_ref(ctx).apply_effective_dock_icon_visibility(ctx); let (background_blur_radius_pixels, background_blur_texture) = { let window_settings = WindowSettings::as_ref(ctx); diff --git a/app/src/settings/mod.rs b/app/src/settings/mod.rs index 7c99c7998..113484f58 100644 --- a/app/src/settings/mod.rs +++ b/app/src/settings/mod.rs @@ -340,6 +340,10 @@ pub struct QuakeModeSettings { /// user focuses on another warp window or another app. #[schemars(description = "Whether to hide the hotkey window when it loses focus.")] pub hide_window_when_unfocused: bool, + #[schemars( + description = "macOS only. Whether Warp should hide its Dock icon while the dedicated hotkey window is the active global hotkey mode and a keybinding is configured. Has no effect on Linux or Windows." + )] + pub hide_dock_icon: bool, } impl Default for QuakeModeSettings { @@ -351,6 +355,7 @@ impl Default for QuakeModeSettings { pin_screen: Default::default(), // Defaults to `true` only when it's supported on this platform. hide_window_when_unfocused: QUAKE_WINDOW_AUTOHIDE_SUPPORTED, + hide_dock_icon: false, } } } diff --git a/app/src/settings_view/features_page.rs b/app/src/settings_view/features_page.rs index ffeab1ea9..9fd2072c9 100644 --- a/app/src/settings_view/features_page.rs +++ b/app/src/settings_view/features_page.rs @@ -598,6 +598,7 @@ pub enum FeaturesPageAction { QuakeEditorSetHeightPercentage, QuakeEditorResetWidthHeight, QuakeEditorTogglePinWindow, + QuakeEditorToggleHideDockIcon, OpenUrl(String), SetExtraMetaKeys(ExtraMetaKeys), ToggleLeftMetaKey, @@ -913,6 +914,10 @@ impl FeaturesPageAction { .hide_window_when_unfocused, ), }, + Self::QuakeEditorToggleHideDockIcon => TelemetryEvent::FeaturesPageAction { + action: "QuakeEditorToggleHideDockIcon".to_string(), + value: to_string(KeysSettings::as_ref(ctx).quake_mode_settings.hide_dock_icon), + }, Self::ToggleLongRunningNotifications => TelemetryEvent::FeaturesPageAction { action: "ToggleLongRunningNotifications".to_string(), value: to_string( @@ -1168,6 +1173,8 @@ struct MouseStateHandles { quake_mode_cancel: MouseStateHandle, quake_mode_width_height_reset: MouseStateHandle, quake_mode_pin_window_check: MouseStateHandle, + #[cfg(target_os = "macos")] + quake_mode_hide_dock_icon_check: MouseStateHandle, long_running_notifications_checkbox: MouseStateHandle, agent_task_completed_notifications_checkbox: MouseStateHandle, agent_needs_attention_notifications_checkbox: MouseStateHandle, @@ -1457,6 +1464,12 @@ impl TypedActionView for FeaturesPageView { ) }); } + QuakeEditorToggleHideDockIcon => { + KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { + keys_settings + .toggle_hide_dock_icon_when_using_quake_mode_and_write_to_user_defaults(ctx) + }); + } SetExtraMetaKeys(extra_meta_keys) => { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { report_if_error!(keys_settings @@ -3603,6 +3616,47 @@ impl FeaturesPageView { .finish() } + #[cfg(target_os = "macos")] + fn render_quake_mode_hide_dock_icon_row( + &self, + quake_mode_settings: &QuakeModeSettings, + appearance: &Appearance, + ) -> Box { + Container::new( + Flex::row() + .with_child( + appearance + .ui_builder() + .checkbox( + self.button_mouse_states + .quake_mode_hide_dock_icon_check + .clone(), + None, + ) + .check(quake_mode_settings.hide_dock_icon) + .build() + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action( + FeaturesPageAction::QuakeEditorToggleHideDockIcon, + ) + }) + .finish(), + ) + .with_child( + appearance + .ui_builder() + .span("Hide Warp from the Dock while a dedicated hotkey is configured (also removes Warp from Cmd-Tab)") + .build() + .with_margin_left(5.) + .finish(), + ) + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .finish(), + ) + .with_margin_bottom(2.) + .finish() + } + fn render_quake_mode_position_row( &self, quake_mode_settings: &QuakeModeSettings, @@ -5447,6 +5501,14 @@ impl SettingsWidget for GlobalHotkeyWidget { } else { Empty::new().finish() }, + // Hiding the Dock icon depends on macOS NSApplicationActivationPolicy. + #[cfg(target_os = "macos")] + view.render_quake_mode_hide_dock_icon_row( + KeysSettings::as_ref(app).quake_mode_settings.value(), + appearance, + ), + #[cfg(not(target_os = "macos"))] + Empty::new().finish(), ], appearance, )); diff --git a/app/src/terminal/keys_settings.rs b/app/src/terminal/keys_settings.rs index 301596ad2..ecb524901 100644 --- a/app/src/terminal/keys_settings.rs +++ b/app/src/terminal/keys_settings.rs @@ -88,6 +88,8 @@ impl KeysSettings { report_if_error!(self .activation_hotkey_enabled .set_value(enable_activation_hotkey, ctx)); + + self.apply_effective_dock_icon_visibility(ctx); } // Note that registering an empty keybinding when enabling quake mode will be a no-op. @@ -101,6 +103,8 @@ impl KeysSettings { quake_mode_settings.keybinding = keystroke; report_if_error!(self.quake_mode_settings.set_value(quake_mode_settings, ctx)); + + self.apply_effective_dock_icon_visibility(ctx); } pub fn set_activation_hotkey_keybinding_and_write_to_user_defaults( @@ -189,6 +193,54 @@ impl KeysSettings { report_if_error!(self.quake_mode_settings.set_value(quake_mode_settings, ctx)); } + pub fn set_hide_dock_icon_when_using_quake_mode_and_write_to_user_defaults( + &mut self, + value: bool, + ctx: &mut ModelContext, + ) { + let mut quake_mode_settings = self.quake_mode_settings.value().clone(); + quake_mode_settings.hide_dock_icon = value; + + report_if_error!(self.quake_mode_settings.set_value(quake_mode_settings, ctx)); + + self.apply_effective_dock_icon_visibility(ctx); + } + + pub fn toggle_hide_dock_icon_when_using_quake_mode_and_write_to_user_defaults( + &mut self, + ctx: &mut ModelContext, + ) { + let mut quake_mode_settings = self.quake_mode_settings.value().clone(); + quake_mode_settings.hide_dock_icon = !quake_mode_settings.hide_dock_icon; + + report_if_error!(self.quake_mode_settings.set_value(quake_mode_settings, ctx)); + + self.apply_effective_dock_icon_visibility(ctx); + } + + /// Returns true when Warp should hide the Dock icon based on the current + /// effective hotkey configuration. Only true on macOS, when Quake Mode is + /// the global hotkey mode, the user enabled Dock hiding, and a dedicated + /// hotkey window keybinding is configured. The "hide" framing means + /// non-macOS and unsupported paths naturally default to visible. + pub fn should_hide_dock_icon(&self, app: &AppContext) -> bool { + let quake_mode_settings = self.quake_mode_settings.value(); + compute_should_hide_dock_icon( + cfg!(target_os = "macos"), + self.global_hotkey_mode(app), + quake_mode_settings.hide_dock_icon, + quake_mode_settings.keybinding.is_some(), + ) + } + + /// Computes the effective Dock visibility state and asks the platform to + /// apply it. Safe to call from settings change paths and at startup. No-op + /// on non-macOS via the platform delegate's default implementation. + pub fn apply_effective_dock_icon_visibility(&self, app: &AppContext) { + let visible = !self.should_hide_dock_icon(app); + app.set_dock_icon_visible(visible); + } + pub fn global_hotkey_mode(&self, app: &AppContext) -> GlobalHotkeyMode { let mut selected = GlobalHotkeyMode::Disabled; @@ -209,3 +261,85 @@ impl KeysSettings { selected } } + +/// Pure decision function for whether Warp should hide its Dock icon, given +/// the effective hotkey configuration. Extracted so it can be unit-tested +/// without an `AppContext`. Returns true only when all four conditions hold: +/// running on macOS, Quake Mode is selected, the user opted in, and a +/// dedicated hotkey window keybinding is configured. +pub(crate) fn compute_should_hide_dock_icon( + is_macos: bool, + mode: GlobalHotkeyMode, + hide_dock_icon_setting: bool, + has_keybinding: bool, +) -> bool { + is_macos + && matches!(mode, GlobalHotkeyMode::QuakeMode) + && hide_dock_icon_setting + && has_keybinding +} + +#[cfg(test)] +mod tests { + use super::{compute_should_hide_dock_icon, GlobalHotkeyMode}; + + #[test] + fn hides_dock_icon_when_all_conditions_met() { + assert!(compute_should_hide_dock_icon( + true, + GlobalHotkeyMode::QuakeMode, + true, + true, + )); + } + + #[test] + fn default_setting_never_hides() { + assert!(!compute_should_hide_dock_icon( + true, + GlobalHotkeyMode::QuakeMode, + false, + true, + )); + } + + #[test] + fn activation_hotkey_mode_does_not_hide() { + assert!(!compute_should_hide_dock_icon( + true, + GlobalHotkeyMode::ActivationHotkey, + true, + true, + )); + } + + #[test] + fn disabled_mode_does_not_hide() { + assert!(!compute_should_hide_dock_icon( + true, + GlobalHotkeyMode::Disabled, + true, + true, + )); + } + + #[test] + fn missing_keybinding_does_not_hide() { + assert!(!compute_should_hide_dock_icon( + true, + GlobalHotkeyMode::QuakeMode, + true, + false, + )); + } + + #[test] + fn non_macos_never_hides_even_when_all_other_conditions_met() { + assert!(!compute_should_hide_dock_icon( + false, + GlobalHotkeyMode::QuakeMode, + true, + true, + )); + } +} diff --git a/crates/warpui/src/platform/mac/delegate.rs b/crates/warpui/src/platform/mac/delegate.rs index 3fdd628fd..9939a7d9d 100644 --- a/crates/warpui/src/platform/mac/delegate.rs +++ b/crates/warpui/src/platform/mac/delegate.rs @@ -394,6 +394,14 @@ impl platform::Delegate for AppDelegate { } } + fn set_dock_icon_visible(&self, visible: bool) { + // NSApplicationActivationPolicyRegular = 0, NSApplicationActivationPolicyAccessory = 1. + let policy: i64 = if visible { 0 } else { 1 }; + dispatch::Queue::main().exec_async(move || unsafe { + let _: BOOL = msg_send![NSApp(), setActivationPolicy: policy]; + }); + } + fn terminate_app(&self, termination_mode: TerminationMode) { // Execute `[NSApp terminate]` asynchronously on the main thread to // ensure we don't accidentally run into any double-borrow errors. diff --git a/crates/warpui_core/src/core/app.rs b/crates/warpui_core/src/core/app.rs index 70d99978e..625a5e196 100644 --- a/crates/warpui_core/src/core/app.rs +++ b/crates/warpui_core/src/core/app.rs @@ -1126,6 +1126,11 @@ impl AppContext { self.platform_delegate.register_global_shortcut(shortcut); } + /// Show or hide the Dock icon (macOS only — no-op elsewhere). + pub fn set_dock_icon_visible(&self, visible: bool) { + self.platform_delegate.set_dock_icon_visible(visible); + } + fn dispatch_draw_frame_error_callback(&mut self, window_id: WindowId) { let callback = self.on_draw_frame_error_callback.take(); if let Some(callback) = &callback { diff --git a/crates/warpui_core/src/platform/mod.rs b/crates/warpui_core/src/platform/mod.rs index 20d33b2b2..7f3284672 100644 --- a/crates/warpui_core/src/platform/mod.rs +++ b/crates/warpui_core/src/platform/mod.rs @@ -262,6 +262,10 @@ pub trait Delegate: 'static { fn register_global_shortcut(&self, shortcut: Keystroke); fn unregister_global_shortcut(&self, shortcut: &Keystroke); + /// Show or hide the application's Dock icon (macOS only). + /// Default no-op for platforms without a Dock concept. + fn set_dock_icon_visible(&self, _visible: bool) {} + fn terminate_app(&self, termination_mode: TerminationMode); /// Returns whether or not a screen reader is enabled, or None if we do not