Ariver
2026-06-10 fe5c1fa0d397938511e6102bbe5562f03f02607f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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: "|")
    }
}