mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-24 05:40:15 +00:00
macOS: fix App Icon update in Finders
Looks like `NSWorkspace.shared.setIcon` can only be called from the main App, DockTilePlugin is sandboxed and doesn't have the permission to `file-write-finderinfo`. It works fine in debug, but not in release. This fixes #11489, #11290
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user