tcut — 终端会话转视频
tcut 会录制真实的 shell(PTY + 无界面 Ghostty 终端)并渲染出确定性的视频。脚本使用纯 TypeScript —— 无需学习领域特定语言。相同的录制,每次产生相同的像素。
安装
bun add -g termcut # 需要 Bun >= 1.4,安装 `tcut` 命令
MP4/GIF/WebM 格式需要系统中存在 ffmpeg。SVG 和 HTML 播放器无需额外依赖。所有输出都支持 --json 以获取机器可读的结果;tcut 永远不会主动弹出提示。
两种录制方式
脚本录制(推荐用于 Agent) — 编写 demo.video.ts,运行 tcut demo.video.ts:
import { defineVideo } from "tcut";
export default defineVideo(
{ output: ["demo.mp4", "demo.gif"], preset: "x", theme: "Catppuccin Mocha", keys: true, maxPause: "1.5s" },
async (t) => {
await t.run("bun --version"); // 自动输入命令、按回车,等待提示符返回
await t.expect(/1\.\d+/); // 断言屏幕上显示出的内容(如果不匹配则视频录制失败)
await t.sleep("1.5s");
},
);
实时录制 — tcut rec -o demo.gif 会打开一个 shell,录制直到输入 exit,同时生成精确的录制文件(demo.cast)和一个可编辑的脚本(demo.video.ts),其中记录了所有键入的内容。tcut rec -o demo.mp4 -- npm create vite 直接录制单条命令。
t API(在 defineVideo 内部使用)
t.run(cmd, { wait? })— 输入命令 + 回车 + 等待 shell 提示符(基于屏幕内容,非计时器)t.type(text)/t.paste(text)/t.enter()/t.escape()/t.tab()— 逐个按键驱动 TUIt.up/down/left/right/pageUp/pageDown/home/end()、t.ctrl("c")、t.alt("b")、t.shift("tab")、t.key("f5")、t.scrollUp/scrollDown()t.wait(regex?, { scope: "line" | "screen" })— 等待屏幕上出现指定内容;t.expect(re)— 断言t.sleep("800ms")— 持续时间支持"500ms"、"1.5s"或数字(毫秒)t.hide(async () => { ... })— 在摄像头外执行准备操作(状态会保持;不要在此处杀后台进程)t.print(markdown)/t.title(text)— 将 Markdown 字幕渲染到视频中,而不实际输入文本t.zoom({ rows: [0, 5], cols: [0, 60], duration: "500ms" })— 放大某个区域;t.zoom(null)恢复全屏t.slide("部署到云端", { eyebrow: "3", subtitle: "...", during })— 在功能演示之间插入全屏过渡卡片:居中大字标题使用真实排版,淡入淡出,并记录同名章节;during: async () => …可在卡片背后运行cd/clear/服务器启动t.chapter("Install")— 实际的 MP4 章节元数据,同时标记切分点:可在渲染时使用--chapters Install/--split-chapters分割t.expect(/…/, { scope: "scrollback" })用于检测已经滚出屏幕的输出;t.scrollback()返回完整记录文本;-o demo.log将记录写入文件- 当程序启用了应用程序光标模式时,方向键会以 SS3 序列发送,
t.paste()在程序请求时使用带括号粘贴——编辑器表现与真实终端一致;print("[text](url)")可在 SVG/HTML 中创建可点击的链接 title: "auto"跟随 OSC 标题;tcut doctor demo.cast解释录制使用了什么功能以及 tcut 无法显示的内容(如内嵌图片)t.timelapse(async () => { await t.run("bun install") }, { speed: 8 })— 内部的任何操作都以 8 倍速播放(maxPause仅去除静默时间;此压缩输出)t.snapshot("shot.png")/t.snapshot("hero.svg")— 在每次渲染时抓取当前时刻的静态截图(screenshot是别称),t.clear()、t.resize(cols, rows)、t.screen()/t.line()可用于读取屏幕内容- 浏览器面板:在配置中设置
browser: { position: "right" | "overlay", width },然后使用t.browser.goto(url)、t.browser.waitFor(/text/)、t.browser.click(sel)、t.focus("browser" | "terminal")— 在终端旁边/上方录制一个真实的 WebView(用于开发服务器演示)
配置要点
output(数组 = 多种格式)、preset: "readme" | "x" | "youtube" | "square"、theme(约 600 种 Ghostty 主题,tcut themes 查看)、cols/rows 或 width/height(像素)、fps、scale: 2(保持相同布局,像素翻倍——用于 Retina/HiDPI 或任何播放器会缩放的情况)、typingSpeed/typingJitter、keys: true(按键叠加提示,每次显示一个键帽;可设置 { limit, font, color, background, radius, position })、maxPause: "1.5s"(空闲压缩)、requires: ["bun", "eza"](录制前快速检查工具是否存在——也可在 tcut test 中使用)、windowBar: "none" + margin: 0 + borderRadius: 0 实现裸终端效果、title、marginFill("transparent" 在 png/webp/gif/webm/svg/html 中实现真正 alpha 通道;mp4 回退)、shadow: true(柔和阴影;边距默认为 40)、watermark: "© you" 或 { image: "logo.png", position: "top-left", opacity, size }。
剪辑与拼接(无需重新录制)
所有操作基于录制文件的可视时间线,因此每种格式都支持,结果仍是 .cast 文件:
tcut render demo.cast --from 2s --to 10s -o clip.gif # 时间窗口
tcut render demo.cast --chapters Zoom,Intro -o clip.mp4 # 按章节顺序导出
tcut render demo.cast --split-chapters -o demo.mp4 # 生成 demo-01-intro.mp4, demo-02-zoom.mp4 ... (适合作为 Remotion 的剪辑库)
tcut cut demo.cast --from 2s --to 10s # 写入 demo-cut.cast
tcut concat intro.cast demo.cast --gap 500ms -o launch.mp4 # 要求相同的列×行数;在每个拼接处重置屏幕
无需重新运行即可重新渲染
录制(.cast)和渲染是分离的:
tcut render demo.cast --theme "Gruvbox Dark" -o demo.svg
tcut render demo.cast --width 1280 --height 720 --speed 1.5 -o demo.mp4
CI / 测试
tcut test demo.video.ts— 将脚本作为测试运行(快速,不进行视频编码)tcut diff a.cast b.cast [--images dir]— 比较两个录制文件的屏幕文本,若发现差异则退出代码为 1
注意事项
- PTY 中的后台开发服务器:
bun run dev </dev/null >/tmp/dev.log 2>&1 &(标准输入读取的进程会收到 SIGTTIN;杂散日志会覆盖 TUI 界面) - 等待屏幕内容而非时间:优先使用
t.run/t.wait(/Local:/, { scope: "screen" })而非长时间休眠 t.hide会保持状态——在末尾隐藏的kill会泄露到最后一帧- 对于 TUI 程序(nvim、lazygit、claude),使用
t.run(cmd, { wait: /something-on-screen/ })启动,然后用按键驱动
完整参考:https://tcut.amanv.dev/llms.txt
将 tcut 作为库使用
import { defineVideo, renderCast } from "termcut"(仅限 Bun)。defineVideo(config, script) 返回一个 Video:await video.run({ force, log }) 录制并渲染,返回 { outputs, screenshots, durationSeconds, recording };video.record() 和 video.render(recording, { overrides, clip }) 可将步骤拆分;renderCast(file, overrides) 可重新渲染任意录制文件而无需 shell。此外还导出了:recordLive、cutRecording、concatRecordings、selectChapters、buildSvg、buildHtml、replayFrames、diffCasts、diagnoseCast、generateScript、runScriptTests、publishFiles。在 CI 中请显式传递 theme/font("auto" 会读取当前运行中的终端),并且一次只渲染一个视频。