tcut — sessions terminal en vidéo
tcut enregistre un véritable shell (PTY + terminal Ghostty sans tête) et rend des vidéos déterministes. Les scripts sont en TypeScript simple — pas de DSL. Même enregistrement, mêmes pixels, à chaque fois.
Configuration
bun add -g termcut # Bun >= 1.4, installe la commande `tcut`
MP4/GIF/WebM nécessitent ffmpeg dans le PATH. Les lecteurs SVG et HTML ne nécessitent rien d'autre. Tout prend en charge --json pour des résultats lisibles par machine ; jamais de demandes interactives.
Deux façons d'enregistrer
Scripté (recommandé pour les agents) — écrivez demo.video.ts, exécutez 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"); // tape, appuie sur Entrée, attend le retour de l'invite
await t.expect(/1\.\d+/); // vérifie sur l'écran rendu (échoue la vidéo si absent)
await t.sleep("1.5s");
},
);
En direct — tcut rec -o demo.gif ouvre un shell, enregistre jusqu'à exit, et écrit à la fois l'enregistrement exact (demo.cast) et un script modifiable (demo.video.ts) de ce qui a été tapé. tcut rec -o demo.mp4 -- npm create vite enregistre une seule commande.
L'API t (dans defineVideo)
t.run(cmd, { wait? })— tape + Entrée + attend l'invite du shell (basé sur l'écran, pas sur une minuterie)t.type(text)/t.paste(text)/t.enter()/t.escape()/t.tab()— pilote les TUI touche par touchet.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" })— attend que l'écran l'affiche ;t.expect(re)— vérifiet.sleep("800ms")— les durées acceptent"500ms","1.5s", des nombres (ms)t.hide(async () => { ... })— exécute la configuration hors caméra (l'état persiste ; ne tuez pas les tâches en arrière-plan ici)t.print(markdown)/t.title(text)— affiche des légendes Markdown dans la vidéo sans tapert.zoom({ rows: [0, 5], cols: [0, 60], duration: "500ms" })— agrandit une région ;t.zoom(null)réinitialiset.slide("Déployer dans le cloud", { eyebrow: "3", subtitle: "…", during })— carte de transition plein écran entre les démos de fonctionnalités : grand titre centré en typographie réelle, fondu entrée/sortie, enregistre un chapitre du même nom ;during: async () => …exécutecd/clear/configuration du serveur de manière invisible derrièret.chapter("Installer")— vraies métadonnées de chapitre MP4, et un point de coupure :--chapters Installer/--split-chaptersau moment du rendut.expect(/…/, { scope: "scrollback" })voit la sortie qui a défilé ;t.scrollback()renvoie la transcription complète ;-o demo.logl'écrit- Les flèches sont envoyées en SS3 lorsque le programme a activé le mode curseur d'application,
t.paste()est entre crochets lorsque le programme l'a demandé — les éditeurs se comportent comme avec un vrai terminal ;print("[text](url)")crée un lien cliquable en SVG/HTML title: "auto"suit les titres OSC ;tcut doctor demo.castexplique ce qu'un enregistrement a utilisé et ce que tcut ne peut pas afficher (images intégrées)t.timelapse(async () => { await t.run("bun install") }, { speed: 8 })— tout ce qui se trouve à l'intérieur joue 8× plus vite (maxPausesupprime uniquement le silence ; cela compresse la sortie)t.snapshot("shot.png")/t.snapshot("hero.svg")— image fixe de ce moment exact à chaque rendu (screenshot= alias),t.clear(),t.resize(cols, rows),t.screen()/t.line()pour lire l'écran- Panneau navigateur :
browser: { position: "right" | "overlay", width }dans la configuration, puist.browser.goto(url),t.browser.waitFor(/text/),t.browser.click(sel),t.focus("browser" | "terminal")— enregistre une vraie WebView à côté/sur le terminal (démos de serveur de développement)
Éléments essentiels de configuration
output (tableau = plusieurs formats), preset: "readme" | "x" | "youtube" | "square", theme (~600 thèmes Ghostty, tcut themes), cols/rows ou width/height (px), fps, scale: 2 (même mise en page, 2× les pixels — utilisez pour Retina/HiDPI ou tout ce qu'un lecteur agrandira), typingSpeed/typingJitter, keys: true (superposition des touches, une puce à la fois ; { limit, font, color, background, radius, position }), maxPause: "1.5s" (compression de l'inactivité), requires: ["bun", "eza"] (échoue rapidement avant l'enregistrement si un outil manque — également dans tcut test), windowBar: "none" + margin: 0 + borderRadius: 0 pour un terminal nu, title, marginFill ("transparent" = alpha réel en png/webp/gif/webm/svg/html ; mp4 revient à la normale), shadow: true (ombre portée douce ; la marge par défaut est de 40), watermark: "© vous" ou { image: "logo.png", position: "top-left", opacity, size }.
Couper et joindre (sans ré-enregistrement)
Tout cela sur la timeline visible du cast, donc chaque format fonctionne et le résultat est toujours un .cast :
tcut render demo.cast --from 2s --to 10s -o clip.gif # fenêtre temporelle
tcut render demo.cast --chapters Zoom,Intro -o clip.mp4 # chapitres, dans cet ordre
tcut render demo.cast --split-chapters -o demo.mp4 # demo-01-intro.mp4, demo-02-zoom.mp4 … (bibliothèque de clips idéale pour Remotion)
tcut cut demo.cast --from 2s --to 10s # écrit demo-cut.cast
tcut concat intro.cast demo.cast --gap 500ms -o launch.mp4 # mêmes cols×rows requis ; l'écran se réinitialise à chaque jonction
Rendre à nouveau sans ré-exécuter
L'enregistrement (.cast) et le rendu sont séparés :
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 / tests
tcut test demo.video.ts— exécute le script comme un test (rapide, pas d'encodage vidéo)tcut diff a.cast b.cast [--images dir]— compare le texte à l'écran de deux enregistrements, quitte avec le code 1 en cas de dérive
Pièges
- Serveurs de développement en arrière-plan dans le PTY :
bun run dev </dev/null >/tmp/dev.log 2>&1 &(les lecteurs de stdin reçoivent SIGTTIN ; les logs parasites repeignent sur les TUI) - Attendez l'écran, pas le temps : préférez
t.run/t.wait(/Local:/, { scope: "screen" })aux longs sleep t.hideconserve l'état — unkillcaché à la fin fuit dans la dernière image- Pour les TUI (nvim, lazygit, claude), lancez avec
t.run(cmd, { wait: /quelque-chose-sur-écran/ }), puis pilotez avec les touches
Référence complète : https://tcut.amanv.dev/llms.txt
Utiliser tcut comme une bibliothèque
import { defineVideo, renderCast } from "termcut" (Bun uniquement). defineVideo(config, script) renvoie un Video : await video.run({ force, log }) enregistre et rend et renvoie { outputs, screenshots, durationSeconds, recording } ; video.record() et video.render(recording, { overrides, clip }) séparent les étapes ; renderCast(file, overrides) re-rend un cast sans shell. Également exportés : recordLive, cutRecording, concatRecordings, selectChapters, buildSvg, buildHtml, replayFrames, diffCasts, diagnoseCast, generateScript, runScriptTests, publishFiles. En CI, transmettez explicitement theme/font ("auto" lit le terminal en cours d'exécution) et effectuez le rendu d'une vidéo à la fois.