From 9034f4e4d5dc0f1eb75609b40d3a4a2a2bdb4a97 Mon Sep 17 00:00:00 2001 From: Jhan Silva Date: Fri, 1 May 2026 19:19:58 -0700 Subject: [PATCH] Hide Dock icon when using dedicated hotkey window (GH-1154) Adds an opt-in macOS-only setting that hides Warp's Dock icon while a dedicated hotkey window is the active global hotkey mode and a keybinding is configured. When effective, Warp switches NSApplicationActivationPolicy to Accessory; otherwise it stays Regular. Off by default. Recovery is built into the effective-state rule rather than left to the user: clearing the keybinding, switching away from Quake Mode, or turning the setting off restores the Dock icon immediately, so the user can never end up without an obvious way back to the app. Implements the spec at specs/GH1154/. --- app/src/root_view.rs | 1 + app/src/settings/mod.rs | 5 + app/src/settings_view/features_page.rs | 62 ++++++++++ app/src/terminal/keys_settings.rs | 134 +++++++++++++++++++++ crates/warpui/src/platform/mac/delegate.rs | 8 ++ crates/warpui_core/src/core/app.rs | 5 + crates/warpui_core/src/platform/mod.rs | 4 + 7 files changed, 219 insertions(+) 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