7f3c0e88de47
add non-tty plain fallback to nina search (scriptable headless mode); v3.0.3; search test uses </dev/null
- time
- 2026-05-29 20:13 UTC · 4 months ago
- machine
- koh2
- parent
- cc2e989efbf8
↓ nina-7f3c0e88.tar.gzatlas at this save
files changed3 files
build.zig.zon+1 −1src/search.zig+36 −0test/vm/tests/search.sh+3 −3build.zig.zon+1 −1
@@ -9,7 +9,7 @@99 .name = .nina,1010 // This is a [Semantic Version](https://semver.org/).1111 // In a future version of Zig it will be used for package deduplication.12- .version = "3.0.2",12+ .version = "3.0.3",1313 // Together with name, this represents a globally unique package1414 // identifier. This field is generated by the Zig toolchain when the1515 // package is first created, and then *never changes*. This allowssrc/search.zig+36 −0
@@ -16,6 +16,11 @@1616 initial_query: []const u8,1717 mode: Mode,1818 ) !?types.SearchResult {19+ // Non-interactive fallback: when stdin isn't a terminal (piped, scripted,20+ // CI), print plain results and return instead of the raw-mode widget, which21+ // needs a real tty and would block waiting for keys.22+ if (!stdinIsTty()) return runPlain(gpa, io, initial_query, mode);23+1924 var saved: std.posix.termios = undefined;2025 try rawModeEnable(STDIN_FD, &saved);2126 defer rawModeDisable(STDIN_FD, saved);@@ -140,6 +145,37 @@140145 .packages => packages.len,141146 .options => opts.len,142147 };148+}149+150+fn stdinIsTty() bool {151+ _ = std.posix.tcgetattr(STDIN_FD) catch return false;152+ return true;153+}154+155+// Plain, non-interactive rendering for non-tty stdin. Same pink :: header and156+// result count the widget shows, then one line per result; no cursor control.157+fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {158+ var arena = std.heap.ArenaAllocator.init(gpa);159+ defer arena.deinit();160+ var opts: []api.NixOption = &.{};161+ const packages = doSearch(arena.allocator(), io, query, mode, &opts);162+ const count = resultCount(packages, opts, mode);163+ const RST = output.C_RESET;164+ const PKN = output.C_PINK;165+ const DIM = output.C_DIM;166+ const mode_str = if (mode == .packages) "nixpkgs" else "options";167+ output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{ PKN, RST, mode_str, query, DIM, count, RST });168+ if (count == 0) {169+ output.p(" no results\n", .{});170+ output.flush();171+ return null;172+ }173+ switch (mode) {174+ .packages => for (packages) |pkg| output.p(" {s} {s}{s}{s} {s}\n", .{ pkg.pname, DIM, pkg.version, RST, pkg.description }),175+ .options => for (opts) |opt| output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST }),176+ }177+ output.flush();178+ return null;143179 }144180 145181 fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {test/vm/tests/search.sh+3 −3
@@ -9,12 +9,12 @@99 free -m | head -21010 1111 echo "### T-040 nina search firefox (expect firefox + version + description)"12-$N search firefox; echo "SEARCH_FF_RC=$?"12+$N search firefox </dev/null; echo "SEARCH_FF_RC=$?"1313 1414 echo "### T-041 nina search ripgrep"15-$N search ripgrep; echo "SEARCH_RG_RC=$?"15+$N search ripgrep </dev/null; echo "SEARCH_RG_RC=$?"1616 1717 echo "### T-055 nina search nonexistent (expect clean no-results, no crash)"18-$N search xyzzy-nope-abc123; echo "SEARCH_NONE_RC=$?"18+$N search xyzzy-nope-abc123 </dev/null; echo "SEARCH_NONE_RC=$?"1919 2020 echo "KILN_SEARCH_END"