1191127df903

fix debounce crash: clamp selection in search widget — navigating during the 250ms debounce window grew selection against the old result set; when the debounced search returned fewer results, renderWidget indexed packages[selection] out of bounds (segfault in ReleaseFast). Now reset selection=0 when a debounced search lands, and clamp selection to count-1 in renderWidget as defense — build+tests green

time
2026-06-05 02:53 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
nina finishing brief
parent
7abf027a795e
1 file src/search.zig+12 −4
src/search.zig+12 −4
@@ -80,6 +80,10 @@8080                 query_dirty = false;8181                 _ = arena.reset(.free_all);8282                 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);83+                // New result set: reset selection to the top. Navigating during the84+                // debounce window could otherwise leave selection past the new end,85+                // and renderWidget would index out of bounds.86+                selection = 0;8387                 output.cursorUp(WIDGET_LINES);8488                 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);8589                 output.flush();@@ -234,6 +238,10 @@234238     const DIM = output.C_DIM;235239     const CYN = output.C_CYAN;236240 241+    // Clamp to the current result count: a caller could hand us a selection from a242+    // larger, now-replaced result set. Never index past the end.243+    const sel = if (count == 0) 0 else @min(selection, count - 1);244+237245     // Line 1: header238246     output.raw(CLR);239247     const mode_str = if (mode == .packages) "nixpkgs" else "options";@@ -245,13 +253,13 @@245253     output.raw(CLR ++ "\n");246254 247255     // Lines 3-7: results (up to RESULT_ROWS), scrolled so selection stays visible248-    const scroll: usize = if (selection >= RESULT_ROWS) selection - RESULT_ROWS + 1 else 0;256+    const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;249257     var i: usize = 0;250258     while (i < RESULT_ROWS) : (i += 1) {251259         const idx = scroll + i;252260         output.raw(CLR);253261         if (idx < count) {254-            const selected = (idx == selection);262+            const selected = (idx == sel);255263             switch (mode) {256264                 .packages => {257265                     const pkg = packages[idx];@@ -276,7 +284,7 @@276284 277285     // Lines 9-12: selected package detail278286     if (count > 0 and mode == .packages) {279-        const pkg = packages[selection];287+        const pkg = packages[sel];280288         output.raw(CLR);281289         output.p("   {s}{s}{s}{s}  {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });282290         output.raw(CLR);@@ -285,7 +293,7 @@285293         output.raw(CLR);286294         output.p("   {s}attr{s}  {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });287295     } else if (count > 0 and mode == .options) {288-        const opt = opts[selection];296+        const opt = opts[sel];289297         output.raw(CLR);290298         output.p("   {s}  {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });291299         output.raw(CLR);

view file