TamboUI already has a complete recording pipeline: .tape → InteractionPlayer → RecordingBackend captures List<TimedFrame> → AsciinemaAnimation writes .cast files. The .cast format requires external tools (agg) to convert to GIF. Users want an out-of-the-box way to produce animated GIFs from recordings — no extra tooling.
The building blocks already exist:
TimedFrameholdsBuffer+timestampMssnapshotsBufferhas full cell grid withCell.symbol()+Cell.style()(fg, bg, bold, italic, etc.)Color.toRgb()converts any color variant to RGB valuesSvgExporterdefines the cell rendering geometry (charHeight=20, fontAspectRatio=0.61, lineHeight=24.4)ExportPropertieshas default fg/bg colorsImageDataalready usesBufferedImage+Graphics2D(in resize) andImageIO(PNG export)- Java's
ImageIOincludes a GIF writer with animation support viaIIOMetadata
Add a GifAnimationExporter that converts List<TimedFrame> to an animated GIF using pure JDK APIs — no external dependencies.
A utility that renders a Buffer to a java.awt.image.BufferedImage using Graphics2D:
public final class BufferRenderer {
static BufferedImage render(Buffer buffer, Rect region, BufferRenderOptions options);
}Rendering logic (mirrors SvgExporter):
- Calculate image dimensions:
width = cols * cellWidth,height = rows * lineHeight - Create
BufferedImage(width, height, TYPE_INT_RGB) - Fill with default background color
- Set
Graphics2Dfont to a monospace font at the configured size - For each cell in the region:
- Skip
Cell.CONTINUATIONcells (wide char placeholders) - If cell has background color (or
REVERSEDmodifier): fill rect with bg color - Set fg color, apply bold (derive font), apply italic (derive font)
- Draw
cell.symbol()at the correct pixel position
- Skip
- Return the image
Font handling:
- Default to
"Monospaced"(JDK built-in, guaranteed available) - Use
FontMetricsto measure actual glyph width and center within cell - Allow user to specify a preferred font name (e.g., "Fira Code", "JetBrains Mono")
Cell geometry (reuse SvgExporter constants):
charHeight = 20→ font size ~16pt (need FontMetrics calibration)cellWidth = charHeight * fontAspectRatio(default 0.61 → ~12.2px)lineHeight = charHeight * 1.22(→ ~24.4px)- These can be configurable via options
public final class GifAnimationExporter {
static void write(List<TimedFrame> frames, Path output, GifOptions options) throws IOException;
static byte[] write(List<TimedFrame> frames, GifOptions options);
}Logic:
- Deduplicate consecutive identical frames (reuse
AsciinemaAnimation.deduplicateFrames()pattern) - For each unique frame:
- Render
frame.buffer()→BufferedImageusingBufferRenderer - Calculate delay from timestamp difference (in centiseconds for GIF)
- Render
- Write animated GIF using JDK's
ImageIO:- Get
ImageWriterfor "gif" format - Set looping via GIF application extension metadata (
NETSCAPE2.0for infinite loop) - Write each frame with
IIOMetadatacontaining frame delay and disposal method
- Get
- Flush and close
GifOptions:
public final class GifOptions {
String fontName = "Monospaced";
int fontSize = 16; // point size
double fontAspectRatio = 0.61;
boolean chrome = false; // macOS-style window chrome
String title = null; // window title (if chrome)
boolean loop = true; // infinite loop
int maxWidth = 0; // scale down if > 0
StylePropertyResolver styles = null; // custom fg/bg defaults
}The standard approach using javax.imageio:
ImageWriter writer = ImageIO.getImageWritersByFormatName("gif").next();
ImageOutputStream ios = ImageIO.createImageOutputStream(output);
writer.setOutput(ios);
writer.prepareWriteSequence(null);
for (each frame) {
BufferedImage img = renderFrame(frame);
IIOMetadata metadata = writer.getDefaultImageMetadata(
new ImageTypeSpecifier(img), writer.getDefaultWriteParam());
// Set frame delay and disposal
configureGifMetadata(metadata, delayCs, disposalMethod);
// Set looping (on first frame only)
if (firstFrame) {
configureLooping(metadata, 0); // 0 = infinite loop
}
writer.writeToSequence(new IIOImage(img, null, metadata), null);
}
writer.endWriteSequence();The GIF metadata manipulation uses IIOMetadataNode to set:
"delayTime"— frame delay in centiseconds (1/100th second)"disposalMethod"— typically"restoreToBackgroundColor""NETSCAPE2.0"application extension — for looping
The current flow ends at .cast. The new flow adds a parallel output:
RecordingBackend collects List<TimedFrame>
├── AsciinemaAnimation.toCast() → .cast file (existing)
└── GifAnimationExporter.write() → .gif file (new)
RecordingBackend.writeCastFromDrawFrames() already has the List<TimedFrame>. After writing .cast, it can optionally also write .gif based on config.
Alternatively, expose the List<TimedFrame> from RecordingBackend so callers can choose the output format.
For single-frame GIF/PNG export (screenshots), add to the existing fluent API:
export(buffer).gif().options(o -> o.fontName("Fira Code")).toFile(path);
export(buffer).png().options(o -> o.fontSize(14)).toFile(path);This reuses BufferRenderer for single-frame rasterization.
export/raster/BufferRenderer.java— Buffer → BufferedImage renderingexport/raster/BufferRenderOptions.java— font, cell size, colors configexport/gif/GifAnimationExporter.java— List → animated GIFexport/gif/GifOptions.java— animation options (loop, chrome, font)export/gif/GifFormat.java— Format implementation for single-frame GIFexport/gif/GifExporter.java— Encoder implementation for single-frameexport/png/PngFormat.java— Format implementation for single-frame PNGexport/png/PngExporter.java— Encoder for PNG
export/Formats.java— addGIFandPNGformat constantsexport/ExportRequest.java— addgif()andpng()shorthand methodsexport/ExportStep.java— possibly needs binary output support (toBytes()already exists, buttoFile()uses Writer — need OutputStream path for binary formats)internal/record/RecordingBackend.java— optionally write GIF alongside .cast
-
Pure JDK — no external dependencies. Java's
ImageIOGIF writer is basic but sufficient (256 color palette per frame, which matches terminal color space well). -
BufferRenderer as shared utility — used by both animated GIF and single-frame PNG/GIF export. Could later be used for SVG-to-raster conversion too.
-
Color quantization — GIF is limited to 256 colors per frame. Terminal UIs typically use far fewer, so this should rarely be an issue. For complex screens, use a simple median-cut quantizer or rely on ImageIO's built-in dithering.
-
Binary vs text export — current ExportStep uses
Writer/Appendablewhich is text-oriented. For binary formats (GIF, PNG), we need anOutputStreampath. Either extend ExportStep or create a separateBinaryExportStep.
- Write a unit test that creates a Buffer with styled cells and renders to GIF
- Write a demo that records a TUI session and outputs both
.castand.gif - Verify the GIF plays correctly in a browser/image viewer
- Verify font rendering quality with different monospace fonts
- Test with Unicode characters and wide characters (CJK, emoji)