diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index f85f7ddf2..8c787d614 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -151,8 +151,7 @@ class AppDelegate: NSObject, /// Signals private var signals: [DispatchSourceSignal] = [] - /// The custom app icon image that is currently in use. - @Published private(set) var appIcon: NSImage? + private let appIconUpdater = AppIconUpdater() @MainActor private lazy var menuShortcutManager = Ghostty.MenuShortcutManager() @@ -847,13 +846,8 @@ class AppDelegate: NSObject, } private func updateAppIcon(from config: Ghostty.Config) { - // Since this is called after `DockTilePlugin` has been running, - // clean it up here to trigger a correct update of the current config. - UserDefaults.ghostty.removeObject(forKey: "CustomGhosttyIcon") - DispatchQueue.global().async { - UserDefaults.ghostty.appIcon = AppIcon(config: config) - DistributedNotificationCenter.default() - .postNotificationName(.ghosttyIconDidChange, object: nil, userInfo: nil, deliverImmediately: true) + Task.detached { + await self.appIconUpdater.update(icon: AppIcon(config: config)) } } diff --git a/macos/Sources/Features/Custom App Icon/AppIcon.swift b/macos/Sources/Features/Custom App Icon/AppIcon.swift index 13c6b83a1..d2bdd5b0a 100644 --- a/macos/Sources/Features/Custom App Icon/AppIcon.swift +++ b/macos/Sources/Features/Custom App Icon/AppIcon.swift @@ -2,7 +2,7 @@ import AppKit import System /// The icon style for the Ghostty App. -enum AppIcon: Equatable, Codable { +enum AppIcon: Equatable, Codable, Sendable { case official case blueprint case chalkboard @@ -84,3 +84,26 @@ enum AppIcon: Equatable, Codable { } } } + +#if !DOCK_TILE_PLUGIN +/// Making sure that `NSWorkspace.shared.setIcon` executes on only one thread at a time +actor AppIconUpdater { + func update(icon: AppIcon?) { + UserDefaults.ghostty.appIcon = icon + // Notify DockTilePlugin to update dock icon + DistributedNotificationCenter.default() + .postNotificationName( + .ghosttyIconDidChange, + object: nil, + userInfo: nil, + deliverImmediately: true, + ) + + NSWorkspace.shared.setIcon( + icon?.image(in: .main), + forFile: Bundle.main.bundlePath, + ) + NSWorkspace.shared.noteFileSystemChanged(Bundle.main.bundlePath) + } +} +#endif diff --git a/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconImage.swift b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconImage.swift index 8a461699f..d7bae0439 100644 --- a/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconImage.swift +++ b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconImage.swift @@ -4,12 +4,6 @@ extension View { /// Returns the ghostty icon to use for views. func ghosttyIconImage() -> Image { #if os(macOS) - // If we have a specific icon set, then use that - if let delegate = NSApplication.shared.delegate as? AppDelegate, - let nsImage = delegate.appIcon { - return Image(nsImage: nsImage) - } - // Grab the icon from the running application. This is the best way // I've found so far to get the proper icon for our current icon // tinting and so on with macOS Tahoe diff --git a/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift b/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift index de0661cb2..0b2d528e7 100644 --- a/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift +++ b/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift @@ -17,32 +17,6 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn { private var iconChangeObserver: Any? - /// The URL to the enclosing app bundle, determined from the plugin bundle path. - var ghosttyAppURL: URL? { - Self.appBundleURL(for: pluginBundle.bundleURL) - } - - /// Determine the enclosing app bundle for the dock tile plugin bundle. - /// - /// We intentionally avoid matching a specific bundle name (such as - /// "Ghostty.app") so renaming the app in Finder still works. - static func appBundleURL(for pluginBundleURL: URL) -> URL? { - var url = pluginBundleURL - while true { - if url.pathExtension.compare("app", options: .caseInsensitive) == .orderedSame { - return url - } - - let parent = url.deletingLastPathComponent() - if parent.path == url.path { - // Safety stop: this should only happen at filesystem root. - return nil - } - - url = parent - } - } - /// The primary NSDockTilePlugin function. func setDockTile(_ dockTile: NSDockTile?) { // If no dock tile or no access to Ghostty defaults, we can't do anything. @@ -70,25 +44,13 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn { return } - if let appBundleURL = self.ghosttyAppURL { - let appBundlePath = appBundleURL.path - NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath) - NSWorkspace.shared.noteFileSystemChanged(appBundlePath) - } - dockTile.setIcon(appIcon) } /// Reset the application icon and dock tile icon to the default. private func resetIcon(dockTile: NSDockTile) { - let appBundlePath = self.ghosttyAppURL?.path let appIcon: NSImage? if #available(macOS 26.0, *) { - // Reset to the default (glassy) icon. - if let appBundlePath { - NSWorkspace.shared.setIcon(nil, forFile: appBundlePath) - } - #if DEBUG // Use the `Blueprint` icon to distinguish Debug from Release builds. appIcon = pluginBundle.image(forResource: "BlueprintImage")! @@ -99,14 +61,6 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn { } else { // Use the bundled icon to keep the corner radius consistent with pre-Tahoe apps. appIcon = pluginBundle.image(forResource: "AppIconImage")! - if let appBundlePath { - NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath) - } - } - - // Notify Finder/Dock so icon caches refresh immediately. - if let appBundlePath { - NSWorkspace.shared.noteFileSystemChanged(appBundlePath) } dockTile.setIcon(appIcon) } diff --git a/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift b/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift index e492f1a77..c0c71159d 100644 --- a/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift +++ b/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift @@ -1,5 +1,8 @@ import AppKit extension Notification.Name { + /// Distributed Notification for DockTilePlugin to update icon + /// + /// Ghostty -> DockTilePlugin static let ghosttyIconDidChange = Notification.Name("com.mitchellh.ghostty.iconDidChange") }