Ariver
2026-06-05 eeb97e9c41b6939b29fd42402aa5c4d3f97663e9
Apptag/DataLayer.swift
@@ -11,9 +11,50 @@
    let tags: [String]
    let bundleIdentifier: String?
    let localizedNames: [String]
    let localizedNamesByLanguage: [String: String]
    let icon: NSImage  // Pre-loaded during background scan
    var isUncommon: Bool = false
    var note: String? = nil
    var displayName: String {
        localizedDisplayName(for: L10n.currentCode)
    }
    func localizedDisplayName(for languageCode: String) -> String {
        let candidates = AppInfo.displayLanguageFallbacks(for: languageCode)
        for code in candidates {
            if let localizedName = localizedNamesByLanguage[code],
               !localizedName.isEmpty {
                return localizedName
            }
        }
        return name
    }
    private static func displayLanguageFallbacks(for languageCode: String) -> [String] {
        var candidates = [languageCode]
        switch languageCode {
        case "zh-Hans":
            candidates.append("zh-Hant")
        case "zh-Hant":
            candidates.append("zh-Hans")
        case "nb":
            candidates.append("no")
        case "nn":
            candidates.append("no")
        case "no":
            candidates.append(contentsOf: ["nb", "nn"])
        default:
            break
        }
        candidates.append("en")
        return uniqueLanguageCodes(candidates)
    }
    fileprivate static func uniqueLanguageCodes(_ codes: [String]) -> [String] {
        var seen = Set<String>()
        return codes.filter { seen.insert($0).inserted }
    }
    /// True if this is an Apple pre-installed app.
    var isAppleApp: Bool {
@@ -53,14 +94,22 @@
// MARK: - App Scanner
enum AppIndexer {
    private struct SearchPathSignature: Equatable {
        let path: String
        let exists: Bool
        let contentModificationTime: TimeInterval?
    }
    private static let scanCacheLock = NSLock()
    private static var cachedScanApps: [AppInfo]? = nil
    private static var cachedScanAt: Date? = nil
    private static var cachedSearchPathSignature: [SearchPathSignature]? = nil
    private static let scanCacheTTL: TimeInterval = 1800
    static let searchPaths: [URL] = [
        URL(fileURLWithPath: "/Applications"),
        URL(fileURLWithPath: "/System/Applications"),
        URL(fileURLWithPath: "/System/Library/CoreServices/Applications"),
        URL(fileURLWithPath: "/System/Cryptexes/App/System/Applications"),
        URL(fileURLWithPath: "/System/Volumes/Preboot/Cryptexes/App/System/Applications"),
        FileManager.default.homeDirectoryForCurrentUser
@@ -69,6 +118,7 @@
    private static let systemAppPathPrefixes = [
        "/System/Applications/",
        "/System/Library/CoreServices/Applications/",
        "/System/Cryptexes/App/System/Applications/",
        "/System/Volumes/Preboot/Cryptexes/App/System/Applications/"
    ]
@@ -79,12 +129,13 @@
    /// Scan all standard locations. Tags are annotated from TagDatabase by the caller.
    static func scan(useCache: Bool = true) -> [AppInfo] {
        if useCache, let cached = cachedScanIfFresh() {
        let searchPathSignature = currentSearchPathSignature()
        if useCache, let cached = cachedScanIfFresh(matching: searchPathSignature) {
            return cached
        }
        let apps = performScan()
        updateScanCache(apps)
        updateScanCache(apps, searchPathSignature: searchPathSignature)
        return apps
    }
@@ -92,25 +143,59 @@
        scanCacheLock.lock()
        cachedScanApps = nil
        cachedScanAt = nil
        cachedSearchPathSignature = nil
        scanCacheLock.unlock()
    }
    private static func cachedScanIfFresh() -> [AppInfo]? {
    static func shouldRefreshForSearchPathChanges() -> Bool {
        scanCacheLock.lock()
        let hasCachedApps = cachedScanApps != nil
        let cachedSignature = cachedSearchPathSignature
        scanCacheLock.unlock()
        guard hasCachedApps else { return true }
        return cachedSignature != currentSearchPathSignature()
    }
    private static func cachedScanIfFresh(matching searchPathSignature: [SearchPathSignature]) -> [AppInfo]? {
        scanCacheLock.lock()
        defer { scanCacheLock.unlock() }
        guard let cachedScanApps,
              let cachedScanAt,
              cachedSearchPathSignature == searchPathSignature,
              Date().timeIntervalSince(cachedScanAt) < scanCacheTTL
        else { return nil }
        return cachedScanApps
    }
    private static func updateScanCache(_ apps: [AppInfo]) {
    private static func updateScanCache(_ apps: [AppInfo], searchPathSignature: [SearchPathSignature]) {
        scanCacheLock.lock()
        cachedScanApps = apps
        cachedScanAt = Date()
        cachedSearchPathSignature = searchPathSignature
        scanCacheLock.unlock()
    }
    private static func currentSearchPathSignature() -> [SearchPathSignature] {
        searchPaths.map { url in
            var isDirectory: ObjCBool = false
            let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
            guard exists, isDirectory.boolValue else {
                return SearchPathSignature(
                    path: url.path,
                    exists: false,
                    contentModificationTime: nil
                )
            }
            let values = try? url.resourceValues(forKeys: [.contentModificationDateKey])
            return SearchPathSignature(
                path: url.path,
                exists: true,
                contentModificationTime: values?.contentModificationDate?.timeIntervalSinceReferenceDate
            )
        }
    }
    private static func performScan() -> [AppInfo] {
@@ -174,16 +259,21 @@
        guard !isNestedInsideAppBundle(resolvedURL) else { return }
        guard seenResolvedPaths.insert(resolvedURL.path).inserted else { return }
        let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
        let bundleId = bundle?.bundleIdentifier
        guard !isTagLauncherBundle(bundleId) else { return }
        let name = displayURL.deletingPathExtension().lastPathComponent
        let icon = NSWorkspace.shared.icon(forFile: displayURL.path)
        icon.size = NSSize(width: 96, height: 96)
        let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
        let bundleId = bundle?.bundleIdentifier
        let localizedNames = localizedAppNames(
            for: displayURL,
        let localizedNamesByLanguage = localizedAppNameMap(
            bundle: bundle,
            fallbackName: name
        )
        let localizedNames = localizedAppNames(
            for: displayURL,
            fallbackName: name,
            localizedNamesByLanguage: localizedNamesByLanguage
        )
        apps.append(AppInfo(
@@ -192,24 +282,154 @@
            tags: [],
            bundleIdentifier: bundleId,
            localizedNames: localizedNames,
            localizedNamesByLanguage: localizedNamesByLanguage,
            icon: icon
        ))
    }
    private static func localizedAppNames(
        for appURL: URL,
    private static func isTagLauncherBundle(_ bundleIdentifier: String?) -> Bool {
        bundleIdentifier?.caseInsensitiveCompare(AppIdentity.bundleIdentifier) == .orderedSame
    }
    private static func localizedAppNameMap(
        bundle: Bundle?,
        fallbackName: String
    ) -> [String: String] {
        guard let bundle else { return [:] }
        let loctable = infoPlistLoctable(in: bundle)
        var result: [String: String] = [:]
        for language in L10n.supported {
            guard let localizedName = localizedAppName(
                for: language.code,
                bundle: bundle,
                loctable: loctable,
                fallbackName: fallbackName
            ) else { continue }
            result[language.code] = localizedName
        }
        return result
    }
    private static func localizedAppNames(
        for appURL: URL,
        fallbackName: String,
        localizedNamesByLanguage: [String: String]
    ) -> [String] {
        var values: [String?] = []
        values.append(bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String)
        values.append(bundle?.localizedInfoDictionary?["CFBundleName"] as? String)
        values.append(bundle?.infoDictionary?["CFBundleDisplayName"] as? String)
        values.append(bundle?.infoDictionary?["CFBundleName"] as? String)
        var values = L10n.supported.map { localizedNamesByLanguage[$0.code] }
        values.append(spotlightDisplayName(for: appURL))
        values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: ""))
        return uniqueLocalizedNames(values, excluding: fallbackName)
    }
    private static func localizedAppName(
        for languageCode: String,
        bundle: Bundle,
        loctable: [String: [String: Any]],
        fallbackName: String
    ) -> String? {
        for localization in localizationCandidates(for: languageCode) {
            if let table = loctable[localization],
               let value = firstValidLocalizedName(
                   [table["CFBundleDisplayName"], table["CFBundleName"]],
                   excluding: fallbackName
               ) {
                return value
            }
            if let strings = infoPlistStrings(in: bundle, localization: localization),
               let value = firstValidLocalizedName(
                   [strings["CFBundleDisplayName"], strings["CFBundleName"]],
                   excluding: fallbackName
               ) {
                return value
            }
        }
        if languageCode == "en",
           let value = firstValidLocalizedName(
               [
                   bundle.infoDictionary?["CFBundleDisplayName"],
                   bundle.infoDictionary?["CFBundleName"]
               ],
               excluding: fallbackName
           ) {
            return value
        }
        return nil
    }
    private static func infoPlistLoctable(in bundle: Bundle) -> [String: [String: Any]] {
        guard let url = bundle.url(forResource: "InfoPlist", withExtension: "loctable"),
              let rawTable = NSDictionary(contentsOf: url) as? [String: Any]
        else { return [:] }
        var result: [String: [String: Any]] = [:]
        for (key, value) in rawTable {
            if let localizedTable = value as? [String: Any] {
                result[key] = localizedTable
            }
        }
        return result
    }
    private static func infoPlistStrings(
        in bundle: Bundle,
        localization: String
    ) -> [String: Any]? {
        guard let url = bundle.url(
            forResource: "InfoPlist",
            withExtension: "strings",
            subdirectory: nil,
            localization: localization
        ) else { return nil }
        return NSDictionary(contentsOf: url) as? [String: Any]
    }
    private static func localizationCandidates(for languageCode: String) -> [String] {
        var candidates = [languageCode, languageCode.replacingOccurrences(of: "-", with: "_")]
        switch languageCode {
        case "zh-Hans":
            candidates.append(contentsOf: ["zh_CN", "zh"])
        case "zh-Hant":
            candidates.append(contentsOf: ["zh_TW", "zh_HK", "zh"])
        case "pt-BR":
            candidates.append(contentsOf: ["pt_BR", "pt"])
        case "sr-Cyrl":
            candidates.append(contentsOf: ["sr_Cyrl", "sr"])
        case "ar-Najdi":
            candidates.append(contentsOf: ["ar_Najdi", "ar"])
        case "nb":
            candidates.append(contentsOf: ["nb", "no"])
        case "nn":
            candidates.append(contentsOf: ["nn", "no"])
        case "no":
            candidates.append(contentsOf: ["no", "nb", "nn"])
        default:
            if let base = languageCode.split(separator: "-").first.map(String.init) {
                candidates.append(base)
            }
        }
        return AppInfo.uniqueLanguageCodes(candidates)
    }
    private static func firstValidLocalizedName(
        _ values: [Any?],
        excluding excludedValue: String
    ) -> String? {
        let normalizedExcluded = normalizedLocalizedName(excludedValue)
        for value in values {
            guard let value = value as? String else { continue }
            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !trimmed.isEmpty,
                  normalizedLocalizedName(trimmed) != normalizedExcluded
            else { continue }
            return trimmed
        }
        return nil
    }
    private static func spotlightDisplayName(for appURL: URL) -> String? {
@@ -272,7 +492,7 @@
    private static func deduplicationIdentity(for app: AppInfo) -> String {
        if let bundleIdentifier = app.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
           !bundleIdentifier.isEmpty {
            return "bundle:\(bundleIdentifier.lowercased())"
            return "bundle:\(bundleIdentifier.lowercased())|name:\(normalizedAppName(app.name))"
        }
        return "name:\(normalizedAppName(app.name))"
    }
@@ -1081,6 +1301,7 @@
                name: app.name, path: app.path, tags: appTags,
                bundleIdentifier: app.bundleIdentifier,
                localizedNames: app.localizedNames,
                localizedNamesByLanguage: app.localizedNamesByLanguage,
                icon: app.icon,
                isUncommon: uncommonPaths.contains(app.path.path),
                note: store.appNotes[app.path.path]