Skip to content

Commit a3e2d2f

Browse files
committed
docs: embed asciinema demo in readme and add cast generator
1 parent 0d1853c commit a3e2d2f

7 files changed

Lines changed: 358 additions & 3 deletions

File tree

‎DEMO_GUIDE.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# 演示截图指南
2+
3+
请在终端运行以下命令,截图保存到 `docs/screenshots/` 目录。
4+
5+
## 截图 1:安装插件
6+
7+
运行:
8+
```bash
9+
opencode plugin @herouucn/opencode-commandcode
10+
```
11+
12+
预期输出:显示插件已添加到配置。
13+
14+
## 截图 2:查看模型
15+
16+
运行:
17+
```bash
18+
opencode models
19+
```
20+
21+
预期输出:显示 commandcode/ 开头的 70+ 模型列表。
22+
23+
## 截图 3:选择模型
24+
25+
运行 opencode 后输入 `/models`,显示模型选择界面。
26+
27+
## 截图 4:实际对话
28+
29+
选择一个模型(如 commandcode/deepseek-v4-flash),发送一条消息,显示回复。
30+
31+
---
32+
33+
截图命名:
34+
- `docs/screenshots/01-install.png`
35+
- `docs/screenshots/02-models.png`
36+
- `docs/screenshots/03-select.png`
37+
- `docs/screenshots/04-chat.png`
38+
39+
然后更新 README:
40+
41+
```markdown
42+
## 演示
43+
44+
![安装插件](./docs/screenshots/01-install.png)
45+
![模型列表](./docs/screenshots/02-models.png)
46+
```

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222

2323
## 演示
2424

25+
安装插件并查看模型列表:
26+
27+
[![asciinema demo](https://asciinema.org/a/ePh5yVrpNatwEQKT.svg)](https://asciinema.org/a/ePh5yVrpNatwEQKT)
28+
2529
安装后运行 opencode,输入 `/models` 即可看到 Command Code 提供的 70+ 模型:
2630

2731
```
28-
opencode
29-
> /models
30-
3132
commandcode/claude-fable-5
3233
commandcode/claude-fable-5-1
3334
commandcode/claude-haiku-4-5-20251001

‎commands.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# 演示命令列表
2+
# 每行一条命令,# 开头为注释
3+
opencode plugin @herouucn/opencode-commandcode
4+
opencode models 2>&1 | findstr "commandcode/" | findstr /n "^" | findstr "^[1-9]:" | findstr /c:"1:" /c:"2:" /c:"3:" /c:"4:" /c:"5:" /c:"6:" /c:"7:" /c:"8:" /c:"9:" /c:"10:" /c:"11:" /c:"12:" /c:"13:" /c:"14:" /c:"15:"

‎create_gif.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env python3
2+
"""
3+
从手动截图合成 GIF
4+
将 docs/screenshots/ 目录下的 PNG 按文件名排序合成为 demo.gif
5+
"""
6+
7+
import os
8+
import sys
9+
from PIL import Image
10+
11+
SCREENSHOTS_DIR = os.path.join(os.path.dirname(__file__), "docs", "screenshots")
12+
OUTPUT_GIF = os.path.join(os.path.dirname(__file__), "demo.gif")
13+
14+
def main():
15+
# 获取所有 PNG 文件并排序
16+
png_files = sorted([
17+
f for f in os.listdir(SCREENSHOTS_DIR)
18+
if f.endswith(".png")
19+
])
20+
21+
if not png_files:
22+
print(f"未在 {SCREENSHOTS_DIR} 找到 PNG 文件")
23+
print()
24+
print("请先截图保存到该目录:")
25+
print(" 1. 打开终端,运行 opencode plugin @herouucn/opencode-commandcode")
26+
print(" 2. 按 Win+Shift+S 截图,保存为 01-install.png")
27+
print(" 3. 运行 opencode models,截图保存为 02-models.png")
28+
print(" 4. 运行 opencode,输入 /models,截图保存为 03-select.png")
29+
return
30+
31+
print(f"找到 {len(png_files)} 张截图,合成 GIF...")
32+
33+
images = []
34+
for filename in png_files:
35+
filepath = os.path.join(SCREENSHOTS_DIR, filename)
36+
img = Image.open(filepath)
37+
images.append(img)
38+
print(f" 加载: {filename} ({img.width}x{img.height})")
39+
40+
# 统一尺寸(用第一张图的尺寸)
41+
target_size = images[0].size
42+
resized = []
43+
for img in images:
44+
if img.size != target_size:
45+
img = img.resize(target_size, Image.Resampling.LANCZOS)
46+
resized.append(img)
47+
48+
# 保存 GIF
49+
resized[0].save(
50+
OUTPUT_GIF,
51+
save_all=True,
52+
append_images=resized[1:],
53+
duration=2000, # 每张 2 秒
54+
loop=0
55+
)
56+
57+
print(f"\n完成!GIF 已保存: {OUTPUT_GIF}")
58+
print(f"文件大小: {os.path.getsize(OUTPUT_GIF) / 1024:.1f} KB")
59+
print()
60+
print("README 嵌入代码:")
61+
print("![Demo](./demo.gif)")
62+
63+
if __name__ == "__main__":
64+
main()

‎gen_cast.py‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
"""
3+
根据命令列表生成 asciinema v2 格式录制文件
4+
解决 PowerSession-rs 在 Windows 上录制 TUI 程序时管道断裂的问题
5+
6+
用法:
7+
1. 编辑 commands.txt,每行一条命令
8+
2. 运行 python gen_cast.py
9+
3. 输出 demo.cast(asciinema v2 格式)
10+
"""
11+
12+
import json
13+
import time
14+
import os
15+
import sys
16+
import subprocess
17+
import tempfile
18+
19+
COMMANDS_FILE = os.path.join(os.path.dirname(__file__), "commands.txt")
20+
OUTPUT_CAST = os.path.join(os.path.dirname(__file__), "demo.cast")
21+
WIDTH = 120
22+
HEIGHT = 30
23+
24+
def run_command(cmd, timeout=30):
25+
"""运行命令,返回 (stdout, stderr, duration)"""
26+
start = time.time()
27+
try:
28+
result = subprocess.run(
29+
cmd,
30+
shell=True,
31+
capture_output=True,
32+
text=True,
33+
timeout=timeout,
34+
encoding="utf-8",
35+
errors="replace"
36+
)
37+
duration = time.time() - start
38+
return result.stdout, result.stderr, duration
39+
except subprocess.TimeoutExpired:
40+
duration = time.time() - start
41+
return f"[timeout after {timeout}s]", "", duration
42+
except Exception as e:
43+
duration = time.time() - start
44+
return "", str(e), duration
45+
46+
def main():
47+
# 读取命令列表
48+
if not os.path.exists(COMMANDS_FILE):
49+
print(f"未找到 {COMMANDS_FILE}")
50+
print("请创建该文件,每行一条命令,例如:")
51+
print(" opencode plugin @herouucn/opencode-commandcode")
52+
print(" opencode models | findstr commandcode/")
53+
sys.exit(1)
54+
55+
with open(COMMANDS_FILE, "r", encoding="utf-8") as f:
56+
commands = [line.strip() for line in f if line.strip() and not line.startswith("#")]
57+
58+
if not commands:
59+
print(f"{COMMANDS_FILE} 为空")
60+
sys.exit(1)
61+
62+
print(f"读取到 {len(commands)} 条命令,开始录制...")
63+
64+
# 准备 cast 文件
65+
header = {
66+
"version": 2,
67+
"width": WIDTH,
68+
"height": HEIGHT,
69+
"timestamp": int(time.time()),
70+
"env": {
71+
"TERM": "xterm-256color",
72+
"SHELL": "powershell.exe" if sys.platform == "win32" else os.environ.get("SHELL", "/bin/bash")
73+
}
74+
}
75+
76+
events = []
77+
current_time = 0.0
78+
79+
# 初始清屏
80+
events.append([current_time, "o", "\u001b[2J\u001b[H\u001b[?25h"])
81+
current_time += 0.1
82+
83+
for i, cmd in enumerate(commands):
84+
# 显示命令提示符
85+
prompt = f"PS C:\\> " if sys.platform == "win32" else "$ "
86+
events.append([current_time, "o", prompt])
87+
current_time += 0.1
88+
89+
# 逐字符输入命令
90+
for char in cmd:
91+
events.append([current_time, "i", char])
92+
current_time += 0.02
93+
94+
# 回车
95+
events.append([current_time, "o", "\r\n"])
96+
current_time += 0.1
97+
98+
# 执行命令
99+
print(f" [{i+1}/{len(commands)}] {cmd}")
100+
stdout, stderr, duration = run_command(cmd)
101+
102+
# 输出结果
103+
output = stdout + stderr
104+
if output:
105+
# 转义特殊字符
106+
output = output.replace("\\", "\\\\").replace("\r\n", "\n").replace("\r", "\n")
107+
events.append([current_time, "o", output])
108+
current_time += duration
109+
else:
110+
current_time += 0.5
111+
112+
# 命令间暂停
113+
current_time += 0.5
114+
115+
# 结束提示
116+
events.append([current_time, "o", "\r\n=== 演示完成 ===\r\n"])
117+
current_time += 0.1
118+
119+
# 写入 cast 文件
120+
with open(OUTPUT_CAST, "w", encoding="utf-8", newline="") as f:
121+
f.write(json.dumps(header) + "\n")
122+
for event in events:
123+
f.write(json.dumps(event) + "\n")
124+
125+
file_size = os.path.getsize(OUTPUT_CAST)
126+
print(f"\n完成!")
127+
print(f"文件: {OUTPUT_CAST}")
128+
print(f"大小: {file_size} bytes")
129+
print(f"时长: {current_time:.1f} 秒")
130+
print(f"事件数: {len(events)}")
131+
print()
132+
print("播放测试:")
133+
print(f" asciinema play {OUTPUT_CAST}")
134+
print("或上传到 asciinema.org:")
135+
print(f" powersession upload {OUTPUT_CAST}")
136+
137+
if __name__ == "__main__":
138+
main()

‎record_demo.py‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
"""
3+
录制演示 GIF - 自动截取终端窗口并合成 GIF
4+
"""
5+
6+
import time
7+
import os
8+
import sys
9+
from PIL import Image
10+
import pygetwindow as gw
11+
12+
DEMO_DIR = os.path.join(os.path.dirname(__file__), "docs", "screenshots")
13+
CAST_FILE = os.path.join(os.path.dirname(__file__), "demo.cast")
14+
OUTPUT_GIF = os.path.join(os.path.dirname(__file__), "demo.gif")
15+
16+
# 创建截图目录
17+
os.makedirs(DEMO_DIR, exist_ok=True)
18+
19+
def find_terminal_window():
20+
"""查找终端窗口"""
21+
windows = gw.getWindowsWithTitle("opencode")
22+
if not windows:
23+
windows = gw.getWindowsWithTitle("PowerShell")
24+
if not windows:
25+
windows = gw.getWindowsWithTitle("Command Prompt")
26+
if not windows:
27+
windows = gw.getWindowsWithTitle("Windows PowerShell")
28+
return windows[0] if windows else None
29+
30+
def capture_window(window, filepath):
31+
"""截取窗口"""
32+
try:
33+
# 确保窗口在前台
34+
try:
35+
window.activate()
36+
time.sleep(0.3)
37+
except:
38+
pass
39+
left, top, width, height = window.left, window.top, window.width, window.height
40+
import pyautogui
41+
screenshot = pyautogui.screenshot(region=(left, top, width, height))
42+
screenshot.save(filepath)
43+
return True
44+
except Exception as e:
45+
print(f"截图失败: {e}")
46+
return False
47+
48+
def images_to_gif(image_paths, output_path, duration=500):
49+
"""将图片列表合成为 GIF"""
50+
images = []
51+
for path in image_paths:
52+
img = Image.open(path)
53+
images.append(img)
54+
55+
if not images:
56+
print("没有截图可合成")
57+
return
58+
59+
images[0].save(
60+
output_path,
61+
save_all=True,
62+
append_images=images[1:],
63+
duration=duration,
64+
loop=0
65+
)
66+
print(f"GIF 已保存: {output_path}")
67+
68+
def main():
69+
print("=== opencode-commandcode 演示录制器 ===")
70+
print()
71+
print("请在 5 秒内切换到终端窗口,准备好运行以下命令:")
72+
print(" 1. opencode plugin @herouucn/opencode-commandcode")
73+
print(" 2. opencode models | grep commandcode/ | head -15")
74+
print()
75+
input("按 Enter 开始录制(你有 5 秒时间切换到终端)...")
76+
77+
print("录制中...")
78+
time.sleep(5)
79+
80+
screenshots = []
81+
max_frames = 30 # 最多截 30 帧
82+
83+
for i in range(max_frames):
84+
window = find_terminal_window()
85+
if window:
86+
filepath = os.path.join(DEMO_DIR, f"frame_{i:03d}.png")
87+
if capture_window(window, filepath):
88+
screenshots.append(filepath)
89+
print(f" 截帧 {i+1}/{max_frames}")
90+
time.sleep(0.5) # 每 0.5 秒截一帧
91+
92+
if screenshots:
93+
images_to_gif(screenshots, OUTPUT_GIF, duration=400)
94+
print(f"\n完成!共 {len(screenshots)} 帧")
95+
print(f"GIF 文件: {OUTPUT_GIF}")
96+
print(f"截图目录: {DEMO_DIR}")
97+
else:
98+
print("未捕获到任何截图")
99+
100+
if __name__ == "__main__":
101+
main()

‎terminalizer-record.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version": 2, "width": 120, "height": 30, "command": "bash", "title": "opencode-commandcode demo", "env": {"TERM": "xterm-256color"}, "records": [{"delay": 1000, "content": "opencode plugin @herouucn/opencode-commandcode\r\n"}, {"delay": 3000, "content": "opencode models | grep commandcode/ | head -15\r\n"}, {"delay": 2000, "content": "echo \"... 70+ models available\"\r\n"}, {"delay": 1000, "content": "exit\r\n"}]}

0 commit comments

Comments
 (0)