Week 4: Search Aliases & Componentization
Theme — Ship v0.2.0, then delete the second implementation of the same program.
Skip this if — you have one implementation and no release process to compare against.
Takeaways
- Copy this: when two codebases implement the same behaviour, delete one before the next feature, not after. The cost of the clone is paid on every change until it is gone.
- Avoid this: keeping an experiment alive because it might be useful.
mojigoanswered its question in Week 2 and cost us duplicate work for two weeks after.- The check that caught it: a read buffer big enough to hold a whole event, and a parser that carries the remainder. Everything else here was a UX preference; that one was a bug.
The v0.2.0 Release and Cleaning Technical Debt
The most consequential commit of Week 4 did not add a feature — it deleted 2,450 lines. Keeping the experimental Go port alive alongside the Zig picker meant every search-scoring tweak had to be made twice, and the two silently drifted. This week is the case study behind the playbook’s single-language rule: ship the release, then kill the clone.
During Week 4 (June 19 – June 25, 2026), the emojig project achieved its major v0.2.0 release. The code was restructured to improve UX responsiveness (fast scrolling, prompt editing) and address codebase maintainability by removing experimental structures and language duplication.
Week 4 by the Numbers
- Total Commits: 49.
- Technical Debt Elimination: Deleted the duplicate Go clone
mojigo, removing 2,450 lines of duplicate code in a single commit, eliminating code divergence risk.
Daily Chronology
June 19, 2026: The v0.2.0 Release
We finalized release v0.2.0, fixing coordinate alignments on category screens and adding test suites for Go and Zig input readers.
- LoC Modified:
build.zig.zonversion bump.src/main.zigcategory highlight corrections (+34).
June 20, 2026: Vim Muscle Commands & Color Schemas
TUI users frequently have deep Vim muscle memory. Forcing a developer to exit using a specific desktop key binding (like standard Ctrl+C) when they are used to typing :q can cause user frustration.
- Interception Logic: We implemented pre-emptive command interception at the entry of the keyboard input loop. If the input buffer matches
:q,:quit, or/quit, the app exits immediately with return code 0 before passing characters to the search engine. - Color System Overhaul (Issue #21): Unified xterm-256 color indexes under a declarative
spec/colors.jsonschema.
Code Archaeology: Pre-emptive Command Interception
Here is how Vim exit commands were intercepted in src/main.zig (Line 412 onwards):
// Check input buffer before fuzzy search processing
if (std.mem.eql(u8, query_buf[0..query_len], ":q") or
std.mem.eql(u8, query_buf[0..query_len], ":quit") or
std.mem.eql(u8, query_buf[0..query_len], "/quit")) {
// Clean exit path
try writeAll(stdout_fd, "\x1b[?1000l\x1b[?1006l\x1b[?1049l\x1b[?25h");
std.process.exit(0);
}
June 21, 2026: Modularization & Scrolling Performance
We decomposed the monolithic src/main.zig and src/root.zig into smaller files to make code navigation easier.
- Fast Render Scrolling: During fast scrollbar drags, foot’s glyph rasterizer (FreeType/HarfBuzz) would lag (taking up to 150ms per screenful of new emojis). We implemented a
fast_renderflag. If the TTY has pending inputs, the grid is rendered as blank background spaces, allowing foot to draw instantly. Once the queue drains, the full emojis are rendered.
Dead End: The Search Box That Typed 12M
Dragging the scrollbar hard enough to expose the rasterizer lag also exposed something far stranger, and the two symptoms arrived in the same session.
- What we saw: during fast drags, garbage appeared in the search field. Not random
garbage — short, structured fragments like
12Mand;24;— and the result grid would then filter itself down to nothing. Releasing the mouse and clearing the query recovered cleanly. It never happened when scrolling slowly. - What we assumed: a hover-handler bug. The obvious theory was that the drag path was reaching the same code that appends a typed character, so we went looking for a mis-routed branch in the mouse handler. There wasn’t one.
- What it actually was: the read, not the handler. With any-motion tracking on, the
terminal emits SGR mouse reports (
\x1b[<Cb;Cx;CyM) at up to 125 Hz. Our read buffer was smaller than a burst of them, so aread()would return with an event cut in half — ending at, say,\x1b[<32;32;. The remaining bytes,12M, arrived on the next read with no leading escape byte in front of them. The parser did exactly what it was told: no escape, therefore text, therefore type it into the search box. The characters in the query were the coordinates of the mouse.
The fix has three parts and all three are necessary: a read buffer of at least 512 bytes, a loop that drains every complete event out of one read instead of just the first, and a carry buffer holding any trailing fragment to be prepended to the next read. Half an escape sequence is not an error condition — it is the normal case at 125 Hz, and a parser that cannot rejoin the halves will type your mouse position into your application. This is TUI Restoration §4.
June 22, 2026: Prompt Editing Keybindings
To make search input feel like a native shell prompt, we implemented advanced keybinding shortcuts, enabling forward Delete and Ctrl+Left/Right word-by-word cursor jumps.
Code Archaeology: Word-by-Word Cursor Navigation
Here is the index jumping logic implemented in src/input.zig (Line 84 onwards) to calculate Ctrl+Arrow jumps:
pub fn findNextWordBoundary(text: []const u8, current_pos: usize, direction: enum { left, right }) usize {
var pos = current_pos;
if (direction == .left) {
if (pos == 0) return 0;
pos -= 1;
while (pos > 0 and text[pos] == ' ') : (pos -= 1) {}
while (pos > 0 and text[pos] != ' ') : (pos -= 1) {}
return if (pos == 0 and text[0] != ' ') 0 else pos + 1;
} else {
while (pos < text.len and text[pos] != ' ') : (pos += 1) {}
while (pos < text.len and text[pos] == ' ') : (pos += 1) {}
return pos;
}
}
June 23 – June 24, 2026: Category Switcher Overhaul
We overhauled TUI categories, adding custom margins, categories background configurations (categories_bg), and compact grid layouts to fit small display areas.
- LoC Added/Modified:
src/switcher.zigcreated: 140 LoC.spec/theme.jsoncategories styling fields (+22).
Week 4 verdict: one language, one codebase, one shipped release — and a picker that finally felt native, from :q to word-jump editing. Two frontiers remained: Wayland window decoration and raw search speed. Week 5 takes on both.