From 8696bef64486a51bb3b637c74d93a6f6079a900b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Aug 2026 11:22:47 -0700 Subject: [PATCH] macos: guard fullscreen tab presentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #13611 Route new-tab window presentation through an Objective-C exception catcher. AppKit can raise an NSInternalInconsistencyException while selecting a new tab in native fullscreen. Catch the presentation exception, report it through the existing error logging path, and leave Ghostty running when AppKit’s fullscreen window stack is inconsistent. This was pretty hard to reproduce but I was able to reproduce it about 1/3rd of the time via AppleScript automation... --- .../Terminal/TerminalController.swift | 6 ++- .../Extensions/NSWindow+Extension.swift | 15 ++++++++ macos/Sources/Helpers/ObjCExceptionCatcher.h | 7 ++++ macos/Sources/Helpers/ObjCExceptionCatcher.m | 38 +++++++++++++++---- 4 files changed, 57 insertions(+), 9 deletions(-) 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;