脚本系统
使用 Python 脚本自动化 macOS 任务——Leader 键、快捷键、提示、剪贴板等。
目录
- 快速开始
- Leader 键
- 启动器
- API 参考
- wz.leader(trigger_key, mappings)
- wz.app.launch(name)
- wz.app.frontmost()
- wz.alert(text, duration=2.0)
- wz.notify(title, message="")
- wz.pasteboard.get()
- wz.pasteboard.set(text)
- wz.keystroke(key, modifiers=None)
- wz.execute(command, background=True)
- wz.timer.after(seconds, callback)
- wz.timer.every(seconds, callback)
- wz.timer.cancel(timer_id)
- wz.date(format="%Y-%m-%d")
- wz.reload()
- wz.chooser.show(initial_query=None)
- wz.chooser.close()
- wz.chooser.toggle()
- wz.chooser.show_source(prefix)
- wz.chooser.register_source(source)
- wz.chooser.unregister_source(name)
- wz.chooser.pick(items, callback, placeholder="Choose...")
- @wz.chooser.on(event)
- wz.chooser.register_command(name, title, action, ...)
- wz.chooser.unregister_command(name)
- @wz.chooser.command(name, title, ...)
- @wz.chooser.source(name, prefix=None, priority=0, description="")
- 使用示例
- 启动器配置
- 脚本运行环境
- 安全说明
- 常见问题
闻字 内置了一个基于 Python 的脚本系统,可以用来自动化 macOS 常见操作——通过 Leader 键启动应用、绑定全局快捷键、显示提示、操作剪贴板等。
快速开始
- 启用脚本系统:在 设置 → 通用 → Scripting 中打开开关,或者直接编辑
config.json:
{
"scripting": {
"enabled": true
}
}
- 创建脚本文件
~/.config/WenZi/scripts/init.py:
wz.leader("cmd_r", [
{"key": "w", "app": "WeChat"},
{"key": "s", "app": "Slack"},
{"key": "t", "app": "iTerm"},
])
- 重启闻字。按住右 Command 键,屏幕上会显示快捷键面板,再按字母键即可启动对应应用。
Leader 键
Leader 键的使用方式:按住一个触发键(如右 Command),屏幕上会浮现可用映射列表,然后按第二个键执行对应操作。松开触发键后面板自动消失。
wz.leader("cmd_r", [
{"key": "w", "app": "WeChat"},
{"key": "f", "app": "Safari"},
{"key": "g", "app": "/Users/me/Applications/Google Chrome.app"},
{"key": "i", "exec": "/usr/local/bin/code ~/work/projects", "desc": "projects"},
{"key": "d", "desc": "日期", "func": lambda: (
wz.pasteboard.set(wz.date("%Y-%m-%d")),
wz.notify("日期已复制", wz.date("%Y-%m-%d")),
)},
{"key": "r", "desc": "重载脚本", "func": lambda: wz.reload()},
])
触发键
任何修饰键都可以作为触发键,可用名称如下:
| 按键 | 名称 |
|---|---|
| 右 Command | cmd_r |
| 右 Alt/Option | alt_r |
| 右 Shift | shift_r |
| 右 Control | ctrl_r |
| 左 Command | cmd |
| 左 Alt/Option | alt |
| 左 Shift | shift |
| 左 Control | ctrl |
可以用不同的触发键注册多组 Leader:
wz.leader("cmd_r", [...]) # 右 Command 启动应用
wz.leader("alt_r", [...]) # 右 Alt 执行工具操作
映射动作
每个映射字典需要 "key" 字段加一个动作:
| 字段 | 类型 | 说明 |
|---|---|---|
key |
str |
子键名称(如 "w"、"1"、"f") |
app |
str |
应用名称或 .app 完整路径,启动/聚焦该应用 |
func |
callable |
要调用的 Python 函数 |
exec |
str |
要执行的 Shell 命令 |
desc |
str |
可选描述,显示在浮窗面板中 |
如果省略 desc,面板会显示应用名称或命令。
启动器
启动器是一个键盘驱动的搜索面板(类似 Alfred 或 Raycast),可以快速查找并打开应用、文件、书签、剪贴板历史和代码片段。它内置于脚本系统中,通过可配置的快捷键激活。
激活方式
- 默认快捷键:
Cmd+Space(可通过配置scripting.chooser.hotkey修改) - 数据源专用快捷键: 每个数据源可以绑定独立的快捷键(见下方配置)
- 脚本 API:
wz.chooser.show()/wz.chooser.toggle()
搜索模式
启动器支持两种搜索模式:
- 全局搜索: 直接输入关键词,搜索所有无前缀数据源(如应用)。结果按优先级和模糊匹配得分排序。
- 前缀搜索: 输入前缀加空格激活特定数据源。例如
f readme搜索文件名包含 "readme" 的文件。
内置数据源
| 数据源 | 前缀 | 说明 |
|---|---|---|
| 应用 | (无) | 搜索已安装的应用。全局搜索时始终参与。 |
| 计算器 | (无) | 数学运算和单位转换。全局搜索时始终参与。 |
| 命令 | > |
命令面板——脚本在此注册命名命令。 |
| 文件 | f |
通过 macOS Spotlight 按文件名搜索。 |
| 文件夹 | fd |
通过 macOS Spotlight 搜索文件夹。 |
| 剪贴板 | cb |
浏览剪贴板历史(文本和图片)。 |
| 代码片段 | sn |
搜索文本片段,支持关键词自动展开。 |
| 书签 | bm |
搜索浏览器书签(Chrome、Safari、Arc、Edge、Brave、Firefox)。 |
前缀可通过配置 scripting.chooser.prefixes 修改。> 前缀保留给命令面板,不可更改。
键盘快捷键
| 快捷键 | 操作 |
|---|---|
↑ ↓ |
上下导航 |
Enter |
打开/执行选中项 |
⌘+Enter |
在 Finder 中显示(适用于文件类项目) |
⌘1 – ⌘9 |
按位置快速选择 |
Tab |
自动补全(如在 > 模式下补全命令名) |
Esc |
关闭启动器 |
Alt / Ctrl / Shift(按住) |
显示选中项的替代操作 |
自定义数据源
可以通过 @wz.chooser.source 装饰器注册自定义数据源:
@wz.chooser.source("todos", prefix="td", priority=5)
def search_todos(query):
return [
{"title": "修复 bug #123", "subtitle": "后端", "action": lambda: ...},
{"title": "写文档", "subtitle": "前端", "action": lambda: ...},
]
命令
脚本可以注册命名命令,这些命令会出现在命令面板中(在启动器中输入 > 激活)。命令支持参数传递、修饰键和 Tab 补全。
# 装饰器方式
@wz.chooser.command("greet", title="Greet", subtitle="Say hello")
def greet(args):
name = args.strip() or "World"
wz.notify("Hello", f"Hello, {name}!")
# 直接注册
wz.chooser.register_command(
name="open-url",
title="Open URL",
subtitle="Open a URL in browser",
action=lambda args: wz.execute(f"open {args.strip()}"),
)
参数传递: 输入完整命令名后加空格即可进入参数模式。例如 > greet Alice 会将 "Alice" 作为 args 参数传递。Tab 补全命令名后也会自动进入参数模式。
修饰键: 命令可以为修饰键定义替代动作:
wz.chooser.register_command(
name="deploy",
title="Deploy",
action=lambda args: deploy(args),
modifiers={
"alt": {"subtitle": "Force deploy", "action": lambda args: force_deploy(args)},
},
)
推广命令(promoted): 默认情况下,命令仅在 > 前缀激活时显示。设置 promoted=True 可使其同时出现在主搜索中(与应用等并列):
@wz.chooser.command("reload", title="Reload Scripts", promoted=True)
def reload(args):
wz.reload()
内置 help 命令: 系统始终提供一个推广的 help 命令。在启动器中输入 "help" 并按回车,即可查看所有可用的前缀及其说明。
使用学习
启用后(默认开启),启动器会跟踪你对每个查询选择了哪些项目,并在后续搜索中提升常用项目的排名。数据存储在 ~/.config/WenZi/chooser_usage.json。
API 参考
wz.leader(trigger_key, mappings)
注册一组 Leader 键配置。
wz.leader("cmd_r", [
{"key": "w", "app": "WeChat"},
])
wz.app.launch(name)
启动或聚焦应用。支持应用名称或完整路径。
wz.app.launch("Safari")
wz.app.launch("/Applications/Visual Studio Code.app")
wz.app.frontmost()
返回当前前台应用的名称。
name = wz.app.frontmost() # 例如 "Finder"
wz.alert(text, duration=2.0)
在屏幕上显示一个浮动提示,duration 秒后自动消失。
wz.alert("你好!", duration=3.0)
wz.notify(title, message="")
发送 macOS 系统通知。
wz.notify("构建完成", "所有测试已通过")
wz.pasteboard.get()
获取当前剪贴板文本,没有内容则返回 None。
text = wz.pasteboard.get()
wz.pasteboard.set(text)
设置剪贴板文本。
wz.pasteboard.set("Hello, world!")
wz.keystroke(key, modifiers=None)
通过 Quartz CGEvent 模拟按键。
wz.keystroke("c", modifiers=["cmd"]) # Cmd+C
wz.keystroke("v", modifiers=["cmd"]) # Cmd+V
wz.keystroke("space") # 空格
wz.keystroke("a", modifiers=["cmd", "shift"]) # Cmd+Shift+A
wz.execute(command, background=True)
执行 Shell 命令。
wz.execute("open ~/Downloads") # 后台执行(返回 None)
output = wz.execute("date", background=False) # 前台执行(返回 stdout)
wz.timer.after(seconds, callback)
延迟执行一次。返回 timer_id。
tid = wz.timer.after(5.0, lambda: wz.alert("5 秒到了"))
wz.timer.every(seconds, callback)
按间隔重复执行。返回 timer_id。
tid = wz.timer.every(60.0, lambda: wz.notify("提醒", "该休息了"))
wz.timer.cancel(timer_id)
取消定时器。
tid = wz.timer.every(10.0, my_func)
wz.timer.cancel(tid)
wz.date(format="%Y-%m-%d")
返回格式化的当前日期/时间字符串。
wz.date() # "2025-03-15"
wz.date("%H:%M:%S") # "14:30:00"
wz.date("%Y-%m-%d %H:%M") # "2025-03-15 14:30"
wz.reload()
重新加载所有脚本。停止当前监听器,清除脚本目录下已缓存的模块,重新读取 init.py(及其导入的所有子模块),然后重启。所有文件变更都会在重载后生效。
wz.reload()
wz.chooser.show(initial_query=None)
显示启动器面板。可选预填搜索输入。
wz.chooser.show()
wz.chooser.show(initial_query="f readme")
wz.chooser.close()
关闭启动器面板。
wz.chooser.toggle()
切换启动器面板的显示/隐藏。
wz.chooser.show_source(prefix)
以指定数据源激活状态显示启动器。
wz.chooser.show_source("cb") # 打开并显示剪贴板历史
wz.chooser.register_source(source)
注册一个 ChooserSource 对象作为数据源。
wz.chooser.unregister_source(name)
按名称移除已注册的数据源。
wz.chooser.pick(items, callback, placeholder="Choose...")
将启动器用作通用选择 UI。显示一组固定选项;用户选择后调用 callback(item_dict),如果关闭则调用 callback(None)。
wz.chooser.pick(
[{"title": "选项 A"}, {"title": "选项 B"}],
callback=lambda item: print(item),
placeholder="请选择...",
)
@wz.chooser.on(event)
装饰器,注册启动器事件处理函数。
支持的事件:open、close、select、delete。
@wz.chooser.on("select")
def on_select(item_info):
print(f"选中了: {item_info['title']}")
wz.chooser.register_command(name, title, action, ...)
在命令面板(> 前缀)中注册一个命名命令。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
str |
(必填) | 唯一命令名(单个 token,如 "reload-scripts") |
title |
str |
(必填) | 显示在启动器中的标题 |
action |
callable |
(必填) | 回调函数,接收参数字符串:action(args_str) |
subtitle |
str |
"" |
标题下方的描述 |
icon |
str |
"" |
图标 URL(file:// 或 data: URI) |
modifiers |
dict |
None |
修饰键动作(见上方命令章节) |
promoted |
bool |
False |
同时出现在无前缀的主搜索中 |
wz.chooser.register_command(
name="greet",
title="Greet",
action=lambda args: wz.notify("Hello", args.strip() or "World"),
promoted=True,
)
wz.chooser.unregister_command(name)
按名称移除已注册的命令。
@wz.chooser.command(name, title, ...)
装饰器,将函数注册为启动器命令。参数与 register_command 相同,但不需要 action(被装饰的函数即为 action)。
@wz.chooser.command("greet", title="Greet", promoted=True)
def greet(args):
wz.notify("Hello", args.strip() or "World")
@wz.chooser.source(name, prefix=None, priority=0, description="")
装饰器,将搜索函数注册为启动器数据源。设置 description 可使该源出现在内置 help 命令的输出中。
@wz.chooser.source("notes", prefix="n", priority=5, description="Search notes")
def search_notes(query):
return [{"title": "...", "action": lambda: ...}]
使用示例
应用启动器
wz.leader("cmd_r", [
{"key": "1", "app": "1Password"},
{"key": "b", "app": "Obsidian"},
{"key": "c", "app": "Calendar"},
{"key": "f", "app": "Safari"},
{"key": "g", "app": "/Users/me/Applications/Google Chrome.app"},
{"key": "n", "app": "Notes"},
{"key": "s", "app": "Slack"},
{"key": "t", "app": "iTerm"},
{"key": "v", "app": "Visual Studio Code"},
{"key": "w", "app": "WeChat"},
{"key": "z", "app": "zoom.us"},
])
工具快捷键
wz.leader("alt_r", [
{"key": "d", "desc": "日期 → 剪贴板", "func": lambda: (
wz.pasteboard.set(wz.date("%Y-%m-%d")),
wz.notify("日期已复制", wz.date("%Y-%m-%d")),
)},
{"key": "t", "desc": "时间戳", "func": lambda: (
wz.pasteboard.set(wz.date("%Y-%m-%d %H:%M:%S")),
wz.alert("时间戳已复制"),
)},
{"key": "r", "desc": "重载脚本", "func": lambda: wz.reload()},
])
定时提醒
# 每 30 分钟提醒休息
wz.timer.every(1800, lambda: wz.notify("休息", "站起来活动一下!"))
全局快捷键
# Ctrl+Cmd+N 打开备忘录
wz.hotkey.bind("ctrl+cmd+n", lambda: wz.execute("open -a Notes"))
启动器配置
启动器的配置位于 config.json 的 scripting.chooser 下:
{
"scripting": {
"chooser": {
"enabled": true,
"hotkey": "cmd+space",
"app_search": true,
"file_search": true,
"clipboard_history": false,
"snippets": false,
"bookmarks": true,
"usage_learning": true,
"prefixes": {
"clipboard": "cb",
"files": "f",
"snippets": "sn",
"bookmarks": "bm"
},
"source_hotkeys": {
"clipboard": "",
"files": "",
"snippets": "",
"bookmarks": ""
}
}
}
}
| 选项 | 默认值 | 说明 |
|---|---|---|
enabled |
false |
启动器总开关 |
hotkey |
"cmd+space" |
切换启动器的全局快捷键 |
app_search |
true |
启用应用搜索 |
file_search |
true |
启用 Spotlight 文件搜索 |
clipboard_history |
false |
启用剪贴板历史跟踪 |
snippets |
false |
启用代码片段搜索和自动展开 |
bookmarks |
true |
启用浏览器书签搜索 |
usage_learning |
true |
跟踪选择频率以优化排序 |
prefixes |
(见上方) | 各数据源的前缀字符串 |
source_hotkeys |
(空) | 直接打开启动器并预选数据源的快捷键 |
脚本运行环境
- 脚本作为标准 Python 代码运行,可以使用
import导入任何模块 wz对象在init.py中作为全局变量直接可用,无需导入- 在子模块中通过
from wenzi.scripting.api import wz获取wz对象 - 脚本中的错误会被捕获并以浮窗提示显示
- 脚本在启动时加载一次,修改后需调用
wz.reload()重新加载 - 脚本路径:
~/.config/WenZi/scripts/init.py - 可通过
"scripting": {"script_dir": "/path/to/scripts"}自定义脚本目录
多文件脚本
可以将脚本拆分为多个 .py 文件。init.py 是入口文件,同目录下的其他文件可通过标准 import 语句导入:
~/.config/WenZi/scripts/
├── init.py # 入口文件
├── my_sources.py # 自定义启动器数据源
└── utils/
├── __init__.py
└── formatting.py
# init.py
import my_sources
from utils.formatting import fmt_date
wz.chooser.register_source(my_sources.build_source())
wz.hotkey.bind("cmd+shift+d", lambda: wz.type_text(fmt_date()))
# my_sources.py
from wenzi.scripting.api import wz # 子模块中需要导入 wz
def build_source():
@wz.chooser.source("todos", prefix="td")
def search_todos(query):
return [{"title": "示例待办", "action": lambda: wz.alert("Done!")}]
调用 wz.reload() 时,脚本目录下的所有文件都会被重新加载,而不仅仅是 init.py。
注意: 仅支持绝对导入(
import helper、from utils import foo)。不支持相对导入(from . import foo),因为init.py不是作为 Python 包加载的。注意: 不要在用户脚本中定义 PyObjC 的
NSObject子类。Objective-C 运行时不支持重复注册同名类,重载时会导致崩溃。
安全说明
脚本以未沙箱化的 Python 运行,拥有与 闻字 相同的系统权限。这意味着脚本可以:
- 读写你的用户账户能访问的任何文件
- 执行任意 Shell 命令
- 访问网络
- 读取剪贴板内容
- 模拟按键并与其他应用交互
请只运行你自己编写或仔细审查过的脚本。 不要从不可信的来源直接复制粘贴脚本。恶意脚本可能会在你不知情的情况下窃取数据、安装软件或修改文件。
出于安全考虑,脚本系统默认处于禁用状态。
常见问题
脚本没有加载?
- 确认 config.json 中 "scripting": {"enabled": true} 已设置
- 启用后需要重启闻字
- 查看日志 ~/Library/Logs/WenZi/wenzi.log 排查错误
Leader 键没有响应?
- 确保 闻字 已获得辅助功能权限(系统设置 → 隐私与安全性 → 辅助功能)
- 检查触发键名称是否正确(如 cmd_r 而非 right_cmd)
提示面板不可见? - 面板需要辅助功能权限才能显示在其他应用之上
脚本报错?
- 语法错误和异常会记录到日志并以浮窗提示
- 查看 ~/Library/Logs/WenZi/wenzi.log 获取完整错误信息