不授全盘权限的 Spotlight 搜索进阶:Tinycast 白名单三件套 不授全盘权限的 Spotlight 搜索进阶Tinycast 白名单三件套【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycastmacOS 上的文件搜索几乎绕不开一个尴尬选择要么接受全盘磁盘访问Full Disk Access这种宽到近乎失控的授权要么接受系统自带 Spotlight 那点贫瘠的交互。Tinycast 提供了一条第三条路——它只读 Spotlight 索引的元数据不扫描磁盘、不建私有索引、不记录历史自然也拿不到也不需要那份全盘授权。支撑这种零权限搜索的是三个白名单机制Scopes 搜索范围、Ignore Patterns 忽略规则、Type Filter 类型过滤。它们共同决定了哪些目录能被搜、哪些文件永远不会出现、结果以什么形态返回。本文直接对照 FileSearch 模块 的真实源码拆解这套白名单从配置、编译到落入 Spotlight 查询语句的全过程。白名单 Scopes把搜索什么变成一道显式声明Tinycast 的文件搜索默认范围是[~]FileSearchScope.swift而这一个波浪号并不等于搜索整个用户目录。FileSearchPolicy在构建策略时把配置的根目录拆成两类能直接交给 Spotlight 的directRoots以及必须展开的 home 根目录FileSearchPolicy.swift。home 被展开为可见的子目录 两个云存储根——Library/CloudStorage与 iCloud Drive——而~/Library本身永远不可能成为一个范围static func expand(_ scope: String, homeDirectory: URL) - URL { guard scope.hasPrefix(~) else { return URL(fileURLWithPath: scope, isDirectory: true).standardizedFileURL } ... }~/Library的排除是结构性的不是规则性的没有任何用户设置能把它加回来这正是功能保持免授权的根基。如果用户手动添加~/Library下的某个文件夹那是用户明确要了什么就给什么。三个值得注意的工程细节范围以~缩写形式存储。abbreviate(_:homeDirectory:)把/Users/foo/Projects存成~/Projects跨机器备份时仍然指向另一台机器上的同名目录而不是把某台机器的绝对路径写死。策略按设置变更重建绝不按每次击键重建。glob 编译与波浪号展开都发生在打字路径之外FileSearchSession.swift 的apply(scopes:ignorePatterns:)所以 120ms 防抖窗口里跑的是纯粹的 Spotlight 查询。空列表 明确不搜索。清空全部范围不会回退到 homerecent(scopes:)在scopes.isEmpty时直接返回空数组——清空是一个主动决定而不是一个未设置状态。设置界面FileSearchSettingsView.swift用NSOpenPanel添加目录、对缺失路径逐行标记、并提供 Restore Defaults 一键还原。整个功能由一个fileSearchEnabled开关门控关闭时没有任何入口点也没有任何 Spotlight 查询发生全局快捷键直接空操作。内置忽略规则编译进二进制、不可关闭的结构级排除白名单的另一半是忽略规则。Tinycast 内置六个默认模式写在 FileSearchIgnoreList.swift 里static let defaults [node_modules, DerivedData, build, dist, target, Pods]这六个规则编译进二进制而不是持久化。fileSearchIgnorePatterns只保存用户自己添加的模式所以修改defaults能立即覆盖所有已安装的副本代价是这六条无法被用户关闭——这正是设计意图DerivedData、node_modules这类目录对开发者的搜索结果毫无价值而不可关闭本身就是对结果质量的结构性保障。用户规则按形状进入三个桶这是匹配成本足够低的关键模式形状匹配对象示例无/、无元字符任意路径组件大小写折叠走Setnode_modules无/、含*?[任意路径组件走fnmatch*.tmp含/整个绝对路径走fnmatch**/[Cc]ache/**fnmatch带FNM_CASEFOLD但刻意不带FNM_PATHNAME因此*可以跨过/**/…/**按字面语义工作。模式预先以ContiguousArrayCChar形式终结存储热点路径上不再做 String→C 缓冲的重复编码。更妙的是裸*名称 glob 会被推入 Spotlight 查询表达式本身作为kMDItemFSName ! …cd子句FileSearchQuery.swift 的expression(for:excluding:filter:)。这样被忽略的文件连 1,000 候选上限都不占用——它们还没到本地就被 Spotlight 排除掉了。只有这种形状能被推送Spotlight 把?和[当字面量读而kMDItemPath根本不可查询所以路径 glob 在服务端没有拼写只能留在本地。在这之外还有一层不依赖任何规则的结构性过滤隐藏路径组件.前缀与.app应用包内容在isExcludedPath中被硬性剔除。文档里写得很直白Hidden paths and application-bundle contents are structural, not patterns——它们是免授权承诺的组成部分因此同样没有用户设置可以重新放行。类型过滤把筛选写进查询而不是写进结果FileSearchFilter.swift 定义了搜索头部的类型菜单All Types、Folders、Documents、Images、Audio、Videos、Archives。每个 case 声明自己接纳的UTType列表其余一切从该列表派生case .documents: return [.text, .compositeContent, .spreadsheet, .presentation, .pdf] case .images: return [.image]类型被翻译成 Spotlight 谓词kMDItemContentTypeTree public.image多类型时加括号用||连接作为查询的一部分发送给 Spotlight而不是在返回结果后做二次遍历。这是性能设计的关键被类型过滤拒绝的文件同样不会消耗 1,000 候选上限。FileSearchSession的去重与过期检查以查询 过滤器为键所以收窄类型是用同样的关键词重新查询而不是在一个已经被裁到 200 行的结果集里再做减法。有两个容易忽略的细节过滤器挂在PaletteState上PaletteState.swift每次召唤面板时重置为 All Types从不持久化——上次用的过滤条件不会污染下一次搜索的预期。⌘P 或头部按钮打开过滤器菜单经由 PaletteFilterAction.swift 的fileSearchFilter路由切换过滤器会重置选区、吸附滚动并重新执行查询。变更生效路径在 RootPaletteView.swift 里清晰可见onChange(of: vm.fileSearchFilter)直接触发fileSearch.search(vm.query, filter:)。由于类型是从磁盘解析的 UTType 回答两个问题一个.pages文档包被归入 Documents而普通文件夹被归入 Folders——分类与 Spotlight 自身完全一致不会出现文件包算不算文件夹的边界争议。多词模糊匹配AND 语义 双通道排序多词匹配是查询构造器 FileSearchQuery.swift 的核心。输入先按空白切分成词每个词生成一条kMDItemFSName *term*cd子句子句之间用连接let matches terms.map { kMDItemFSName \*\(escape($0))*\cd } return (matches types excludes).joined(separator: )所以annual report要求文件名同时含annual和report两个词但不要求相邻、也不要求顺序。注意cd后缀Spotlight 的大小写与变音符号不敏感匹配这保证了英文搜索的宽容度。与此同时用户键入的词是字面量——*和?会被转义中和防止把用户的打字意外变成通配符而用户配置的忽略模式则保留*因为那是 Spotlight 唯一会评估的通配符。候选回来后本地用FuzzyMatch做双通道排序rank函数整词模糊分 每个词独立模糊分的累加。整词分优先词分次之再以本地化文件名、路径顺序打破平局——整个流程在 1,000 候选中只折叠一次然后prefix(resultLimit)截到 200 行。排序是确定性的同样的查询永远不会给出前后摇摆的结果集。零缓存检索只有路径离开 Spotlight整个文件搜索的零缓存承诺可以压缩成一句代码级事实kMDItemPath是唯一从结果里读取的属性。MDQuery把路径从自己的缓存里免费交出来其他任何属性都是一次约半毫秒的元数据抓取——在 1,000 个候选上仅内容类型一项就能耗掉整整一秒这正是旧实现的全部延迟来源FileSearchService.swift。新实现把行还需要什么是否文件夹、是否隐藏、是否应用包压缩成对未被忽略列表淘汰的候选的一次resourceValuesstat200 个 URL stat 耗时 13ms而 200 次元数据抓取耗时 200ms。MDQuerySetMaxCount把候选钉死在 1,000这是MDQuery而非NSMetadataQuery的原因——后者没有源头级上限宽泛的文件名查询可能突破 Tinycast 的 100MB 内存预算。查询路径上还有 120ms 防抖 单 worker 串行化FileSearchSession保留上一批结果、合并到最新待查请求较慢的键入不会堆叠重叠查询被取代的查询永不发布晚到的结果无法覆盖新查询的行。2026-09-12 的基线数据Tests/file-search-performance.swift对开发者 home 的实测可以佐证这套设计的收益场景首次重复空屏最近使用列表184ms41msa/e/swift/pdf/project键入查询88–397ms54–107ms作为对照改版前每个候选都抓内容类型与隐藏标记同一组查询是 192–831ms 首次 / 191–668ms 重复——路径化重写把重复查询中位数压到了 54–107ms模式匹配本身只占其中几毫秒。快捷键与最近使用列表零缓存检索的交互层在零缓存的前提下快捷键与最近使用列表的设计逻辑完全一致一切数据都来自系统Tinycast 自己不记录任何东西。快捷键。Search Files 与所有内建命令一样可绑定全局快捷键CommandID.searchFiles的hotKeyAction返回.command(self)CommandID.swift经 HotKeyAction.swift 的case command(CommandID)注册绑定后启动器行会以 keycap 显示当前和弦。Settings ▸ File Search 面板统一管理开关、范围、忽略模式与 Search Files 命令行禁用功能会取消会话并把已打开的搜索屏退回启动器——不会改变面板可见性。最近使用列表。空查询本身就是一个请求且跳过打字防抖——没有下一次击键需要合并。FileSearchQuery.RecentStamp询问两个时间戳各自带窗口FileSearchQuery.swiftkMDItemFSContentChangeDate最近 3 天$time.now(-259200)kMDItemLastUsedDate最近 30 天$time.now(-2592000)两个都要的原因很实在macOS 现在对绝大多数打开操作不再写kMDItemLastUsedDate仅靠 used 窗口得到的结果只有寥寥几个下载而更短的 change 窗口负责把繁忙机器上的命中数压到候选上限之内。Spotlight 只能按单一属性排序所以服务层对每个时间戳跑一次有序查询按日期合并头部、最新的在前发布 20 行——只有每张列表前 20 行才值得读日期超出者永远进不了合并列表。排序属性必须在MDQueryCreate时命名事后用MDQuerySetSortOrder设置会被忽略——这是第一次尝试实测到的坑。行级操作则是命令面板的标准动作集↵ 打开文件/文件夹、⌘↵ 在 Finder 中显示、⌘Y 面板内 Quick Look、⇧⌘C 复制文件本体、⌥⌘C 复制文件名、⌃⌘C 复制路径、⇧⌘V 粘贴文件到唤起面板的应用、⌃X 移入废纸篓FileSearchScreen.swift。三个 ⌘C 变体只差第二个修饰键PaletteShortcut按 ⇧→⌥→⌃ 的顺序读取裸 ⌘C 始终留给搜索框。复制文件与复制名称不会被标记为内部剪贴板类型——它们会像普通复制一样进入剪贴板历史与零缓存的搜索承诺互不干扰。三件套的本质把信任边界写成代码把 Scopes、Ignore Patterns、Type Filter 放在一起看Tinycast 的思路其实是一条清晰的主线凡是能在查询阶段排除的东西绝不拖到结果阶段。范围排除在MDQuerySetSearchScope层面生效忽略规则的裸名称 glob 与类型过滤被拼进 Spotlight 谓词本身连被排除的候选都不会占用 1,000 的上限剩下的结构性排除隐藏路径、应用包写死在isExcludedPath里与免授权的承诺绑定不留任何用户开关。而不建索引、不记历史、不缓存查询则由kMDItemPath唯一属性读取、MDQuerySetMaxCount候选上限与每次召唤重置过滤器共同落实。对不想授予全盘权限、又希望文件搜索拥有命令面板级交互的人来说这套白名单组合把能搜什么、绝不搜什么、结果长什么样全部变成了可配置、可审计、有源码可查的显式声明——这大概就是它和传统全盘索引方案最本质的区别。【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考