macos: encode surface tree in state restore

This commit is contained in:
Mitchell Hashimoto
2023-12-23 12:59:45 -08:00
parent a5d249eb48
commit 2390668834
7 changed files with 144 additions and 32 deletions

View File

@ -47,6 +47,7 @@
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 */; };
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.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 */; };
@ -97,6 +98,7 @@
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = "<group>"; };
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = "<group>"; };
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
@ -153,6 +155,7 @@
isa = PBXGroup;
children = (
A5CEAFFE29C2410700646FDA /* Backport.swift */,
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
A59630962AEE163600D64628 /* HostingWindow.swift */,
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
@ -369,6 +372,7 @@
files = (
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,

View File

@ -58,7 +58,7 @@ class AppDelegate: NSObject,
private var dockMenu: NSMenu = NSMenu()
/// The ghostty global state. Only one per process.
private let ghostty: Ghostty.AppState = Ghostty.AppState()
let ghostty: Ghostty.AppState = Ghostty.AppState()
/// Manages our terminal windows.
let terminalManager: TerminalManager

View File

@ -255,14 +255,11 @@ class TerminalController: NSWindowController, NSWindowDelegate,
self.relabelTabs()
}
// Called when the window will be encoded. We handle the data encoding here in the
// window controller.
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
let data = TerminalRestorableState()
let data = TerminalRestorableState(from: self)
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

View File

@ -1,42 +1,48 @@
import Cocoa
/// The state stored for terminal window restoration.
class TerminalRestorableState: NSObject, NSSecureCoding {
public static var supportsSecureCoding = true
static let coderKey = "state"
class TerminalRestorableState: Codable {
static let selfKey = "state"
static let versionKey = "version"
static let version: Int = 1
static let version: Int = 3
override init() {
super.init()
let surfaceTree: Ghostty.SplitNode?
init(from controller: TerminalController) {
self.surfaceTree = controller.surfaceTree
}
required init?(coder aDecoder: NSCoder) {
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
}
func encode(with coder: NSCoder) {
coder.encode(Self.version, forKey: Self.versionKey)
coder.encode(self, forKey: Self.coderKey)
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 {
enum RestoreError: Error {
case delegateInvalid
case identifierUnknown
case stateDecodeFailed
case windowDidNotLoad
}
static func restoreWindow(
withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
@ -44,14 +50,14 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
) {
// Verify the identifier is what we expect
guard identifier == .init(String(describing: Self.self)) else {
completionHandler(nil, RestoreError.identifierUnknown)
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, RestoreError.delegateInvalid)
completionHandler(nil, TerminalRestoreError.delegateInvalid)
return
}
@ -64,7 +70,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
// 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)
completionHandler(nil, TerminalRestoreError.stateDecodeFailed)
return
}
@ -74,11 +80,13 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
// be.
let c = appDelegate.terminalManager.createWindow(withBaseConfig: nil)
guard let window = c.window else {
completionHandler(nil, RestoreError.windowDidNotLoad)
completionHandler(nil, TerminalRestoreError.windowDidNotLoad)
return
}
// Setup our restored state on the controller
c.surfaceTree = state.surfaceTree
completionHandler(window, nil)
AppDelegate.logger.warning("state RESTORE")
}
}

View File

@ -11,10 +11,33 @@ extension Ghostty {
/// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These
/// values can further be split infinitely.
///
enum SplitNode: Equatable, Hashable {
enum SplitNode: Equatable, Hashable, Codable {
case leaf(Leaf)
case split(Container)
/// The parent of this node.
var parent: Container? {
get {
switch (self) {
case .leaf(let leaf):
return leaf.parent
case .split(let container):
return container.parent
}
}
set {
switch (self) {
case .leaf(let leaf):
leaf.parent = newValue
case .split(let container):
container.parent = newValue
}
}
}
/// Returns the view that would prefer receiving focus in this tree. This is always the
/// top-left-most view. This is used when creating a split or closing a split to find the
/// next view to send focus to.
@ -91,7 +114,7 @@ extension Ghostty {
}
}
class Leaf: ObservableObject, Equatable, Hashable {
class Leaf: ObservableObject, Equatable, Hashable, Codable {
let app: ghostty_app_t
@Published var surface: SurfaceView
@ -115,9 +138,27 @@ extension Ghostty {
static func == (lhs: Leaf, rhs: Leaf) -> Bool {
return lhs.app == rhs.app && lhs.surface === rhs.surface
}
// MARK: - Codable
required convenience init(from decoder: Decoder) throws {
// Decoding uses the global Ghostty app
guard let del = NSApplication.shared.delegate,
let appDel = del as? AppDelegate,
let app = appDel.ghostty.app else {
throw TerminalRestoreError.delegateInvalid
}
self.init(app, nil)
}
func encode(to encoder: Encoder) throws {
// We don't currently encode anything, but in the future we will
// want to encode pwd, etc...
}
}
class Container: ObservableObject, Equatable, Hashable {
class Container: ObservableObject, Equatable, Hashable, Codable {
let app: ghostty_app_t
let direction: SplitViewDirection
@ -213,6 +254,43 @@ extension Ghostty {
lhs.topLeft == rhs.topLeft &&
lhs.bottomRight == rhs.bottomRight
}
// MARK: - Codable
enum CodingKeys: String, CodingKey {
case direction
case split
case topLeft
case bottomRight
}
required init(from decoder: Decoder) throws {
// Decoding uses the global Ghostty app
guard let del = NSApplication.shared.delegate,
let appDel = del as? AppDelegate,
let app = appDel.ghostty.app else {
throw TerminalRestoreError.delegateInvalid
}
let container = try decoder.container(keyedBy: CodingKeys.self)
self.app = app
self.direction = try container.decode(SplitViewDirection.self, forKey: .direction)
self.split = try container.decode(CGFloat.self, forKey: .split)
self.topLeft = try container.decode(SplitNode.self, forKey: .topLeft)
self.bottomRight = try container.decode(SplitNode.self, forKey: .bottomRight)
// Fix up the parent references
self.topLeft.parent = self
self.bottomRight.parent = self
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(direction, forKey: .direction)
try container.encode(split, forKey: .split)
try container.encode(topLeft, forKey: .topLeft)
try container.encode(bottomRight, forKey: .bottomRight)
}
}
/// This keeps track of the "neighbors" of a split: the immediately above/below/left/right

View File

@ -0,0 +1,25 @@
import Cocoa
/// A wrapper that allows a Swift Codable to implement NSSecureCoding.
class CodableBridge<Wrapped: Codable>: NSObject, NSSecureCoding {
let value: Wrapped
init(_ value: Wrapped) { self.value = value }
static var supportsSecureCoding: Bool { return true }
required init?(coder aDecoder: NSCoder) {
// TODO: This outputs a warning with deprecation on decode. I don't know how to
// fix that yet but there must be something we can change with the encode/decode here
// to resolve it.
guard let data = aDecoder.decodeData() else { return nil }
guard let archiver = try? NSKeyedUnarchiver(forReadingFrom: data) else { return nil }
guard let value = archiver.decodeDecodable(Wrapped.self, forKey: "value") else { return nil }
self.value = value
}
func encode(with aCoder: NSCoder) {
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
try? archiver.encodeEncodable(value, forKey: "value")
aCoder.encode(archiver.encodedData)
}
}

View File

@ -163,6 +163,6 @@ struct SplitView<L: View, R: View>: View {
}
}
enum SplitViewDirection {
enum SplitViewDirection: Codable {
case horizontal, vertical
}