From 59f54a1d887f77acaf0dd0cda29e23899b2c93dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 22 Dec 2023 21:10:21 -0800 Subject: [PATCH] macos: initial window save/restore is working for frames only --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + macos/Sources/AppDelegate.swift | 21 +++++ .../Terminal/TerminalController.swift | 15 ++++ .../Features/Terminal/TerminalManager.swift | 4 +- .../Terminal/TerminalRestorable.swift | 77 +++++++++++++++++++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Features/Terminal/TerminalRestorable.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 641133f89..71877844a 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; }; A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; }; A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; }; + A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; }; A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; }; A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; }; A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; @@ -95,6 +96,7 @@ A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = ""; }; A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; + A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = ""; }; A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; @@ -213,6 +215,7 @@ A59630992AEE1C6400D64628 /* Terminal.xib */, A596309F2AEF6AEB00D64628 /* TerminalManager.swift */, A596309B2AEE1C9E00D64628 /* TerminalController.swift */, + A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, @@ -366,6 +369,7 @@ files = ( A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, + A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index 41c6ea5de..f0e835d08 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -96,6 +96,12 @@ class AppDelegate: NSObject, // Disable this so that repeated key events make it through to our terminal views. "ApplePressAndHoldEnabled": false, ]) + + // TODO: make this configurable via ghostty + // reset to system defaults + //UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows") + // force state save + UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") // Hook up updater menu menuCheckForUpdates?.target = updaterController @@ -293,6 +299,21 @@ class AppDelegate: NSObject, private func focusedSurface() -> ghostty_surface_t? { return terminalManager.focusedSurface?.surface } + + //MARK: - Restorable State + + /// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways. + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } + + func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) { + Self.logger.debug("application will save window state") + } + + func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) { + Self.logger.debug("application will restore window state") + } //MARK: - UNUserNotificationCenterDelegate diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7e200cef2..186c0a198 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -148,6 +148,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, override func windowDidLoad() { guard let window = window else { return } + // Setting all three of these is required for restoration to work. + window.isRestorable = true + window.restorationClass = TerminalWindowRestoration.self + window.identifier = .init(String(describing: TerminalWindowRestoration.self)) + // If window decorations are disabled, remove our title if (!ghostty.windowDecorations) { window.styleMask.remove(.titled) } @@ -250,6 +255,16 @@ class TerminalController: NSWindowController, NSWindowDelegate, self.relabelTabs() } + func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { + let data = TerminalRestorableState() + data.encode(with: state) + AppDelegate.logger.warning("state window del encode") + } + + func window(_ window: NSWindow, didDecodeRestorableState state: NSCoder) { + AppDelegate.logger.warning("state window del restore") + } + //MARK: - First Responder @IBAction func newWindow(_ sender: Any?) { diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 24e742971..b5c76a8fd 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -139,7 +139,7 @@ class TerminalManager { } /// Creates a window controller, adds it to our managed list, and returns it. - private func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration?) -> TerminalController { + func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration?) -> TerminalController { // Initialize our controller to load the window let c = TerminalController(ghostty, withBaseConfig: base) @@ -162,7 +162,7 @@ class TerminalManager { return c } - private func removeWindow(_ controller: TerminalController) { + func removeWindow(_ controller: TerminalController) { // Remove it from our managed set guard let idx = self.windows.firstIndex(where: { $0.controller == controller }) else { return } let w = self.windows[idx] diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift new file mode 100644 index 000000000..6894bcb92 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -0,0 +1,77 @@ +import Cocoa + +/// The state stored for terminal window restoration. +class TerminalRestorableState: NSObject, NSSecureCoding { + public static var supportsSecureCoding = true + + static let coderKey = "state" + static let versionKey = "version" + static let version: Int = 1 + + override init() { + super.init() + } + + required 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 + } + } + + func encode(with coder: NSCoder) { + coder.encode(Self.version, forKey: Self.versionKey) + coder.encode(self, forKey: Self.coderKey) + } +} + +/// 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 { + enum RestoreError: Error { + case delegateInvalid + case identifierUnknown + case stateDecodeFailed + case windowDidNotLoad + } + + 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, RestoreError.identifierUnknown) + 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, RestoreError.stateDecodeFailed) + 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, RestoreError.delegateInvalid) + 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, RestoreError.windowDidNotLoad) + return + } + + completionHandler(window, nil) + AppDelegate.logger.warning("state RESTORE") + } +}