From 68866ef8295cf2e190737b041ae2bd7cbf4781ae Mon Sep 17 00:00:00 2001 From: Morten Trydal Date: Tue, 17 Mar 2026 21:54:03 +0100 Subject: [PATCH] Fix notification click launching second app instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add single-instance guard in ShellraiserApp.init() that activates the existing instance and exits if another copy with the same bundle ID is already running — guards against Launch Services conflicts from builds across checkouts - Switch WindowGroup to Window (semantically correct for single-window) - Add applicationShouldHandleReopen to restore window on dock-click - Remove -n flag from Makefile run-isolated to prevent duplicate launches - Add tests for applicationShouldHandleReopen covering both hasVisibleWindows states --- Makefile | 2 +- Sources/Shellraiser/App/ShellraiserApp.swift | 23 ++++++++++++++++++- .../ShellraiserAppTests.swift | 16 +++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fa8168d..763a846 100644 --- a/Makefile +++ b/Makefile @@ -24,4 +24,4 @@ run: build-app open $(APP_PATH) run-isolated: build-isolated - open -n --env SHELLRAISER_APP_SUPPORT_SUBDIRECTORY=$(ISOLATED_APP_SUPPORT_SUBDIRECTORY) $(ISOLATED_APP_PATH) + open --env SHELLRAISER_APP_SUPPORT_SUBDIRECTORY=$(ISOLATED_APP_SUPPORT_SUBDIRECTORY) $(ISOLATED_APP_PATH) diff --git a/Sources/Shellraiser/App/ShellraiserApp.swift b/Sources/Shellraiser/App/ShellraiserApp.swift index ecc6466..f9a2b36 100644 --- a/Sources/Shellraiser/App/ShellraiserApp.swift +++ b/Sources/Shellraiser/App/ShellraiserApp.swift @@ -94,6 +94,17 @@ final class ShellraiserAppDelegate: NSObject, NSApplicationDelegate { ShellraiserScriptingController.shared.insertSurfaceConfiguration(configuration) } + /// Brings the main window to front when the dock icon is clicked with no visible windows. + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + if !flag { + for window in sender.windows where window.canBecomeMain { + window.makeKeyAndOrderFront(nil) + break + } + } + return true + } + /// Intercepts standard app termination and requires explicit user confirmation. func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { guard confirmQuit() else { @@ -112,7 +123,17 @@ struct ShellraiserApp: App { @StateObject private var manager: WorkspaceManager /// Disables native macOS window tabbing so the app's own pane tabs remain the only tab UI. + /// Exits immediately if another instance with the same bundle ID is already running, + /// activating the existing instance instead — guards against Launch Services conflicts. init() { + if let bundleID = Bundle.main.bundleIdentifier { + let others = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + .filter { $0 != .current } + if let existing = others.first { + existing.activate() + exit(0) + } + } NSWindow.allowsAutomaticWindowTabbing = false let manager = WorkspaceManager() _manager = StateObject(wrappedValue: manager) @@ -120,7 +141,7 @@ struct ShellraiserApp: App { } var body: some Scene { - WindowGroup { + Window("Shellraiser", id: "main") { ContentView(manager: manager) .frame(minWidth: 1100, minHeight: 760) } diff --git a/Tests/ShellraiserTests/ShellraiserAppTests.swift b/Tests/ShellraiserTests/ShellraiserAppTests.swift index 1d05be9..d0aeb69 100644 --- a/Tests/ShellraiserTests/ShellraiserAppTests.swift +++ b/Tests/ShellraiserTests/ShellraiserAppTests.swift @@ -40,4 +40,20 @@ final class ShellraiserAppTests: WorkspaceTestCase { XCTAssertEqual(reply, .terminateCancel) XCTAssertFalse(manager.isTerminating) } + + /// Verifies the delegate returns true (allow reopen handling) when no windows are visible, + /// so AppKit proceeds to make a window key — required for dock-click restoration. + func testApplicationShouldHandleReopenReturnsTrueWithNoVisibleWindows() { + let delegate = ShellraiserAppDelegate() + let result = delegate.applicationShouldHandleReopen(NSApplication.shared, hasVisibleWindows: false) + XCTAssertTrue(result) + } + + /// Verifies the delegate returns true even when windows are already visible, + /// keeping reopen handling consistent regardless of window visibility state. + func testApplicationShouldHandleReopenReturnsTrueWithVisibleWindows() { + let delegate = ShellraiserAppDelegate() + let result = delegate.applicationShouldHandleReopen(NSApplication.shared, hasVisibleWindows: true) + XCTAssertTrue(result) + } }