tcut — sessões de terminal para vídeo
tcut grava um shell real (PTY + terminal Ghostty headless) e renderiza vídeos determinísticos. Scripts são TypeScript puro — sem DSL. Mesma gravação, mesmos pixels, sempre.
Configuração
bun add -g termcut # Bun >= 1.4, instala o comando `tcut`
MP4/GIF/WebM precisam do ffmpeg no PATH. Os players SVG e HTML não precisam de nada mais. Tudo suporta --json para resultados legíveis por máquina; nunca solicita confirmação.
Duas formas de gravar
Com script (preferido para agentes) — escreva demo.video.ts, execute 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"); // digita, pressiona Enter, espera o prompt retornar
await t.expect(/1\.\d+/); // afirma o que aparece na tela renderizada (falha o vídeo se ausente)
await t.sleep("1.5s");
},
);
Ao vivo — tcut rec -o demo.gif abre um shell, grava até exit, e escreve tanto a gravação exata (demo.cast) quanto um script editável (demo.video.ts) do que foi digitado. tcut rec -o demo.mp4 -- npm create vite grava um único comando.
A API t (dentro do defineVideo)
t.run(cmd, { wait? })— digita + Enter + espera pelo prompt do shell (baseado na tela, não em temporizador)t.type(text)/t.paste(text)/t.enter()/t.escape()/t.tab()— controla TUIs tecla por teclat.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" })— espera até que a tela mostre;t.expect(re)— afirmat.sleep("800ms")— durações aceitam"500ms","1.5s", números (ms)t.hide(async () => { ... })— executa configuração fora da câmera (o estado persiste; não mate jobs em background aqui)t.print(markdown)/t.title(text)— renderiza legendas Markdown no vídeo sem digitart.zoom({ rows: [0, 5], cols: [0, 60], duration: "500ms" })— amplia uma região;t.zoom(null)redefinet.slide("Implantar na nuvem", { eyebrow: "3", subtitle: "…", during })— cartão de transição de quadro inteiro entre demos de funcionalidades: título central grande em tipografia real, desbotado, grava um capítulo com o mesmo nome;during: async () => …executacd/clear/configuração de servidor invisivelmente atrás delet.chapter("Instalar")— metadados reais de capítulo MP4 e um ponto de corte:--chapters Instalar/--split-chaptersno momento da renderizaçãot.expect(/…/, { scope: "scrollback" })vê a saída que rolou para fora;t.scrollback()retorna a transcrição completa;-o demo.logescreve- Setas são enviadas como SS3 quando o programa ativou o modo cursor de aplicação;
t.paste()é suportado entre colchetes quando o programa solicitou — editores se comportam como com um terminal real;print("[texto](url)")cria um link clicável em SVG/HTML title: "auto"segue títulos OSC;tcut doctor demo.castexplica o que uma gravação usou e o que o tcut não pode mostrar (imagens inline)t.timelapse(async () => { await t.run("bun install") }, { speed: 8 })— tudo dentro executa 8× mais rápido (maxPauseapenas remove silêncio; isso comprime a saída)t.snapshot("shot.png")/t.snapshot("hero.svg")— instantâneo daquele momento exato em cada renderização (screenshot= alias),t.clear(),t.resize(cols, rows),t.screen()/t.line()para ler a tela- Painel do navegador:
browser: { position: "right" | "overlay", width }na configuração, entãot.browser.goto(url),t.browser.waitFor(/text/),t.browser.click(sel),t.focus("browser" | "terminal")— grava um WebView real ao lado/sobre o terminal (demos de servidor de desenvolvimento)
Configurações essenciais
output (array = múltiplos formatos), preset: "readme" | "x" | "youtube" | "square", theme (~600 temas Ghostty, tcut themes), cols/rows ou width/height (px), fps, scale: 2 (mesmo layout, 2× os pixels — use para Retina/HiDPI ou qualquer coisa que um player vai escalar), typingSpeed/typingJitter, keys: true (overlay de pressionamento de tecla, um chip por vez; { limit, font, color, background, radius, position }), maxPause: "1.5s" (compressão de inatividade), requires: ["bun", "eza"] (falhe rapidamente antes de gravar se uma ferramenta estiver faltando — também em tcut test), windowBar: "none" + margin: 0 + borderRadius: 0 para um terminal simples, title, marginFill ("transparent" = alfa real em png/webp/gif/webm/svg/html; mp4 usa fallback), shadow: true (sombra suave; margem padrão de 40), watermark: "© você" ou { image: "logo.png", position: "top-left", opacity, size }.
Cortar e juntar (sem regravar)
Tudo na linha do tempo visível do cast, então todo formato funciona e o resultado ainda é um .cast:
tcut render demo.cast --from 2s --to 10s -o clip.gif # janela de tempo
tcut render demo.cast --chapters Zoom,Intro -o clip.mp4 # capítulos, nessa ordem
tcut render demo.cast --split-chapters -o demo.mp4 # demo-01-intro.mp4, demo-02-zoom.mp4 … (biblioteca de clipes ideal para Remotion)
tcut cut demo.cast --from 2s --to 10s # escreve demo-cut.cast
tcut concat intro.cast demo.cast --gap 500ms -o launch.mp4 # mesmas cols×rows necessárias; tela reseta em cada emenda
Renderizar novamente sem reexecutar
Gravação (.cast) e renderização são separadas:
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 / testes
tcut test demo.video.ts— executa o script como teste (rápido, sem codificação de vídeo)tcut diff a.cast b.cast [--images dir]— compara texto da tela de duas gravações, sai com código 1 se houver diferença
Armadilhas
- Servidores de desenvolvimento em background no PTY:
bun run dev </dev/null >/tmp/dev.log 2>&1 &(leitores de stdin recebem SIGTTIN; logs perdidos repintam sobre TUIs) - Espere na tela, não no tempo: prefira
t.run/t.wait(/Local:/, { scope: "screen" })em vez de longos sleeps t.hidemantém o estado — umkilloculto no final vaza para o último quadro- Para TUIs (nvim, lazygit, claude), inicie com
t.run(cmd, { wait: /algo-na-tela/ }), depois controle com teclas
Referência completa: https://tcut.amanv.dev/llms.txt
Usar tcut como biblioteca
import { defineVideo, renderCast } from "termcut" (apenas Bun). defineVideo(config, script) retorna um Video: await video.run({ force, log }) grava e renderiza e retorna { outputs, screenshots, durationSeconds, recording }; video.record() e video.render(recording, { overrides, clip }) separam as etapas; renderCast(file, overrides) re-renderiza qualquer cast sem um shell. Também exportados: recordLive, cutRecording, concatRecordings, selectChapters, buildSvg, buildHtml, replayFrames, diffCasts, diagnoseCast, generateScript, runScriptTests, publishFiles. Em CI, passe theme/font explícitos ("auto" lê o terminal em execução) e renderize um vídeo por vez.