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 surfaceTree: Ghostty.SplitNode? init(from controller: TerminalController) { 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, forKey: Self.selfKey) else { return nil } self.surfaceTree = v.value.surfaceTree } 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 completionHandler(window, nil) } }