Ariver
2026-06-30 4c6f31c43a03ecb5a85ffb6a3ea3905a661af0f4
src/Apptag/QuickSearch.swift
@@ -7,6 +7,7 @@
    static let tagLauncherQuickSearchDismissRequested = Notification.Name("TagLauncherQuickSearchDismissRequested")
    static let tagLauncherQuickSearchVisibilityChanged = Notification.Name("TagLauncherQuickSearchVisibilityChanged")
    static let tagLauncherHotkeyRegistrationChanged = Notification.Name("TagLauncherHotkeyRegistrationChanged")
    static let tagLauncherProEntitlementChanged = Notification.Name("TagLauncherProEntitlementChanged")
}
enum QuickSearchOpenSource {
@@ -80,6 +81,10 @@
            }
            return "Key \(keyCode)"
        }
    }
    static var printableKeyCodes: Set<Int> {
        Set(keyCodeToPrintableScalar.keys)
    }
    private static let keyCodeToPrintableScalar: [Int: Character] = [
@@ -294,31 +299,33 @@
enum QuickSearchEngine {
    static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
        apps.map { app in
            let localizedNames = uniqueOrdered(app.localizedNames)
            let internalBundleNames = internalBundleNames(for: app)
            let note = store.appNotes[app.path.path] ?? app.note ?? ""
            let bundleIdentifier = app.bundleIdentifier ?? ""
            let searchableFields = makeSearchableFields(
                appName: app.name,
                localizedNames: localizedNames,
                internalBundleNames: internalBundleNames,
                tagNames: app.tags,
                note: note,
                bundleIdentifier: bundleIdentifier
            )
            return QuickSearchDocument(
                app: app,
                localizedNames: localizedNames,
                internalBundleNames: internalBundleNames,
                tagNames: app.tags,
                note: note,
                bundleIdentifier: bundleIdentifier,
                lastOpenedAt: store.appLastOpenedAt[app.path.path],
                openCount: store.appOpenCounts[app.path.path] ?? 0,
                searchableFields: searchableFields
            )
        }
        apps
            .filter { !AppIndexer.isInternalHelperAppPath($0.path) }
            .map { app in
                let localizedNames = uniqueOrdered(AppDisplayNameResolver.searchAliases(for: app))
                let internalBundleNames = internalBundleNames(for: app)
                let note = store.appNotes[app.path.path] ?? app.note ?? ""
                let bundleIdentifier = app.bundleIdentifier ?? ""
                let searchableFields = makeSearchableFields(
                    appName: app.name,
                    localizedNames: localizedNames,
                    internalBundleNames: internalBundleNames,
                    tagNames: app.tags,
                    note: note,
                    bundleIdentifier: bundleIdentifier
                )
                return QuickSearchDocument(
                    app: app,
                    localizedNames: localizedNames,
                    internalBundleNames: internalBundleNames,
                    tagNames: app.tags,
                    note: note,
                    bundleIdentifier: bundleIdentifier,
                    lastOpenedAt: store.appLastOpenedAt[app.path.path],
                    openCount: store.appOpenCounts[app.path.path] ?? 0,
                    searchableFields: searchableFields
                )
            }
    }
    static func search(_ query: String, documents: [QuickSearchDocument], limit: Int = 50) -> [QuickSearchResult] {
@@ -690,19 +697,6 @@
        return normalizeField(String(parts))
    }
    private static func localizedNames(for app: AppInfo) -> [String] {
        guard let bundle = Bundle(url: app.path) else { return [] }
        let values = [
            bundle.localizedInfoDictionary?["CFBundleDisplayName"] as? String,
            bundle.infoDictionary?["CFBundleDisplayName"] as? String,
            FileManager.default.displayName(atPath: app.path.path).replacingOccurrences(of: ".app", with: "")
        ]
        return uniqueOrdered(values.compactMap { $0 }.compactMap { value in
            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
            return trimmed.isEmpty || trimmed == app.name ? nil : trimmed
        })
    }
    private static func internalBundleNames(for app: AppInfo) -> [String] {
        guard let bundle = Bundle(url: app.path) else { return [] }
        let values = [
@@ -858,6 +852,7 @@
            if panel.frame != frame {
                panel.setFrame(frame, display: true)
            }
            NSApp.activate(ignoringOtherApps: true)
            panel.makeKeyAndOrderFront(nil)
            panel.orderFrontRegardless()
        }
@@ -882,7 +877,7 @@
            let panel = QuickSearchPanel(
                contentRect: .zero,
                styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
                styleMask: [.borderless, .fullSizeContentView],
                backing: .buffered,
                defer: false
            )
@@ -953,8 +948,52 @@
    override var canBecomeKey: Bool { true }
    override var canBecomeMain: Bool { false }
    override func sendEvent(_ event: NSEvent) {
        if event.type == .leftMouseDown,
           let row = quickSearchResultRow(at: event.locationInWindow) {
            row.launchFromMouseClick()
            return
        }
        super.sendEvent(event)
    }
    override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
        frameRect
    }
    private func quickSearchResultRow(at windowPoint: NSPoint) -> QuickSearchResultRowView? {
        guard let contentView else { return nil }
        if let row = contentView.quickSearchResultRowFromHitTest(atWindowPoint: windowPoint) {
            return row
        }
        return contentView.quickSearchResultRowDescendant(containingWindowPoint: windowPoint)
    }
}
private extension NSView {
    func quickSearchResultRowFromHitTest(atWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
        let localPoint = convert(windowPoint, from: nil)
        var hitView = hitTest(localPoint)
        while let candidate = hitView {
            if let row = candidate as? QuickSearchResultRowView {
                return row
            }
            hitView = candidate.superview
        }
        return nil
    }
    func quickSearchResultRowDescendant(containingWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
        for subview in subviews.reversed() {
            if let row = subview as? QuickSearchResultRowView,
               row.containsWindowPointForLaunch(windowPoint) {
                return row
            }
            if let row = subview.quickSearchResultRowDescendant(containingWindowPoint: windowPoint) {
                return row
            }
        }
        return nil
    }
}
@@ -1443,6 +1482,15 @@
        trackingAreaRef = area
    }
    override func hitTest(_ point: NSPoint) -> NSView? {
        guard !isHidden, alphaValue > 0, bounds.contains(point) else { return nil }
        return self
    }
    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
        true
    }
    override func mouseEntered(with event: NSEvent) {
        super.mouseEntered(with: event)
        if let result {
@@ -1451,13 +1499,31 @@
    }
    override func mouseDown(with event: NSEvent) {
        if let result {
            DispatchQueue.main.async { [onLaunch] in
                onLaunch(result)
            }
        } else {
        if !launchFromMouseClick() {
            super.mouseDown(with: event)
        }
    }
    @discardableResult
    func launchFromMouseClick() -> Bool {
        guard let result else { return false }
        DispatchQueue.main.async { [onLaunch, result] in
            onLaunch(result)
        }
        return true
    }
    func containsWindowPointForLaunch(_ windowPoint: NSPoint) -> Bool {
        guard window != nil else { return false }
        var view: NSView? = self
        while let candidate = view {
            if candidate.isHidden || candidate.alphaValue <= 0 {
                return false
            }
            view = candidate.superview
        }
        let localPoint = convert(windowPoint, from: nil)
        return bounds.contains(localPoint) && visibleRect.contains(localPoint)
    }
    private func setup() {
@@ -1575,6 +1641,8 @@
        field.placeholderString = placeholder
        field.delegate = context.coordinator
        field.onCommand = onCommand
        field.target = context.coordinator
        field.action = #selector(Coordinator.submitAction(_:))
        context.coordinator.onCommand = onCommand
        field.setAccessibilityLabel(tr("quickSearch.inputAccessibility"))
        context.coordinator.field = field
@@ -1588,6 +1656,8 @@
        }
        field.placeholderString = placeholder
        field.onCommand = onCommand
        field.target = context.coordinator
        field.action = #selector(Coordinator.submitAction(_:))
        context.coordinator.onCommand = onCommand
        if context.coordinator.lastFocusToken != focusToken {
            context.coordinator.lastFocusToken = focusToken
@@ -1629,6 +1699,10 @@
            text.wrappedValue = field.stringValue
        }
        @objc func submitAction(_ sender: Any?) {
            onCommand?(.submit)
        }
        func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
            switch commandSelector {
            case #selector(NSResponder.moveUp(_:)):