// api.zig — nix data access: search, eval, options // No HTTP. No curl. nix is on PATH by definition on NixOS. const std = @import("std"); const types = @import("types.zig"); const errors = @import("errors.zig"); const output = @import("output.zig"); // Pin the nixpkgs registry once so `nix search nixpkgs` resolves locally instead // of refetching github on every query. Gated by a ~/.nina-registry-pinned flag: // absent -> pin + announce + create the flag; present -> no-op, silent. Failure // to pin is non-fatal — search still runs (just slower), and the flag is left // absent so a later run retries. pub fn ensureRegistryPinned(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) void { const home = environ.get("HOME") orelse return; const flag_path = std.fmt.allocPrint(gpa, "{s}/.nina-registry-pinned", .{home}) catch return; defer gpa.free(flag_path); if (std.Io.Dir.openFile(.cwd(), io, flag_path, .{})) |f| { f.close(io); return; // already pinned } else |_| {} output.pinningRegistry(); const r = std.process.run(gpa, io, .{ .argv = &.{ "nix", "registry", "pin", "nixpkgs" }, }) catch return; defer gpa.free(r.stdout); defer gpa.free(r.stderr); switch (r.term) { .exited => |code| if (code != 0) return, else => return, } const file = std.Io.Dir.createFile(.cwd(), io, flag_path, .{}) catch return; file.close(io); } pub fn searchPackages(gpa: std.mem.Allocator, io: std.Io, query: []const u8) ![]types.NixPackage { const result = std.process.run(gpa, io, .{ .argv = &.{ "nix", "search", "nixpkgs", query, "--json", "--no-update-lock-file" }, }) catch { errors.error_info.setSuggestion("run nix-channel --update and try again", .{}); return error.SearchFailed; }; defer gpa.free(result.stderr); defer gpa.free(result.stdout); switch (result.term) { .exited => |code| if (code != 0) { errors.error_info.setSuggestion("run nix-channel --update and try again", .{}); return error.SearchFailed; }, else => return error.SearchFailed, } return parseSearchJson(gpa, result.stdout); } fn parseSearchJson(gpa: std.mem.Allocator, json_str: []const u8) ![]types.NixPackage { const parsed = std.json.parseFromSlice(std.json.Value, gpa, json_str, .{}) catch return error.SearchFailed; defer parsed.deinit(); if (parsed.value != .object) return error.SearchFailed; var packages: std.ArrayList(types.NixPackage) = .empty; var it = parsed.value.object.iterator(); while (it.next()) |entry| { const key = entry.key_ptr.*; const val = entry.value_ptr.*; if (val != .object) continue; // Key format: "legacyPackages.x86_64-linux.firefox" const last_dot = std.mem.lastIndexOfScalar(u8, key, '.') orelse continue; const attr = key[last_dot + 1 ..]; const pname = if (val.object.get("pname")) |v| if (v == .string) v.string else attr else attr; const version = if (val.object.get("version")) |v| if (v == .string) v.string else "" else ""; const description = if (val.object.get("description")) |v| if (v == .string) v.string else "" else ""; try packages.append(gpa, .{ .attr = try gpa.dupe(u8, attr), .pname = try gpa.dupe(u8, pname), .version = try gpa.dupe(u8, version), .description = try gpa.dupe(u8, description), }); } return packages.toOwnedSlice(gpa); } pub fn getLicense(gpa: std.mem.Allocator, io: std.Io, attr: []const u8) ?[]const u8 { const expr = std.fmt.allocPrint(gpa, "nixpkgs#{s}.meta.license.shortName", .{attr}) catch return null; defer gpa.free(expr); const result = std.process.run(gpa, io, .{ .argv = &.{ "nix", "eval", "--raw", expr }, }) catch return null; defer gpa.free(result.stderr); switch (result.term) { .exited => |code| if (code != 0) { gpa.free(result.stdout); return null; }, else => { gpa.free(result.stdout); return null; }, } const license = std.mem.trim(u8, result.stdout, " \t\n\r"); if (license.len == 0) { gpa.free(result.stdout); return null; } return license; } pub const PkgMeta = struct { homepage: []const u8 = "", license: []const u8 = "", }; // Evaluate a package's meta for the expanded search detail pane. One // `nix eval --json .meta` call; returns null if the package can't be evaluated // (broken, renamed, etc.). Returned strings are duped into `arena`. pub fn packageMeta(arena: std.mem.Allocator, io: std.Io, attr: []const u8) ?PkgMeta { const ref = std.fmt.allocPrint(arena, "nixpkgs#{s}.meta", .{attr}) catch return null; const result = std.process.run(arena, io, .{ .argv = &.{ "nix", "eval", "--json", ref }, }) catch return null; switch (result.term) { .exited => |code| if (code != 0) return null, else => return null, } const parsed = std.json.parseFromSlice(std.json.Value, arena, result.stdout, .{}) catch return null; defer parsed.deinit(); if (parsed.value != .object) return null; const o = parsed.value.object; return .{ .homepage = jsonFirstString(arena, o.get("homepage")), .license = licenseStr(arena, o.get("license")), }; } // homepage is usually a string but occasionally a list — take the first string. fn jsonFirstString(arena: std.mem.Allocator, v: ?std.json.Value) []const u8 { const val = v orelse return ""; switch (val) { .string => |s| return arena.dupe(u8, s) catch "", .array => |arr| { if (arr.items.len > 0 and arr.items[0] == .string) return arena.dupe(u8, arr.items[0].string) catch ""; return ""; }, else => return "", } } // license may be a single attrset, a list of attrsets, or a bare string. Join the // fullName (or shortName) fields into one readable line. Result is duped into arena. fn licenseStr(arena: std.mem.Allocator, v: ?std.json.Value) []const u8 { const val = v orelse return ""; switch (val) { .string => |s| return arena.dupe(u8, s) catch "", .object => |obj| return arena.dupe(u8, licenseName(obj)) catch "", .array => |arr| { var buf: std.ArrayList(u8) = .empty; for (arr.items) |item| { if (item != .object) continue; const name = licenseName(item.object); if (name.len == 0) continue; if (buf.items.len > 0) buf.appendSlice(arena, ", ") catch {}; buf.appendSlice(arena, name) catch {}; } return buf.toOwnedSlice(arena) catch ""; }, else => return "", } } // Returns a slice into the parsed JSON arena (caller dupes before that frees). fn licenseName(obj: std.json.ObjectMap) []const u8 { if (obj.get("fullName")) |n| { if (n == .string) return n.string; } if (obj.get("shortName")) |n| { if (n == .string) return n.string; } return ""; } pub fn searchOptions(gpa: std.mem.Allocator, io: std.Io, query: []const u8) ![]NixOption { const result = std.process.run(gpa, io, .{ .argv = &.{ "nix", "eval", "--json", "nixpkgs#lib.optionAttrSetToDocList", "nixpkgs#nixosModules" }, }) catch return error.SearchFailed; defer gpa.free(result.stderr); defer gpa.free(result.stdout); switch (result.term) { .exited => |code| if (code != 0) return error.SearchFailed, else => return error.SearchFailed, } return filterOptions(gpa, result.stdout, query); } pub const NixOption = struct { name: []const u8, type_str: []const u8, default: ?[]const u8, description: []const u8, }; fn filterOptions(gpa: std.mem.Allocator, json_str: []const u8, query: []const u8) ![]NixOption { const parsed = std.json.parseFromSlice(std.json.Value, gpa, json_str, .{}) catch return error.SearchFailed; defer parsed.deinit(); if (parsed.value != .array) return error.SearchFailed; var opts: std.ArrayList(NixOption) = .empty; for (parsed.value.array.items) |item| { if (item != .object) continue; const name = if (item.object.get("name")) |v| if (v == .string) v.string else continue else continue; if (query.len > 0 and std.mem.indexOf(u8, name, query) == null) continue; const type_str = if (item.object.get("type")) |v| if (v == .string) v.string else "any" else "any"; const description = if (item.object.get("description")) |v| if (v == .string) v.string else "" else ""; const default_val: ?[]const u8 = if (item.object.get("default")) |v| switch (v) { .string => |s| s, else => null, } else null; try opts.append(gpa, .{ .name = try gpa.dupe(u8, name), .type_str = try gpa.dupe(u8, type_str), .default = if (default_val) |d| try gpa.dupe(u8, d) else null, .description = try gpa.dupe(u8, description), }); } return opts.toOwnedSlice(gpa); } pub fn findSystemPackagesLine(io: std.Io, config_path: []const u8) ?u32 { var buf: [65536]u8 = undefined; const content = std.Io.Dir.readFile(.cwd(), io, config_path, &buf) catch return null; var lines = std.mem.splitScalar(u8, content, '\n'); var line_num: u32 = 1; while (lines.next()) |line| : (line_num += 1) { if (std.mem.indexOf(u8, line, "systemPackages") != null) return line_num; } return null; }