macos: guard fullscreen tab presentation with objc catcher (#13636)

#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...
This commit is contained in:
Mitchell Hashimoto
2026-08-05 11:46:02 -07:00
committed by GitHub
4 changed files with 57 additions and 9 deletions

View File

@@ -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.

View File

@@ -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? {

View File

@@ -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
);

View File

@@ -2,6 +2,16 @@
#import <AppKit/AppKit.h>
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;