tcut — 터미널 세션을 비디오로
tcut은 실제 셸(PTY + 헤드리스 Ghostty 터미널)을 녹화하고 결정론적 비디오를 렌더링합니다. 스크립트는 별도의 DSL 없이 일반 TypeScript입니다. 동일한 녹화, 동일한 픽셀, 매번 같습니다.
설정
bun add -g termcut # Bun >= 1.4, `tcut` 명령어 설치
MP4/GIF/WebM은 ffmpeg가 PATH에 있어야 합니다. SVG와 HTML 플레이어는 추가로 필요하지 않습니다. 모든 출력은 --json 옵션을 지원하여 기계가 읽을 수 있는 결과를 반환하며, 프롬프트를 절대 띄우지 않습니다.
두 가지 녹화 방식
스크립트 방식 (에이전트에 권장) — 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"); // 입력하고, Enter 누르고, 프롬프트가 돌아올 때까지 기다림
await t.expect(/1\.\d+/); // 렌더링된 화면에 대해 어서션 (없으면 비디오 실패)
await t.sleep("1.5s");
},
);
라이브 방식 — tcut rec -o demo.gif는 셸을 열고 exit 입력 시까지 녹화한 후, 정확한 녹화 (demo.cast)와 입력된 내용의 편집 가능한 스크립트 (demo.video.ts)를 함께 저장합니다. tcut rec -o demo.mp4 -- npm create vite는 단일 명령어만 녹화합니다.
t API (defineVideo 내부)
t.run(cmd, { wait? })— 입력 + Enter + 셸 프롬프트가 돌아올 때까지 대기 (타이머 기반 아님, 화면 기반)t.type(text)/t.paste(text)/t.enter()/t.escape()/t.tab()— 키 하나하나로 TUI 조작t.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", 숫자 (ms) 허용t.hide(async () => { ... })— 카메라 밖에서 설정 실행 (상태는 유지되나, 여기서 백그라운드 작업을 종료하지 말 것)t.print(markdown)/t.title(text)— 타자 없이 비디오에 마크다운 캡션 렌더링t.zoom({ rows: [0, 5], cols: [0, 60], duration: "500ms" })— 영역 확대;t.zoom(null)은 초기화t.slide("Deploy to the cloud", { 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 (px), fps, scale: 2 (동일 레이아웃, 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에서 실제 알파; mp4는 대체), shadow: true (부드러운 그림자; margin 기본값 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 # 동일한 cols×rows 필요; 각 이음매에서 화면 초기화
재실행 없이 다시 렌더링
녹화(.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]— 두 녹화의 화면 텍스트 비교, 차이 시 exit 1 반환
주의사항
- PTY 내부의 백그라운드 개발 서버:
bun run dev </dev/null >/tmp/dev.log 2>&1 &(stdin 리더는 SIGTTIN 수신; 로그가 TUI 위에 덮어쓰기) - 시간이 아닌 화면을 기준으로 대기: 긴 sleep 대신
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)는 셸 없이 캐스트를 다시 렌더링. 또한 내보내짐: recordLive, cutRecording, concatRecordings, selectChapters, buildSvg, buildHtml, replayFrames, diffCasts, diagnoseCast, generateScript, runScriptTests, publishFiles. CI에서는 명시적으로 theme/font를 전달하고 ("auto"는 실행 중인 터미널을 읽음) 한 번에 하나의 비디오만 렌더링하세요.