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
3 files build.zig.zon+1 −1src/search.zig+36 −0test/vm/tests/search.sh+3 −3
build.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 allows

view file

src/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 {

view file

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"

view file