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
↓ nina-6ce61f19.tar.gzatlas at this save
files changed2 files
src/output.zig+28 −0src/search.zig+12 −7src/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 }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;