From 7c97841d07d4a596508f663c887d1f67d105faf6 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 20 May 2026 13:07:20 +0800
Subject: [PATCH] perf: reduce launcher scroll and search jank

---
 Apptag/QuickSearch.swift |  484 +++++++++++++++++++++--------------------------------
 1 files changed, 193 insertions(+), 291 deletions(-)

diff --git a/Apptag/QuickSearch.swift b/Apptag/QuickSearch.swift
index 679f4c6..4995618 100644
--- a/Apptag/QuickSearch.swift
+++ b/Apptag/QuickSearch.swift
@@ -6,7 +6,7 @@
     static let tagLauncherQuickSearchRequested = Notification.Name("TagLauncherQuickSearchRequested")
     static let tagLauncherQuickSearchDismissRequested = Notification.Name("TagLauncherQuickSearchDismissRequested")
     static let tagLauncherQuickSearchVisibilityChanged = Notification.Name("TagLauncherQuickSearchVisibilityChanged")
-    static let tagLauncherHotkeysChanged = Notification.Name("TagLauncherHotkeysChanged")
+    static let tagLauncherHotkeyRegistrationChanged = Notification.Name("TagLauncherHotkeyRegistrationChanged")
 }
 
 enum QuickSearchOpenSource {
@@ -20,10 +20,6 @@
 struct LauncherHotkey: Equatable {
     let keyCode: UInt32
     let modifiers: UInt32
-
-    var serialized: String {
-        "\(keyCode):\(modifiers)"
-    }
 
     var displayString: String {
         let ordered: [(UInt32, String)] = [
@@ -40,37 +36,12 @@
         return modifierGlyphs + LauncherHotkey.keyDisplayName(for: keyCode)
     }
 
-    static var defaultMain: LauncherHotkey {
+    static var main: LauncherHotkey {
         LauncherHotkey(keyCode: UInt32(kVK_Space), modifiers: UInt32(shiftKey | optionKey))
     }
 
-    static var defaultQuickSearch: LauncherHotkey {
+    static var quickSearch: LauncherHotkey {
         LauncherHotkey(keyCode: UInt32(kVK_Space), modifiers: UInt32(kEventKeyModifierFnMask))
-    }
-
-    static func deserialize(_ value: String?) -> LauncherHotkey? {
-        guard let value, !value.isEmpty else { return nil }
-        let parts = value.split(separator: ":")
-        guard parts.count == 2,
-              let keyCode = UInt32(parts[0]),
-              let modifiers = UInt32(parts[1])
-        else { return nil }
-        return LauncherHotkey(keyCode: keyCode, modifiers: modifiers)
-    }
-
-    static func from(event: NSEvent) -> LauncherHotkey? {
-        let ignoredKeyCodes: Set<UInt16> = [54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
-        guard !ignoredKeyCodes.contains(event.keyCode) else { return nil }
-
-        var carbonModifiers: UInt32 = 0
-        if event.modifierFlags.contains(.shift) { carbonModifiers |= UInt32(shiftKey) }
-        if event.modifierFlags.contains(.option) { carbonModifiers |= UInt32(optionKey) }
-        if event.modifierFlags.contains(.control) { carbonModifiers |= UInt32(controlKey) }
-        if event.modifierFlags.contains(.command) { carbonModifiers |= UInt32(cmdKey) }
-        if event.modifierFlags.contains(.function) { carbonModifiers |= UInt32(kEventKeyModifierFnMask) }
-        guard carbonModifiers != 0 else { return nil }
-
-        return LauncherHotkey(keyCode: UInt32(event.keyCode), modifiers: carbonModifiers)
     }
 
     static func keyDisplayName(for keyCode: UInt32) -> String {
@@ -121,31 +92,31 @@
     case main
     case quickSearch
 
-    var storageKey: String {
+    var stateKey: String {
         switch self {
-        case .main: return "mainHotkey"
-        case .quickSearch: return "quickSearchHotkey"
+        case .main: return LauncherHotkeyRegistrationStore.mainStateKey
+        case .quickSearch: return LauncherHotkeyRegistrationStore.quickSearchStateKey
         }
     }
 
-    var statusKey: String {
+    var failureCodeKey: String {
         switch self {
-        case .main: return "mainHotkeyStatus"
-        case .quickSearch: return "quickSearchHotkeyStatus"
+        case .main: return LauncherHotkeyRegistrationStore.mainFailureCodeKey
+        case .quickSearch: return LauncherHotkeyRegistrationStore.quickSearchFailureCodeKey
         }
     }
 
-    var conflictMessageKey: String {
+    var attentionKey: String {
         switch self {
-        case .main: return "mainHotkeyConflictMessage"
-        case .quickSearch: return "quickSearchHotkeyConflictMessage"
+        case .main: return LauncherHotkeyRegistrationStore.mainNeedsAttentionKey
+        case .quickSearch: return LauncherHotkeyRegistrationStore.quickSearchNeedsAttentionKey
         }
     }
 
-    var pendingStorageKey: String {
+    var hotkey: LauncherHotkey {
         switch self {
-        case .main: return "mainHotkeyPending"
-        case .quickSearch: return "quickSearchHotkeyPending"
+        case .main: return .main
+        case .quickSearch: return .quickSearch
         }
     }
 
@@ -157,77 +128,135 @@
     }
 }
 
-enum LauncherHotkeyStatus: String {
-    case active = "Active"
-    case conflict = "Conflict"
-    case disabled = "Disabled"
+enum LauncherHotkeyRegistrationState: String {
+    case active
+    case failed
 }
 
-enum LauncherHotkeyStore {
-    static func hotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey? {
-        if kind == .main {
-            return .defaultMain
-        }
-        if kind == .quickSearch && !AppDefaults.hasStoredValue(for: kind.storageKey) {
-            return .defaultQuickSearch
-        }
-        return LauncherHotkey.deserialize(UserDefaults.standard.string(forKey: kind.storageKey))
+enum LauncherHotkeyRegistrationStore {
+    static let mainStateKey = "mainHotkeyRegistrationState"
+    static let quickSearchStateKey = "quickSearchHotkeyRegistrationState"
+    static let mainFailureCodeKey = "mainHotkeyRegistrationFailureCode"
+    static let quickSearchFailureCodeKey = "quickSearchHotkeyRegistrationFailureCode"
+    static let mainNeedsAttentionKey = "mainHotkeyRegistrationNeedsAttention"
+    static let quickSearchNeedsAttentionKey = "quickSearchHotkeyRegistrationNeedsAttention"
+
+    static func state(for kind: LauncherHotkeyKind) -> LauncherHotkeyRegistrationState {
+        let rawValue = UserDefaults.standard.string(forKey: kind.stateKey)
+        return LauncherHotkeyRegistrationState(rawValue: rawValue ?? "") ?? .active
     }
 
-    static func save(_ hotkey: LauncherHotkey?, for kind: LauncherHotkeyKind) {
-        UserDefaults.standard.set(hotkey?.serialized ?? "", forKey: kind.storageKey)
-        UserDefaults.standard.removeObject(forKey: kind.pendingStorageKey)
+    static func failureCode(for kind: LauncherHotkeyKind) -> Int? {
+        let defaults = UserDefaults.standard
+        guard defaults.object(forKey: kind.failureCodeKey) != nil else { return nil }
+        return defaults.integer(forKey: kind.failureCodeKey)
     }
 
-    static func savePending(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) {
-        UserDefaults.standard.set(hotkey.serialized, forKey: kind.pendingStorageKey)
+    static func setActive(for kind: LauncherHotkeyKind) {
+        setState(.active, failureCode: nil, for: kind)
     }
 
-    static func pendingHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey? {
-        LauncherHotkey.deserialize(UserDefaults.standard.string(forKey: kind.pendingStorageKey))
+    static func setFailed(_ failureCode: OSStatus, for kind: LauncherHotkeyKind) {
+        setState(.failed, failureCode: Int(failureCode), for: kind)
     }
 
-    static func status(for kind: LauncherHotkeyKind) -> LauncherHotkeyStatus {
-        let raw = UserDefaults.standard.string(forKey: kind.statusKey)
-        return LauncherHotkeyStatus(rawValue: raw ?? "") ?? (hotkey(for: kind) == nil ? .disabled : .active)
+    static func consumeNeedsAttention(for kind: LauncherHotkeyKind) -> Bool {
+        let defaults = UserDefaults.standard
+        let needsAttention = defaults.bool(forKey: kind.attentionKey)
+        defaults.set(false, forKey: kind.attentionKey)
+        return needsAttention
     }
 
-    static func setStatus(_ status: LauncherHotkeyStatus, message: String?, for kind: LauncherHotkeyKind) {
-        UserDefaults.standard.set(status.rawValue, forKey: kind.statusKey)
-        if let message, !message.isEmpty {
-            UserDefaults.standard.set(message, forKey: kind.conflictMessageKey)
+    private static func setState(
+        _ state: LauncherHotkeyRegistrationState,
+        failureCode: Int?,
+        for kind: LauncherHotkeyKind
+    ) {
+        let defaults = UserDefaults.standard
+        let previousState = defaults.string(forKey: kind.stateKey)
+        let previousFailureCode = defaults.object(forKey: kind.failureCodeKey) == nil
+            ? nil
+            : defaults.integer(forKey: kind.failureCodeKey)
+        let stateChanged = previousState != state.rawValue || previousFailureCode != failureCode
+
+        defaults.set(state.rawValue, forKey: kind.stateKey)
+        if let failureCode {
+            defaults.set(failureCode, forKey: kind.failureCodeKey)
         } else {
-            UserDefaults.standard.removeObject(forKey: kind.conflictMessageKey)
+            defaults.removeObject(forKey: kind.failureCodeKey)
         }
-        NotificationCenter.default.post(name: .tagLauncherHotkeysChanged, object: nil)
-    }
 
-    static func conflictMessage(for kind: LauncherHotkeyKind) -> String {
-        UserDefaults.standard.string(forKey: kind.conflictMessageKey) ?? ""
-    }
-
-    static func knownConflictMessage(for hotkey: LauncherHotkey, status: OSStatus) -> String? {
-        guard status != noErr else { return nil }
-        switch (hotkey.keyCode, hotkey.modifiers) {
-        case (UInt32(kVK_Space), UInt32(cmdKey)):
-            return tr("quickSearch.hotkeyConflict.spotlight")
-        case (UInt32(kVK_Space), UInt32(optionKey | cmdKey)):
-            return tr("quickSearch.hotkeyConflict.finderSearch")
-        case (UInt32(kVK_Space), UInt32(controlKey)):
-            return tr("quickSearch.hotkeyConflict.previousInput")
-        case (UInt32(kVK_Space), UInt32(controlKey | optionKey)):
-            return tr("quickSearch.hotkeyConflict.nextInput")
-        case (UInt32(kVK_Space), UInt32(controlKey | cmdKey)):
-            return tr("quickSearch.hotkeyConflict.emoji")
-        case (UInt32(kVK_Space), UInt32(kEventKeyModifierFnMask)):
-            return tr("quickSearch.hotkeyConflict.fnSpace")
-        default:
-            return nil
+        if state == .active {
+            defaults.set(false, forKey: kind.attentionKey)
+        } else if stateChanged {
+            defaults.set(true, forKey: kind.attentionKey)
         }
+
+        NotificationCenter.default.post(
+            name: .tagLauncherHotkeyRegistrationChanged,
+            object: nil,
+            userInfo: ["kind": kind.rawValue]
+        )
     }
 }
 
 // MARK: - Search Documents
+
+private enum QuickSearchFieldKind: Int {
+    case name = 0
+    case tag = 1
+    case note = 2
+    case bundleIdentifier = 3
+    case internalBundleName = 4
+
+    var weight: Double {
+        switch self {
+        case .name: return 100
+        case .tag: return 70
+        case .note: return 45
+        case .internalBundleName: return 30
+        case .bundleIdentifier: return 20
+        }
+    }
+}
+
+private enum QuickSearchMatchKind {
+    case exact
+    case prefix
+    case substring
+    case acronym
+    case fuzzy
+
+    var weight: Double {
+        switch self {
+        case .exact: return 100
+        case .prefix: return 80
+        case .substring: return 60
+        case .acronym: return 55
+        case .fuzzy: return 35
+        }
+    }
+}
+
+private struct QuickSearchIndexedField {
+    let kind: QuickSearchFieldKind
+    let text: String
+    let normalized: String
+    let acronym: String
+    let pinyinCandidates: [String]
+}
+
+private struct QuickSearchMatchOptions {
+    let allowSubstring: Bool
+    let allowFuzzySubsequence: Bool
+}
+
+private struct QuickSearchTokenMatch {
+    let score: Double
+    let fieldRank: Int
+    let fieldKind: QuickSearchFieldKind
+    let originalText: String
+}
 
 struct QuickSearchDocument: Identifiable {
     var id: URL { app.id }
@@ -239,6 +268,7 @@
     let bundleIdentifier: String
     let lastOpenedAt: Date?
     let openCount: Int
+    fileprivate let searchableFields: [QuickSearchIndexedField]
 }
 
 struct QuickSearchResult: Identifiable {
@@ -254,73 +284,30 @@
 }
 
 enum QuickSearchEngine {
-    private enum FieldKind: Int {
-        case name = 0
-        case tag = 1
-        case note = 2
-        case bundleIdentifier = 3
-        case internalBundleName = 4
-
-        var weight: Double {
-            switch self {
-            case .name: return 100
-            case .tag: return 70
-            case .note: return 45
-            case .internalBundleName: return 30
-            case .bundleIdentifier: return 20
-            }
-        }
-    }
-
-    private enum MatchKind {
-        case exact
-        case prefix
-        case substring
-        case acronym
-        case fuzzy
-
-        var weight: Double {
-            switch self {
-            case .exact: return 100
-            case .prefix: return 80
-            case .substring: return 60
-            case .acronym: return 55
-            case .fuzzy: return 35
-            }
-        }
-    }
-
-    private struct Field {
-        let kind: FieldKind
-        let text: String
-        let normalized: String
-        let acronym: String
-        let pinyinCandidates: [String]
-    }
-
-    private struct MatchOptions {
-        let allowSubstring: Bool
-        let allowFuzzySubsequence: Bool
-    }
-
-    private struct TokenMatch {
-        let score: Double
-        let fieldRank: Int
-        let fieldKind: FieldKind
-        let originalText: String
-    }
-
     static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
         apps.map { app in
-            QuickSearchDocument(
-                app: app,
-                localizedNames: localizedNames(for: app),
-                internalBundleNames: internalBundleNames(for: app),
+            let localizedNames = localizedNames(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: store.appNotes[app.path.path] ?? app.note ?? "",
-                bundleIdentifier: app.bundleIdentifier ?? "",
+                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
+                openCount: store.appOpenCounts[app.path.path] ?? 0,
+                searchableFields: searchableFields
             )
         }
     }
@@ -347,7 +334,7 @@
     }
 
     private static func result(for document: QuickSearchDocument, tokens: [String]) -> QuickSearchResult? {
-        let fields = searchableFields(for: document)
+        let fields = document.searchableFields
         var textScore: Double = 0
         var bestFieldRank = Int.max
         var matchedTagName: String?
@@ -380,10 +367,17 @@
         )
     }
 
-    private static func searchableFields(for document: QuickSearchDocument) -> [Field] {
-        let names = uniqueOrdered([document.app.name] + document.localizedNames)
+    private static func makeSearchableFields(
+        appName: String,
+        localizedNames: [String],
+        internalBundleNames: [String],
+        tagNames: [String],
+        note: String,
+        bundleIdentifier: String
+    ) -> [QuickSearchIndexedField] {
+        let names = uniqueOrdered([appName] + localizedNames)
         let nameFields = names.map {
-            Field(
+            QuickSearchIndexedField(
                 kind: .name,
                 text: $0,
                 normalized: normalizeField($0),
@@ -391,8 +385,8 @@
                 pinyinCandidates: pinyinCandidates(for: $0)
             )
         }
-        let tagFields = document.tagNames.map {
-            Field(
+        let tagFields = tagNames.map {
+            QuickSearchIndexedField(
                 kind: .tag,
                 text: $0,
                 normalized: normalizeField($0),
@@ -400,26 +394,26 @@
                 pinyinCandidates: pinyinCandidates(for: $0)
             )
         }
-        let noteFields = document.note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? [] : [
-            Field(
+        let noteFields = note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? [] : [
+            QuickSearchIndexedField(
                 kind: .note,
-                text: document.note,
-                normalized: normalizeField(document.note),
+                text: note,
+                normalized: normalizeField(note),
                 acronym: "",
-                pinyinCandidates: pinyinCandidates(for: document.note)
+                pinyinCandidates: pinyinCandidates(for: note)
             )
         ]
-        let bundleFields = document.bundleIdentifier.isEmpty ? [] : [
-            Field(
+        let bundleFields = bundleIdentifier.isEmpty ? [] : [
+            QuickSearchIndexedField(
                 kind: .bundleIdentifier,
-                text: document.bundleIdentifier,
-                normalized: normalizeField(document.bundleIdentifier),
+                text: bundleIdentifier,
+                normalized: normalizeField(bundleIdentifier),
                 acronym: "",
                 pinyinCandidates: []
             )
         ]
-        let internalBundleNameFields = document.internalBundleNames.map {
-            Field(
+        let internalBundleNameFields = internalBundleNames.map {
+            QuickSearchIndexedField(
                 kind: .internalBundleName,
                 text: $0,
                 normalized: normalizeField($0),
@@ -430,11 +424,11 @@
         return nameFields + tagFields + noteFields + bundleFields + internalBundleNameFields
     }
 
-    private static func match(token: String, field: Field) -> TokenMatch? {
+    private static func match(token: String, field: QuickSearchIndexedField) -> QuickSearchTokenMatch? {
         guard let candidate = bestMatchCandidate(token: token, field: field) else { return nil }
         let positionBoost = candidate.0 == .exact ? 0 : max(0, 10 - min(candidate.1, 10))
         let score = field.kind.weight + candidate.0.weight + Double(positionBoost)
-        return TokenMatch(
+        return QuickSearchTokenMatch(
             score: score,
             fieldRank: field.kind.rawValue,
             fieldKind: field.kind,
@@ -442,8 +436,8 @@
         )
     }
 
-    private static func bestMatchCandidate(token: String, field: Field) -> (MatchKind, Int)? {
-        var candidates: [(MatchKind, Int)] = []
+    private static func bestMatchCandidate(token: String, field: QuickSearchIndexedField) -> (QuickSearchMatchKind, Int)? {
+        var candidates: [(QuickSearchMatchKind, Int)] = []
 
         if let textCandidate = matchCandidate(
             token: token,
@@ -456,7 +450,11 @@
             candidates.append((.acronym, 0))
         }
         for pinyin in field.pinyinCandidates {
-            if let pinyinCandidate = matchPinyinCandidate(token: token, normalized: pinyin) {
+            if let pinyinCandidate = matchPinyinCandidate(
+                token: token,
+                normalized: pinyin,
+                allowSubstring: field.kind == .note
+            ) {
                 candidates.append(pinyinCandidate)
             }
         }
@@ -467,24 +465,24 @@
         }
     }
 
-    private static func matchOptions(for fieldKind: FieldKind) -> MatchOptions {
+    private static func matchOptions(for fieldKind: QuickSearchFieldKind) -> QuickSearchMatchOptions {
         switch fieldKind {
         case .name:
-            return MatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
+            return QuickSearchMatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
         case .tag:
-            return MatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
+            return QuickSearchMatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
         case .note:
-            return MatchOptions(allowSubstring: true, allowFuzzySubsequence: false)
+            return QuickSearchMatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
         case .bundleIdentifier, .internalBundleName:
-            return MatchOptions(allowSubstring: false, allowFuzzySubsequence: false)
+            return QuickSearchMatchOptions(allowSubstring: false, allowFuzzySubsequence: false)
         }
     }
 
     private static func matchCandidate(
         token: String,
         normalized: String,
-        options: MatchOptions
-    ) -> (MatchKind, Int)? {
+        options: QuickSearchMatchOptions
+    ) -> (QuickSearchMatchKind, Int)? {
         guard !normalized.isEmpty else { return nil }
         if normalized == token {
             return (.exact, 0)
@@ -501,13 +499,20 @@
         return nil
     }
 
-    private static func matchPinyinCandidate(token: String, normalized: String) -> (MatchKind, Int)? {
+    private static func matchPinyinCandidate(
+        token: String,
+        normalized: String,
+        allowSubstring: Bool = false
+    ) -> (QuickSearchMatchKind, Int)? {
         guard !normalized.isEmpty else { return nil }
         if normalized == token {
             return (.exact, 0)
         }
         if normalized.hasPrefix(token) {
             return (.prefix, 0)
+        }
+        if allowSubstring, let range = normalized.range(of: token) {
+            return (.substring, normalized.distance(from: normalized.startIndex, to: range.lowerBound))
         }
         return nil
     }
@@ -972,109 +977,6 @@
             onCommand?(.dismiss)
         default:
             super.keyDown(with: event)
-        }
-    }
-}
-
-struct QuickSearchBackdropClickView: NSViewRepresentable {
-    let onClick: () -> Void
-
-    func makeNSView(context: Context) -> QuickSearchBackdropNSView {
-        let view = QuickSearchBackdropNSView()
-        view.onClick = onClick
-        return view
-    }
-
-    func updateNSView(_ view: QuickSearchBackdropNSView, context: Context) {
-        view.onClick = onClick
-    }
-}
-
-final class QuickSearchBackdropNSView: NSView {
-    var onClick: (() -> Void)?
-
-    override func hitTest(_ point: NSPoint) -> NSView? {
-        bounds.contains(point) ? self : nil
-    }
-
-    override func mouseDown(with event: NSEvent) {
-        onClick?()
-    }
-}
-
-// MARK: - Hotkey Capture UI
-
-struct HotkeyCaptureView: NSViewRepresentable {
-    let isActive: Bool
-    let onCapture: (LauncherHotkey) -> Void
-    let onCancel: () -> Void
-
-    func makeNSView(context: Context) -> HotkeyCaptureNSView {
-        let view = HotkeyCaptureNSView()
-        view.onCapture = onCapture
-        view.onCancel = onCancel
-        return view
-    }
-
-    func updateNSView(_ view: HotkeyCaptureNSView, context: Context) {
-        view.onCapture = onCapture
-        view.onCancel = onCancel
-        view.setActive(isActive)
-    }
-}
-
-final class HotkeyCaptureNSView: NSView {
-    var onCapture: ((LauncherHotkey) -> Void)?
-    var onCancel: (() -> Void)?
-    private var keyMonitor: Any?
-
-    override var acceptsFirstResponder: Bool { true }
-
-    deinit {
-        removeKeyMonitor()
-    }
-
-    func setActive(_ active: Bool) {
-        if active {
-            installKeyMonitorIfNeeded()
-            DispatchQueue.main.async { [weak self] in
-                guard let self else { return }
-                self.window?.makeFirstResponder(self)
-            }
-        } else {
-            removeKeyMonitor()
-        }
-    }
-
-    override func keyDown(with event: NSEvent) {
-        handle(event)
-    }
-
-    private func installKeyMonitorIfNeeded() {
-        guard keyMonitor == nil else { return }
-        keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
-            guard let self else { return event }
-            self.handle(event)
-            return nil
-        }
-    }
-
-    private func removeKeyMonitor() {
-        if let keyMonitor {
-            NSEvent.removeMonitor(keyMonitor)
-            self.keyMonitor = nil
-        }
-    }
-
-    private func handle(_ event: NSEvent) {
-        if event.keyCode == UInt16(kVK_Escape) {
-            onCancel?()
-            return
-        }
-        if let hotkey = LauncherHotkey.from(event: event) {
-            onCapture?(hotkey)
-        } else {
-            NSSound.beep()
         }
     }
 }

--
Gitblit v1.9.3