<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ethan Moore</title>
    <description>The latest articles on DEV Community by Ethan Moore (@ethanmoorenotes).</description>
    <link>https://dev.to/ethanmoorenotes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4208923%2F26c7b2ad-e5e0-4dcf-a193-de60c6c6bc02.png</url>
      <title>DEV Community: Ethan Moore</title>
      <link>https://dev.to/ethanmoorenotes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethanmoorenotes"/>
    <language>en</language>
    <item>
      <title>Generating Lightweight Terminal GIFs with asciinema, agg, and gifsicle</title>
      <dc:creator>Ethan Moore</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:17:48 +0000</pubDate>
      <link>https://dev.to/ethanmoorenotes/generating-lightweight-terminal-gifs-with-asciinema-agg-and-gifsicle-1c7l</link>
      <guid>https://dev.to/ethanmoorenotes/generating-lightweight-terminal-gifs-with-asciinema-agg-and-gifsicle-1c7l</guid>
      <description>&lt;p&gt;When building small web tools and command-line utilities, I frequently need to demonstrate workflows directly inside project README files and documentation sites. Recording the screen with a generic GUI capture tool often yields bulky files: a 10-second terminal clip can easily balloon to several megabytes, stutter on mobile connections, and introduce blurry text compression artifacts.&lt;/p&gt;

&lt;p&gt;To keep documentation snappy, I rely on a pipeline of standard open-source utilities: &lt;code&gt;asciinema&lt;/code&gt; to record timing data, &lt;code&gt;agg&lt;/code&gt; (asciinema gif generator) to render crisp frames, &lt;code&gt;ffmpeg&lt;/code&gt; for palette calculations when fine-tuning, and &lt;code&gt;gifsicle&lt;/code&gt; for lossy payload reduction. This workflow keeps terminal recordings sharp and, for short clips, often gets them down to a few hundred kilobytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Record Text Streams with asciinema
&lt;/h2&gt;

&lt;p&gt;Instead of capturing screen pixels, &lt;code&gt;asciinema&lt;/code&gt; records the raw terminal output along with its timing. A pause costs only a timestamp, not a stream of identical frames.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;asciinema&lt;/code&gt; via your package manager and start recording to a local &lt;code&gt;.cast&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asciinema rec demo.cast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your commands naturally. When you finish, press &lt;code&gt;Ctrl+D&lt;/code&gt; or type &lt;code&gt;exit&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Before moving to the next step, you can edit &lt;code&gt;demo.cast&lt;/code&gt; with any text editor. It is newline-delimited JSON: a header line, then one event per line. You can trim stray events by hand, but for long hesitations it is easier to let tools cap them: &lt;code&gt;asciinema rec --idle-time-limit 2&lt;/code&gt; while recording, or agg's &lt;code&gt;--idle-time-limit&lt;/code&gt; when rendering (agg defaults to 5 seconds).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Render Crisp Terminal Frames with agg
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;agg&lt;/code&gt; utility converts &lt;code&gt;.cast&lt;/code&gt; timing data into an animated GIF. Unlike a screen recorder, it renders the text itself from a font onto a fixed character grid, using a terminal color theme, so the glyphs come out clean.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;agg&lt;/code&gt; with Cargo or grab a prebuilt binary from its GitHub releases page, then generate a baseline GIF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agg &lt;span class="nt"&gt;--fps-cap&lt;/span&gt; 12 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--font-size&lt;/span&gt; 14 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--theme&lt;/span&gt; monokai &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--speed&lt;/span&gt; 1.2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--rows&lt;/span&gt; 18 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cols&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
    demo.cast raw.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few flags make an enormous difference here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--fps-cap 12&lt;/code&gt;: agg's default cap is 30 fps. For a text terminal, around 12 fps still looks smooth and produces fewer frames.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--speed 1.2&lt;/code&gt;: Speeding up execution slightly keeps visitors engaged without making keystrokes unreadable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--rows&lt;/code&gt; and &lt;code&gt;--cols&lt;/code&gt;: Override the recorded terminal size, which keeps the output dimensions predictable. Record at the same size, or long lines will wrap differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Palette Control with ffmpeg
&lt;/h2&gt;

&lt;p&gt;A GIF frame can use at most 256 colors. If your terminal uses smooth syntax highlighting or gradient progress bars, default conversion algorithms will produce noisy color banding. &lt;/p&gt;

&lt;p&gt;When I need tighter control over color mapping than &lt;code&gt;agg&lt;/code&gt; provides out of the box, I render a lossless intermediate file or pass the frames through &lt;code&gt;ffmpeg&lt;/code&gt; using a two-pass palette generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; raw.gif &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=12,palettegen=max_colors=64:reserve_transparent=0"&lt;/span&gt; palette.png

ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; raw.gif &lt;span class="nt"&gt;-i&lt;/span&gt; palette.png &lt;span class="nt"&gt;-lavfi&lt;/span&gt; &lt;span class="s2"&gt;"fps=12 [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=3"&lt;/span&gt; palette_mapped.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By capping &lt;code&gt;max_colors&lt;/code&gt; to 64 (or 128 for complex themes) and applying an ordered Bayer dither, the file usually shrinks. Terminal backgrounds stay flat, which gives the subsequent LZW compression algorithm repetitive pixel runs to compress efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Final Shrinkage with gifsicle
&lt;/h2&gt;

&lt;p&gt;The raw output from rendering engines often retains duplicate pixels and unoptimized frame metadata. &lt;code&gt;gifsicle&lt;/code&gt; is the standard tool for stripping overhead, dropping redundant frame data, and applying lossy quantization.&lt;/p&gt;

&lt;p&gt;Run this compression pass on your rendered GIF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gifsicle &lt;span class="nt"&gt;-O3&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--lossy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--colors&lt;/span&gt; 64 &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--no-extensions&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         raw.gif &lt;span class="nt"&gt;-o&lt;/span&gt; demo_optimized.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what these arguments do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-O3&lt;/code&gt;: The highest optimization level. Like the lower levels it stores only the changed part of each frame, but it tries several methods and keeps the smallest (slower to run).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--lossy=80&lt;/code&gt;: Allows small visual noise in exchange for massive file savings. gifsicle's docs suggest values around 30 to 200; on dark terminal backgrounds 60 to 80 is usually hard to notice. Compare before and after on your own clip.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--colors 64&lt;/code&gt;: Reduces the palette to 64 colors. A smaller palette means fewer bits per pixel and longer repeated runs, so LZW compresses better.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-extensions&lt;/code&gt;: Drops non-essential extension blocks such as comments and app metadata. gifsicle still keeps the loop setting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Baseline Reference Numbers
&lt;/h2&gt;

&lt;p&gt;Across my open-source projects, I aim for these constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target size: Under 800 KB for a 15-second clip.&lt;/li&gt;
&lt;li&gt;Frame rate: 12 fps (never exceed 15 fps for CLI tooling).&lt;/li&gt;
&lt;li&gt;Color count: 32 to 64 colors for standard mono-like shells; 128 colors maximum for rich terminal UIs.&lt;/li&gt;
&lt;li&gt;Dimensions: 80 columns by 16 to 24 rows, agg font size 14.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Text-based capture plus strict frame optimization gives you crisp documentation demos that load fast.&lt;/p&gt;

</description>
      <category>terminal</category>
      <category>cli</category>
      <category>gif</category>
      <category>documentation</category>
    </item>
  </channel>
</rss>
