diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d19323dde..471f010f8 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -480,8 +480,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } - controller.showWindow(self) - window.makeKeyAndOrderFront(self) + // showWindow makes regular windows key and ordered front. AppKit can + // throw while selecting a tab if its fullscreen stack is inconsistent, + // so this must cross the Objective-C exception bridge. + controller.showWindowSafely(self) // We also activate our app so that it becomes front. This may be // necessary for the dock menu. diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index 762b67f75..c95d1a05e 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -58,6 +58,21 @@ extension NSWindow { } } +extension NSWindowController { + /// Wraps `showWindow` with an Objective-C exception catcher because selecting + /// a tab can raise an AppKit fullscreen window-stack exception. + @discardableResult + func showWindowSafely(_ sender: Any?) -> Bool { + var error: NSError? + let success = GhosttyShowWindowSafely(self, sender, &error) + if let error { + Ghostty.logger.error("showWindow failed: \(error.localizedDescription, privacy: .public)") + } + + return success + } +} + /// Native tabbing private API usage. :( extension NSWindow { var titlebarView: NSView? { diff --git a/macos/Sources/Helpers/ObjCExceptionCatcher.h b/macos/Sources/Helpers/ObjCExceptionCatcher.h index 7906b5945..58b342ebe 100644 --- a/macos/Sources/Helpers/ObjCExceptionCatcher.h +++ b/macos/Sources/Helpers/ObjCExceptionCatcher.h @@ -11,3 +11,10 @@ FOUNDATION_EXPORT BOOL GhosttyAddTabbedWindowSafely( NSInteger ordered, NSError * _Nullable * _Nullable error ); + +/// NSWindowController.showWindow wrapper +FOUNDATION_EXPORT BOOL GhosttyShowWindowSafely( + id _Nonnull controller, + id _Nullable sender, + NSError * _Nullable * _Nullable error +); diff --git a/macos/Sources/Helpers/ObjCExceptionCatcher.m b/macos/Sources/Helpers/ObjCExceptionCatcher.m index e91fb14a7..6a5768a8e 100644 --- a/macos/Sources/Helpers/ObjCExceptionCatcher.m +++ b/macos/Sources/Helpers/ObjCExceptionCatcher.m @@ -2,6 +2,16 @@ #import +static NSError *GhosttyErrorFromException(NSException *exception, NSInteger code) { + NSString *reason = exception.reason ?: @"Unknown Objective-C exception"; + return [NSError errorWithDomain:@"Ghostty.ObjCException" + code:code + userInfo:@{ + NSLocalizedDescriptionKey: reason, + @"exception_name": exception.name, + }]; +} + BOOL GhosttyAddTabbedWindowSafely( id parent, id child, @@ -18,13 +28,27 @@ BOOL GhosttyAddTabbedWindowSafely( return YES; } @catch (NSException *exception) { if (error != NULL) { - NSString *reason = exception.reason ?: @"Unknown Objective-C exception"; - *error = [NSError errorWithDomain:@"Ghostty.ObjCException" - code:1 - userInfo:@{ - NSLocalizedDescriptionKey: reason, - @"exception_name": exception.name, - }]; + *error = GhosttyErrorFromException(exception, 1); + } + + return NO; + } +} + +BOOL GhosttyShowWindowSafely( + id controller, + id _Nullable sender, + NSError * _Nullable * _Nullable error +) { + // Selecting a newly added tab can throw from NSWindowStackController when + // the tab group contains windows in inconsistent native fullscreen states. + // Catch the exception here so the AppKit assertion doesn't abort the app. + @try { + [((NSWindowController *)controller) showWindow:sender]; + return YES; + } @catch (NSException *exception) { + if (error != NULL) { + *error = GhosttyErrorFromException(exception, 2); } return NO;