首页
/ lazygit 演示录制系统实战:复用集成测试框架录制 GIF/MP4 演示视频

lazygit 演示录制系统实战:复用集成测试框架录制 GIF/MP4 演示视频

2026-09-03 17:51:10作者:蔡怀权

lazygit 的文档和 README 中大量使用 GIF 演示视频来展示各项功能,这些视频并非人工操作截屏,而是由一套"演示录制系统"自动生成。本文基于仓库内 Demo_Recordings.md 文档展开,系统讲解如何借用 lazygit 的集成测试框架编写 demo、设置字幕、通过 assets 分支 worktree 管理产物,以及 terminalizer + gifsicle/ffmpeg 的完整录制流水线。读完后你可以独立为任意新功能录制出可嵌入文档的演示视频。

整体思路:把 demo 当作一种特殊的集成测试

lazygit 之所以能低成本地保持演示视频与 UI 同步更新,关键在于:演示视频直接复用集成测试的录制系统。每个 demo 本质上就是一个普通的集成测试,只是多了一个 IsDemo: true 标记,并额外配置了适合"表演"的界面行为。因此,写 demo 前建议先熟悉集成测试的写法,见 pkg/integration/README.md,其核心结构为两步:

  1. Setup step(准备阶段):通过 shell 在临时仓库中执行 git 命令,构造出想要的初始状态(分支、提交、远端、冲突等);
  2. Run step(运行阶段):由测试驱动 t 模拟用户按键、选单、确认弹窗,并做断言。

集成测试通过 justfile 中的 just 配方运行(just e2e 无头跑全套,just e2e-tui 打开测试浏览界面),这些同样是录制 demo 的前置技能。

环境准备

录制链路依赖三个外部工具加一套带图标的字体,原文档给出的安装命令如下:

# 用于录制
npm i -g terminalizer
# 用于 GIF 压缩
npm i -g gifsicle
# 用于 MP4 转码
brew install ffmpeg

# 带图标(nerd icons)的字体
wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/DejaVuSansMono.tar.xz && \
  tar -xf DejaVuSansMono.tar.xz -C /usr/local/share/fonts && \
  rm DejaVuSansMono.tar.xz

各工具在流水线中的分工(可在 demo/record_demo.sh 中逐一印证):

工具 用途 脚本中的位置
terminalizer 把终端会话录制成 YAML 描述文件,再渲染成 GIF L67 录制、L69 渲染
gifsicle 对原始 GIF 压缩调色板,产出可入库的小体积 GIF L78
ffmpeg 将 GIF 转为 faststart MP4(备用输出格式) L75
Nerd Font 字体 让终端中的 git 图标(分支/提交/文件)能正常显示 demo/config.ymlfontFamily 引用

脚本开头会用 command -v 检查 terminalizergifsicle 是否存在,缺失时打印安装命令并退出(L42-L54),所以直接运行脚本即可得到明确提示。

什么是 Demo:IsDemo 标记及其 UI 效果

所有 demo 存放在 pkg/integration/tests/demo/ 目录下,当前仓库包含 15 个演示文件(如 interactive_rebase.gonuke_working_tree.gostage_lines.go 等)以及一个共享配置文件 shared.go

在测试定义中设置 IsDemo: true 会产生三个"表演模式"效果(见 Demo_Recordings.md "Creating a demo" 一节):

  1. 底部按键提示行(options map)变安静:底部那一行平时显示当前上下文的快捷键绑定,在 demo 中改为渲染字幕内容,从而可以在画面底部打出解说文字;
  2. Fetch/Push/Pull 加入人为延迟:模拟真实网络请求,避免画面中远端操作"瞬间完成"显得不真实;
  3. 右下角的加载指示器(loader)不出现,避免一闪一闪的刷新动画干扰观看。

这三个效果在源码中都有落点,可以逐一验证:

  • 标记定义:IsDemo bool 字段位于 test.goNewIntegrationTestArgs 中,并以 IsDemo() 方法暴露在测试类型接口 types.go 上;
  • 效果 1 的实现:options_map.go 中,GUI 渲染底部按键提示前会先判断
func (gui *Gui) renderContextOptionsMap() {
	// In demos, we render our own content to this view
	if gui.integrationTest != nil && gui.integrationTest.IsDemo() {
		return
	}
	// ...
}

demo 模式下直接跳过按键提示的渲染,把这一行让给字幕;

  • 通用的 demo 判断入口是 gui_common.go 中的 InDemo()(内部同样是 integrationTest != nil && integrationTest.IsDemo()),GUI 其他需要区分"演示中"与"普通测试"的逻辑(如 loader 行为)可通过它分支。

原文档还特别提醒:demo 的断言不需要像正式测试那么严格,但仍应保留基础断言——这样将来若自动化"批量更新 demo"流程,断言失败能立刻告诉你某个演示已经和当前 UI 脱节。

编写一个 Demo:三步走

写 demo 的流程与写集成测试一致:

  1. SetupRepo 把仓库搭好;
  2. 用沙箱模式(sandbox mode)手动跑一遍,感受需要发生什么;
  3. 回来把操作序列写成代码。

下面以 interactive_rebase.go 为例拆解完整结构:

var InteractiveRebase = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Interactive rebase",
	ExtraCmdArgs: []string{"log", "--screen-mode=full"},
	Skip:         false,
	IsDemo:       true,
	SetupConfig: func(config *config.AppConfig) {
		setDefaultDemoConfig(config)
	},
	SetupRepo: func(shell *Shell) {
		shell.CreateRepoHistory()
		shell.NewBranch("feature/demo")
		shell.CreateNCommitsWithRandomMessages(10)
		shell.CloneIntoRemote("origin")
		shell.SetBranchUpstream("feature/demo", "origin/feature/demo")
	},
	Run: func(t *TestDriver, keys config.KeybindingConfig) {
		t.SetCaptionPrefix("Interactive rebase")
		t.Wait(1000)
		// ...按键操作序列,中间穿插断言与字幕切换
	},
})

几个要点:

  • SetupRepo:创建带历史的仓库、建 feature/demo 分支、生成 10 个提交、克隆到 origin 远端并设置 upstream——一次性把"交互变基 + 强制推送"所需的完整场景搭好,让 Run 阶段专注于"表演";
  • ExtraCmdArgs:以 --screen-mode=full 全屏模式启动,演示画面更干净;
  • Run 阶段的操作序列:聚焦提交列表 → 触发交互变基(keys.Commits.StartInteractiveRebase)→ 范围选中两行(RangeSelectDownPressFast 快速连按)→ 标记 fixup(MarkCommitAsFixup)→ 下移两行 → 删除一项(Remove)→ 下压合并(SquashDown)→ 在"Rebase options"弹窗中选择 continue → 切换字幕前缀为 "Push to remote" → 全屏模式下按 Push,并在 "Force push" 确认弹窗中确认;
  • 断言:通过 t.ExpectPopup().Menu().Title(Contains("Rebase options")).Select(...) 等形式,确认预期的弹窗与标题真的出现了,保证录制结果可复现;
  • PressFastDelay:快速连按用于范围选中等不需要停顿的操作,Delay() 则在关键节点给观众留一点阅读时间。

Demo 的共享配置:图标与作者配色

sdemos 共享文件 shared.go 提供了两个助手函数,几乎所有 demo 都会用到:

func setDefaultDemoConfig(config *config.AppConfig) {
	// demos look much nicer with icons shown
	config.GetUserConfig().Gui.NerdFontsVersion = "3"
}

它把 NerdFontsVersion 设为 "3",让 lazygit 在演示画面中渲染 nerd 图标(配合前面安装的 Nerd Font 字体),这是 demo 观感的重要一环。另一个函数 setGeneratedAuthorColoursshell.CreateRepoHistory() 生成的几位"虚拟作者"(Fredrica Greenhill、Oscar Reuenthal 等)指定固定颜色,使提交图的分支配色在每次录制中保持一致、可预期。

添加字幕(captions)

原文档建议为演示添加字幕说明当前正在执行的任务,做法是在 Run 阶段调用 t.SetCaptionPrefix("..."),并像 interactive_rebase.go 中那样在任务切换时再次调用(例如从 "Interactive rebase" 切到 "Push to remote")。其调用链为:test_driver.goTestDriver.SetCaptionPrefix 直接转发给 GUI 的 SetCaptionPrefix,最终内容渲染在 demo 模式下被"让出来"的底部按键行上。写作新 demo 时,直接参考现有 demo 的字幕风格即可。

准备工作产物仓库:assets 分支与 linked worktree

lazygit 把所有静态资源(包括演示视频)存放在一个名为 assets 的独立分支上。这个分支与主分支不共享任何历史,唯一用途就是存放大二进制文件,避免把代码分支的克隆体积撑大。

脚本和 demo 定义在代码分支中,而输出产物在 assets 分支中。因此录制前需要为 assets 分支创建一个 linked worktree:

git worktree add .worktrees/assets assets

这个前置条件被脚本强制校验:demo/record_demo.sh 会用 git worktree list | grep assets 找到 assets worktree 的路径,找不到就打印上面这条 git worktree add 命令并以非零码退出。

worktree 就绪后,脚本把输出目录定为 assets worktree 下的 demo 目录(OUTPUT_DIR="$WORKTREE_PATH/demo",L40)。每次录制会产出三份文件:

  1. 录制的 YAML 描述文件(terminalizer 的中间产物,$NAME);
  2. 原始 GIF($NAME.gif);
  3. 压缩后的 GIF($NAME-compressed.gif)或 MP4($NAME.mp4),取决于你选择的输出格式。

其中 $NAME 取自测试文件路径的 basename 并去掉扩展名(脚本 L59,例如 pkg/integration/tests/demo/interactive_rebase.gointeractive_rebase),所以输出文件名由 demo 文件名天然决定,无需额外指定。

录制命令与底层流水线

确认 demo 跑通后,执行:

scripts/record_demo.sh [gif|mp4] <path>
# 例如:
scripts/record_demo.sh gif pkg/integration/tests/demo/interactive_rebase.go

入口 scripts/record_demo.sh 只是把参数原样转发给 demo/record_demo.sh。后者在参数校验(必须恰好两个参数、TYPE 必须是 gifmp4)之后,依次完成:

# 1. 注册 demo:重新生成测试清单,确保该 demo 可被测试框架发现
go generate pkg/integration/tests/tests.go

# 2. 用 terminalizer 录制终端会话(以 --slow 慢速模式驱动 demo),产出 YAML
terminalizer -c demo/config.yml record --skip-sharing -d \
  "go run cmd/integration_test/main.go cli --slow $TEST" "$OUTPUT_DIR/$NAME"

# 3. 将 YAML 渲染成 GIF
terminalizer render "$OUTPUT_DIR/$NAME" -o "$OUTPUT_DIR/$NAME.gif"

# 4a. gif 输出:gifsicle 压缩(256 色调色板 + web 安全色 + -O3)
gifsicle --colors 256 --use-col=web -O3 < "$OUTPUT_DIR/$NAME.gif" > "$OUTPUT_DIR/$NAME-compressed.gif"

# 4b. mp4 输出:ffmpeg 转 faststart MP4
ffmpeg -y -i "$OUTPUT_DIR/$NAME.gif" -movflags faststart -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "$OUTPUT_DIR/$NAME.mp4"

几个值得注意的实现细节:

  • --slow 参数:go run cmd/integration_test/main.go cli --slow $TEST 以慢速模式运行集成测试 CLI,在按键与鼠标操作之间加入预设延迟,让演示节奏适合人眼观看(与 pkg/integration/README.md 中"watch a test run at a realistic speed"的 --slow 机制一致);
  • --skip-sharing:terminalizer 默认录制完会上传并返回分享链接,这里跳过该行为,保证产物完全本地化;
  • gifsicle 压缩参数:--colors 256 --use-col=web -O3 把调色板收敛到 256 色的 web 安全色并做三级优化,这是"原始 GIF 大、入库 GIF 小"的关键步骤,也解释了为何 demo/config.ymlquality: 100 的注释写着"高质量看起来没差别,但过一遍 gifsicle 压缩效果远好得多";
  • MP4 的现实约束:原文档用删除线标记了早期"gif 用于 README 首图、mp4 用于其余位置"的分工,并注明结论——README 中无法可靠地存放并链接 mp4,所以目前统一使用 GIF(自动播放、可循环)。脚本仍保留 mp4 分支,但实际产物以 -compressed.gif 为准。

terminalizer 配置:demos 为什么看起来"干净"

录制画面观感由 demo/config.yml 统一控制,关键参数包括:

配置 作用
cols / rows 120 / 35 固定终端尺寸,保证每次录制的画面构图一致
repeat 0 GIF 无限循环播放(1 次/-1 次/循环均可配)
quality 100 原始 GIF 质量拉满,压缩交给 gifsicle
frameDelay auto 使用真实录制延迟,操作节奏自然
maxIdleTime 2000 帧间最大空闲 2 秒,避免画面长时间静止拖长视频
frameBox type: floating,title: Lazygit 给画面加一个带 "Lazygit" 标题的浮动窗口框,背景 #1d1d1d
fontFamily / fontSize "DejaVuSansM Nerd Font" / 8 使用 Nerd 字体渲染 git 图标;注释特别说明不选 mono 字体是因为图标会显得太小
theme 一组自定义颜色 背景透明、前景 #dddad6 等,与整体文档风格统一
env recording: true 向录制进程注入环境变量,供程序感知"正在录制"

这套配置意味着:demo 的视觉规范(尺寸、字体、边框、配色)是集中管理的,UI 改版后只需更新配置与 demo 代码,重跑录制脚本即可批量刷新全部演示视频。

将演示视频嵌入 README 与文档

产物就绪后,发布分两步(assets 与代码分支解耦):

  1. assets worktree 中,把三份输出文件(YAML、原始 GIF、压缩 GIF)全部暂存,向 assets 分支发起 PR;
  2. 回到代码分支,在文档中嵌入视频。嵌入路径以 assets 分支为基准,例如原文档给出的示例:
Nuke working tree

由于文档只引用 assets 分支上的固定路径,之后更新视频(比如 UI 改版后重新录制)只需再对 assets 分支发 PR,无需改动任何嵌入该视频的文档——这就是 assets 分支"无共享历史、纯资源存储"设计的直接收益。

小结:完整工作流一览

  1. 安装 terminalizergifsicleffmpeg 与 Nerd Font 字体;
  2. pkg/integration/tests/demo/ 下新建 demo 文件,IsDemo: true + SetupConfig(调用 setDefaultDemoConfig)+ SetupRepo + Run(操作序列 + 基础断言 + SetCaptionPrefix 字幕);
  3. 用沙箱模式手动验证一遍,再补全代码;
  4. git worktree add .worktrees/assets assets 建立产物 worktree;
  5. scripts/record_demo.sh gif pkg/integration/tests/demo/xxx.go,产物落在 assets worktree 的 demo/ 目录;
  6. 在 assets worktree 中提交三份产物并 PR 到 assets 分支;
  7. 在代码分支的文档中以 demo/xxx-compressed.gif 相对路径嵌入。

整套体系把"演示视频"纳入了与集成测试相同的自动化框架:断言保证录制可复现,terminalizer 配置保证画面一致性,assets worktree 保证代码仓库不被大二进制文件污染,三者共同支撑了 lazygit 文档中演示视频可以低成本、批量地跟随 UI 演进持续更新。

登录后查看全文
热门项目推荐
相关项目推荐