11import AppKit
22
3+ @MainActor
4+ enum StartMenuWindowChrome {
5+ static func apply( to window: NSWindow ) {
6+ window. title = " Try Omarchy "
7+ // The start menu draws its own heading inside a full-size content view.
8+ // Keep the native title as the window identity, but do not composite a
9+ // second copy over that custom heading in the transparent title bar.
10+ window. titleVisibility = . hidden
11+ window. titlebarAppearsTransparent = true
12+ window. isMovableByWindowBackground = true
13+ window. isReleasedWhenClosed = false
14+ }
15+ }
16+
317private final class MouseIgnoringTextField : NSTextField {
418 override func hitTest( _ point: NSPoint ) -> NSView ? { nil }
519}
@@ -80,6 +94,42 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
8094 private weak var startMenuScrollView : NSScrollView ?
8195 private( set) var portForwardingEditor : PortForwardingEditor ?
8296 private weak var immersiveCaption : NSTextField ?
97+ private lazy var permissionWindowRestorer = PermissionWindowRestorer (
98+ canRestore: { [ weak self] in
99+ guard let self else { return false }
100+ return self . window. isVisible
101+ && !self . launchInProgress
102+ && !self . resetInProgress
103+ && !self . microphoneRequestInFlight
104+ && !self . cameraRequestInFlight
105+ && self . window. attachedSheet == nil
106+ && NSApp . modalWindow == nil
107+ && self . portForwardingEditor == nil
108+ } ,
109+ isApplicationActive: { NSApp . isActive } ,
110+ orderFrontRegardless: { [ weak self] frame in
111+ guard let self else { return }
112+ self . window. setFrame ( frame, display: false )
113+ self . window. orderFrontRegardless ( )
114+ } ,
115+ activateApplication: {
116+ // `activate(ignoringOtherApps:)` is deprecated on the deployment
117+ // target. The system permission UI cooperatively yields to this
118+ // modern activation request as it closes.
119+ NSApp . activate ( )
120+ } ,
121+ makeKeyAndOrderFront: { [ weak self] frame in
122+ guard let self else { return }
123+ self . window. setFrame ( frame, display: false )
124+ self . window. makeKeyAndOrderFront ( nil )
125+ } ,
126+ retryDelays: [ 0.1 , 0.3 ] ,
127+ schedule: { delay, action in
128+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + delay) {
129+ action ( )
130+ }
131+ }
132+ )
83133
84134 init (
85135 accessibilityStatus: @escaping ( ) -> Bool ,
@@ -140,10 +190,7 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
140190 )
141191 super. init ( )
142192
143- window. title = " Try Omarchy "
144- window. titlebarAppearsTransparent = true
145- window. isMovableByWindowBackground = true
146- window. isReleasedWhenClosed = false
193+ StartMenuWindowChrome . apply ( to: window)
147194 window. delegate = self
148195 window. contentView = content
149196 }
@@ -168,13 +215,21 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
168215 render ( )
169216 }
170217
218+ func applicationDidBecomeActive( ) {
219+ refreshPermissionStatus ( )
220+ // Refresh replaces the view hierarchy, so key/front restoration must
221+ // be the final operation rather than something a render can disturb.
222+ permissionWindowRestorer. applicationDidBecomeActive ( )
223+ }
224+
171225 func promptForReset( ) {
172226 guard canResetStorage else { return }
173227 window. makeKeyAndOrderFront ( nil )
174228 confirmReset ( )
175229 }
176230
177231 func dismiss( ) {
232+ permissionWindowRestorer. cancel ( )
178233 portForwardingEditor? . dismiss ( )
179234 portForwardingEditor = nil
180235 window. orderOut ( nil )
@@ -889,25 +944,29 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
889944 }
890945
891946 @objc private func beginAccessibilityRequest( ) {
947+ permissionWindowRestorer. cancel ( )
892948 requestAccessibility ( )
893949 render ( )
894950 }
895951
896952 @objc private func beginMicrophoneRequest( ) {
897953 guard microphoneStatus ( ) == . notDetermined, !microphoneRequestInFlight else { return }
954+ permissionWindowRestorer. cancel ( )
955+ let windowFrame = window. frame
898956 microphoneRequestInFlight = true
899957 render ( )
900958 requestMicrophone { [ weak self] _ in
901959 DispatchQueue . main. async {
902960 guard let self else { return }
903961 self . microphoneRequestInFlight = false
904962 self . render ( )
905- self . restoreAfterPermissionRequest ( )
963+ self . permissionWindowRestorer . requestDidFinish ( preserving : windowFrame )
906964 }
907965 }
908966 }
909967
910968 @objc private func openMicrophoneSettings( ) {
969+ permissionWindowRestorer. cancel ( )
911970 guard let url = URL (
912971 string: " x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone "
913972 ) else { return }
@@ -916,34 +975,30 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
916975
917976 @objc private func beginCameraRequest( ) {
918977 guard cameraStatus ( ) == . notDetermined, !cameraRequestInFlight else { return }
978+ permissionWindowRestorer. cancel ( )
979+ let windowFrame = window. frame
919980 cameraRequestInFlight = true
920981 render ( )
921982 requestCamera { [ weak self] _ in
922983 DispatchQueue . main. async {
923984 guard let self else { return }
924985 self . cameraRequestInFlight = false
925986 self . render ( )
926- self . restoreAfterPermissionRequest ( )
987+ self . permissionWindowRestorer . requestDidFinish ( preserving : windowFrame )
927988 }
928989 }
929990 }
930991
931- private func restoreAfterPermissionRequest( ) {
932- // The system permission prompt can leave another process active. Since
933- // this launcher is an accessory app, ordering its window alone does not
934- // reliably raise it above windows belonging to that active app.
935- NSApp . activate ( ignoringOtherApps: true )
936- window. makeKeyAndOrderFront ( nil )
937- }
938-
939992 @objc private func openCameraSettings( ) {
993+ permissionWindowRestorer. cancel ( )
940994 guard let url = URL (
941995 string: " x-apple.systempreferences:com.apple.preference.security?Privacy_Camera "
942996 ) else { return }
943997 NSWorkspace . shared. open ( url)
944998 }
945999
9461000 @objc private func openStorageLocation( ) {
1001+ permissionWindowRestorer. cancel ( )
9471002 guard let storageLocationURL = storageLocationURL ( ) else { return }
9481003 do {
9491004 if !FileManager. default. fileExists ( atPath: storageLocationURL. path) {
@@ -993,6 +1048,7 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
9931048
9941049 @objc private func beginStorageLocationSelection( ) {
9951050 guard canResetStorage, !launchInProgress, !resetInProgress else { return }
1051+ permissionWindowRestorer. cancel ( )
9961052 let panel = NSOpenPanel ( )
9971053 panel. title = " Choose where to keep the Omarchy VM "
9981054 panel. message = " Omarchy puts its VM files straight into the folder you choose \u{2014} it does not create a folder inside it. Pick an empty folder, or one Omarchy already uses. The drive must be APFS. "
@@ -1057,6 +1113,7 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
10571113 !resetInProgress,
10581114 !microphoneRequestInFlight,
10591115 !cameraRequestInFlight else { return }
1116+ permissionWindowRestorer. cancel ( )
10601117 let panel = NSOpenPanel ( )
10611118 panel. title = " Choose a folder to share with Omarchy "
10621119 panel. message = " Omarchy will be able to read and change everything inside this folder, linked as ~/<folder name>. "
@@ -1095,6 +1152,7 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
10951152
10961153 @objc private func beginPortForwardingConfiguration( ) {
10971154 guard !launchInProgress, !resetInProgress, portForwardingEditor == nil else { return }
1155+ permissionWindowRestorer. cancel ( )
10981156 let editor = PortForwardingEditor (
10991157 mappings: portForwardingStatus ( ) ,
11001158 save: { [ weak self] mappings in
@@ -1136,6 +1194,7 @@ final class StartMenuWindow: NSObject, NSWindowDelegate {
11361194 !resetInProgress,
11371195 !microphoneRequestInFlight,
11381196 !cameraRequestInFlight else { return }
1197+ permissionWindowRestorer. cancel ( )
11391198 let estimate = storageSpaceEstimate ( )
11401199 let alert = NSAlert ( )
11411200 alert. alertStyle = . critical
0 commit comments