From 2caa8a3fe117121bd91ec86ccf1b9ec64e3f5688 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 May 2025 14:54:59 -0700 Subject: [PATCH] macOS: move window title handling fully to AppKit Fixes #7236 Supersedes #7249 This removes all of our `focusedValue`-based tracking of the surface title and moves it completely to the window controller. The window controller now sets up event listeners (via Combine) when the focused surface changes and updates the window title accordingly. There is some complicated logic here to handle when we lose focus to something other than a surface. In this case, we want our title to be the last focused surface so long as it exists. --- .../Terminal/BaseTerminalController.swift | 23 +++++++++++++++++++ .../Features/Terminal/TerminalView.swift | 15 ------------ .../Ghostty/Ghostty.TerminalSplit.swift | 3 --- macos/Sources/Ghostty/InspectorView.swift | 1 - macos/Sources/Ghostty/SurfaceView.swift | 12 ---------- 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 98e3b87f9..62384586a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1,5 +1,6 @@ import Cocoa import SwiftUI +import Combine import GhosttyKit /// A base class for windows that can contain Ghostty windows. This base class implements @@ -71,6 +72,9 @@ class BaseTerminalController: NSWindowController, /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + /// The cancellables related to our focused surface. + private var focusedSurfaceCancellables: Set = [] + struct SavedFrame { let window: NSRect let screen: NSRect @@ -286,7 +290,26 @@ class BaseTerminalController: NSWindowController, func surfaceTreeDidChange() {} func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { + let lastFocusedSurface = focusedSurface focusedSurface = to + + // Important to cancel any prior subscriptions + focusedSurfaceCancellables = [] + + // Setup our title listener. If we have a focused surface we always use that. + // Otherwise, we try to use our last focused surface. In either case, we only + // want to care if the surface is in the tree so we don't listen to titles of + // closed surfaces. + if let titleSurface = focusedSurface ?? lastFocusedSurface, + surfaceTree?.contains(view: titleSurface) ?? false { + // If we have a surface, we want to listen for title changes. + titleSurface.$title + .sink { [weak self] in self?.titleDidChange(to: $0) } + .store(in: &focusedSurfaceCancellables) + } else { + // There is no surface to listen to titles for. + titleDidChange(to: "👻") + } } func titleDidChange(to: String) { diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 1178c75a5..7caceb071 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -8,9 +8,6 @@ protocol TerminalViewDelegate: AnyObject { /// Called when the currently focused surface changed. This can be nil. func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) - /// The title of the terminal should change. - func titleDidChange(to: String) - /// The URL of the pwd should change. func pwdDidChange(to: URL?) @@ -59,19 +56,10 @@ struct TerminalView: View { // Various state values sent back up from the currently focused terminals. @FocusedValue(\.ghosttySurfaceView) private var focusedSurface - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle @FocusedValue(\.ghosttySurfacePwd) private var surfacePwd @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize - // The title for our window - private var title: String { - if let surfaceTitle, !surfaceTitle.isEmpty { - return surfaceTitle - } - return "👻" - } - // The pwd of the focused surface as a URL private var pwdURL: URL? { guard let surfacePwd, surfacePwd != "" else { return nil } @@ -105,9 +93,6 @@ struct TerminalView: View { self.delegate?.focusedSurfaceDidChange(to: newValue) } } - .onChange(of: title) { newValue in - self.delegate?.titleDidChange(to: newValue) - } .onChange(of: pwdURL) { newValue in self.delegate?.pwdDidChange(to: newValue) } diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index 127c925e1..3e942d774 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -45,8 +45,6 @@ extension Ghostty { /// this one. @Binding var zoomedSurface: SurfaceView? - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? - var body: some View { let center = NotificationCenter.default let pubZoom = center.publisher(for: Notification.didToggleSplitZoom) @@ -77,7 +75,6 @@ extension Ghostty { .onReceive(pubZoom) { onZoom(notification: $0) } } } - .navigationTitle(surfaceTitle ?? "Ghostty") .id(node) // Needed for change detection on node } else { // On these events we want to reset the split state and call it. diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index b6147647e..a6e80bd47 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -31,7 +31,6 @@ extension Ghostty { }, right: { InspectorViewRepresentable(surfaceView: surfaceView) .focused($inspectorFocus) - .focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceView, surfaceView) }) } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 3b9c10067..1e9a4cfef 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -6,14 +6,12 @@ extension Ghostty { /// Render a terminal for the active app in the environment. struct Terminal: View { @EnvironmentObject private var ghostty: Ghostty.App - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? var body: some View { if let app = self.ghostty.app { SurfaceForApp(app) { surfaceView in SurfaceWrapper(surfaceView: surfaceView) } - .navigationTitle(surfaceTitle ?? "Ghostty") } } } @@ -83,7 +81,6 @@ extension Ghostty { Surface(view: surfaceView, size: geo.size) .focused($surfaceFocus) - .focusedValue(\.ghosttySurfaceTitle, title) .focusedValue(\.ghosttySurfacePwd, surfaceView.pwd) .focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) @@ -496,15 +493,6 @@ extension FocusedValues { typealias Value = Ghostty.SurfaceView } - var ghosttySurfaceTitle: String? { - get { self[FocusedGhosttySurfaceTitle.self] } - set { self[FocusedGhosttySurfaceTitle.self] = newValue } - } - - struct FocusedGhosttySurfaceTitle: FocusedValueKey { - typealias Value = String - } - var ghosttySurfacePwd: String? { get { self[FocusedGhosttySurfacePwd.self] } set { self[FocusedGhosttySurfacePwd.self] = newValue }