mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-03 18:34:50 +00:00
macOS: fix crash when adding tab from tab overview (#11009)
(moved from #11008) I have this branch up to fix #10252. It was written with AI (in Cursor), but only after I made sure I thoroughly understood what was happening (almost to an excessive extent). I had already determined that an Objective-C helper was necessary, I just asked GPT 5.3 Codex in Cursor to write it for me. **TL;DR: deep within AppKit, there is an Objective-C exception that is _always_ thrown when opening a new tab from the visual tab picker ("tab overview"). [Objective-C exceptions *cannot* be safely recovered from in Swift.](http://developer.apple.com/documentation/swift/handling-cocoa-errors-in-swift#Handle-Exceptions-in-Objective-C-Only) As Ghostty is primarily Swift, we must introduce some Objective-C wrapper around tab creation to safely swallow this exception.** There is a lot more I know about this than the above, and can discuss it at length if desired. Interestingly, it seems debug builds of Ghostty (`zig build run`) *do* gracefully recover and don't crash. Release builds (`zig build run -Doptimize=ReleaseFast`), however, *do* crash. The crashing seems to be expected behavior and **_I don't think there's any feasible way to get release builds to recover as debug builds do._** The debug builds do, arguably, have better animation behavior. Not sure how I can approach that part. Release build off of my commit: https://github.com/user-attachments/assets/c81927be-b2d2-48b3-a18f-30b389a90f04 Debug build off `db1e31c7a69924913e8faafcedb290de3cb4a8b6` (current `main`, as of writing): https://github.com/user-attachments/assets/76367154-b039-4453-8d39-8a0465973deb
This commit is contained in:
@@ -231,6 +231,7 @@
|
||||
Helpers/LastWindowPosition.swift,
|
||||
Helpers/MetalView.swift,
|
||||
Helpers/NonDraggableHostingView.swift,
|
||||
Helpers/ObjCExceptionCatcher.m,
|
||||
Helpers/PermissionRequest.swift,
|
||||
Helpers/Private/CGS.swift,
|
||||
Helpers/Private/Dock.swift,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// C imports here are exposed to Swift.
|
||||
|
||||
#import "ObjCExceptionCatcher.h"
|
||||
#import "VibrantLayer.h"
|
||||
|
||||
@@ -411,14 +411,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
// If we already have a tab group and we want the new tab to open at the end,
|
||||
// then we use the last window in the tab group as the parent.
|
||||
if let last = parent.tabGroup?.windows.last {
|
||||
last.addTabbedWindow(window, ordered: .above)
|
||||
last.addTabbedWindowSafely(window, ordered: .above)
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
|
||||
case "current": fallthrough
|
||||
default:
|
||||
parent.addTabbedWindow(window, ordered: .above)
|
||||
parent.addTabbedWindowSafely(window, ordered: .above)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +863,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
controller.showWindow(nil)
|
||||
if let firstWindow = firstController.window,
|
||||
let newWindow = controller.window {
|
||||
firstWindow.addTabbedWindow(newWindow, ordered: .above)
|
||||
firstWindow.addTabbedWindowSafely(newWindow, ordered: .above)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,9 +952,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
if tabIndex < tabGroup.windows.count {
|
||||
// Find the window that is currently at that index
|
||||
let currentWindow = tabGroup.windows[tabIndex]
|
||||
currentWindow.addTabbedWindow(window, ordered: .below)
|
||||
currentWindow.addTabbedWindowSafely(window, ordered: .below)
|
||||
} else {
|
||||
tabGroup.windows.last?.addTabbedWindow(window, ordered: .above)
|
||||
tabGroup.windows.last?.addTabbedWindowSafely(window, ordered: .above)
|
||||
}
|
||||
|
||||
// Make it the key window
|
||||
@@ -1386,7 +1386,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
if #available(macOS 26, *) {
|
||||
if window is TitlebarTabsTahoeTerminalWindow {
|
||||
tabGroup.removeWindow(selectedWindow)
|
||||
targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
|
||||
targetWindow.addTabbedWindowSafely(selectedWindow, ordered: action.amount < 0 ? .below : .above)
|
||||
DispatchQueue.main.async {
|
||||
selectedWindow.makeKey()
|
||||
}
|
||||
@@ -1401,7 +1401,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
|
||||
// Remove and re-add the window in the correct position
|
||||
tabGroup.removeWindow(selectedWindow)
|
||||
targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
|
||||
targetWindow.addTabbedWindowSafely(selectedWindow, ordered: action.amount < 0 ? .below : .above)
|
||||
|
||||
// Ensure our window remains selected
|
||||
selectedWindow.makeKey()
|
||||
|
||||
@@ -39,6 +39,23 @@ extension NSWindow {
|
||||
guard let firstWindow = tabGroup?.windows.first else { return true }
|
||||
return firstWindow === self
|
||||
}
|
||||
|
||||
/// Wraps `addTabbedWindow` with an Objective-C exception catcher because AppKit can
|
||||
/// throw NSExceptions in visual tab picker flows. Swift cannot safely recover from
|
||||
/// those exceptions, so we route through Obj-C and log a recoverable failure.
|
||||
@discardableResult
|
||||
func addTabbedWindowSafely(
|
||||
_ child: NSWindow,
|
||||
ordered: NSWindow.OrderingMode
|
||||
) -> Bool {
|
||||
var error: NSError?
|
||||
let success = GhosttyAddTabbedWindowSafely(self, child, ordered.rawValue, &error)
|
||||
if let error {
|
||||
Ghostty.logger.error("addTabbedWindow failed: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
/// Native tabbing private API usage. :(
|
||||
|
||||
@@ -296,13 +296,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
if tabIndex == 0 {
|
||||
// We were previously the first tab. Add it before ("below")
|
||||
// the first window in the tab group currently.
|
||||
tabGroup.windows.first!.addTabbedWindow(window, ordered: .below)
|
||||
tabGroup.windows.first!.addTabbedWindowSafely(window, ordered: .below)
|
||||
} else if tabIndex <= tabGroup.windows.count {
|
||||
// We were somewhere in the middle
|
||||
tabGroup.windows[tabIndex - 1].addTabbedWindow(window, ordered: .above)
|
||||
tabGroup.windows[tabIndex - 1].addTabbedWindowSafely(window, ordered: .above)
|
||||
} else {
|
||||
// We were at the end
|
||||
tabGroup.windows.last!.addTabbedWindow(window, ordered: .below)
|
||||
tabGroup.windows.last!.addTabbedWindowSafely(window, ordered: .below)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
macos/Sources/Helpers/ObjCExceptionCatcher.h
Normal file
13
macos/Sources/Helpers/ObjCExceptionCatcher.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// This file contains wrappers around various ObjC functions so we can catch
|
||||
/// exceptions, since you can't natively catch ObjC exceptions from Swift
|
||||
/// (at least at the time of writing this comment).
|
||||
|
||||
/// NSWindow.addTabbedWindow wrapper
|
||||
FOUNDATION_EXPORT BOOL GhosttyAddTabbedWindowSafely(
|
||||
id _Nonnull parent,
|
||||
id _Nonnull child,
|
||||
NSInteger ordered,
|
||||
NSError * _Nullable * _Nullable error
|
||||
);
|
||||
32
macos/Sources/Helpers/ObjCExceptionCatcher.m
Normal file
32
macos/Sources/Helpers/ObjCExceptionCatcher.m
Normal file
@@ -0,0 +1,32 @@
|
||||
#import "ObjCExceptionCatcher.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
BOOL GhosttyAddTabbedWindowSafely(
|
||||
id parent,
|
||||
id child,
|
||||
NSInteger ordered,
|
||||
NSError * _Nullable * _Nullable error
|
||||
) {
|
||||
// AppKit occasionally throws NSException while adding tabbed windows,
|
||||
// in particular when creating tabs from the tab overview page since some
|
||||
// macOS update recently in 2025/2026 (unclear).
|
||||
//
|
||||
// We must catch it in Objective-C; letting this cross into Swift is unsafe.
|
||||
@try {
|
||||
[((NSWindow *)parent) addTabbedWindow:(NSWindow *)child ordered:(NSWindowOrderingMode)ordered];
|
||||
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,
|
||||
}];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user