mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-20 16:38:38 +03:00

Previously, we would access the `ghostty.config` object from anywhere. The issue with this is that memory lifetime access to the underlying `ghostty_config_t` was messy. It was easy when the apprt owned every reference but since automatic theme changes were implemented, this isn't always true anymore. To fix this, we move to the same pattern we use internally in the core of ghostty: whenever the config changes, we handle an event, derive our desired values out of the config (copy them), and then let the caller free the config if they want to. This way, we can be sure that any information we need from the config is always owned by us.
140 lines
5.5 KiB
Swift
140 lines
5.5 KiB
Swift
import Cocoa
|
|
|
|
/// The state stored for terminal window restoration.
|
|
class TerminalRestorableState: Codable {
|
|
static let selfKey = "state"
|
|
static let versionKey = "version"
|
|
static let version: Int = 2
|
|
|
|
let focusedSurface: String?
|
|
let surfaceTree: Ghostty.SplitNode?
|
|
|
|
init(from controller: TerminalController) {
|
|
self.focusedSurface = controller.focusedSurface?.uuid.uuidString
|
|
self.surfaceTree = controller.surfaceTree
|
|
}
|
|
|
|
init?(coder aDecoder: NSCoder) {
|
|
// If the version doesn't match then we can't decode. In the future we can perform
|
|
// version upgrading or something but for now we only have one version so we
|
|
// don't bother.
|
|
guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else {
|
|
return nil
|
|
}
|
|
|
|
guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else {
|
|
return nil
|
|
}
|
|
|
|
self.surfaceTree = v.value.surfaceTree
|
|
self.focusedSurface = v.value.focusedSurface
|
|
}
|
|
|
|
func encode(with coder: NSCoder) {
|
|
coder.encode(Self.version, forKey: Self.versionKey)
|
|
coder.encode(CodableBridge(self), forKey: Self.selfKey)
|
|
}
|
|
}
|
|
|
|
enum TerminalRestoreError: Error {
|
|
case delegateInvalid
|
|
case identifierUnknown
|
|
case stateDecodeFailed
|
|
case windowDidNotLoad
|
|
}
|
|
|
|
/// The NSWindowRestoration implementation that is called when a terminal window needs to be restored.
|
|
/// The encoding of a terminal window is handled elsewhere (usually NSWindowDelegate).
|
|
class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
|
static func restoreWindow(
|
|
withIdentifier identifier: NSUserInterfaceItemIdentifier,
|
|
state: NSCoder,
|
|
completionHandler: @escaping (NSWindow?, Error?) -> Void
|
|
) {
|
|
// Verify the identifier is what we expect
|
|
guard identifier == .init(String(describing: Self.self)) else {
|
|
completionHandler(nil, TerminalRestoreError.identifierUnknown)
|
|
return
|
|
}
|
|
|
|
// The app delegate is definitely setup by now. If it isn't our AppDelegate
|
|
// then something is royally fucked up but protect against it anyhow.
|
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
|
|
completionHandler(nil, TerminalRestoreError.delegateInvalid)
|
|
return
|
|
}
|
|
|
|
// If our configuration is "never" then we never restore the state
|
|
// no matter what. Note its safe to use "ghostty.config" directly here
|
|
// because window restoration is only ever invoked on app start so we
|
|
// don't have to deal with config reloads.
|
|
if (appDelegate.ghostty.config.windowSaveState == "never") {
|
|
completionHandler(nil, nil)
|
|
return
|
|
}
|
|
|
|
// Decode the state. If we can't decode the state, then we can't restore.
|
|
guard let state = TerminalRestorableState(coder: state) else {
|
|
completionHandler(nil, TerminalRestoreError.stateDecodeFailed)
|
|
return
|
|
}
|
|
|
|
// The window creation has to go through our terminalManager so that it
|
|
// can be found for events from libghostty. This uses the low-level
|
|
// createWindow so that AppKit can place the window wherever it should
|
|
// be.
|
|
let c = appDelegate.terminalManager.createWindow(withSurfaceTree: state.surfaceTree)
|
|
guard let window = c.window else {
|
|
completionHandler(nil, TerminalRestoreError.windowDidNotLoad)
|
|
return
|
|
}
|
|
|
|
// Setup our restored state on the controller
|
|
if let focusedStr = state.focusedSurface,
|
|
let focusedUUID = UUID(uuidString: focusedStr),
|
|
let view = c.surfaceTree?.findUUID(uuid: focusedUUID) {
|
|
c.focusedSurface = view
|
|
restoreFocus(to: view, inWindow: window)
|
|
}
|
|
|
|
completionHandler(window, nil)
|
|
}
|
|
|
|
/// This restores the focus state of the surfaceview within the given window. When restoring,
|
|
/// the view isn't immediately attached to the window since we have to wait for SwiftUI to
|
|
/// catch up. Therefore, we sit in an async loop waiting for the attachment to happen.
|
|
private static func restoreFocus(to: Ghostty.SurfaceView, inWindow: NSWindow, attempts: Int = 0) {
|
|
// For the first attempt, we schedule it immediately. Subsequent events wait a bit
|
|
// so we don't just spin the CPU at 100%. Give up after some period of time.
|
|
let after: DispatchTime
|
|
if (attempts == 0) {
|
|
after = .now()
|
|
} else if (attempts > 40) {
|
|
// 2 seconds, give up
|
|
return
|
|
} else {
|
|
after = .now() + .milliseconds(50)
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: after) {
|
|
// If the view is not attached to a window yet then we repeat.
|
|
guard let viewWindow = to.window else {
|
|
restoreFocus(to: to, inWindow: inWindow, attempts: attempts + 1)
|
|
return
|
|
}
|
|
|
|
// If the view is attached to some other window, we give up
|
|
guard viewWindow == inWindow else { return }
|
|
|
|
inWindow.makeFirstResponder(to)
|
|
|
|
// If the window is main, then we also make sure it comes forward. This
|
|
// prevents a bug found in #1177 where sometimes on restore the windows
|
|
// would be behind other applications.
|
|
if (viewWindow.isMainWindow) {
|
|
viewWindow.orderFront(nil)
|
|
}
|
|
}
|
|
}
|
|
}
|