tcut — Terminal-Sessions als Video
tcut zeichnet eine echte Shell (PTY + Headless-Ghostty-Terminal) auf und rendert deterministische Videos. Skripte sind reines TypeScript – keine DSL. Gleiche Aufnahme, gleiche Pixel, jedes Mal.
Einrichtung
bun add -g termcut # Bun >= 1.4, installiert den Befehl `tcut`
MP4/GIF/WebM benötigen ffmpeg im PATH. SVG und HTML-Player benötigen nichts weiter. Alles unterstützt --json für maschinenlesbare Ergebnisse; nie werden Abfragen gestellt.
Zwei Aufnahmemöglichkeiten
Skriptbasiert (bevorzugt für Agenten) – schreibe demo.video.ts, führe tcut demo.video.ts aus:
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"); // tippt es, drückt Enter, wartet auf die Rückkehr der Eingabeaufforderung
await t.expect(/1\.\d+/); // überprüft den gerenderten Bildschirm (bricht das Video ab, wenn nicht vorhanden)
await t.sleep("1.5s");
},
);
Live – tcut rec -o demo.gif öffnet eine Shell, zeichnet auf, bis exit eingegeben wird, und schreibt sowohl die exakte Aufnahme (demo.cast) als auch ein bearbeitbares Skript (demo.video.ts) des Getippten. tcut rec -o demo.mp4 -- npm create vite zeichnet einen einzelnen Befehl auf.
Die t-API (innerhalb von defineVideo)
t.run(cmd, { wait? })– tippen + Enter + auf die Shell-Eingabeaufforderung warten (bildschirmbasiert, kein Timer)t.type(text)/t.paste(text)/t.enter()/t.escape()/t.tab()– TUIs Taste für Taste steuernt.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" })– warten, bis der Bildschirm den Text anzeigt;t.expect(re)– überprüfent.sleep("800ms")– Dauern akzeptieren"500ms","1.5s", Zahlen (ms)t.hide(async () => { ... })– Einrichtung außerhalb des Sichtfelds ausführen (Zustand bleibt erhalten; Hintergrundjobs hier nicht beenden)t.print(markdown)/t.title(text)– Markdown-Bildunterschriften in das Video rendern, ohne zu tippent.zoom({ rows: [0, 5], cols: [0, 60], duration: "500ms" })– einen Bereich vergrößern;t.zoom(null)setzt zurückt.slide("In die Cloud bereitstellen", { eyebrow: "3", subtitle: "…", during })– Vollbild-Übergangskarte zwischen Feature-Demos: große zentrierte Überschrift in echter Typografie, ein- und ausgeblendet, zeichnet ein Kapitel mit demselben Namen auf;during: async () => …führtcd/clear/Server-Setup unsichtbar im Hintergrund aust.chapter("Installieren")– echte MP4-Kapitel-Metadaten und ein Schnittpunkt:--chapters Installieren/--split-chapterszur Renderzeitt.expect(/…/, { scope: "scrollback" })sieht Ausgaben, die aus dem Bild gelaufen sind;t.scrollback()gibt das gesamte Transkript zurück;-o demo.logschreibt es- Pfeile werden als SS3 gesendet, wenn das Programm den Cursor-Anwendungsmodus aktiviert hat,
t.paste()wird in Klammern gesetzt, wenn das Programm danach gefragt hat – Editoren verhalten sich wie bei einem echten Terminal;print("[text](url)")erstellt einen klickbaren Link in SVG/HTML title: "auto"folgt OSC-Titeln;tcut doctor demo.casterklärt, was eine Aufnahme verwendet hat und was tcut nicht anzeigen kann (Inline-Bilder)t.timelapse(async () => { await t.run("bun install") }, { speed: 8 })– alles darin wird 8× schneller abgespielt (maxPauseentfernt nur Stille; dies komprimiert die Ausgabe)t.snapshot("shot.png")/t.snapshot("hero.svg")– Standbild genau dieses Moments bei jedem Rendering (screenshot= Alias),t.clear(),t.resize(cols, rows),t.screen()/t.line()zum Lesen des Bildschirms- Browser-Bereich:
browser: { position: "right" | "overlay", width }in der Konfiguration, dannt.browser.goto(url),t.browser.waitFor(/text/),t.browser.click(sel),t.focus("browser" | "terminal")– zeichnet eine echte WebView neben/über dem Terminal auf (Dev-Server-Demos)
Konfigurationsgrundlagen
output (Array = mehrere Formate), preset: "readme" | "x" | "youtube" | "square", theme (~600 Ghostty-Themes, tcut themes), cols/rows oder width/height (px), fps, scale: 2 (gleiches Layout, 2× die Pixel – für Retina/HiDPI oder alles, was ein Spieler hochskaliert), typingSpeed/typingJitter, keys: true (Tastendruck-Overlay, ein Chip nach dem anderen; { limit, font, color, background, radius, position }), maxPause: "1.5s" (Leerlaufkomprimierung), requires: ["bun", "eza"] (vor der Aufnahme schnell fehlschlagen, wenn ein Tool fehlt – auch in tcut test), windowBar: "none" + margin: 0 + borderRadius: 0 für ein nacktes Terminal, title, marginFill ("transparent" = echte Alpha in png/webp/gif/webm/svg/html; mp4 fällt zurück), shadow: true (weicher Schlagschatten; margin standardmäßig 40), watermark: "© du" oder { image: "logo.png", position: "top-left", opacity, size }.
Schneiden und Zusammenfügen (keine Neuaufnahme)
Alles auf der sichtbaren Zeitleiste der Aufnahme, sodass jedes Format funktioniert und das Ergebnis immer noch eine .cast ist:
tcut render demo.cast --from 2s --to 10s -o clip.gif # Zeitfenster
tcut render demo.cast --chapters Zoom,Intro -o clip.mp4 # Kapitel, in dieser Reihenfolge
tcut render demo.cast --split-chapters -o demo.mp4 # demo-01-intro.mp4, demo-02-zoom.mp4 … (ideale Clip-Bibliothek für Remotion)
tcut cut demo.cast --from 2s --to 10s # schreibt demo-cut.cast
tcut concat intro.cast demo.cast --gap 500ms -o launch.mp4 # gleiche cols×rows erforderlich; Bildschirm wird an jeder Naht zurückgesetzt
Erneutes Rendern ohne erneute Ausführung
Aufnahme (.cast) und Rendering sind getrennt:
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 / Testen
tcut test demo.video.ts– führe das Skript als Test aus (schnell, keine Video-Kodierung)tcut diff a.cast b.cast [--images dir]– vergleiche den Bildschirmtext zweier Aufnahmen, beende mit 1 bei Abweichung
Fallstricke
- Hintergrund-Dev-Server in der PTY:
bun run dev </dev/null >/tmp/dev.log 2>&1 &(stdin-Leser erhalten SIGTTIN; verlorene Logs übermalen TUIs) - Warte auf den Bildschirm, nicht auf die Zeit: bevorzuge
t.run/t.wait(/Local:/, { scope: "screen" })gegenüber langen Wartezeiten t.hidebehält den Zustand bei – ein versteckteskillam Ende sickert in das letzte Bild ein- Für TUIs (nvim, lazygit, claude) starte mit
t.run(cmd, { wait: /etwas-auf-dem-bildschirm/ })und steuere dann mit Tasten
Vollständige Referenz: https://tcut.amanv.dev/llms.txt
tcut als Bibliothek verwenden
import { defineVideo, renderCast } from "termcut" (nur Bun). defineVideo(config, script) gibt ein Video zurück: await video.run({ force, log }) zeichnet auf und rendert und gibt { outputs, screenshots, durationSeconds, recording } zurück; video.record() und video.render(recording, { overrides, clip }) teilen die Schritte auf; renderCast(file, overrides) rendert jede Aufnahme ohne Shell neu. Ebenfalls exportiert: recordLive, cutRecording, concatRecordings, selectChapters, buildSvg, buildHtml, replayFrames, diffCasts, diagnoseCast, generateScript, runScriptTests, publishFiles. Übergib in CI explizites theme/font ("auto" liest das laufende Terminal) und rendere jeweils ein Video.