import AppKit
|
import Foundation
|
|
@MainActor
|
public protocol ApplicationIconResolving {
|
func applicationURL(forBundleIdentifier bundleIdentifier: String) -> URL?
|
func icon(forFile path: String) -> NSImage
|
func fallbackApplicationIcon() -> NSImage
|
}
|
|
@MainActor
|
public final class WorkspaceApplicationIconResolver: ApplicationIconResolving {
|
public init() {}
|
|
public func applicationURL(forBundleIdentifier bundleIdentifier: String) -> URL? {
|
NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier)
|
}
|
|
public func icon(forFile path: String) -> NSImage {
|
NSWorkspace.shared.icon(forFile: path)
|
}
|
|
public func fallbackApplicationIcon() -> NSImage {
|
NSWorkspace.shared.icon(for: .applicationBundle)
|
}
|
}
|
|
@MainActor
|
public final class WorkspaceAppIconProvider: AppIconProviderProtocol {
|
private let resolver: any ApplicationIconResolving
|
private var iconCache: [String: NSImage] = [:]
|
|
public init(resolver: any ApplicationIconResolving = WorkspaceApplicationIconResolver()) {
|
self.resolver = resolver
|
}
|
|
public func icon(for app: AlignerApp) -> NSImage {
|
let key = cacheKey(for: app)
|
if let cachedIcon = iconCache[key] {
|
return cachedIcon
|
}
|
|
let icon = resolvedIcon(for: app)
|
iconCache[key] = icon
|
return icon
|
}
|
|
public func fallbackIcon() -> NSImage {
|
resolver.fallbackApplicationIcon()
|
}
|
|
private func resolvedIcon(for app: AlignerApp) -> NSImage {
|
if let applicationURL = resolver.applicationURL(forBundleIdentifier: app.bundleIdentifier) {
|
let icon = resolver.icon(forFile: applicationURL.path)
|
if icon.isValid {
|
return icon
|
}
|
}
|
|
if let executablePath = app.executablePath {
|
let icon = resolver.icon(forFile: executablePath)
|
if icon.isValid {
|
return icon
|
}
|
}
|
|
return fallbackIcon()
|
}
|
|
private func cacheKey(for app: AlignerApp) -> String {
|
[
|
app.bundleIdentifier,
|
app.executablePath ?? "",
|
app.processIdentifier.map(String.init) ?? ""
|
].joined(separator: "|")
|
}
|
}
|