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 1: Foundations & Shell Widgets

Theme — Taking the terminal hostage safely: raw mode, teardown, and the /dev/tty split.

Skip this if — you already have a TUI that exits cleanly on every path, including panics.

Takeaways

  • Copy this: write the restore sequence before the first feature, and call it from normal exit, every signal handler, and the panic handler — one function, four call sites.
  • Avoid this: rendering to stdout. The moment you want a shell widget, stdout has to be a data channel; render to /dev/tty from day one and you never pay for the split.
  • The check that caught it: launching from a real VTE terminal, not just foot. The alt-screen cursor warp below is invisible on the terminal we developed in.

The Genesis of the TUI Core

Every terminal program begins the same way: with a single escape code that takes the terminal hostage. Get raw mode right and nobody notices; get it wrong and the user’s shell is wrecked. Week 1 was entirely about earning that trust — before a single emoji was ever drawn.

During Week 1 (May 29 – June 4, 2026), the emojig repository was initialized. The core objective was to build a zero-dependency, ultra-lightweight, and lightning-fast emoji fuzzy search tool in Zig.

Week 1 by the Numbers

  • Total Commits: 171 — very nearly as many as the remaining five weeks combined (187). See Reading the Curve for why that is a property of exploration rather than of productivity.
  • Codebase Expansion: From 0 lines of code to 1,250 LoC in Zig, 180 LoC in Go (pack script), and 420 LoC in shell integrations.

Daily Chronology

May 29, 2026: The Initial Prototype (cd0029a)

The project began by writing the raw terminal interface and the fuzzy subsequence search engine.

  • Features Added: Fuzzy search matcher, embedded database loader, and raw termios attributes hook.
  • Lines of Code (LoC) Added:
    • src/main.zig: 204 LoC.
    • src/root.zig: 134 LoC.
    • tools/pack_emojis.py: 82 LoC.

Code Archaeology: Initial Termios & Rendering Loop

The very first commit established the raw mode, signal safety, and the double-buffered rendering surface. Here is how standard terminal attributes were captured and modified in src/main.zig (Line 29 onwards):

// Switch to alternate screen, enable mouse tracking, hide cursor
const stdout_fd = std.posix.STDOUT_FILENO;
try writeAll(stdout_fd, "\x1b[?1049h\x1b[?1000h\x1b[?1006h\x1b[?25l");

// Save and configure raw termios
const stdin_fd = std.posix.STDIN_FILENO;
const orig_termios = try std.posix.tcgetattr(stdin_fd);
var raw = orig_termios;

raw.iflag.IGNBRK = false;
raw.iflag.BRKINT = false;
raw.iflag.PARMRK = false;
raw.iflag.ISTRIP = false;
raw.iflag.INLCR = false;
raw.iflag.IGNCR = false;
raw.iflag.ICRNL = false;
raw.iflag.IXON = false;

raw.oflag.OPOST = false;

raw.lflag.ECHO = false;
raw.lflag.ECHONL = false;
raw.lflag.ICANON = false;
raw.lflag.ISIG = false;
raw.lflag.IEXTEN = false;

raw.cflag.CSIZE = .CS8;
raw.cflag.PARENB = false;

The orig_termios value captured here is the single most important variable in the program: it is the only copy of the user’s shell settings, and every exit path has to put it back.

Terminal Safety Overrides (5420d21, eab5e91)

In raw mode, if the application crashes or receives a termination signal, the terminal is left in an uncooked state (cursor hidden, mouse tracking active, characters not echoed). To prevent leaving the developer’s console broken, POSIX signal handlers and a custom panic function were written to intercept failures and restore cooked properties:

pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
    // Restore standard termios settings before crashing
    const stdin_fd = std.posix.STDIN_FILENO;
    const stdout_fd = std.posix.STDOUT_FILENO;
    std.posix.tcsetattr(stdin_fd, .FLUSH, g_orig_termios) catch {};
    writeAll(stdout_fd, "\x1b[?1000l\x1b[?1006l\x1b[?1049l\x1b[?25h") catch {};

    // Resume standard stack dumping
    std.builtin.default_panic(msg, error_return_trace, ret_addr);
}

Note the order of the restore string: mouse tracking off, then screen, then cursor. Also note the \x1b[?1049l sitting in the middle of it. That sequence is correct here — this early build entered the alternate screen — and it is the exact byte that would cost us a day once the picker learned to render inline. The story is below.


May 30, 2026: The SGR Mouse and Layout Grid

With the raw TUI functioning, mouse tracking and theme state persistence were added.

  • Features Added: SGR mouse coordinates query, hover selections, click confirmations, and theme storage mapping to ~/.config/emojig/config.
  • LoC Added:
    • src/main.zig expanded to 410 LoC (+206).
    • src/config.zig created: 64 LoC.

May 31, 2026: Splitting TTY & stdout (Shell Widgets)

The initial prototype copied the selected emoji to the clipboard using wl-copy. However, to build interactive shell integrations (e.g. hitting Ctrl+E at a shell prompt to search and insert the emoji directly at the cursor), the TUI could not print to stdout, as the shell needs to read the selected codepoint directly from the tool’s stdout pipe.

  • The Split Solution (c2aa663): We redirected all rendering writes explicitly to /dev/tty rather than stdout. This left stdout completely clean to emit only the selected emoji string.
  • LoC Added:
    • src/main.zig modifications (+42).
    • shell/emojig.zsh, shell/emojig.bash created: 110 LoC.
# How Zsh intercepts stdout of emojig
emojig_widget() {
    local selected
    selected=$(emojig --tui)
    LBUFFER="${LBUFFER}${selected}"
    zle reset-prompt
}

Figure 1: the payoff of the split. The picker paints to /dev/tty below the prompt; the chosen emoji arrives on stdout and lands inside the half-typed echo command above it.


June 1, 2026: Resize Epic & Alt-Screen Isolation

As the picker gained adoption as an inline TUI, users running narrow terminals reported visual bugs: resizing the screen would wrap contents, and scrolling would duplicate viewports.

  • The Fixes (d60c66b, f5ced19):
    1. CPR Queries: Wrote cursor positioning queries (\x1b[6n) to determine the TUI’s exact launch row.
    2. Relative Positioning: Changed all redraw offsets to use relative coordinates based on the launch row rather than absolute page rows, keeping the TUI anchored.
    3. Hiding Overflow: If the terminal width shrunk below 40 columns or 6 rows, the renderer collapsed layout details gracefully (hiding description and switcher rows) rather than warping characters.
  • LoC Added/Modified:
    • src/main.zig expanded to 730 LoC (+278).
    • src/resize.zig created: 112 LoC.

Dead End: The Cursor That Jumped Home

Going inline meant the picker no longer entered the alternate screen — but the teardown string written back in the first commit still ended with \x1b[?1049l. Leaving it in felt harmless. Nothing is harmless in a teardown.

  • What we saw: on exit, the shell prompt came back in the wrong place. Sometimes it overwrote the line above; sometimes it landed several rows up, on top of scrollback that had nothing to do with us. Nothing was corrupted — the terminal was cooked and usable — the cursor was simply somewhere else. And it only happened on some machines.
  • What we assumed: a row-arithmetic bug in the brand-new relative-positioning code. We had just replaced absolute page rows with offsets from the CPR-detected launch row, so an off-by-N in the erase loop was the obvious suspect. We spent the search there.
  • What it actually was: ?1049h/l is two operations welded together — switch the screen buffer, and save/restore the cursor. VTE-based terminals (GNOME Terminal, Tilix, Ptyxis) honour the cursor-restore half even when you never entered the alternate screen, warping the cursor to a stale saved position. foot and tmux ignore a ?1049l that has no matching ?1049h, which is why the bug was invisible in our own development terminal and reproduced instantly on someone else’s. The arithmetic had been right the whole time.

The fix was to stop maintaining one restore string and maintain two: a RESTORE for inline mode that never mentions ?1049, and a RESTORE_ALT for the alt-screen path that does. The rule it became is TUI Restoration §2: only emit ?1049l if you explicitly emitted ?1049h. The habit it became is more general — verify teardown on a VTE terminal, because the terminal you develop in is the one that forgives you.


June 2 – June 4, 2026: Cross-Terminal Detours

We verified the static binaries across terminal environments (kitty, alacritty, ghostty, tmux, and Linux virtual consoles via fbterm). We automated multi-platform builds using GoReleaser and prepared the v0.1.5 compiler tags.

  • LoC Added:
    • .goreleaser.yaml created: 120 LoC.
    • scripts/test_tui.go created: 135 LoC.

Week 1 verdict: a working picker with safe teardown, shell widgets, and releases — but every string, color, and key binding was hardcoded in Zig. That debt sets the agenda for Week 2.