mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #1161 from mitchellh/new-surface-uuid
macOS: restore focused split on app state restore
This commit is contained in:
@ -52,7 +52,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
|
|
||||||
// Initialize our initial surface.
|
// Initialize our initial surface.
|
||||||
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
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
|
// Setup our notifications for behaviors
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
|
@ -4,11 +4,13 @@ import Cocoa
|
|||||||
class TerminalRestorableState: Codable {
|
class TerminalRestorableState: Codable {
|
||||||
static let selfKey = "state"
|
static let selfKey = "state"
|
||||||
static let versionKey = "version"
|
static let versionKey = "version"
|
||||||
static let version: Int = 1
|
static let version: Int = 2
|
||||||
|
|
||||||
|
let focusedSurface: String?
|
||||||
let surfaceTree: Ghostty.SplitNode?
|
let surfaceTree: Ghostty.SplitNode?
|
||||||
|
|
||||||
init(from controller: TerminalController) {
|
init(from controller: TerminalController) {
|
||||||
|
self.focusedSurface = controller.focusedSurface?.uuid.uuidString
|
||||||
self.surfaceTree = controller.surfaceTree
|
self.surfaceTree = controller.surfaceTree
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +27,7 @@ class TerminalRestorableState: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.surfaceTree = v.value.surfaceTree
|
self.surfaceTree = v.value.surfaceTree
|
||||||
|
self.focusedSurface = v.value.focusedSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(with coder: NSCoder) {
|
func encode(with coder: NSCoder) {
|
||||||
@ -86,7 +89,31 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
|||||||
|
|
||||||
// Setup our restored state on the controller
|
// Setup our restored state on the controller
|
||||||
c.surfaceTree = state.surfaceTree
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
// MARK: - Equatable
|
||||||
|
|
||||||
static func == (lhs: SplitNode, rhs: SplitNode) -> Bool {
|
static func == (lhs: SplitNode, rhs: SplitNode) -> Bool {
|
||||||
@ -121,9 +137,9 @@ extension Ghostty {
|
|||||||
weak var parent: SplitNode.Container?
|
weak var parent: SplitNode.Container?
|
||||||
|
|
||||||
/// Initialize a new leaf which creates a new terminal surface.
|
/// 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: UUID? = nil) {
|
||||||
self.app = app
|
self.app = app
|
||||||
self.surface = SurfaceView(app, baseConfig)
|
self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Hashable
|
// MARK: - Hashable
|
||||||
@ -143,6 +159,7 @@ extension Ghostty {
|
|||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case pwd
|
case pwd
|
||||||
|
case uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
required convenience init(from decoder: Decoder) throws {
|
required convenience init(from decoder: Decoder) throws {
|
||||||
@ -154,15 +171,17 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let uuid = UUID(uuidString: try container.decode(String.self, forKey: .uuid))
|
||||||
var config = SurfaceConfiguration()
|
var config = SurfaceConfiguration()
|
||||||
config.workingDirectory = try container.decode(String?.self, forKey: .pwd)
|
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 {
|
func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(surface.pwd, forKey: .pwd)
|
try container.encode(surface.pwd, forKey: .pwd)
|
||||||
|
try container.encode(surface.uuid.uuidString, forKey: .uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +209,7 @@ extension Ghostty {
|
|||||||
// state since this is a new split.
|
// state since this is a new split.
|
||||||
self.topLeft = .leaf(from)
|
self.topLeft = .leaf(from)
|
||||||
|
|
||||||
let bottomRight: Leaf = .init(app, baseConfig)
|
let bottomRight: Leaf = .init(app, baseConfig: baseConfig)
|
||||||
self.bottomRight = .leaf(bottomRight)
|
self.bottomRight = .leaf(bottomRight)
|
||||||
|
|
||||||
from.parent = self
|
from.parent = self
|
||||||
|
@ -25,7 +25,7 @@ extension Ghostty {
|
|||||||
@StateObject private var surfaceView: SurfaceView
|
@StateObject private var surfaceView: SurfaceView
|
||||||
|
|
||||||
init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) {
|
init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) {
|
||||||
_surfaceView = StateObject(wrappedValue: SurfaceView(app, nil))
|
_surfaceView = StateObject(wrappedValue: SurfaceView(app))
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,8 +259,13 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Surface View
|
||||||
|
|
||||||
/// The NSView implementation for a terminal surface.
|
/// The NSView implementation for a terminal surface.
|
||||||
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
|
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
|
||||||
|
/// Unique ID per surface
|
||||||
|
let uuid: UUID
|
||||||
|
|
||||||
// The current title of the surface as defined by the pty. This can be
|
// 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
|
// changed with escape codes. This is public because the callbacks go
|
||||||
// to the app level and it is set from there.
|
// to the app level and it is set from there.
|
||||||
@ -342,8 +347,9 @@ extension Ghostty {
|
|||||||
case pendingHidden
|
case pendingHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ app: ghostty_app_t, _ baseConfig: SurfaceConfiguration?) {
|
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||||
self.markedText = NSMutableAttributedString()
|
self.markedText = NSMutableAttributedString()
|
||||||
|
self.uuid = uuid ?? .init()
|
||||||
|
|
||||||
// Initialize with some default frame size. The important thing is that this
|
// 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
|
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||||
|
Reference in New Issue
Block a user