状态:当前代码实现备份
用途:在后续调整 Quick Search / 快速建议规则前,保存当前行为,便于回看、对照或恢复
生成日期:2026-05-20
主要代码来源:
Apptag/QuickSearch.swiftApptag/ContentView.swiftApptag/ApptagApp.swiftApptag/PreferencesView.swiftApptag/AppDefaults.swiftApptag/DataLayer.swift当前代码中的 Quick Search 已经包含以下能力:
当前默认主界面快捷键:
⇧ ⌥ Space
Shift + Option + Space
代码定义:
static var defaultMain: LauncherHotkey {
LauncherHotkey(keyCode: UInt32(kVK_Space), modifiers: UInt32(shiftKey | optionKey))
}
当前行为:
当前主界面内快捷键:
Space
触发条件:
代码判断逻辑位于 Apptag/ApptagApp.swift 的 shouldOpenQuickSearch(for:)。
当前默认全局 Quick Search 快捷键:
Fn + Space
代码定义:
static var defaultQuickSearch: LauncherHotkey {
LauncherHotkey(keyCode: UInt32(kVK_Space), modifiers: UInt32(kEventKeyModifierFnMask))
}
当前行为:
Fn + Space。Fn + Space。Fn + Space。全局 Quick Search 快捷键触发后:
quickSearchCloseHidesOverlay = true。代码行为:
private func showQuickSearchFromGlobalHotkey() {
guard overlayWindow?.isVisible != true else { return }
showOverlay(initialQuickSearchSource: QuickSearchOpenSource.globalHidden)
}
当前使用 Carbon RegisterEventHotKey 注册全局快捷键。
注册成功:
Active。注册失败:
Conflict。当前支持三种状态:
Active
Conflict
Disabled
设置页展示逻辑:
当前代码对以下快捷键有特殊冲突文案:
| 快捷键 | 文案含义 |
|---|---|
⌘ Space |
Spotlight 冲突 |
⌥ ⌘ Space |
Finder 搜索窗口冲突 |
⌃ Space |
上一个输入法冲突 |
⌃ ⌥ Space |
下一个输入法冲突 |
⌃ ⌘ Space |
表情与符号冲突 |
Fn + Space |
Fn / Globe / 输入法相关冲突 |
其他冲突使用 generic 文案:
This shortcut is already used by macOS or another app.
Choose another shortcut, or change the shortcut in the other app and retry.
录制快捷键时:
Esc 取消录制。Shift、Option、Control、Command、Fn。Quick Search 打开时:
canOpenQuickSearch 判断。canOpenQuickSearch 条件:
Quick Search 关闭时:
quickSearchVisible = false。quickSearchCloseHidesOverlay。如果 Quick Search 是通过全局快捷键在隐藏主界面时直接打开的:
当前行为:
Esc 会关闭 Quick Search。Esc 会关闭 Quick Search。当前每个应用会生成一个 QuickSearchDocument。
文档字段:
app
localizedNames
tagNames
note
bundleIdentifier
lastOpenedAt
openCount
字段来源:
app 来自扫描并经过 TagEditor 标注后的应用。localizedNames 从应用 Bundle 中读取本地化名称、Bundle 名称和 Finder 展示名。tagNames 来自应用当前标签。note 优先使用 store.appNotes[path],其次使用 app.note,最后为空字符串。bundleIdentifier 来自应用包标识,没有则为空字符串。lastOpenedAt 来自 store.appLastOpenedAt[path]。openCount 来自 store.appOpenCounts[path],没有则为 0。搜索索引刷新时机:
查询标准化:
如果标准化后的查询为空:
如果标准化后的查询不为空:
当前搜索字段:
| 字段 | 权重 |
|---|---|
| 应用名称 / 本地化名称 | 100 |
| 标签名称 | 70 |
| 应用备注 | 45 |
| 包标识 | 20 |
字段优先级也会用于排序:
应用名称优先于标签
标签优先于备注
备注优先于包标识
当前支持的匹配类型:
| 匹配类型 | 权重 |
|---|---|
| 精确匹配 | 100 |
| 前缀匹配 | 80 |
| 子串匹配 | 60 |
| 缩写匹配 | 55 |
| 模糊子序列匹配 | 35 |
字段标准化文本与 token 完全相等。
字段标准化文本以 token 开头。
字段标准化文本包含 token。
仅应用名称字段支持缩写匹配。
缩写生成规则:
示例:
Google Chrome -> gc
Final Cut Pro -> fcp
当前缩写匹配要求:
应用名称缩写必须以 token 开头。
当 token 长度至少为 2 时,支持非连续字符子序列匹配。
示例:
ps 可以匹配 Photoshop
非精确匹配有位置加分:
positionBoost = max(0, 10 - min(matchStartIndex, 10))
含义:
单个 token 在单个字段上的分数:
字段权重 + 匹配类型权重 + 位置加分
当前代码对包含汉字的字段生成拼音候选。
拼音候选生成规则:
kCFStringTransformToLatin。示例:
微信 -> wei xin / weixin / wx
拼音候选参与普通匹配流程:
应用名称、标签、备注都可以生成拼音候选。
包标识不生成拼音候选。
对于非空查询:
textScore。finalScore。公式:
finalScore = textScore + behaviorBoost
行为加分最高为 20:
behaviorBoost = min(recentBoost + frequencyBoost, 20)
最近启动加分:
| 最近启动时间 | 加分 |
|---|---|
| 24 小时内 | 15 |
| 7 天内 | 10 |
| 30 天内 | 5 |
| 更早 | 2 |
| 从未启动 | 0 |
启动频率加分:
frequencyBoost = min(openCount, 10)
说明:
openCount,没有 7 天 / 30 天分窗口。搜索结果排序规则:
finalScore 高的排前面。textScore 高的排前面。默认返回结果上限:
50
当查询为空时,当前代码调用:
emptyQueryResults(documents: documents, limit: min(limit, 6))
也就是说,空查询最多显示 6 个结果。
空查询只使用两类候选:
不会使用:
最近启动列表规则:
lastOpenedAt != nil 的应用。高频启动列表规则:
openCount > 0 的应用。空查询建议的合并规则:
ordered = recent + frequent
然后按应用 URL 去重:
空查询结果的 finalScore 使用行为加分:
finalScore = behaviorBoost
textScore = 0
bestFieldRank = Int.max
matchedTagName = nil
noteSnippet = nil
但由于空查询结果已经在 emptyQueryResults 内部按 recent + frequent 排好,后续没有再调用通用 rank 排序。
当前空查询建议可以概括为:
优先最近启动。
最近启动不足时,用总启动次数补充。
最多 6 个。
没有历史时为空。
每个结果行展示:
图标尺寸:
46 × 46
圆角:
10
应用名称:
详情文本优先级:
noteSnippet,显示 noteSnippet。备注截断规则:
...。noteSnippet 如果原备注长度大于 80,则取前 77 个字符并加 ...。当前行为说明:
右侧标签取值:
matchedTagName ?? document.tagNames.first
含义:
标签样式:
Quick Search 面板:
.regularMaterial。输入区域:
结果行:
选中态:
面板水平居中。
顶部位置:
max(notchHeight + 54, min(112, windowHeight * 0.12))
面板中心点根据估算高度计算:
estimatedPanelHeight = visibleRows * 76 + 122
panelCenterY = panelTopY + estimatedPanelHeight / 2
最大可见行数根据窗口高度动态计算:
availableHeight = windowHeight - panelTopY - 84 - 122
maxVisibleRows = max(1, min(8, floor(availableHeight / 76)))
含义:
注意:
结果刷新后:
键盘行为:
| 按键 | 行为 |
|---|---|
↑ |
选择上一项 |
↓ |
选择下一项 |
Enter |
打开选中应用 |
Esc |
关闭 Quick Search |
上下移动规则:
鼠标行为:
从 Quick Search 启动应用成功后:
记录的历史:
appOpenCounts[path] += 1
appLastOpenedAt[path] = Date()
从 Quick Search 启动应用失败后:
错误文案 key:
quickSearch.launchFailed
记录启动历史时,如果应用的 uncommon 来源是自动标记,并且启动次数达到自动阈值:
这部分逻辑位于 TagEditor.recordLauncherOpen(for:)。
以下是当前代码没有实现、但后续新方案可能会加入的规则:
如果后续新规则出问题,需要恢复当前行为,重点恢复以下代码逻辑:
Fn + Space。emptyQueryResults(documents:limit:):.regularMaterial 背景。