18 KiB
Note
本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
English · 原始项目 · 上游 README
原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
Ghostty Blackhole
Landing page: s13k.dev/blackhole
一个漂浮在你 Ghostty 终端里的黑洞。它从小开始生长,最终吞噬整个屏幕,具体行为取决于你选择的 尺寸模式(size mode):内置的 番茄钟(pomodoro) 时钟(在一小时内逐渐变大、提醒你休息、休息后就不再打扰),或 token 模式(token mode),实时追踪 Claude Code 上下文窗口(context window) 的填充程度。
本效果基于 Eric Bruneton 的黑洞着色器,,该着色器通过光束追踪(beam-trace)史瓦西(Schwarzschild)测地线,并对预计算的查找表进行采样。Ghostty 自定义着色器是单遍 Shadertoy 风格的片段着色器,没有自定义纹理,因此查找表被替换为实时物理计算:黑洞附近每个像素都会沿史瓦西度规(Schwarzschild metric)积分各自的零测地线(null geodesic)(Binet 形式的加速度 a = -3/2 h² x/r⁵,可给出精确的光子偏折)。你的终端内容充当透镜背景天空的角色。下方没有任何东西是画上去的——一切都来自光线追踪:
渲染内容
- 阴影(shadow) — 碰撞参数低于
b_crit = (3√3/2) r_s的光线螺旋穿过视界后返回纯黑。边缘附近的文字在消失前会被拉伸进光子环(photon ring);黑洞后方的文字确实消失了。 - 引力透镜(gravitational lensing) — 逃逸光线被投影回终端的「天空」平面:文字弯曲、放大,并在爱因斯坦环(Einstein ring)内呈现镜像的次级像。远离黑洞处会交接给解析弱场偏折(
α = 2r_s/b),因此只有黑洞附近的像素需要承担积分开销。远处蓝光比红光偏折略多,带来一丝色差(chromatic aberration)。 - 吸积盘(accretion disk) — 薄开普勒盘(Keplerian disk),光线可多次穿透:远侧在阴影上下方形成弧线(星际穿越 风格),光子环区域显示更高阶的盘面像。着色遵循物理:Shakura–Sunyaev 温度分布以黑体色渲染,再经相对论因子
g = √(1 − 1.5 r_s/r)/(1 − β·k̂)红移与束流化(beaming)—— 接近侧蓝热,并由g^N增强;远离侧暗淡偏红。 - 光子环(photon ring) — 在
1.5 r_s光子球(photon sphere)附近缠绕的光线会拾取每一次盘面穿越;明亮细环是涌现效果,并非直接绘制。 - 透镜星场(lensed starfield) — 以 弯曲 后的光线方向采样的微弱程序化天空,恒星在黑洞周围拖成弧线(默认关闭 — 提高
STAR_GAIN即可启用)。 - 引力时间膨胀(gravitational time dilation) — 半径
r处的盘面图案以固有时速率√(1 − 1.5 r_s/r)前进,内轨道可见地「冻结」;随着黑洞变重(DILATION_MIN),整个盘面也会减速。 - 黑洞沿缓慢的利萨如(Lissajous)路径漂移,限制在屏幕上半部分 — 底部
WORK_AREA比例区域(你的提示符)永远不会被扭曲。漂移速度与活动范围随尺寸变化:小时平静,大时躁动。
番茄钟模式
休息提醒完全在着色器内计算 — 无需守护进程、shell 钩子,blackhole.glsl 之外什么都不需要。
着色器是无状态的(帧间不保留缓冲区,且 Ghostty 没有自定义 uniform),因此着色器无法记住 你的 工作时段何时开始。取而代之的是,时间表通过 iDate 锚定到挂钟时间:
- 周期(cycle):工作时黑洞始终存在 — 在周期下限处从小开始,在
WORK_PERIOD_MIN(默认 55 分钟)内逐渐变大,最后一分钟缩回小尺寸,并在BREAK_MIN(默认 5 分钟)内保持小尺寸。55+5 时,峰值落在每小时差 5 分 — 固定、可预测的节奏。 - 输入检测器(typing detector):
iTimeCursorChange追踪终端中的光标活动。停止使用IDLE_FADE_SEC(默认 90 秒)后,黑洞会实时缩小,安静几分钟后完全消失 — 你实际没在工作时它不会烦你。
自包含的代价是:周期不会在你于非常规时间休息时重新锚定 — 它是每小时一次的钟声,而非按工作 streak 计时的秒表。
注意: 原版 Ghostty(截至 1.3)声明了
iDate但从未填充 — 它始终为零 — 因此在当前版本中挂钟时间表不会推进:黑洞停留在小尺寸周期下限,只有输入检测器有效。一旦 Ghostty 接入iDate,完整周期就会生效(今天可用TIME_SCALE预览,它改用iTime运行)。Token 模式(默认)不受影响。
尺寸模式
驱动黑洞的因素由 blackhole.glsl 顶部附近的 SIZE_MODE 选择:
MODE_POMODORO— 上文所述的自包含挂钟时间表。可独立工作,除着色器外无需配置(但请参阅上文iDate的注意项)。MODE_TOKENS(默认) — 黑洞追踪 Claude Code 上下文窗口填充度。需要配套命令(见下文)。MODE_DEMO— 自运行的 42 秒展示循环,用于录制演示:黑洞从角落种子长到 100%,与 token 模式完全一致,同时盘面外观依次浏览调谐预设(Inferno → Gargantua → M87* 甜甜圈 → Face-on ember → Quasar → Blazar → Pure lens → Inferno),在每个约 5 秒的时段边界交叉淡入淡出。一切都在单个编译着色器内由iTime驱动 — 无需重载,录制不会卡顿。用./demo-mode.sh on|off切换(也会重载 Ghostty);演示模式下忽略光标通道,因此实时 Claude 会话不会干扰录制。可录制任意完整周期 — 循环重启很明显(黑洞会弹回角落种子)。
Token 模式
黑洞实时反映 Claude 上下文窗口的填充程度:
- 空上下文 — 右上角 一个小黑洞,大小覆盖终端面积的
TOKEN_AREA_MIN(默认 0.06 %)— 任意窗口形状下体感大小一致。 - 逐渐填满 — 向
TOKEN_AREA_MAX生长(100 % 上下文时默认约为终端的 ~3 % — 那是 阴影;明亮盘面可延伸到约 3 倍之外,因此视觉上更大),移动 更快,允许漫游区域从角落向左、向下扩展,直到覆盖工作区上方整个可玩屏幕;黑洞在其中伪随机游荡。轨道按区域缩放(永不裁剪 — 裁剪会把它钉死在边界),边距确保阴影和明亮内盘在较小时仍留在屏幕上。 /compact或新会话 — 弹回角落种子。- 未运行 Claude 会话 — 黑洞完全消失;你得到普通终端。
工作原理
Ghostty 自定义着色器不接受自定义 uniform — 但 会 获得光标颜色(iCurrentCursorColor),任何程序都可用标准 OSC 12 转义序列设置光标颜色。因此 token 计数通过光标传入:单个配套脚本 claude-token.py 以三种方式接入 Claude Code,将上下文填充编码进琥珀色光标颜色的低 4 位(#f5b000 空 → #f0bf0a 满);着色器每帧解码。固定高 4 位加上 4 位校验和构成 16 位签名,因此主题自带的光标颜色不会意外召唤黑洞。不重写文件、不重载、无重编译卡顿 — 更新在下一帧生效。
级别阶梯也会平滑过渡:Ghostty 在任何光标变化(包括颜色)时递增 iTimeCursorChange,并将旧颜色快照到 iPreviousCursorColor,着色器解码两者并在两级之间平滑过渡 — 离散更新呈现为连续运动,而非整个扭曲场突然弹出。平滑时间随跳变幅度缩放(TOKEN_GLIDE_*):1 % 步进在 0.3 秒内缓动,10 % 跳变需 1 秒,上限 1.5 秒。
| 接线 | 触发时机 | 作用 |
|---|---|---|
statusLine |
每次 assistant 回合 | 将上下文填充(0..1,1/250 步进)编码进光标颜色,并打印内置风格行:⚫️ ██████░░░░ 61% · Fable 5 · Projects/blackhole · ⎇ main · $1.27 · 5h 24% · wk 41%(最后一段是你的 Claude 用量限制;窗口超过 80 % 时还会显示重置时间) |
SessionStart hook |
会话开始 / 恢复 / /clear |
重置为角落种子(0.0) |
SessionEnd hook |
会话退出(/exit、ctrl-d、…) |
重置光标颜色(OSC 112)— 无签名即无黑洞 |
没有签名特征的光标颜色表示「无会话」,因此全新安装后不会显示任何效果,直到活跃会话编码出真实的填充值。(着色器中的 TOKEN_LEVEL 定义仍可作为手动覆盖,用于手动测试尺寸;它仅在光标未携带信号时生效。)
安装 token 模式
需要两部分——着色器和命令:
-
将 Ghostty 指向该着色器(见下方 安装),使用
SIZE_MODE MODE_TOKENS(默认值)。 -
将以下内容添加到
~/.claude/settings.json(请调整路径),然后启动新的 Claude Code 会话:{ "statusLine": { "type": "command", "command": "/path/to/blackhole_ghostty/claude-token.py" }, "hooks": { "SessionStart": [{ "hooks": [{ "type": "command", "command": "/path/to/blackhole_ghostty/claude-token.py" }] }], "SessionEnd": [{ "hooks": [{ "type": "command", "command": "/path/to/blackhole_ghostty/claude-token.py" }] }] } }
这是全局配置,因此黑洞会对任意 Claude Code 会话做出反应。几点说明:
- 光标颜色是按 surface 维护的状态,因此每个 Ghostty 分屏/窗口都有各自的黑洞——不同 surface 中的并发会话不会互相干扰。
- 会话活跃时,光标会变为吸积盘琥珀色(那就是数据通道)。其他任何会重着色光标的设置都会在下次 statusline 刷新时被覆盖;没有签名特征的光标 simply 表示「无黑洞」。
- 在 tmux/screen 内,OSC 需要透传才能到达 Ghostty——token 模式面向纯 Ghostty 会话设计。
- 若要退出,请设置
SIZE_MODE MODE_POMODORO并移除上述条目。
安装
需要 Ghostty 1.3+(用于光标着色器 uniform)。
克隆仓库,然后添加到你的 Ghostty 配置(macOS 上使用 ~/.config/ghostty/config 或 ~/Library/Application Support/com.mitchellh.ghostty/config):
custom-shader = /path/to/blackhole_ghostty/blackhole.glsl
custom-shader-animation = true
重新加载配置(macOS 上使用 cmd+shift+,)或打开新窗口。
调参
调参应用(macOS)
一款原生 SwiftUI 控制面板,风格类似 Bruneton's demo page
位于 tuner/:为每个着色器可调参数提供分组滑块、预设(Gargantua、Quasar、M87* donut、Blazar、Inferno、Zen 等),与文件双向实时同步,且每次微调都会通过 SIGUSR2 立即热重载 Ghostty。
cd tuner && swift run # run it
./tuner/make-app.sh # or bundle tuner/dist/Black Hole Tuner.app
或直接编辑 blackhole.glsl 顶部的常量并重新加载(cmd+shift+,)。
常量
位于 blackhole.glsl 顶部。半径以史瓦西半径(r_s)为单位;ISCO(最内稳定圆轨道,innermost stable circular orbit)位于 3 r_s。
| Constant | Effect |
|---|---|
HOLE_RADIUS |
尺寸旋钮——番茄钟:满尺寸时的阴影半径(占屏幕高度的比例);token 模式:缩放面积校准(在 0.08 时精确) |
LENS_DEPTH |
黑洞到终端「天空」平面的距离,单位为 r_s——数值越大,文字弯曲越明显 |
STAR_GAIN |
透镜化星场的亮度(0 = 关闭) |
DISK_INNER / DISK_OUTER |
吸积盘内/外缘,单位为 r_s(内缘会钳制在光子球之外) |
DISK_INCL |
吸积盘倾角,弧度:0 为正面朝向,π/2 为边缘朝向 |
DISK_ROLL |
整个系统在屏幕平面内的旋转 |
DISK_GAIN |
吸积盘发射亮度 |
DISK_OPACITY |
近侧吸积盘遮挡后方内容的程度 |
DISK_TEMP |
最热环带的黑体温度,开尔文 |
DOPPLER_MIX |
相对论性颜色/亮度不对称:0 关闭,1 全开 |
DISK_BEAM |
束流指数——强度按 g^N 缩放 |
DISK_SPEED |
条纹图案速度;负值反转轨道方向 |
DISK_WIND |
条纹螺旋缠绕紧密度 |
DISK_CONTRAST |
条纹对比度:0 = 柔和雾状,更高 = 锐利丝状结构 |
EXPOSURE |
吸积盘光的色调映射曝光(文字从不做色调映射) |
DRIFT_SPEED |
黑洞漂浮移动的速度 |
WORK_AREA |
屏幕底部保持完全无畸变的比例 |
DILATION_MIN |
黑洞完全展开时吸积盘图案的时间速率(更低 = 减速更明显) |
TOKEN_AREA_MIN |
token 模式:0% context 时的阴影面积,占终端面积的比例(默认 0.06%) |
TOKEN_AREA_MAX |
token 模式:100% context 时的阴影面积(默认约为终端的 ~3%——渲染成本随之缩放) |
TOKEN_HOME_X / TOKEN_HOME_Y |
token 模式:角落归位位置,uv 坐标(1,0 = 精确右上角;y 轴自上而下) |
TOKEN_EASE |
token 模式:增长曲线指数——1 = 按比例,<1 前置增长,>1 直到后期才保持较小 |
TOKEN_REACH |
token 模式:100% context 时漫游框覆盖的可玩屏幕比例 |
TOKEN_CALM / TOKEN_RUSH |
token 模式:0% / 100% context 时的漂移速度 |
WORK_PERIOD_MIN |
每个番茄钟周期的工作分钟数(增长阶段) |
BREAK_MIN |
每个周期的休息分钟数(黑洞保持较小) |
IDLE_FADE_SEC |
停止输入后黑洞开始淡出的时间 |
TIME_SCALE |
仅用于测试:1 = 真实日程;>1 通过 iTime 快进增长 |
N_STEPS(一个 #define)设置每像素的测地线积分预算;只有黑洞周围光线追踪圆内的像素才需要支付这一成本。它是主要的性能旋钮——该圆随黑洞缩放,因此大屏幕高 DPI 上的大黑洞正是帧率崩溃之处。若终端在高 context 填充时变慢,请降低 N_STEPS(和/或 TOKEN_AREA_MAX)。
若要在没有 Claude 会话的情况下目测任意 token 级别,可在 Ghostty 内的普通 shell 中手动驱动光标颜色通道:./token-test.sh 0.42 保持某一级别,./token-test.sh sweep 在 25 秒内从 0 → 100% 运行,./token-test.sh off 再次隐藏黑洞。(不要在有活跃会话的 surface 中运行——其 statusline 每次刷新都会重新发出真实级别。)请注意,尺寸平滑过渡仅在光标保持静止时才能持续——任何光标移动都会让 Ghostty 将 previous 快照为 current,从而提前结束过渡——这也是 hold 模式在退出回到提示符前会停留约 1.6 秒的原因。
若要快速调试循环,将 TIME_SCALE 设为例如 100,即可在约 36 秒内观看完整的番茄钟周期——增长、坍缩、休息——然后将其设回 1。(它通过 iTime 快进,而非挂钟,因此在 iDate 卡在零的构建上也能工作。)周期旋钮也接受小数分钟,若你更希望缩短真实日程本身。
Ghostty 提供给自定义着色器的 Uniform(1.3)
iResolution、iTime、iTimeDelta、iFrameRate、iFrame、iMouse(未使用)、iDate(挂钟——已声明但在 Ghostty 1.3 之前一直卡在零,见番茄钟注意事项)、iChannel0(终端,iChannel1-3 未使用)、iCurrentCursor/iPreviousCursor(xy 位置,zw 尺寸)、iCurrentCursorColor/iPreviousCursorColor、iCurrentCursorStyle/iPreviousCursorStyle、iCursorVisible、iTimeCursorChange、iFocus、iTimeFocus、iPalette[256]、iBackgroundColor、iForegroundColor、iCursorColor、iCursorText、iSelectionForegroundColor、iSelectionBackgroundColor。帧之间没有持久缓冲区——着色器是无状态的,因此番茄钟锚定在挂钟时间上。
若你要动手修改,有三点值得注意:
- Ghostty 的
fragCoordy 轴为自上而下,与其在其他方面遵循的 Shadertoy 约定相反。 - 要从脚本触发配置重载,请发送
SIGUSR2——但请用ps查找 PID,不要用pgrep/pkill:它们会静默排除自身祖先,而 Ghostty 是任何在其内运行的 shell 的祖先。 - Claude Code 启动 statusLine/hook 命令时没有控制终端,因此
/dev/tty在那里会失败——claude-token.py通过ps -o ppid=,tty=遍历其祖先来找到会话的 pty。
License
MIT — 参见 LICENSE。
灵感来自 Eric Bruneton 的黑洞着色器(black hole shader))(BSD-3-Clause)。本项目未使用该项目中的任何代码——本着色器是独立编写的屏幕空间(screen-space)近似实现,从零开始编写;致谢是针对其思路及其所展示的物理学原理。

