> [!NOTE] > 本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。 > [English](./README.en.md) · [原始项目](https://github.com/s0xDk/ghostty-blackhole) · [上游 README](https://github.com/s0xDk/ghostty-blackhole/blob/HEAD/README.md) > 原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。 # Ghostty Blackhole ![Ghostty Blackhole demo](demo.gif) **Landing page:** [s13k.dev/blackhole](https://s13k.dev/blackhole/) 一个漂浮在你 [Ghostty](https://ghostty.org) 终端里的黑洞。它从小开始生长,最终吞噬整个屏幕,具体行为取决于你选择的 **尺寸模式(size mode)**:内置的 *番茄钟(pomodoro)* 时钟(在一小时内逐渐变大、提醒你休息、休息后就不再打扰),或 *token 模式(token mode)*,实时追踪 **Claude Code 上下文窗口(context window)** 的填充程度。 本效果基于 [Eric Bruneton 的黑洞着色器](https://ebruneton.github.io/black_hole_shader/),,该着色器通过光束追踪(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 模式 需要两部分——着色器和命令: 1. 将 Ghostty 指向该着色器(见下方 [安装](#install)),使用 `SIZE_MODE MODE_TOKENS`(默认值)。 2. 将以下内容添加到 **`~/.claude/settings.json`**(请调整路径),然后启动新的 Claude Code 会话: ```json { "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`): ```ini custom-shader = /path/to/blackhole_ghostty/blackhole.glsl custom-shader-animation = true ``` 重新加载配置(macOS 上使用 `cmd+shift+,`)或打开新窗口。 ## 调参 ### 调参应用(macOS) 一款原生 SwiftUI 控制面板,风格类似 [Bruneton's demo page](https://ebruneton.github.io/black_hole_shader/demo/demo.html) 位于 `tuner/`:为每个着色器可调参数提供分组滑块、预设(*Gargantua*、*Quasar*、*M87\* donut*、*Blazar*、*Inferno*、*Zen* 等),与文件双向实时同步,且每次微调都会通过 `SIGUSR2` 立即热重载 Ghostty。 ![预设——Defaults、Gargantua、Quasar、Face-on ember、M87* donut、Blazar、Inferno、Pure lens、Zen](presets-grid.png) ```sh 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 的 `fragCoord` y 轴为**自上而下**,与其在其他方面遵循的 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](LICENSE)。 灵感来自 [Eric Bruneton 的黑洞着色器(black hole shader)](https://github.com/ebruneton/black_hole_shader))(BSD-3-Clause)。本项目未使用该项目中的任何代码——本着色器是独立编写的屏幕空间(screen-space)近似实现,从零开始编写;致谢是针对其思路及其所展示的物理学原理。