From a321ef515d24a31e14c71a0640d8421ca02d4bbf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Dec 2023 14:24:05 -0800 Subject: [PATCH 1/3] macos: assign unique UUID per surface, store in app state --- .../Features/Terminal/TerminalController.swift | 2 +- macos/Sources/Ghostty/Ghostty.SplitNode.swift | 11 +++++++---- macos/Sources/Ghostty/SurfaceView.swift | 10 ++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index e2ba359ad..906f78ee2 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -52,7 +52,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, // Initialize our initial surface. guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } - self.surfaceTree = .leaf(.init(ghostty_app, base)) + self.surfaceTree = .leaf(.init(ghostty_app, baseConfig: base)) // Setup our notifications for behaviors let center = NotificationCenter.default diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift index 35240b5fa..4e87feda0 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitNode.swift @@ -121,9 +121,9 @@ extension Ghostty { weak var parent: SplitNode.Container? /// Initialize a new leaf which creates a new terminal surface. - init(_ app: ghostty_app_t, _ baseConfig: SurfaceConfiguration?) { + init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: NSUUID? = nil) { self.app = app - self.surface = SurfaceView(app, baseConfig) + self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid) } // MARK: - Hashable @@ -143,6 +143,7 @@ extension Ghostty { enum CodingKeys: String, CodingKey { case pwd + case uuid } required convenience init(from decoder: Decoder) throws { @@ -154,15 +155,17 @@ extension Ghostty { } let container = try decoder.container(keyedBy: CodingKeys.self) + let uuid = NSUUID(uuidString: try container.decode(String.self, forKey: .uuid)) var config = SurfaceConfiguration() config.workingDirectory = try container.decode(String?.self, forKey: .pwd) - self.init(app, config) + self.init(app, baseConfig: config, uuid: uuid) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(surface.pwd, forKey: .pwd) + try container.encode(surface.uuid.uuidString, forKey: .uuid) } } @@ -190,7 +193,7 @@ extension Ghostty { // state since this is a new split. self.topLeft = .leaf(from) - let bottomRight: Leaf = .init(app, baseConfig) + let bottomRight: Leaf = .init(app, baseConfig: baseConfig) self.bottomRight = .leaf(bottomRight) from.parent = self diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 6ad8122a6..b53c42c94 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -25,7 +25,7 @@ extension Ghostty { @StateObject private var surfaceView: SurfaceView init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) { - _surfaceView = StateObject(wrappedValue: SurfaceView(app, nil)) + _surfaceView = StateObject(wrappedValue: SurfaceView(app)) self.content = content } @@ -259,8 +259,13 @@ extension Ghostty { } } + // MARK: - Surface View + /// The NSView implementation for a terminal surface. class SurfaceView: NSView, NSTextInputClient, ObservableObject { + /// Unique ID per surface + let uuid: NSUUID + // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go // to the app level and it is set from there. @@ -342,8 +347,9 @@ extension Ghostty { case pendingHidden } - init(_ app: ghostty_app_t, _ baseConfig: SurfaceConfiguration?) { + init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: NSUUID? = nil) { self.markedText = NSMutableAttributedString() + self.uuid = uuid ?? .init() // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer From 98041940f051b1af0d934164e76f48fa733b038f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Dec 2023 14:45:27 -0800 Subject: [PATCH 2/3] macos: restore focused split on app restore --- .../Terminal/TerminalRestorable.swift | 27 +++++++++++++++++++ macos/Sources/Ghostty/Ghostty.SplitNode.swift | 20 ++++++++++++-- macos/Sources/Ghostty/SurfaceView.swift | 4 +-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 68695f5ee..642b1679f 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -6,9 +6,11 @@ class TerminalRestorableState: Codable { 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 } @@ -25,6 +27,7 @@ class TerminalRestorableState: Codable { } self.surfaceTree = v.value.surfaceTree + self.focusedSurface = v.value.focusedSurface } func encode(with coder: NSCoder) { @@ -86,7 +89,31 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // 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) + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift index 4e87feda0..d9403b62f 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitNode.swift @@ -101,6 +101,22 @@ extension Ghostty { } } + /// Find a surface view by UUID. + func findUUID(uuid: UUID) -> SurfaceView? { + switch (self) { + case .leaf(let leaf): + if (leaf.surface.uuid == uuid) { + return leaf.surface + } + + return nil + + case .split(let container): + return container.topLeft.findUUID(uuid: uuid) ?? + container.bottomRight.findUUID(uuid: uuid) + } + } + // MARK: - Equatable static func == (lhs: SplitNode, rhs: SplitNode) -> Bool { @@ -121,7 +137,7 @@ extension Ghostty { weak var parent: SplitNode.Container? /// Initialize a new leaf which creates a new terminal surface. - init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: NSUUID? = nil) { + init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { self.app = app self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid) } @@ -155,7 +171,7 @@ extension Ghostty { } let container = try decoder.container(keyedBy: CodingKeys.self) - let uuid = NSUUID(uuidString: try container.decode(String.self, forKey: .uuid)) + let uuid = UUID(uuidString: try container.decode(String.self, forKey: .uuid)) var config = SurfaceConfiguration() config.workingDirectory = try container.decode(String?.self, forKey: .pwd) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index b53c42c94..0ebcf5a15 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -264,7 +264,7 @@ extension Ghostty { /// The NSView implementation for a terminal surface. class SurfaceView: NSView, NSTextInputClient, ObservableObject { /// Unique ID per surface - let uuid: NSUUID + let uuid: UUID // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go @@ -347,7 +347,7 @@ extension Ghostty { case pendingHidden } - init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: NSUUID? = nil) { + init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { self.markedText = NSMutableAttributedString() self.uuid = uuid ?? .init() From 2aa7f124173cd19f652bccaf6d6ab35c14a66dd2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Dec 2023 14:45:39 -0800 Subject: [PATCH 3/3] macos: increment restorable state version --- macos/Sources/Features/Terminal/TerminalRestorable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 642b1679f..b6d01890b 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -4,7 +4,7 @@ import Cocoa class TerminalRestorableState: Codable { static let selfKey = "state" static let versionKey = "version" - static let version: Int = 1 + static let version: Int = 2 let focusedSurface: String? let surfaceTree: Ghostty.SplitNode?