本文记录一套已经在当前机器落地的键盘方案:用 Caps+T 进入 Herdr 前缀模式,Ctrl+1..9 切换当前 Space 内的 Tab,Alt+1..9 切换 Space,Caps+Alt+A 在 Agent 之间轮询。重点不只是最终配置,还包括中间踩过的坑,以及为什么最后要让 Karabiner 和 Herdr 各做一半工作。

环境快照

这套配置整理于 2026 年 8 月,当前机器上的主要版本如下:

组件当前状态
操作系统macOS
Herdr0.8.2
Goku0.5.7
终端Ghostty、iTerm2
Ghostty Bundle IDcom.mitchellh.ghostty
iTerm2 Bundle IDcom.googlecode.iterm2
Herdr Sessiondefault,持久运行
已安装 Herdr 插件herdr-sidebar 0.8.1

本文只摘录与 Herdr 有关的 Karabiner 规则。原文件中还有应用启动、文本编辑、窗口控制和其他自动化,其中可能包含私人信息,不适合直接放进公开博客。

想解决什么问题

Herdr 的默认前缀是 Ctrl+B。它和 tmux 很像,但在 Agent、Shell、编辑器之间频繁移动时,我更想要一个不占用常用控制键、又容易按到的入口。

实际目标有四个:

  1. Caps+T 代替 Ctrl+B,但保留 Caps 单击为 Esc 的习惯。
  2. Ctrl+1..9 直接切换当前 Space 内的 Tab。
  3. Alt+1..9 直接切换 Space。
  4. Caps+Alt+A 聚焦下一个 Agent,重复按就继续轮询。

看起来只是几条快捷键,真正麻烦的是它们要经过三层系统。

1
2
3
4
5
6
7
8
9
物理键盘事件
    ↓
Karabiner / Goku
    ↓ F13 + 动作键
Ghostty 或 iTerm2
    ↓
Herdr 按键解析
    ↓
Space / Tab / Agent

Karabiner 看得到前台应用,却看不到终端内部究竟运行的是 Herdr、普通 Shell 还是 Neovim。Herdr 正好相反:它很清楚自己的 Space、Tab、Pane 和 Agent,但拿不到 macOS 在输入法层已经转换掉的原始 Option 组合。最终方案必须顺着这条事件链设计。

先把 Herdr 的对象关系理清

Herdr 界面里的 Space,在 CLI 和配置文档里叫 Workspace。它们的层级是:

1
2
3
4
5
Session
└── Workspace / Space
    └── Tab
        └── Pane
            └── Agent 或普通前台进程

因此,Ctrl+数字Alt+数字 虽然看起来相似,作用域并不一样:

  • Ctrl+1..9 只在当前 Space 内选择 Tab。
  • Alt+1..9 选择整个 Session 中的 Space。
  • Agent 轮询可能跨越 Space、Tab 和 Pane。

为什么选择 F13 作为前缀

我没有直接把 Herdr 前缀设成一串 Hyper 修饰键,而是让 Karabiner 把 Caps+T 翻译为 F13

1
[:t :f13] ; Herdr prefix

Herdr 再把 F13 解释为 prefix:

1
2
[keys]
prefix = "f13"

这样做有几个实际好处:

  • F13 在当前 Karabiner 配置中没有其他用途。
  • 它不抢占 Shell 的 Ctrl+ACtrl+ECtrl+W 等编辑键。
  • Ghostty 和 iTerm2 都能把功能键事件交给 Herdr。
  • Caps 仍然保留原来的双重角色:单击是 Esc,按住进入 Hyper layer。

最终操作是先按住 Caps,点一下 T,松开后再按 Herdr 动作键。这个动作比五个修饰键同时按下稳定得多。

第一个坑:Alt+数字变成了特殊字符

最初我在 Herdr 里直接写:

1
switch_workspace = "alt+1..9"

结果按下 Alt+1..9 后,终端里出现了:

1
¡ ™ £ ¢ ∞ …

这不是 Herdr 的解析错误。macOS 把 Option 当作字符输入修饰键,按键还没到 Herdr,就已经被 US 键盘布局转换成特殊字符。

修复方法不是继续调整 Herdr,而是在 Karabiner 这一层拦截按键。只有当前应用是 Ghostty 或 iTerm2 时,Alt+数字 才会被转换为两个连续事件:

1
F13, 数字

Herdr 收到后,等价于“进入 prefix 模式,然后选择编号对应的 Space”。

第二个坑:为了兼容键位交换,误伤了 Command

第一版还为 quickfire 设备补了 Command+1..9 规则。当时的想法是:这些键盘在已有配置中交换了 Command 和 Option,所以同时接收逻辑 Command,可以兼容物理 Alt。

问题是 Karabiner 的 from 条件匹配原始修饰键事件。这 18 条补偿规则不是在识别“交换后的 Alt”,而是在明确劫持左右 Command:

1
2
3
;; 错误示例,不应保留
[:!C1 [:f13 :1] [:herdr-terminals :quickfire]]
[:!Q1 [:f13 :1] [:herdr-terminals :quickfire]]

结果就是 Command+1..9Alt+1..9 都会切换 Space。修正时删除所有 Herdr 规则中的 C/Q 补偿,数字和 Agent 轮询都只监听 O/E。设备原有的 Command/Option 交换规则不在本次修改范围内,这样其他应用中的既有键位习惯不会一起变化。

第三个坑:Caps+Alt+A 没有反应

第一版 Agent 快捷键把 Caps+Alt+A 映射成 F14,再让 Herdr 直接监听 F14。配置能生成,Herdr 也能重载,但实际按键没有形成可靠的完整链路。

最后没有继续堆补丁,而是复用已经验证过的 F13 前缀:

1
2
3
4
5
6
7
Caps+Alt+A
    ↓ Karabiner
F13, A
    ↓ Herdr
prefix+A
    ↓
next_agent

这少了一种新的功能键语义,调试也简单很多。使用时最好先按住 Caps,再按 Alt+A,让 Hyper layer 先进入激活状态。

Karabiner 配置

当前配置使用 Goku 的 EDN 格式,源文件位于:

1
~/.config/karabiner.edn

当前机器把几把外接键盘归入 quickfire 设备组,并在其他规则中交换了 Command 与 Option:

1
2
3
4
5
6
7
:devices {
  :quickfire [{:vendor_id 13652 :product_id 64009}
              {:vendor_id 6785  :product_id 8762}
              {:vendor_id 1241  :product_id 521}
              {:vendor_id 6785  :product_id 8249}
              {:vendor_id 1133  :product_id 49278}]
}

如果复制到另一台机器,应从 Karabiner-EventViewer 读取自己的 vendor_idproduct_id,不要照搬这些数字。

终端应用条件

先声明 Ghostty、iTerm2 和一个组合条件:

1
2
3
4
5
6
:applications {
  :iterm2 ["^com\\.googlecode\\.iterm2$"]
  :ghostty ["^com\\.mitchellh\\.ghostty$"]
  :herdr-terminals ["^com\\.googlecode\\.iterm2$"
                    "^com\\.mitchellh\\.ghostty$"]
}

这里故意使用准确的 Bundle ID,而不是按应用名称猜测。可以用下面的命令重新确认:

1
2
3
4
5
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
  /Applications/Ghostty.app/Contents/Info.plist

/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
  /Applications/iTerm.app/Contents/Info.plist

Caps Hyper layer 与 F13

当前机器的 Caps 单击为 Esc,按住后进入 Hyper layer:

1
2
3
:layers {
  :hyper-mode {:key :caps_lock :alone {:key :escape}}
}

与 Herdr 相关的 Hyper 规则如下:

1
2
3
4
5
6
7
8
{:des "Hyper Mode - Common"
 :rules [:hyper-mode
         [:t :f13] ; Caps+T -> Herdr prefix

         ;; Caps+Alt+A -> F13,A -> Herdr prefix+A
         [:!Oa [:f13 :a] [:herdr-terminals]]
         [:!Ea [:f13 :a] [:herdr-terminals]]
         ]}

Goku 配置顶部对修饰键缩写的约定是:

缩写含义
OLeft Option
ERight Option
CLeft Command
QRight Command
!该修饰键必须存在

Alt+1..9 到 Space 的映射

下面是当前机器实际使用的规则,共 18 条,只覆盖左右 Option。这里有意不监听 CQ,确保左右 Command 继续交给终端或当前应用处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{:des "Herdr shortcuts in Ghostty and iTerm2"
 :rules [
         ;; 左右 Option:Alt+1..9 -> F13,1..9
         [:!O1 [:f13 :1] [:herdr-terminals]]
         [:!O2 [:f13 :2] [:herdr-terminals]]
         [:!O3 [:f13 :3] [:herdr-terminals]]
         [:!O4 [:f13 :4] [:herdr-terminals]]
         [:!O5 [:f13 :5] [:herdr-terminals]]
         [:!O6 [:f13 :6] [:herdr-terminals]]
         [:!O7 [:f13 :7] [:herdr-terminals]]
         [:!O8 [:f13 :8] [:herdr-terminals]]
         [:!O9 [:f13 :9] [:herdr-terminals]]

         [:!E1 [:f13 :1] [:herdr-terminals]]
         [:!E2 [:f13 :2] [:herdr-terminals]]
         [:!E3 [:f13 :3] [:herdr-terminals]]
         [:!E4 [:f13 :4] [:herdr-terminals]]
         [:!E5 [:f13 :5] [:herdr-terminals]]
         [:!E6 [:f13 :6] [:herdr-terminals]]
         [:!E7 [:f13 :7] [:herdr-terminals]]
         [:!E8 [:f13 :8] [:herdr-terminals]]
         [:!E9 [:f13 :9] [:herdr-terminals]]

         ]}

Herdr 配置

Herdr 配置位于:

1
~/.config/herdr/config.toml

当前文件很短,完整内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[keys]
prefix = "f13"
switch_tab = "ctrl+1..9"
switch_workspace = "prefix+1..9"
next_agent = "prefix+a"

[ui]
agent_panel_sort = "spaces"

[theme]
name = "dracula"
auto_switch = false

这几行和 Karabiner 规则正好拼起来:

  • Caps+T 产生 F13,进入 Herdr prefix 模式。
  • Ctrl+数字 被 Herdr 直接截获,用于切换 Tab。
  • Alt+数字 先被 Karabiner 改写成 F13,数字,再触发 switch_workspace
  • Caps+Alt+A 被改写成 F13,A,再触发 next_agent
  • Agent 面板按 Space 分组,轮询时更容易预判下一个目标。

如何生成、加载和验证

修改 karabiner.edn 后,先让 Goku 生成 Karabiner-Elements 配置:

1
goku

成功时输出:

1
Done!

然后重新加载 Herdr:

1
herdr server reload-config

当前机器的成功返回是:

1
2
3
4
5
6
7
{
  "result": {
    "diagnostics": [],
    "status": "applied",
    "type": "config_reload"
  }
}

仅看“命令成功”还不够。最好继续验证 Goku 生成后的 JSON,而不是只检查 EDN 源文件:

1
2
3
4
5
6
jq -e '
  ([.profiles[]
    | .complex_modifications.rules[]?
    | select(.description == "Herdr shortcuts in Ghostty and iTerm2")
    | .manipulators[]] | length) == 18
' ~/.config/karabiner/karabiner.json

还要单独确认这组规则没有监听 Command。下面的检查应返回 true

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
jq -e '
  [.profiles[]
   | .complex_modifications.rules[]?
   | select(.description == "Herdr shortcuts in Ghostty and iTerm2")
   | .manipulators[]
   | select(
       (.from.modifiers.mandatory // [])
       | any(. == "left_command" or . == "right_command")
     )]
  | length == 0
' ~/.config/karabiner/karabiner.json

查看 Herdr 是否真的识别到了 Agent:

1
herdr agent list

查看当前 Space:

1
herdr workspace list

最后的成品

最终的按键表很简单:

动作实际按键生效范围内部事件
Herdr prefixCaps+T全局 Karabiner;Herdr 内消费F13
切换当前 Space 的 TabCtrl+1..9Herdrctrl+1..9
切换 SpaceAlt+1..9Ghostty、iTerm2F13,1..9
聚焦下一个 AgentCaps+Alt+AGhostty、iTerm2F13,A
Caps 单击Caps全局Esc

Command+1..9 不参与 Herdr 映射,继续保留终端和应用原有行为。

这套设计最重要的不是某一个键,而是分工:Karabiner 只负责修正 macOS 和硬件键盘带来的输入差异,Herdr 只负责理解 Space、Tab 和 Agent。两边通过一个冷门但稳定的 F13 事件连接起来。

边界和已知限制

Karabiner 不能识别终端内部的 Herdr

应用条件只能判断前台窗口属于 Ghostty 或 iTerm2。它无法读取终端子进程的 HERDR_ENV=1,所以在这两个终端的普通 Shell 中按 Alt+数字,同样会发送 F13,数字

如果这个副作用不可接受,就只能把 Space 切换恢复成显式的 Herdr prefix 操作,或者为不同终端 Profile 使用独立 Karabiner 变量。仅靠 frontmost_application_if 做不到“只在 Herdr 进程内生效”。

Caps+Alt+A 对按键顺序更敏感

这条规则依赖 Caps 的 hyper-mode 先被激活。实际操作时,先按住 Caps,再按 Alt+A,比三个键完全同时落下更稳定。

不要直接公开完整 Karabiner 配置

个人 Karabiner 文件常常混有应用路径、设备 ID、文本宏、自动输入内容和工作流名称。写博客时只摘录与主题相关的规则,并检查代码块中是否混入账号、手机号、Token 或固定文本。

还能继续怎么扩展

Herdr 本身已有 goto 会话导航器,默认动作是 prefix+g。它可以搜索 Workspace、Tab、Pane,并用键盘定位。如果想要更接近 Vim 的全局 Jump,可以考虑以下插件:

Herdr Navigator

Herdr Navigator 可以从同一个模糊搜索界面跳到 Workspace、Agent、Project、Session、Remote、目录或 Action,并支持 vim_mode = true

1
herdr plugin install thanhdat77/herdr-navigator --ref v0.3.3 --yes

建议绑定:

1
2
3
4
5
[[keys.command]]
key = "prefix+t"
type = "plugin_action"
command = "herdr-navigator.open"
description = "jump to anything"

Herdr 与 Neovim split 互通

herdr-splits.nvimvim-herdr-navigation 都能让 Ctrl+h/j/k/l 在 Neovim split 与 Herdr pane 之间移动。前者还支持 resize、边缘 wrap 和 auto-unzoom。

从 Agent 输出跳到文件

termscope 会扫描当前 Pane 的可见文本,把其中真实存在的文件、行号和链接做成选择器。Agent 刚输出 src/main.ts:42 时,不需要复制路径,可以直接跳进 Neovim 对应位置。

Neovim 窗口内的标签跳转

如果需求只发生在 Neovim 内部,flash.nvim 更合适。它支持搜索标签、多窗口跳转和 Treesitter 节点选择,但它不会跨越 Herdr Pane。

参考资料