From 24e9a7663d56de64bd959a29949f0fb19addf00f Mon Sep 17 00:00:00 2001 From: Felix Riestra Date: Mon, 7 Sep 2026 17:34:57 +0200 Subject: [PATCH 1/2] macOS launcher: handle reopen while already running with no window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quitting the real GUI binary (Cmd+Q) leaves the launcher itself still running — that's by design, so it can keep relaying opens for the session. But reopening the app afterward (Dock icon, double-clicking it in Finder again) reactivates that same long-lived launcher process rather than triggering a fresh cold launch, since Launch Services still considers the bundle running. Neither of the delegate methods already handled covered that: applicationDidFinishLaunching: only fires once, at the launcher's own original cold start, and application:openURLs: only fires when a document was named. The net effect: the Dock icon would appear, but no window ever showed. applicationShouldHandleReopen:hasVisibleWindows: is exactly the callback AppKit fires for this reactivation case. Reproduced and verified the fix against the real sequence (cold launch, kill only the GUI process to simulate Cmd+Q, reopen) rather than assuming it from the delegate method's docs alone. Co-Authored-By: Claude Sonnet 5 --- src/bin/ocs_launcher.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/bin/ocs_launcher.rs b/src/bin/ocs_launcher.rs index 7f538b68e..9e2c4aab7 100644 --- a/src/bin/ocs_launcher.rs +++ b/src/bin/ocs_launcher.rs @@ -147,6 +147,23 @@ declare_class!( .collect(); deliver_or_launch(&files); } + + /// Fires when this already-running process is reactivated with no + /// visible windows to show — clicking the Dock icon, or Finder + /// launching the bundle again while Launch Services still considers + /// it running (this launcher's own ASN never dies between opens; see + /// the module doc). Without this, quitting the real GUI window while + /// this relay stays alive left the app in a state where reopening it + /// did nothing: no fresh `applicationDidFinishLaunching:` (that only + /// fires once, at this process's own cold start) and no + /// `application:openURLs:` (no document was named). + #[method(applicationShouldHandleReopen:hasVisibleWindows:)] + fn should_handle_reopen(&self, _sender: &NSApplication, has_visible_windows: bool) -> bool { + if !has_visible_windows { + deliver_or_launch(&[]); + } + true + } } ); From a39c7b8f71af1e995ec6c1962294756d12b527b1 Mon Sep 17 00:00:00 2001 From: Felix Riestra Date: Mon, 7 Sep 2026 17:51:23 +0200 Subject: [PATCH 2/2] macOS launcher: re-assert Accessory policy after handling an event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handling a document-open Apple Event (a warm "Open With" while the app is already running) silently promotes this process from NSApplicationActivationPolicyAccessory to a normal foreground app — confirmed with lsappinfo, not assumed: its own entry flips from type="UIElement" to type="Foreground" the instant it handles a second Apple Event (a cold launch's own first event doesn't trigger this). That gave the launcher its own, contentless Dock icon sitting next to the real editor's, both apparently "the same app" to the user but actually two separate Dock entries. Re-set Accessory explicitly right after relaying, in the one place every open (cold fallback, document open, and reopen-while-running) already funnels through. Co-Authored-By: Claude Sonnet 5 --- src/bin/ocs_launcher.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/bin/ocs_launcher.rs b/src/bin/ocs_launcher.rs index 9e2c4aab7..ed8e461b4 100644 --- a/src/bin/ocs_launcher.rs +++ b/src/bin/ocs_launcher.rs @@ -103,13 +103,32 @@ fn real_binary_path() -> PathBuf { /// running or about to claim it on its own. fn deliver_or_launch(files: &[String]) { let paths: Vec = files.iter().map(PathBuf::from).collect(); - if let Some(stream) = single_instance::try_connect_existing() { - if single_instance::handoff(stream, &paths) { - return; + let delivered = single_instance::try_connect_existing() + .map(|stream| single_instance::handoff(stream, &paths)) + .unwrap_or(false); + if !delivered { + if let Err(err) = std::process::Command::new(real_binary_path()).args(files).spawn() { + eprintln!("OpenCADStudio launcher: failed to launch the real binary: {err}"); } } - if let Err(err) = std::process::Command::new(real_binary_path()).args(files).spawn() { - eprintln!("OpenCADStudio launcher: failed to launch the real binary: {err}"); + reassert_accessory_policy(); +} + +/// AppKit activates this process — promoting it from +/// NSApplicationActivationPolicyAccessory (no Dock icon, set at startup) to +/// a normal foreground app with its own Dock icon — as a side effect of +/// handling an open-document Apple Event, even though nothing here ever +/// shows a window. Measured via `lsappinfo`, not assumed: the launcher's own +/// entry flips from type="UIElement" to type="Foreground" the moment it +/// handles a *second* Apple Event (a fresh cold launch's own first event +/// doesn't trigger this — the policy is presumably still settling at that +/// point). Set it back immediately after relaying, so a warm "Open With" +/// doesn't leave a second, contentless Dock icon sitting next to the real +/// editor's. +fn reassert_accessory_policy() { + if let Some(mtm) = MainThreadMarker::new() { + NSApplication::sharedApplication(mtm) + .setActivationPolicy(NSApplicationActivationPolicy::Accessory); } }