From fe3dab9467b1b6f5de70afea1e22c374463bb477 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 3 Sep 2025 07:27:12 -0700 Subject: [PATCH] macOS: SurfaceView should implement Identifiable This has no meaningful functionality yet, it was one of the paths I was looking at for #8505 but didn't pursue further. But I still think that this makes more sense in general for the macOS app and will likely be more useful later. --- macos/Sources/App/macOS/AppDelegate.swift | 2 +- .../App Intents/Entities/TerminalEntity.swift | 6 ++--- macos/Sources/Features/Splits/SplitTree.swift | 26 ++++++++++++++++++- .../Terminal/TerminalController.swift | 4 +-- .../Terminal/TerminalRestorable.swift | 4 +-- .../Sources/Ghostty/SurfaceView_AppKit.swift | 12 +++++---- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 310a46d6c..f8cf95de2 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -937,7 +937,7 @@ class AppDelegate: NSObject, func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in TerminalController.all { for view in c.surfaceTree { - if view.uuid == uuid { + if view.id == uuid { return view } } diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 974f1b07f..e805466a2 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -34,7 +34,7 @@ struct TerminalEntity: AppEntity { /// Returns the view associated with this entity. This may no longer exist. @MainActor var surfaceView: Ghostty.SurfaceView? { - Self.defaultQuery.all.first { $0.uuid == self.id } + Self.defaultQuery.all.first { $0.id == self.id } } @MainActor @@ -46,7 +46,7 @@ struct TerminalEntity: AppEntity { @MainActor init(_ view: Ghostty.SurfaceView) { - self.id = view.uuid + self.id = view.id self.title = view.title self.workingDirectory = view.pwd if let nsImage = ImageRenderer(content: view.screenshot()).nsImage { @@ -80,7 +80,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { @MainActor func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] { return all.filter { - identifiers.contains($0.uuid) + identifiers.contains($0.id) }.map { TerminalEntity($0) } diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 53adf1dc2..23b597591 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -1,7 +1,7 @@ import AppKit /// SplitTree represents a tree of views that can be divided. -struct SplitTree { +struct SplitTree { /// The root of the tree. This can be nil to indicate the tree is empty. let root: Node? @@ -127,6 +127,13 @@ extension SplitTree { root: try root.insert(view: view, at: at, direction: direction), zoomed: nil) } + /// Find a node containing a view with the specified ID. + /// - Parameter id: The ID of the view to find + /// - Returns: The node containing the view if found, nil otherwise + func find(id: ViewType.ID) -> Node? { + guard let root else { return nil } + return root.find(id: id) + } /// Remove a node from the tree. If the node being removed is part of a split, /// the sibling node takes the place of the parent split. @@ -396,6 +403,23 @@ extension SplitTree.Node { typealias SplitError = SplitTree.SplitError typealias Path = SplitTree.Path + /// Find a node containing a view with the specified ID. + /// - Parameter id: The ID of the view to find + /// - Returns: The node containing the view if found, nil otherwise + func find(id: ViewType.ID) -> Node? { + switch self { + case .leaf(let view): + return view.id == id ? self : nil + + case .split(let split): + if let found = split.left.find(id: id) { + return found + } + + return split.right.find(id: id) + } + } + /// Returns the node in the tree that contains the given view. func node(view: ViewType) -> Node? { switch (self) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cadbb40e0..bdf3abeb6 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -860,7 +860,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, - let focusTarget = surfaceTree.first(where: { $0.uuid == focusedUUID }) { + let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) { DispatchQueue.main.async { Ghostty.moveFocus(to: focusTarget, from: nil) } @@ -875,7 +875,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return .init( frame: window.frame, surfaceTree: surfaceTree, - focusedSurface: focusedSurface?.uuid, + focusedSurface: focusedSurface?.id, tabIndex: window.tabGroup?.windows.firstIndex(of: window), tabGroup: window.tabGroup) } diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 7bad563ab..1e640967e 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -10,7 +10,7 @@ class TerminalRestorableState: Codable { let surfaceTree: SplitTree init(from controller: TerminalController) { - self.focusedSurface = controller.focusedSurface?.uuid.uuidString + self.focusedSurface = controller.focusedSurface?.id.uuidString self.surfaceTree = controller.surfaceTree } @@ -96,7 +96,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { if let focusedStr = state.focusedSurface { var foundView: Ghostty.SurfaceView? for view in c.surfaceTree { - if view.uuid.uuidString == focusedStr { + if view.id.uuidString == focusedStr { foundView = view break } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index eef4bccb3..1c5c8eb6a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -6,9 +6,11 @@ import GhosttyKit extension Ghostty { /// The NSView implementation for a terminal surface. - class SurfaceView: OSView, ObservableObject, Codable { + class SurfaceView: OSView, ObservableObject, Codable, Identifiable { + typealias ID = UUID + /// Unique ID per surface - let uuid: UUID + let id: 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 @@ -180,7 +182,7 @@ extension Ghostty { init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { self.markedText = NSMutableAttributedString() - self.uuid = uuid ?? .init() + self.id = uuid ?? .init() // Our initial config always is our application wide config. if let appDelegate = NSApplication.shared.delegate as? AppDelegate { @@ -1468,7 +1470,7 @@ extension Ghostty { content.body = body content.sound = UNNotificationSound.default content.categoryIdentifier = Ghostty.userNotificationCategory - content.userInfo = ["surface": self.uuid.uuidString] + content.userInfo = ["surface": self.id.uuidString] let uuid = UUID().uuidString let request = UNNotificationRequest( @@ -1576,7 +1578,7 @@ extension Ghostty { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(pwd, forKey: .pwd) - try container.encode(uuid.uuidString, forKey: .uuid) + try container.encode(id.uuidString, forKey: .uuid) try container.encode(title, forKey: .title) try container.encode(titleFromTerminal != nil, forKey: .isUserSetTitle) }