6ce61f1948e5

fix search plain rendering: route through color-gated output (no ANSI leak to non-tty pipes) and reject empty query instead of dumping the whole catalog

time
2026-06-04 23:06 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
search plain output
parent
9c82a195ab39
2 files src/output.zig+28 −0src/search.zig+12 −7
src/output.zig+28 −0
@@ -744,6 +744,34 @@744744     flush();745745 }746746 747+// --- Plain (non-tty) search rendering ---748+// Color-gated equivalents of the raw-ANSI TUI widget, for piped/scripted search.749+// Using the gated header + c() keeps ANSI out of non-tty output (NO_COLOR / pipes).750+751+pub fn searchPlainHeader(mode_str: []const u8, query: []const u8, count: u64) void {752+    p("{s}search {s} {s}  {s}{d} results{s}\n", .{ hdr(), mode_str, query, c(DIM), count, c(RESET) });753+}754+755+pub fn searchPlainNoResults() void {756+    p("   no results\n", .{});757+}758+759+pub fn searchPlainPackage(pname: []const u8, pkg_version: []const u8, description: []const u8, unfree: bool) void {760+    if (unfree) {761+        p("   {s}   {s}{s}{s}   {s}unfree{s}   {s}\n", .{ pname, c(DIM), pkg_version, c(RESET), c(YELLOW), c(RESET), description });762+    } else {763+        p("   {s}   {s}{s}{s}   {s}\n", .{ pname, c(DIM), pkg_version, c(RESET), description });764+    }765+}766+767+pub fn searchPlainOption(name: []const u8, type_str: []const u8) void {768+    p("   {s}   {s}{s}{s}\n", .{ name, c(DIM), type_str, c(RESET) });769+}770+771+pub fn searchEmptyQuery() void {772+    p("{s}search needs a query  {s}e.g. nina search firefox{s}\n", .{ hdr(), c(DIM), c(RESET) });773+}774+747775 pub fn setupEnabling() void {748776     p("{s}enabling experimental features\n", .{hdr()});749777 }

view file

src/search.zig+12 −7
@@ -154,24 +154,29 @@154154 // Plain, non-interactive rendering for non-tty stdin. Same pink :: header and155155 // result count the widget shows, then one line per result; no cursor control.156156 fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {157+    // An empty query matches the entire catalog (100k+ rows); refuse it cleanly.158+    if (std.mem.trim(u8, query, " \t").len == 0) {159+        output.searchEmptyQuery();160+        output.flush();161+        return null;162+    }157163     var arena = std.heap.ArenaAllocator.init(gpa);158164     defer arena.deinit();159165     var opts: []api.NixOption = &.{};160166     const packages = doSearch(arena.allocator(), io, query, mode, &opts);161167     const count = resultCount(packages, opts, mode);162-    const RST = output.C_RESET;163-    const PKN = output.C_PINK;164-    const DIM = output.C_DIM;165168     const mode_str = if (mode == .packages) "nixpkgs" else "options";166-    output.p("{s}:: {s}search {s} {s}  {s}{d} results{s}\n", .{ PKN, RST, mode_str, query, DIM, count, RST });169+    output.searchPlainHeader(mode_str, query, count);167170     if (count == 0) {168-        output.p("   no results\n", .{});171+        output.searchPlainNoResults();169172         output.flush();170173         return null;171174     }172175     switch (mode) {173-        .packages => for (packages) |pkg| output.p("   {s}   {s}{s}{s}   {s}\n", .{ pkg.pname, DIM, pkg.version, RST, pkg.description }),174-        .options => for (opts) |opt| output.p("   {s}   {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST }),176+        // unfree=false: per-result meta.unfree would need an eval per package (too177+        // slow for the list); the TUI detail pane is the place to surface it.178+        .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),179+        .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),175180     }176181     output.flush();177182     return null;

view file