Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Week 3: Virtualization & PNG Art Compilers

Theme — Scale and craft: 2,249 entries through a 12-cell viewport, and PNGs compiled into block characters.

Skip this if — your list fits on screen and you draw no pictures. Nothing else here generalises.

Takeaways

  • Copy this: virtualize by computing the scroll top from the selection, not by tracking it as mutable state. One pure function, trivially testable, no drift between scroll and cursor.
  • Avoid this: hand-authoring terminal art. Compile it from a source image so the art is editable in an image editor and the terminal encoding is a build step.
  • The check that caught it: golden files rendered through cati. Block art is exactly the kind of output that looks fine to the author and wrong to everyone else’s font.

Handling Scales: Emojis and Animations

A picker that knows fifty emojis is a toy; a picker that knows all of them is a rendering problem. Week 3 was the collision with scale — 2,249 entries against a terminal that repaints one character at a time — and, because engineers cannot be trusted with free time, it was also the week the About screen learned to wink.

During Week 3 (June 12 – June 18, 2026), emojig transitioned from rendering a small subset of icons to handling the entire Unicode emoji catalog (2,249 entries). Simultaneously, we wanted to build a visually engaging “About” screen featuring custom-compiled terminal block-art animations (a winking face and falling emojis).

Week 3 by the Numbers

  • Total Commits: 53.
  • Compilation Speedup: Achieved a 12x build speedup in local dev cycles by disabling LLVM compilation paths (-fno-llvm) and utilizing Zig’s self-hosted x86_64 backend.
  • Codebase Expansion: Added 480 LoC of viewport math and 320 LoC of PNG compiler logic.

Daily Chronology

June 13, 2026: Scrollable Virtualization

Drawing 2,249 emojis in a terminal window is a performance hazard. Redrawing the entire list on every keystroke causes major rendering lag.

  • The Virtualized Viewport: We implemented a virtualized scrollable grid. Only the emojis currently visible in the 4x3 viewport are rendered. The remaining elements are virtually offset.
  • Scroll Bounds Math: Wrote scroll offsets that clamp the viewport boundaries dynamically.

Code Archaeology: Viewport Clamping

Here is the viewport offset calculation implemented in src/tui_draw.zig (Line 142 onwards):

pub fn calculateViewport(total_items: usize, columns: usize, visible_rows: usize, selected_idx: usize, current_scroll_top: usize) usize {
    const selected_row = selected_idx / columns;

    // If selection moves past the bottom of the viewport, scroll down
    if (selected_row >= current_scroll_top + visible_rows) {
        return selected_row - visible_rows + 1;
    }

    // If selection moves above the top of the viewport, scroll up
    if (selected_row < current_scroll_top) {
        return selected_row;
    }

    return current_scroll_top;
}

The function is deliberately pure: it takes the previous scroll top and returns the next one, holding no state of its own. Scroll position can therefore never disagree with the selection, and the whole scrolling behaviour is testable without a terminal.

Figure 1: the unfiltered catalog. The counter in the status row reports the size of the match set; the grid above it is drawing twelve cells.

Figure 2: the same viewport after typing cat — the match set collapses from four figures to 167, and the number of cells actually painted does not change. That invariance is the point of virtualization.


June 15, 2026: The PNG Art Compiler (gen_about_art)

To render the visual about-page animations, we rejected manual ASCII frame editing as it was too slow and error-prone. We wrote a custom Go script (scripts/gen_about_art.go) that compiles standard .png images directly into terminal-compatible block character string resources embedded in spec/about_art.json.

  • How it Works: The compiler loads each frame’s pixels, maps RGB colors to the nearest xterm-256 color indexes, and outputs Unicode halfblocks (▀) with matching foreground and background escapes.
  • The Warn-Once Cache: If an image contained colors that did not map cleanly to our 256-color schema, the compiler raised warnings. To prevent flooding stdout with 2,000 duplicate warnings (one per pixel), we implemented a warning cache to notify the developer exactly once per color.
// 1. Compile pixel color mapping
for y := 0; y < img.Bounds().Dy(); y += 2 {
    for x := 0; x < img.Bounds().Dx(); x++ {
        topColor := img.At(x, y)
        botColor := img.At(x, y+1)
        
        topIdx := matchColor(topColor) // Map to xterm-256
        botIdx := matchColor(botColor)
        
        // Output Unicode Halfblock
        buffer.WriteString(fmt.Sprintf("\x1b[38;5;%dm\x1b[48;5;%dm▀\x1b[0m", topIdx, botIdx))
    }
    buffer.WriteString("\n")
}

June 18, 2026: sheet.pxo & Animation Pipelines

We integrated the compiled frames directly into the Zig binary using compile-time embeds. We compressed animation sheets into .pxo files (ZIP archives of PNG frames) and configured git attributes (.gitattributes) to treat .pxo binaries as LFS pointer dependencies, ensuring repository commits remained fast.

  • LoC Added/Modified:
    • scripts/build_art.go created: 280 LoC.
    • src/about.zig animation parser created: 195 LoC.
  • Refactor Result: Reduced the compiled binary size of animation frames by 64% by migrating from text frames to compressed binary matrices.

Week 3 verdict: the full catalog scrolled smoothly and the About screen winked back — but the codebase had quietly grown two implementations (Zig + the Go port) and its first layers of debt. Week 4 is the reckoning: release, then delete.