kitty Custom Kittens 开发实战:用 Python 为 GPU 终端编写可复用的交互扩展
【免费下载链接】kittyIf you live in the terminal, kitty is made for you! Cross-platform, fast, feature-rich, GPU based.项目地址: https://gitcode.com/GitHub_Trending/ki/kitty
导读
kitty 提供了一套名为Kitten的扩展机制:任何你熟悉的终端程序式 Python 脚本,都可以一键挂载到 kitty 中运行,并反过来操控正在运行的 kitty 实例——读取当前窗口内容、模拟按键与鼠标、切换布局、粘贴文本、发送远程控制命令。本文以官方文档 docs/kittens/custom.rst 为骨架,结合 kittens/runner.py、kittens/tui/handler.py、kitty/boss.py 与 kitty/mouse.c 等源码,完整讲解自定义 Kitten 的执行模型、两段式函数接口、屏幕输入类型矩阵、无界面脚本 Kitten、鼠标事件模拟与远程控制集成等全套能力。读完你就能从零写出自己的 Kitten 并绑定到快捷键上使用。
Kitten 的运行模型:一个跑在 overlay 窗口里的普通终端程序
官方对 Kitten 的定义非常精炼:"They are just terminal programs written in Python"——Kitten 本质上就是一个 Python 终端程序。当你在 kitty 中启动一个 Kitten 时,kitty 会:
- 在当前窗口之上打开一个overlay(覆盖层)窗口来运行这个程序(对用户表现为一个临时弹出的子界面);
- 可选地把当前窗口/滚动回滚(scrollback)的内容通过STDIN交给 Kitten 进程读取;
- Kitten 程序在 overlay 窗口里照常读写、绘制文本、响应按键,像任何普通终端程序一样工作;
- Kitten 程序结束后,kitty 进程会调用其
handle_result()回调,把 Kitten 运行期间的返回值连同运行上下文一并传入,此时你的代码持有boss对象——即正在运行的 kitty 实例的入口,可以执行"关闭窗口、粘贴文本、切换布局"等任意操作。
"程序本体"与"结果处理"被刻意拆成了两个阶段、两个进程环境,这是理解 Kitten 一切 API 设计的关键。
源码层面,这套流程在 kittens/runner.py 中有完整实现:
launch()(kittens/runner.py)负责真正运行 Kitten 的main():它先把KITTY_CONFIG_DIRECTORY写入环境变量,再调用m'start';若main()返回了非None结果,会把结果用json+base64.b85encode编码后,以 kitty 私有 DCS 序列\x1bP@kitty-kitten-result|...\x1b\\写回 stdout——这就是返回值能跨进程传递回 kitty 主进程的通信协议。import_kitten_main_module()(kittens/runner.py)负责加载 Kitten:路径以.py结尾的被当作自定义 Kitten(直接读取源码文件执行),否则去内置的kittens.<name>.main模块中寻找。create_kitten_handler()(kittens/runner.py)读取main()/handle_result()上附加的各种注解属性(如type_of_input、no_ui、allow_remote_control),并把handle_result与[kitten名] + 原始参数绑定成一个可供回调的对象。
第一个自定义 Kitten:获取输入并粘贴到窗口
文档给出的入门示例是一个"向用户提问、再把答案粘贴回终端"的 Kitten。在你的 kitty 配置目录(Linux 上通常为~/.config/kitty,具体位置参见 docs/conf.rst)下新建mykitten.py:
from kitty.boss import Boss def main(args: list[str]) -> str: # 这是 Kitten 的入口,运行在 overlay 窗口中 answer = input('Enter some text: ') # main() 的返回值会原样传给 handle_result() 的 answer 参数 return answer def handle_result(args: list[str], answer: str, target_window_id: int, boss: Boss) -> None: # 找到发起本次 Kitten 的目标窗口 w = boss.window_id_map.get(target_window_id) if w is not None: w.paste_text(answer)然后在kitty.conf中绑定快捷键:
map ctrl+k kitten mykitten.py重启(或让 kitty 重新加载配置后)按Ctrl+K,overlay 窗口出现并提示输入,输入内容回车后会被原样粘贴回被覆盖的下层窗口。
两点值得注意的机制细节:
main()运行在独立子进程的 overlay 终端里,因此可以直接使用input()、print()做交互;handle_result()则运行在kitty 主进程内,直接面对boss。target_window_id就是"在哪个窗口里按下了快捷键",用它去boss.window_id_map(见 kitty/boss.py,一张以窗口 ID 为键的弱引用字典)取回Window对象,再调用 kitty/window.py 中定义的w.paste_text(text)即可完成"回写"。文档特别指出,所有内部对象访问都建议做is not None之类的存在性检查——毕竟目标窗口可能在 Kitten 运行期间已被关闭。
最推荐的入门方式正如文档所说:直接修改内置 Kitten。它们全部位于源码仓库的 kittens/ 目录(例如 kittens/ask/、kittens/choose_files/、kittens/hints/),从真实实现入手比从空文件开始要快得多。
用 Remote Control API 操控 kitty:更稳的推荐路径
Kitten 代码虽然"拥有完整的内部 kitty API 访问权",但文档明确指出这些内部 API既不稳定也没有文档。因此推荐的可靠做法是调用 kitty 官方的 Remote control API,也就是命令行kitten @背后那套接口。
在handle_result()里只需一行包装调用:
def handle_result(args: list[str], answer: str, target_window_id: int, boss: Boss) -> None: # 找到本次 Kitten 的目标窗口 w = boss.window_id_map.get(target_window_id) if w is not None: boss.call_remote_control(w, ('send-text', f'--match=id:{w.id}', 'hello world'))boss.call_remote_control()的入参与你在终端里执行kitten @ send-text ...的参数一一对应(kitty/boss.py 内部会复用同一套parse_rc_args/parse_subcommand_cli解析管线)。
重要提示:在
handle_result()执行期间,boss.active_window仍然指向运行该 Kitten 的那个窗口(overlay 所在窗口),而不是你希望操作的目标窗口。因此凡是通过远程控制定位目标窗口的命令,都必须显式给出选择参数——最常见的就是上例中的--match=id:{w.id},或者--self。
在 kitty 终端里执行kitten @ --help可以列出全部可用的远程控制命令(发送文本、新建窗口/标签页、切换布局、调整字体大小、截屏等,均可从 Kitten 内触发),相关命令语义见 docs/remote-control.rst。
向 Kitten 传参:固定参数与 @selection
Kitten 支持在键位映射里直接携带静态参数:
map ctrl+k kitten mykitten.py arg1 arg2这些参数会以列表形式出现在main(args)与handle_result(args)的args形参中。与之配套,还有两个自动展开的约定:
- 当前工作目录:Kitten 启动时其cwd 被设置为活动窗口中正在运行程序的工作目录(实现在
run_kitten_with_metadata()中取CwdRequest(w).cwd_of_child,见 kitty/boss.py)。这让 Kitten 天然"跟随"用户当前所在目录。 - 特殊参数
@selection:会被自动替换为活动窗口中当前选中的文本。若没有选中内容,替换为空。实现位于 kitty/boss.py:run_kitten_with_metadata()在真正启动前会遍历args,凡命中@selection就用data_for_at(which='@selection', window=w)的结果顶替。这意味着你可以写map ctrl+k kitten mykitten.py @selection,从而让 Kitten 直接处理用户选中的文本——这是"选中即处理"类工具(翻译、格式化、搜索)最常用的接入点。
读取屏幕内容:type_of_input 输入类型矩阵
很多实用 Kitten 需要拿到"当前窗口里有什么"。做法是在handle_result()上打一个@result_handler(type_of_input=...)注解,kitty 就会按指定类型把屏幕内容经STDIN喂给 Kitten 进程。注意两种环境下的 STDIN 含义完全不同,注释中特意强调:
from kitty.boss import Boss # 在 main 中,STDIN 属于 Kitten 进程,里面装着屏幕内容 def main(args: list[str]) -> str: return sys.stdin.read() # 在 handle_result 中,STDIN 属于 kitty 进程本身,Kitten 不应去读它 from kittens.tui.handler import result_handler @result_handler(type_of_input='text') def handle_result(args: list[str], stdin_data: str, target_window_id: int, boss: Boss) -> None: pass注解被 kittens/tui/handler.py 的result_handler()读取并封装成HandleResult对象;runner.py随后把type_of_input暴露给启动流程(kittens/runner.py),kitty 便据此准备 STDIN 数据。
文档给出的type_of_input取值共 13 种,按"来源 × 格式"两维组合:
| 关键字 | STDIN 中收到的内容 |
|---|---|
text | 活动窗口的纯文本 |
ansi | 活动窗口的带格式文本(含 ANSI 样式序列) |
screen | 活动窗口的纯文本,含换行标记 |
screen-ansi | 活动窗口的带格式文本,含换行标记 |
history | 活动窗口及其回滚(scrollback)的纯文本 |
ansi-history | 活动窗口及其回滚的带格式文本 |
screen-history | 活动窗口及其回滚的纯文本,含换行标记 |
screen-ansi-history | 活动窗口及其回滚的带格式文本,含换行标记 |
output | 上一条已运行命令的输出纯文本 |
output-screen | 上一条已运行命令的输出纯文本,含换行标记 |
output-ansi | 上一条已运行命令的输出的带格式文本 |
output-screen-ansi | 上一条已运行命令的输出的带格式文本,含换行标记 |
selection | 当前用鼠标选中的文本 |
几点补充语义:
screen系列里的"换行标记"用于区分"终端里因宽度导致的软换行"与"真正的回车换行",供需要精确重建行结构的程序使用。- 除
output(最近一次运行命令的输出)之外,还有两个变体:last_visited_output(最近一次被跳转到的命令的输出)和first_output(当前屏幕上第一条命令的输出);它们同样可与screen/ansi组合出带格式/带换行标记的版本。 - 所有基于"命令输出"的类型都依赖 Shell integration(即 kitty 注入 shell 的提示符/命令标记),未启用 shell integration 时这些类型不可用——文档为此单独加了 warning。
背后的取数实现统一收敛在boss.data_for_at(which, window, add_wrap_markers)与模块级函数data_for_at()(见 kitty/boss.py 与 kitty/boss.py):run_kitten_with_metadata()在创建 overlay 前,会依据传入的type_of_input用add_wrap_markers = stdin.endswith('_wrap')这类约定决定是否追加换行标记,再取回文本作为 overlay 子进程的stdin数据(kitty/boss.py)。
脚本化 kitty:no_ui=True,跳过终端界面直接干活
如果你只想让 Kitten "脚本化地操控 kitty"、根本不需要任何交互界面,可以在handle_result()上加@result_handler(no_ui=True),并让main()留空。这样 kitty不会先运行main()与 overlay 界面,而是直接调用handle_result()——等同于一键执行一段带完整boss上下文的 Python 脚本。
文档给出的经典案例是一个等价于内置toggle_layout动作的"缩放(zoom)"切换器。在配置目录下新建zoom_toggle.py:
from kitty.boss import Boss def main(args: list[str]) -> str: pass from kittens.tui.handler import result_handler @result_handler(no_ui=True) def handle_result(args: list[str], answer: str, target_window_id: int, boss: Boss) -> None: tab = boss.active_tab if tab is not None: if tab.current_layout.name == 'stack': tab.last_used_layout() else: tab.goto_layout('stack')再绑定:
map f11 kitten zoom_toggle.py此后按F11即在"stack 布局(当前窗口最大化)"与"之前的布局"之间来回切换。last_used_layout()与goto_layout()是 kitty/tabs.py 与 kitty/tabs.py 中定义的标准标签页布局动作,no_ui分支的快速通道实现在run_kitten_with_metadata()里(kitty/boss.py:检测到end_kitten.no_ui后直接同步调用handle_result并返回,全程不建窗口)。
想玩得更花哨?在handle_result()里加一行boss.toggle_fullscreen()(kitty/boss.py 的实现对应 "Toggle the fullscreen status of the active OS Window" 动作),就能让F11同时完成"布局缩放 + 全屏":
def handle_result(args: list[str], answer: str, target_window_id: int, boss: Boss) -> None: tab = boss.active_tab if tab is not None: if tab.current_layout.name == 'stack': tab.last_used_layout() else: tab.goto_layout('stack') boss.toggle_fullscreen()这种"无 UI Kitten"正是把任意 kitty 动作串成自定义复合快捷键的推荐姿势:不用改一行 C/Go 内核代码,就能自由组合布局、窗口、标签页等高层动作。
模拟鼠标事件:send_mouse_event
当窗口内运行的程序开启了鼠标事件接收(如tmux、vim的鼠标模式、各类 TUI),你的 Kitten 可以调用底层接口把合成的鼠标事件注入给该程序。函数签名如下:
from kitty.fast_data_types import send_mouse_event send_mouse_event(screen, x, y, button, action, mods)参数语义:
screen:目标窗口的screen属性(例如boss.active_window.screen)。x、y:从 0 开始计数的单元格坐标。button:沿用 X11 的按钮编号体系——左键1、中键2、右键3、滚轮上4、滚轮下5、滚轮左6、滚轮右7、后退键8、前进键9。action:PRESS、RELEASE、DRAG或MOVE四者之一。mods:修饰键位掩码GLFW_MOD_{mod},{mod}取SHIFT、CONTROL、ALT(可组合),例如GLFW_MOD_SHIFT | GLFW_MOD_CONTROL。
以上常量全部从kitty.fast_data_types导入。例如向活动窗口的 (x=2, y=3) 位置发送一次左键按下:
from kitty.fast_data_types import send_mouse_event, PRESS send_mouse_event(boss.active_window.screen, 2, 3, 1, PRESS, 0)关键行为(官方文档与源码双重印证):只有当目标程序正在接收此类鼠标事件时,事件才会真的被发出。底层 C 实现位于 kitty/mouse.c:先检查screen->modes.mouse_tracking_mode与当前动作是否匹配(ANY_MODE全收;MOTION_MODE只收非MOVE;BUTTON_MODE只收PRESS/RELEASE),匹配后才经encode_mouse_event_impl()编码成标准 CSI 鼠标转义序列,通过write_escape_code_to_child()写入子进程。因此函数返回True表示事件已发送、False表示程序未开启对应鼠标模式(事件被静默丢弃)。
在 main() 内使用远程控制:@kitten_ui(allow_remote_control=True)
通常远程控制要等handle_result()阶段、Kitten 退出后才可执行。但若你希望在 Kitten展示交互 UI 之前先探查 kitty 状态,或者希望用户在 Kitten 界面上就能直接操控 kitty,可以在main()上启用kitten_ui的allow_remote_control=True——它告诉 kitty:即使全局未开启远程控制,也要以"允许远程控制"的方式运行本 Kitten:
import json import sys from pprint import pprint from kittens.tui.handler import kitten_ui @kitten_ui(allow_remote_control=True) def main(args: list[str]) -> str: # 取得运行 kitten @ ls 的输出 cp = main.remote_control(['ls'], capture_output=True) if cp.returncode != 0: sys.stderr.buffer.write(cp.stderr) raise SystemExit(cp.returncode) output = json.loads(cp.stdout) pprint(output) # 打开一个标题由用户指定的新标签页 title = input('Enter the name of tab: ') window_id = main.remote_control(['launch', '--type=tab', '--tab-title', title], check=True, capture_output=True).stdout.decode() return window_id几点机制解释:
main.remote_control(cmd, **kw)是对 Pythonsubprocess.run的薄封装(实现于 kittens/tui/handler.py),内部会先拼出kitten @前缀,再执行你给出的子命令。- 默认出于安全考虑,Kitten 派生的子进程不能使用远程控制——这就是必须经由
main.remote_control()的原因(它通过显式传递 fd 让子命令拿到权限)。若你的设计确实需要让 Kitten 的子进程也获得远程控制能力,可调用main.allow_indiscriminate_remote_control()放开限制(其实现见 kittens/tui/handler.py,本质是把远程控制 socket fd 设为可继承,必要时还会通过KITTY_RC_PASSWORD环境变量传递口令)。 - 注意
main变量此处指代的是被装饰的函数对象本身:@kitten_ui返回的是KittenUI实例(kittens/tui/handler.py),因此main.remote_control、main.password、main.allow_indiscriminate_remote_control等都是这个装饰器实例提供的方法/属性。
远程控制访问还可以进一步收紧到"白名单":在装饰器中指定remote_control_password参数,kitty 会为本次 Kitten 会话生成一个安全随机口令并只允许列出的命令,例如:
@kitten_ui(allow_remote_control=True, remote_control_password='ls set-colors') def main(args: list[str]) -> str: ...其中remote_control_password的值是一个空格分隔的允许命令列表,完整语义参见配置项remote_control_password(说明见 docs/conf.rst)。生成的密码可通过main.password读取,main.remote_control()会自动携带它完成鉴权,无需手工处理。KittenUI.initialize()(kittens/tui/handler.py)里展示了口令下发流程:kitty 通过 socketpair 一端把密码先行送入,Kitten 侧从rc_fd读取并在后续子命令中通过--password-file fd:N --use-password always提交。
调试 Kitten:print 去了哪里
因为main()和handle_result()运行在两个不同的进程环境,调试输出也需要分情况讨论:
main()是普通程序,print()的输出直接显示在 Kitten 的 overlay 窗口里,肉眼可见。- 若想把这些输出留到 kitty 主进程的 stdout,可以用专用工具:
from kittens.tui.loop import debug debug('whatever')debug()用法与print()一致,但输出会进入运行该 Kitten 的 kitty 进程的 STDOUT。runner.py的set_debug()(kittens/runner.py)甚至把debug直接注入到 Python 内置命名空间,方便在任意位置直接调用。
handle_result()运行在 kitty 主进程内部,其print()输出自然进入kitty 进程自身的 STDOUT。文档给出一条实用技巧:从另一个 kitty 实例中运行被测的 kitty,那么handle_result()的 print 就会显示在外层那个 kitty 的窗口里,实现"跨实例观察调试日志"。
若在 overlay 阶段抛出了未处理异常,runner.py的main()兜底逻辑(kittens/runner.py)会打印 traceback 并提示Press Enter to quit,避免窗口瞬间消失丢失报错。
结语:从内置 Kitten 出发,打造随 kitty 一起分发的 Go 版 Kitten
自定义 Python Kitten 适合放进~/.config/kitty/个人使用;如果希望你的扩展成为"内置 Kitten"(即直接以kitten my-kitten调用、随 kitty 一起分发),则要走 Go 语言开发路线——内置 Kitten 由 Go 主体加薄薄一层 Python CLI 包装组成,完整的新建步骤、main.py/main.go模板与tools/cmd/tool/main.go的注册方法见 developing-builtin-kittens。
社区里也已沉淀出大量用户自建 Kitten 可供参考:例如在 vim 与 kitty 分屏之间用统一热键无缝跳转的导航器、让 kitty 滚动键在全屏应用中生效的 smart-scroll、带预览的标签页模糊切换器、把自然语言提示词交给 LLM 生成 shell 命令的 gattino、只在密码提示符处安全插入密码管理器口令的 Kitten、面向 WeeChat 的 URL 提示 Kitten,以及把 kitty 输出经 weasyprint 导出 PDF 的工具与右键弹出操作菜单的 action menu(完整清单收录于 docs/kittens/custom.rst 末节)。
掌握了两段式函数契约、输入类型矩阵、no_ui脚本通道与远程控制接入方式之后,把任意"想对终端内容做的批量处理"包装成一次快捷键调用,就只是几十行 Python 的事了。
【免费下载链接】kittyIf you live in the terminal, kitty is made for you! Cross-platform, fast, feature-rich, GPU based.项目地址: https://gitcode.com/GitHub_Trending/ki/kitty
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考