Ariver
2026-06-03 ffc50a6b927afd16601e0bc01a53d76ae6069ba3
Refresh app index for quick search
3 files modified
61 ■■■■ changed files
Apptag/AppLibraryController.swift 4 ●●●● patch | view | raw | blame | history
Apptag/ContentView.swift 17 ●●●● patch | view | raw | blame | history
Apptag/DataLayer.swift 40 ●●●●● patch | view | raw | blame | history
Apptag/AppLibraryController.swift
@@ -23,8 +23,8 @@
}
enum AppLibraryController {
    static func refresh() -> AppLibraryRefreshResult {
        let scannedApps = AppIndexer.scan()
    static func refresh(useCache: Bool = true) -> AppLibraryRefreshResult {
        let scannedApps = AppIndexer.scan(useCache: useCache)
        let reconciledStore = TagEditor.reconcileScannedApps(scannedApps)
        let smartStartResult = SmartStartService.runIfNeeded(
            apps: scannedApps,
Apptag/ContentView.swift
@@ -731,6 +731,7 @@
        quickSearchErrorMessage = nil
        quickSearchFocusToken &+= 1
        refreshQuickSearchResults()
        refreshApps()
        NotificationCenter.default.post(
            name: .tagLauncherQuickSearchVisibilityChanged,
            object: nil,
@@ -777,7 +778,10 @@
    private func refreshQuickSearchResults() {
        let previousSelection = quickSearchSelectedID
        quickSearchResults = QuickSearchEngine.search(quickSearchQuery, documents: quickSearchDocuments)
        quickSearchResults = QuickSearchEngine.search(
            quickSearchQuery,
            documents: quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
        )
        if quickSearchResults.isEmpty {
            quickSearchSelectedID = nil
@@ -2369,7 +2373,7 @@
        }
    }
    func refreshApps(forceLayoutRefresh: Bool = false) {
    func refreshApps(forceLayoutRefresh: Bool = false, useCache: Bool = true) {
        guard !refreshInProgress else {
            if forceLayoutRefresh {
                refreshAgainAfterCurrent = true
@@ -2380,7 +2384,7 @@
        refreshInProgress = true
        DispatchQueue.global(qos: .userInitiated).async {
            let result = AppLibraryController.refresh()
            let result = AppLibraryController.refresh(useCache: useCache)
            DispatchQueue.main.async {
                applyAppLibrarySnapshot(result.snapshot)
                handleSmartStartRunResult(result.smartStartResult)
@@ -2417,6 +2421,13 @@
    ) {
        appGridInteraction.appDragModeActive = false
        endTagNavReorder()
        guard FileManager.default.fileExists(atPath: app.path.path) else {
            AppIndexer.invalidateScanCache()
            refreshApps()
            onFailure?()
            return
        }
        let closeQuickSearchAndOverlayBeforeOpening = closeQuickSearchOnSuccess
            && closeOverlayOnSuccess
            && (quickSearchCloseHidesOverlay || quickSearchOnlySession)
Apptag/DataLayer.swift
@@ -94,9 +94,16 @@
// 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] = [
@@ -120,12 +127,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
    }
@@ -133,27 +141,51 @@
        scanCacheLock.lock()
        cachedScanApps = nil
        cachedScanAt = nil
        cachedSearchPathSignature = nil
        scanCacheLock.unlock()
    }
    private static func cachedScanIfFresh() -> [AppInfo]? {
    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] {
        var seenResolvedPaths = Set<String>()
        var apps: [AppInfo] = []