mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-25 02:48:42 +03:00
120 lines
4.6 KiB
Swift
120 lines
4.6 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 = 1
|
|
|
|
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.
|
|
if (appDelegate.terminalManager.ghostty.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(withBaseConfig: nil)
|
|
guard let window = c.window else {
|
|
completionHandler(nil, TerminalRestoreError.windowDidNotLoad)
|
|
return
|
|
}
|
|
|
|
// Setup our restored state on the controller
|
|
c.surfaceTree = state.surfaceTree
|
|
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) {
|
|
DispatchQueue.main.async {
|
|
// If the view is not attached to a window yet then we repeat.
|
|
guard let viewWindow = to.window else {
|
|
restoreFocus(to: to, inWindow: inWindow)
|
|
return
|
|
}
|
|
|
|
// If the view is attached to some other window, we give up
|
|
guard viewWindow == inWindow else { return }
|
|
|
|
inWindow.makeFirstResponder(to)
|
|
}
|
|
}
|
|
}
|