diff --git a/src/main.rs b/src/main.rs index 67d36b02..a0412c52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -638,6 +638,24 @@ fn main() { application.on_open_urls(move |urls| { let _ = external_open_tx.try_send(urls); }); + // macOS relaunching an app that is already running — the Dock icon, a + // double-click on the bundle, `open -a tty7` — only reaches this process + // as `applicationShouldHandleReopen:`, and gpui's delegate does nothing + // with it unless a handler is registered. That is the one entrance a + // tray-resident tty7 has: with `show_tray_icon` on, closing the last + // window keeps the process and its Dock icon alive with nothing on + // screen, and without this the icon's click was a no-op (the only ways + // back in were ⌘N, the tray's "Show tty7", or quitting and relaunching). + // Like `on_open_urls`, the hook lives on `Application`, so it cannot go + // beside the other app-level handlers in `keymap::init`; and like that + // path it defers to the loop rather than opening windows on AppKit's + // delegate stack, which is also how Zed's own reopen handler runs. + application.on_reopen(|cx| { + cx.spawn(async move |cx| { + let _ = cx.update(crate::ui::windows::reopen); + }) + .detach(); + }); application.run(move |cx| { // gpui invokes this callback without an `App` context. Bridge it // back onto the application loop instead of touching UI state on diff --git a/src/ui/windows.rs b/src/ui/windows.rs index e249ba3b..a08bc947 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -480,6 +480,30 @@ fn open_missing_cli_window_with( announce_detached_at_launch(cx, restore); } +/// Brings the UI back when macOS relaunches a tty7 that is already running. +/// +/// AppKit only asks when it found no visible window, and that is a state +/// tty7 lives in on purpose: closing the last window with the tray icon on +/// retires to the tray, process alive and Dock icon up. A window that is +/// still there is activated rather than doubled — `activate_window` is +/// `makeKeyAndOrderFront:`, which is what orders a window AppKit left behind +/// back to the front. None at all gets the last layout back the way a +/// pathless launch and the tray's windowless path do, so the workspace that +/// retired is the one that returns and not a blank one beside it. +pub fn reopen(cx: &mut App) { + reopen_with(cx, open_at); +} + +fn reopen_with( + cx: &mut App, + open: impl FnOnce(&mut App, Option, Option), +) { + match WindowRegistry::most_recent(cx) { + Some(workspace) => activate(cx, workspace), + None => open_missing_cli_window_with(cx, None, open), + } +} + pub fn refresh_menu(cx: &mut App) { crate::ui::theme::set_menus(cx); } @@ -988,6 +1012,65 @@ mod tests { assert_eq!(opened, Some((None, None))); } + #[gpui::test] + fn a_reopen_with_no_window_up_restores_the_workspace_that_retired( + cx: &mut gpui::TestAppContext, + ) { + // The Dock icon after the last window retired to the tray: the + // process is alive with nothing on screen, and the click has to bring + // back the layout that was there, not mint a blank workspace beside it. + let view = WindowView::default(); + let restored = view.id; + let mut opened = None; + + cx.update(|cx| { + WindowRegistry::init(cx); + WorkspaceStore::install_for_test( + cx, + WindowViews { + views: vec![view], + active: Some(restored), + }, + ); + reopen_with(cx, |_, workspace, path| { + opened = Some((workspace, path)); + }); + }); + + assert_eq!(opened, Some((Some(restored), None))); + } + + #[gpui::test] + fn a_reopen_with_a_window_up_activates_it_instead_of_opening_another( + cx: &mut gpui::TestAppContext, + ) { + // AppKit also asks while a window is still there but off screen. That + // window is the thing to activate; a second one would take the + // restore away from it. + use gpui::VisualContext as _; + + let (app, mut vcx) = crate::ui::app::test_window::harness(cx); + let handle = vcx.window_handle(); + app.update_in(&mut vcx, |_, _, cx| { + WindowRegistry::init(cx); + let view = WindowView::default(); + let open = view.id; + WorkspaceStore::install_for_test( + cx, + WindowViews { + views: vec![view], + active: Some(open), + }, + ); + WindowRegistry::register(cx, open, handle, app.downgrade()); + + reopen_with(cx, |_, workspace, _| { + panic!("a reopen with a window up opened another for {workspace:?}"); + }); + assert_eq!(WindowRegistry::count(cx), 1); + }); + } + #[gpui::test] fn a_request_carrying_a_path_restores_the_layout_and_brings_the_path_along( cx: &mut gpui::TestAppContext,