Week 6: Key Merging & Refactoring
Theme — Making the codebase cheap to pick up cold: decomposition and comptime key dispatch.
Skip this if — nobody but you will ever open this repository again.
Takeaways
- Copy this: split by subsystem boundary, not by file size. Each extracted module here owns one screen or one input concern, so a reader can guess the filename before opening it.
- Avoid this: an
if-elsechain over raw byte strings for key dispatch. It grows silently, admits duplicate entries, and fails at runtime rather than at compile time.- The check that caught it: a comptime static map. Duplicate or malformed key bindings become build errors — the refactor made a class of bug unrepresentable rather than rarer.
Decomposing the Monolith and Enum-Based Safety
The last week of a project decides what the next year of the project costs. Features were done; what remained was the difference between a codebase that happened and a codebase that is designed. Week 6 was deliberately unglamorous: take the 4,800-line monolith that five weeks of momentum had produced, and leave behind something a future session — human or agent — can navigate cold.
During Week 6 (July 3 – July 5, 2026), the emojig project achieved its final phase of modularization and maintenance. The core objective was to clean up technical debt in src/main.zig (which had grown to over 4,800 lines) and solidify the keyboard event loop.
Week 6 by the Numbers
- Total Commits: 12 across three days — the floor of the curve, and the intended destination. See Reading the Curve.
- Refactor Volume: Moved 1,067 lines of code out of
main.ziginto dedicated helper modules, reducing the file by 22%.
Daily Chronology
July 3, 2026: The Deconstruction Epic
We broke down src/main.zig by moving independent subsystems into dedicated files.
- Debug Pane (
src/debug_pane.zig): Extracted all log rendering and runtime memory audit display statistics. - Settings Screen (
src/settings_ctl.zig): Moved row controllers and dropdown selections into a standalone component. - Category Switcher (
src/switcher.zig): Moved the visual tab rendering and hover offsets. - Key Event Merging (Issue 46): Merged
spec/keys.yamlintospec/input.yamlunder a unified schema, and transitioned keyevent parsing to type-safe enums insrc/input.zig.
Code Archaeology: Enum-Based Key Dispatch
Before this refactoring, keyboard input was processed by scanning raw string buffers and matching sequences dynamically in a massive if-else chain:
// ❌ Old String-Based Dispatch in main.zig
if (std.mem.eql(u8, read_buf[0..n], "\x1b[A")) {
moveSelectionUp();
} else if (std.mem.eql(u8, read_buf[0..n], "\x1b[B")) {
moveSelectionDown();
}
In Week 6, we replaced this fragile runtime scanning with a type-safe static map (std.StaticStringMap) returning an Action enum:
// ✓ New Type-Safe Enum Dispatch (src/input.zig)
const Action = enum {
move_up,
move_down,
select,
quit,
toggle_theme,
};
const KeyMap = std.StaticStringMap(Action).initComptime(.{
.{ "\x1b[A", .move_up },
.{ "\x1b[B", .move_down },
.{ "\r", .select },
.{ "\x1b", .quit },
.{ "\t", .toggle_theme },
});
The main loop in main.zig was simplified to a clean switch statement:
// ✓ Simplified key event loop in main.zig
const action = KeyMap.get(read_buf[0..n]) orelse .none;
switch (action) {
.move_up => try moveSelectionUp(state),
.move_down => try moveSelectionDown(state),
.select => try confirmSelection(state),
.quit => break,
.toggle_theme => try toggleTheme(state),
.none => {},
}
The switch is exhaustive over Action: adding a binding to the map without handling it is a
compile error, not a key that silently does nothing.
July 4 – July 5, 2026: Completing Self-Containment
We finalized the repository by backporting installation completion scripts and visual screenshots directly to website/, making the codebase completely self-contained.
- LoC Added:
website/install.sh: 110 LoC.- Updated
issues/README.mdto close all active reviews.
Six Weeks, In Hindsight
The chronicle ends where it started: one binary, under 900 KB, that pops up instantly and leaves the terminal exactly as it found it. Along the way the endeavour proved three things this book keeps insisting on:
- Verification carried every week. PTY reels, golden files, spec schema checks — each week’s risky change landed on a net built the week before.
- The specs outlived the code. Strings, layouts, keys, and colors moved into
spec/in Week 2 and survived every refactor since; the Zig around them was rewritten twice. - Deleting was the best feature. The
mojigoremoval (Week 4) and the monolith decomposition (Week 6) did more for maintainability than any addition.
And one thing the weeks show that no single week states: the three bugs that cost the most —
the ?1049l cursor warp, the split SGR read, the
pending-wrap erase — were all invisible in the environment we developed in. Each one
needed a different terminal, a faster input rate, or an exact window width to appear at all. The
verification protocol in the appendix is not a formality; it is the shape of those three bugs,
written down.
emojig is now maintenance-mode software in the best sense: any session can pick it up, run the checks, and safely change it. That is what “done” means in a fleet.
If you are here to build something rather than to read about one, Starting a New Zig TUI is the six weeks compressed into a checklist, and The Recipe Index is the map back into the chronology.