- 桌面应用
- 插件系统
【免费下载链接】Flashlight
The missing Spotlight plugin system
导读
本文以 Flashlight 插件体系中的 evernote-search.bundle 为研究对象,完整讲解这款"Evernote Search"插件的设计思路、AppleScript 集成方式与源码实现细节。读完本文,你将掌握:Flashlight 插件如何通过results()/run()双入口与 Spotlight 结果面板交互、如何用osascript调用 Evernote 脚本桥执行"搜索笔记—收集元数据—打开笔记窗口"的完整链路,以及如何在搜索结果 HTML 中借助flashlight.bash()桥接能力执行系统命令。仓库源码与配置文件给出的路径,均可直接在当前仓库中验证。
一、插件概览:在 Spotlight 中搜索 Evernote
Evernote Search 插件(displayName: "Evernote Search")的目标是:当用户在 Spotlight 输入框输入诸如 "search evernote for workout routines"、"evernote funny documents" 这样的自然语言时,Flashlight 能够调用 Evernote 的搜索能力,把匹配笔记的标题与所属笔记本直接渲染进 Spotlight 的结果列表,点击条目即可在 Evernote 中打开对应笔记窗口。
插件元数据位于 info.json:
{ "categories" : ["Utilities"], "displayName" : "Evernote Search", "name" : "evernote-search", "description" : "Search your Evernotes", "examples" : [ "search evernote for workout routines", "evernote funny documents", "notes for cooking recipes" ], "creator_name": "Richard van der Dys", "creator_url": "http://richardvanderdys.com" }关键字段说明:
name:插件目录内的唯一标识(即evernote-search.bundle的名称部分),也是插件在目录索引中注册的 key;displayName:插件目录/设置界面展示的用户友好名称;categories:归类到Utilities(工具类),与同仓库下如 copycolor.bundle、timestamp.bundle 等插件的分类方式一致;examples:供用户参考的示例查询,这些示例还与examples.txt相互配合;creator_name/creator_url:作者署名信息。
目录 PluginDirectories/1/evernote-search.bundle 还包含以下文件:
plugin.py:插件主逻辑,见下文源码剖析;examples.txt:解析引擎用到的查询模板与可替换参数定义;info.json:插件元数据;1.version:插件版本哈希(内容为 SHA-256 摘要);Icon.png/Screenshot.png:插件图标与效果截图。
版本文件的由来
1.version中存储的是一串 64 位十六进制哈希。仓库的 compute_version.py 揭示了它的生成规则:脚本会对.bundle目录内全部文件(忽略隐藏文件、*.version与*.pyc)递归计算 SHA-256 哈希;每次更新插件后重新哈希,若摘要变化则删除旧N.version并生成N+1.version。因此 evernote-search 的1.version实际就是其插件内容快照的校验和,目录服务据此判断插件是否有新版本可下发。
二、查询模板与参数槽:examples.txt 的语法
examples.txt 内容如下:
search evernote for ~query(Workout Routine) evernote ~query(Funny documents) notes ~query(Cooking Recipes)每行是一条"示例查询模板",其中~query(...)是参数槽:括号内为示例值,而~query则是解析时抽取的命名参数。当用户输入evernote funny documents时,Flashlight 的 Parsnip 解析引擎会把funny documents匹配进~query槽,随后以fields['~query']的形式传入插件。
对比同仓库模板插件 SearchTemplate.bundle:
show a notification saying ~message(hello) toast ~message(hello world) notify ~message(good morning!)可见~参数名(示例值)是 Flashlight 插件的通用占位符语法——示例值同时承担"自动补全提示"与"离线学习语料"双重职责。
三、插件主逻辑源码剖析
插件核心实现在 plugin.py,由四个函数构成。下面逐段拆解。
3.1 入口函数 results():产出搜索结果
def results(fields, original_query): query = fields['~query'] return { "title": "Searching your notes for {0}".format(query), "run_args": [], "html": createHTML(query) }这是 Flashlight 插件协议规定的核心入口。其运行机制可对照 invoke_plugin.py:加载器把args(由解析树转换出的嵌套字典)、query原查询传入plugin.results(...),若results函数声明了第三个参数,还会把ParseTree对象一并注入;返回值以 JSON 序列化输出到 stdout,由 FlashlightKit 的查询引擎解析后渲染为 Spotlight 结果(见 FlashlightQueryEngine.m)。
返回值字典的三个键含义:
title:结果行显示的主标题,这里动态拼接出 "Searching your notes for {查询词}";run_args:回车执行时透传给run()的参数列表,此处为空;html:自定义结果视图的 HTML 内容,由createHTML(query)生成——这是该插件将笔记列表渲染进 Spotlight 的关键。
3.2 run():执行动作
def run(): print ''Flashlight 的插件协议要求提供run()以响应用户对结果项的回车操作。此插件的主结果本身没有关联动作(run_args为空,run()仅打印空行占位),真正的"打开笔记"动作发生在结果 HTML 内部——见 3.5 小节对 JavaScript 桥接的说明。
3.3 asrun():AppleScript 执行器
def asrun(ascript): osa = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) return osa.communicate(ascript)[0]asrun()通过subprocess.Popen启动系统命令osascript -,把 AppleScript 源码从标准输入喂给脚本解释器,并从 stdout 取回脚本输出。这是本插件与 macOS 系统能力交互的底层通道——osascript是 macOS 自带脚本工具,能对支持 Apple Events 的应用程序(Evernote 即属于此类)执行脚本桥调用。
3.4 getEvernoteSearchResults():向 Evernote 发起搜索
def getEvernoteSearchResults(query): script = ''' tell application "Evernote" set find_results to find notes "{0}" set output_list to {} repeat with find_result in find_results set o_title to get title of find_result set o_id to local id of find_result set o_notebook to name of notebook of find_result set end of output_list to {title:o_title, local id:o_id, notebook:o_notebook} end repeat get output_list end tell '''.replace('{0}', query) return asrun(script)该函数动态构造一段 AppleScript 并交给asrun()执行,流程为:
find notes "{query}":利用 Evernote 的 AppleScript 字典执行全文搜索;- 遍历
find_results,逐条提取笔记的三个关键属性:title:笔记标题;local id:笔记在本地数据库中的唯一标识;name of notebook:所属笔记本名称;
- 组装为 AppleScript record 列表(
{title:..., local id:..., notebook:...})并输出。
注意两点工程细节:一是查询词直接以字符串插值方式嵌入脚本(replace('{0}', query)),因此含引号或特殊字符的查询可能导致脚本语法问题,这是早期 Flashlight 插件常见的简朴处理方式;二是输出格式为 AppleScript record 的文本表示(形如{title:foo, local id:123, notebook:bar}),正好与 3.5 小节的解析逻辑相呼应。
3.5 createHTML():渲染结果面板与打开笔记
createHTML(query)负责把 Evernote 搜索结果转成可点击的结果页。代码分三段:
(1)样式与容器:定义浅灰背景(#eaedef)、无下划线链接、悬停描边等内联 CSS,并嵌入一个 16×16 的 base64 PNG 图标作为笔记条目的前置标记。
(2)解析 AppleScript 输出:
for line in re.split(',\stitle:', results[6:]): m = re.match('(.*),\sid:(.*), notebook:(.*)', line)results[6:]跳过 AppleScript record 文本的头部冗余(record 列表前缀{等),再按, title:切分得到逐条记录;正则(.*),\sid:(.*), notebook:(.*)依次捕获标题、本地 id、笔记本名,并用cgi.escape()做 HTML 转义后填入链接。
(3)生成可点击条目与桥接脚本:
<a onclick="openNote('{1}', '{2}', '{0}');"> <header>{0}</header> <i></i> <small>{2}</small> </a>点击任一条目触发openNote(id, notebook, title),该 JavaScript 函数会拼出一条新的 AppleScript 命令:
function openNote(id, notebook, title) { var script = 'osascript'; script += line('tell application "Evernote"'); script += line('open note window with note id "' + id + '" of notebook "' + notebook + '"'); script += line('activate'); script += line('end tell'); flashlight.bash(script); }这里调用了flashlight.bash()——Flashlight 提供给结果 HTML 的 JavaScript 桥接 API。其 Objective-C 实现位于 FlashlightWebScriptObject.m:通过NSTask以/bin/bash -l -c执行传入命令并返回输出,同时该对象还暴露了invoke()(触发打开目标结果)等能力(见 FlashlightWebScriptObject.m)。由此,用户点击笔记标题即执行osascript打开对应笔记窗口并激活 Evernote——"搜索→展示→打开"闭环在 Spotlight 面板内一步完成。
四、运行链路:从输入到打开的完整调用链
综合上述源码,一次完整的搜索经历如下阶段:
- 解析:用户输入匹配 examples.txt 模板,
~query槽被填充为实际查询词; - 加载:FlashlightKit 通过 invoke_plugin.py 在插件目录下导入
plugin模块,以args、query(以及可选的ParseTree)调用results(); - 取数:
results()调createHTML()→getEvernoteSearchResults()→asrun()→osascript -,Evernote 返回笔记记录文本; - 渲染:HTML 被包装进结果项,FlashlightQueryEngine 解析 JSON 后把结果注入 Spotlight 列表(详见 FlashlightQueryEngine.m 中对动态插件的执行与结果合并逻辑);
- 打开:用户点击条目,HTML 内嵌 JavaScript 经
flashlight.bash()执行osascript,在 Evernote 中打开对应笔记窗口。
该链路同时展示了 Flashlight 插件体系的两条并行能力路径:其一是results()/run()返回 JSON 的结构化结果协议(普通插件通用);其二是结果 HTML 内嵌 JavaScript +flashlight.bash()的系统桥接(本插件"就地打开笔记"所依赖)。后者在仓库其他插件中也有体现,例如 say.bundle 与 EasySIMBL 模板插件 SearchTemplate.bundle。
五、使用与注意事项
前提条件:
- 需要 macOS 系统(
osascript、Evernote 桌面客户端均依赖 macOS); - Evernote 桌面版需已安装并登录,且允许 Apple Events(系统设置中的自动化权限需授权 Flashlight 控制 Evernote);
- 需要先安装并运行 Flashlight 本体(通过 SIMBL 机制注入 Spotlight,见 FlashlightApp 目录下的工程结构),并把插件放入插件目录
PluginDirectories/1后启用。
使用方式:在 Spotlight 输入示例查询,如evernote funny documents或search evernote for workout routines,结果面板会实时列出匹配笔记;点击条目即可跳转打开。
局限与改进方向(从源码可推断):
- 查询词以纯字符串拼接嵌入 AppleScript,未做引号转义,包含特殊字符的查询可能失败;
- 依赖 AppleScript record 的文本输出格式做正则解析,对 Evernote 脚本桥输出格式变动较敏感;
- 每条结果都是一个可直接点击的 HTML 链接,但插件本身未提供"回车直达第一个结果"之类的默认动作(
run()为空实现),主要交互完全靠点击完成。
六、关联阅读
- 插件协议与加载器:invoke_plugin.py、run_plugin.py
- 查询引擎对插件结果的处理:FlashlightQueryEngine.m
- HTML 结果与系统命令桥接:FlashlightWebScriptObject.m
- 插件模板与示例语法:SearchTemplate.bundle/examples.txt、SearchTemplate.bundle/plugin.py
- 插件目录与版本机制:PluginDirectories/1、compute_version.py
通过 evernote-search 这一实例,可以清晰看到 Flashlight 插件如何把"自然语言解析 + 系统脚本桥 + Web 结果视图"三者组合起来,在 Spotlight 中落地一个完整的第三方搜索体验。
- 桌面应用
- 插件系统
【免费下载链接】Flashlight
The missing Spotlight plugin system
相关推荐
Flashlight RubyGems 搜索插件解析:从 Spotlight 输入到 RubyGems 检索的完整实现
Flashlight RubyGems 搜索插件解析:从 Spotlight 输入到 RubyGems 检索的完整实现 导读 RubyGems 是 Ruby 生
桌面应用插件系统Flashlight 的 iTerm2 插件:在 Spotlight 中一键用 iTerm2 打开 Finder 选中项与目录
Flashlight 的 iTerm2 插件:在 Spotlight 中一键用 iTerm2 打开 Finder 选中项与目录 导读 本文围绕 Flashlig
桌面应用插件系统终极Flashlight插件指南:500+实用插件让你的Spotlight变身超级搜索神器
终极Flashlight插件指南:500+实用插件让你的Spotlight变身超级搜索神器 想要让Mac自带的Spotlight搜索变得更强大吗?Flashli
桌面应用插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考