Skip to content

Commit 1928d3b

Browse files
Fix Show Binky when the main window is closed
Route menu bar and global hotkey through SwiftUI openWindow(id: "main"). WindowGroup now has an explicit id; BinkyShortcutCommands stays alive for the app lifetime and handles .binkyShowMainWindow. Removes unreliable newWindow: fallback when ContentView is unmounted. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 946f01c commit 1928d3b

5 files changed

Lines changed: 34 additions & 50 deletions

File tree

Binky/BinkyApp.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ struct BinkyApp: App {
2525
@StateObject private var updater = UpdateChecker()
2626

2727
var body: some Scene {
28-
WindowGroup {
28+
// Explicit `id` lets the menu bar / hotkey bridge re-open this window via
29+
// `openWindow(id: "main")` even when ContentView has been unmounted (closed).
30+
WindowGroup(id: "main") {
2931
ContentView(vm: root.organizerVM)
3032
.environmentObject(root.prefs)
3133
.environmentObject(updater)
@@ -103,6 +105,10 @@ private struct LastSortSummaryCommands: View {
103105

104106
private struct BinkyShortcutCommands: View {
105107
@ObservedObject var prefs: BinkyPreferences
108+
// `openWindow` lives in the App's environment and is callable any time the App scene
109+
// tree is alive — even when no WindowGroup window is currently presented. This is the
110+
// only reliable path for "Show Binky" after the user has closed the main window.
111+
@Environment(\.openWindow) private var openWindow
106112

107113
private var sortNowMenuTitle: String {
108114
let enabledCount = prefs.savedPresets.filter {
@@ -123,6 +129,15 @@ private struct BinkyShortcutCommands: View {
123129
NotificationCenter.default.post(name: .binkyStartSort, object: nil)
124130
}
125131
.keyboardShortcut(prefs.shortcut(for: .sortNow).swiftUIKeyboardShortcut)
132+
// Bridges the AppKit menu bar's "Show Binky" / global hotkey into SwiftUI's
133+
// `openWindow(id:)`. SwiftUI keeps Commands views alive for the full app lifetime
134+
// (they back the main menu bar), so this `.onReceive` fires whether or not the
135+
// main window is currently presented. `openWindow(id:)` brings an existing instance
136+
// forward, or creates a fresh one if the user previously closed the window.
137+
.onReceive(NotificationCenter.default.publisher(for: .binkyShowMainWindow)) { _ in
138+
NSApp.activate()
139+
openWindow(id: "main")
140+
}
126141
}
127142
}
128143

Binky/ContentView.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,9 @@ private struct MenuBridgeObservers: ViewModifier {
163163
NSApp.activate()
164164
openSettings()
165165
}
166-
.onReceive(NotificationCenter.default.publisher(for: .binkyShowMainWindow)) { _ in
167-
NSApp.activate()
168-
BinkyMenuBarController.bringMainOrganizerWindowForward()
169-
}
166+
// `.binkyShowMainWindow` is handled at App scope in `BinkyShortcutCommands`
167+
// via `openWindow(id: "main")` — that path works whether or not this view is
168+
// currently mounted (and thus survives closing/reopening the main window).
170169
.onReceive(NotificationCenter.default.publisher(for: .binkyCheckUpdates)) { _ in
171170
Task {
172171
let result = await updater.check(manual: true)

Binky/Localizable.xcstrings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10197,6 +10197,9 @@
1019710197
},
1019810198
"Sort" : {
1019910199
"comment" : "Menu bar: parent menu for choosing which folder to sweep.\nOpen panel button."
10200+
},
10201+
"Sort ${files} with Binky" : {
10202+
1020010203
},
1020110204
"Sort All" : {
1020210205
"comment" : "File menu: run sort across every enabled automation.\nPrimary sort button: sort all enabled automations."

Binky/Services/BinkyMenuBarController.swift

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,14 @@ final class BinkyMenuBarController: NSObject, NSMenuDelegate {
304304
}
305305

306306
@objc private func showMainWindow() {
307-
// Activate first so the SwiftUI scene receiver below can run inside a foregrounded app.
308-
NSApp.activate()
309-
// Primary path: SwiftUI scene picks up the notification and uses the `openWindow`
310-
// environment action (handles closed-window / menu-bar-only scenarios reliably).
311-
NotificationCenter.default.post(name: .binkyShowMainWindow, object: nil)
312-
// AppKit fallback in case ContentView isn't currently mounted (e.g. very early startup).
307+
// SwiftUI Commands bridge in `BinkyApp` (`BinkyShortcutCommands`) listens for this
308+
// notification and calls `openWindow(id: "main")` — the only reliable way to
309+
// (re)create or refocus a `WindowGroup` window from the AppKit menu bar, including
310+
// when the user has closed the main window entirely. We post on the next runloop tick
311+
// so AppKit finishes dismissing the status menu first.
313312
DispatchQueue.main.async {
314-
GlobalHotkeyManager.activateMainWindow()
313+
NSApp.activate()
314+
NotificationCenter.default.post(name: .binkyShowMainWindow, object: nil)
315315
}
316316
}
317317

@@ -328,24 +328,4 @@ final class BinkyMenuBarController: NSObject, NSMenuDelegate {
328328
}
329329
}
330330
}
331-
332-
/// Bring an existing organizer window forward. Called from `ContentView` after the
333-
/// `binkyShowMainWindow` notification fires; if no main window exists, the AppKit fallback
334-
/// path in `showMainWindow()` (via `GlobalHotkeyManager`) handles creating one.
335-
static func bringMainOrganizerWindowForward() {
336-
if let w = NSApp.windows.first(where: { $0.frameAutosaveName == "BinkyMainWindow" && $0.isVisible }) {
337-
w.makeKeyAndOrderFront(nil)
338-
return
339-
}
340-
if let w = NSApp.windows.first(where: { w in
341-
w.isVisible
342-
&& w.canBecomeKey
343-
&& w.title != "Binky Help"
344-
&& w.frameAutosaveName != "help"
345-
}) {
346-
w.makeKeyAndOrderFront(nil)
347-
return
348-
}
349-
NSApp.sendAction(Selector(("newWindow:")), to: nil, from: nil)
350-
}
351331
}

Binky/Services/GlobalHotkeyManager.swift

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,11 @@ final class GlobalHotkeyManager {
6868
// macOS 14+: prefer parameterless `activate()`; older `activate(ignoringOtherApps:)`
6969
// is deprecated and may be a no-op on recent macOS when invoked from a status item.
7070
NSApp.activate()
71-
bringMainWindowForward()
72-
}
73-
74-
private static func bringMainWindowForward() {
75-
if let w = NSApp.windows.first(where: { $0.frameAutosaveName == "BinkyMainWindow" && $0.isVisible }) {
76-
w.makeKeyAndOrderFront(nil)
77-
return
78-
}
79-
if let w = NSApp.windows.first(where: { w in
80-
w.isVisible
81-
&& w.canBecomeKey
82-
&& w.title != "Binky Help"
83-
&& w.frameAutosaveName != "help"
84-
}) {
85-
w.makeKeyAndOrderFront(nil)
86-
return
87-
}
88-
NSApp.sendAction(Selector(("newWindow:")), to: nil, from: nil)
71+
// Routed through SwiftUI Commands (`BinkyShortcutCommands`), which calls
72+
// `openWindow(id: "main")`. The AppKit `newWindow:` selector does not reliably
73+
// open a SwiftUI `WindowGroup` window once the user has closed the only instance;
74+
// `openWindow(id:)` does — and brings an existing window forward when present.
75+
NotificationCenter.default.post(name: .binkyShowMainWindow, object: nil)
8976
}
9077

9178
// MARK: - Carbon event handler

0 commit comments

Comments
 (0)