123const std = @import("std");4const builtin = @import("builtin");5const types = @import("types.zig");6const output = @import("output.zig");7const api = @import("api.zig");89const WIDGET_LINES: usize = 18;10const RESULT_ROWS: usize = 5;11const STDIN_FD = std.posix.STDIN_FILENO;1213pub fn run(14 gpa: std.mem.Allocator,15 io: std.Io,16 initial_query: []const u8,17 mode: Mode,18) !?types.SearchResult {19 20 21 22 if (!stdinIsTty(io)) return runPlain(gpa, io, initial_query, mode);2324 var saved: std.posix.termios = undefined;25 try rawModeEnable(STDIN_FD, &saved);26 defer rawModeDisable(STDIN_FD, saved);2728 var arena = std.heap.ArenaAllocator.init(gpa);29 defer arena.deinit();3031 var query_buf: [256]u8 = undefined;32 var query_len: usize = @min(initial_query.len, query_buf.len - 1);33 @memcpy(query_buf[0..query_len], initial_query[0..query_len]);3435 var packages: []types.NixPackage = &.{};36 var opts: []api.NixOption = &.{};37 var selection: usize = 0;3839 40 _ = arena.reset(.free_all);41 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);4243 44 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);45 output.flush();4647 48 var chosen: ?types.SearchResult = null;49 loop: while (true) {50 var buf: [4]u8 = undefined;51 const n = std.posix.read(STDIN_FD, &buf) catch break;52 if (n == 0) break;5354 var needs_search = false;5556 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {57 switch (buf[2]) {58 'A' => if (selection > 0) { selection -= 1; }, 59 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 60 else => {},61 }62 } else switch (buf[0]) {63 'k' => if (selection > 0) { selection -= 1; },64 'j' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; },65 0x1b, 'q' => break :loop,66 'i', '\r' => {67 if (mode == .packages and packages.len > selection) {68 chosen = .{ .package = packages[selection], .action = .profile };69 }70 break :loop;71 },72 's' => {73 if (mode == .packages and packages.len > selection) {74 chosen = .{ .package = packages[selection], .action = .system };75 }76 break :loop;77 },78 't' => {79 if (mode == .packages and packages.len > selection) {80 chosen = .{ .package = packages[selection], .action = .profile };81 82 }83 break :loop;84 },85 'c' => {86 87 if (mode == .packages and packages.len > selection) {88 copyToClipboard(gpa, io, packages[selection].attr);89 }90 },91 127 => { 92 if (query_len > 0) {93 query_len -= 1;94 needs_search = true;95 selection = 0;96 }97 },98 else => |ch| {99 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {100 query_buf[query_len] = ch;101 query_len += 1;102 needs_search = true;103 selection = 0;104 }105 },106 }107108 if (needs_search) {109 _ = arena.reset(.free_all);110 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);111 }112113 114 output.cursorUp(WIDGET_LINES);115 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);116 output.flush();117 }118119 120 output.cursorUp(WIDGET_LINES);121 var i: usize = 0;122 while (i < WIDGET_LINES) : (i += 1) {123 output.raw(output.C_CLEAR_EOL ++ "\n");124 }125 output.cursorUp(WIDGET_LINES);126 output.flush();127128 return chosen;129}130131pub const Mode = enum { packages, options };132133fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption) []types.NixPackage {134 switch (mode) {135 .packages => return api.searchPackages(arena, io, query) catch &.{},136 .options => {137 opts.* = api.searchOptions(arena, io, query) catch &.{};138 return &.{};139 },140 }141}142143fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {144 return switch (mode) {145 .packages => packages.len,146 .options => opts.len,147 };148}149150fn stdinIsTty(io: std.Io) bool {151 return std.Io.File.stdin().isTty(io) catch false;152}153154155156fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {157 var arena = std.heap.ArenaAllocator.init(gpa);158 defer arena.deinit();159 var opts: []api.NixOption = &.{};160 const packages = doSearch(arena.allocator(), io, query, mode, &opts);161 const count = resultCount(packages, opts, mode);162 const RST = output.C_RESET;163 const PKN = output.C_PINK;164 const DIM = output.C_DIM;165 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 });167 if (count == 0) {168 output.p(" no results\n", .{});169 output.flush();170 return null;171 }172 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 }),175 }176 output.flush();177 return null;178}179180fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {181 const count = resultCount(packages, opts, mode);182 const CLR = output.C_CLEAR_EOL;183 const RST = output.C_RESET;184 const PKN = output.C_PINK;185 const BLD = output.C_BOLD;186 const DIM = output.C_DIM;187188 189 output.raw(CLR);190 const mode_str = if (mode == .packages) "nixpkgs" else "options";191 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{192 PKN, RST, mode_str, query, DIM, count, RST,193 });194195 196 output.raw(CLR ++ "\n");197198 199 const scroll: usize = if (selection >= RESULT_ROWS) selection - RESULT_ROWS + 1 else 0;200 var i: usize = 0;201 while (i < RESULT_ROWS) : (i += 1) {202 const idx = scroll + i;203 output.raw(CLR);204 if (idx < count) {205 const selected = (idx == selection);206 switch (mode) {207 .packages => {208 const pkg = packages[idx];209 const marker = if (selected) PKN ++ "> " ++ RST else " ";210 const name_pre = if (selected) PKN ++ BLD else "";211 const name_suf = if (selected) RST else "";212 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, DIM, pkg.version, RST, pkg.description });213 },214 .options => {215 const opt = opts[idx];216 const marker = if (selected) PKN ++ "> " ++ RST else " ";217 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });218 },219 }220 } else {221 output.raw("\n");222 }223 }224225 226 output.raw(CLR ++ "\n");227228 229 if (count > 0 and mode == .packages) {230 const pkg = packages[selection];231 output.raw(CLR);232 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, DIM, pkg.version, RST });233 output.raw(CLR);234 output.p(" {s}\n", .{pkg.description});235 output.raw(CLR ++ "\n");236 output.raw(CLR);237 output.p(" {s}attr {s}{s}\n", .{ DIM, pkg.attr, RST });238 } else if (count > 0 and mode == .options) {239 const opt = opts[selection];240 output.raw(CLR);241 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });242 output.raw(CLR);243 output.p(" {s}\n", .{opt.description});244 output.raw(CLR ++ "\n");245 output.raw(CLR ++ "\n");246 } else {247 output.raw(CLR ++ "\n");248 output.raw(CLR ++ "\n");249 output.raw(CLR ++ "\n");250 output.raw(CLR ++ "\n");251 }252253 254 output.raw(CLR ++ "\n");255256 257 output.raw(CLR);258 if (mode == .packages) {259 output.p(" {s}[i] profile [s] system [t] try [c] copy [up/down] nav [esc] exit{s}\n", .{ DIM, RST });260 } else {261 output.p(" {s}[i] insert [up/down] nav [esc] exit{s}\n", .{ DIM, RST });262 }263 264 var pad: usize = 14;265 while (pad < WIDGET_LINES) : (pad += 1) {266 output.raw(CLR ++ "\n");267 }268}269270fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {271 _ = gpa;272 273 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };274 for (tools) |tool| {275 var child = std.process.spawn(io, .{276 .argv = &.{tool},277 .stdin = .pipe,278 .stdout = .ignore,279 .stderr = .ignore,280 }) catch continue;281 if (child.stdin) |stdin| {282 stdin.writeStreamingAll(io, text) catch {};283 stdin.close(io);284 }285 _ = child.wait(io) catch {};286 return;287 }288}289290fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {291 var tio = try std.posix.tcgetattr(fd);292 saved.* = tio;293 if (comptime builtin.os.tag == .linux) {294 tio.lflag.ECHO = false;295 tio.lflag.ICANON = false;296 tio.lflag.ISIG = false;297 tio.lflag.IEXTEN = false;298 tio.iflag.ICRNL = false;299 tio.iflag.IXON = false;300 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;301 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;302 }303 304 try std.posix.tcsetattr(fd, .FLUSH, tio);305}306307fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {308 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};309}