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 158 if (std.mem.trim(u8, query, " \t").len == 0) {159 output.searchEmptyQuery();160 output.flush();161 return null;162 }163 var arena = std.heap.ArenaAllocator.init(gpa);164 defer arena.deinit();165 var opts: []api.NixOption = &.{};166 const packages = doSearch(arena.allocator(), io, query, mode, &opts);167 const count = resultCount(packages, opts, mode);168 const mode_str = if (mode == .packages) "nixpkgs" else "options";169 output.searchPlainHeader(mode_str, query, count);170 if (count == 0) {171 output.searchPlainNoResults();172 output.flush();173 return null;174 }175 switch (mode) {176 177 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),180 }181 output.flush();182 return null;183}184185fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {186 const count = resultCount(packages, opts, mode);187 const CLR = output.C_CLEAR_EOL;188 const RST = output.C_RESET;189 const PKN = output.C_PINK;190 const BLD = output.C_BOLD;191 const DIM = output.C_DIM;192193 194 output.raw(CLR);195 const mode_str = if (mode == .packages) "nixpkgs" else "options";196 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{197 PKN, RST, mode_str, query, DIM, count, RST,198 });199200 201 output.raw(CLR ++ "\n");202203 204 const scroll: usize = if (selection >= RESULT_ROWS) selection - RESULT_ROWS + 1 else 0;205 var i: usize = 0;206 while (i < RESULT_ROWS) : (i += 1) {207 const idx = scroll + i;208 output.raw(CLR);209 if (idx < count) {210 const selected = (idx == selection);211 switch (mode) {212 .packages => {213 const pkg = packages[idx];214 const marker = if (selected) PKN ++ "> " ++ RST else " ";215 const name_pre = if (selected) PKN ++ BLD else "";216 const name_suf = if (selected) RST else "";217 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, DIM, pkg.version, RST, pkg.description });218 },219 .options => {220 const opt = opts[idx];221 const marker = if (selected) PKN ++ "> " ++ RST else " ";222 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });223 },224 }225 } else {226 output.raw("\n");227 }228 }229230 231 output.raw(CLR ++ "\n");232233 234 if (count > 0 and mode == .packages) {235 const pkg = packages[selection];236 output.raw(CLR);237 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, DIM, pkg.version, RST });238 output.raw(CLR);239 output.p(" {s}\n", .{pkg.description});240 output.raw(CLR ++ "\n");241 output.raw(CLR);242 output.p(" {s}attr {s}{s}\n", .{ DIM, pkg.attr, RST });243 } else if (count > 0 and mode == .options) {244 const opt = opts[selection];245 output.raw(CLR);246 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });247 output.raw(CLR);248 output.p(" {s}\n", .{opt.description});249 output.raw(CLR ++ "\n");250 output.raw(CLR ++ "\n");251 } else {252 output.raw(CLR ++ "\n");253 output.raw(CLR ++ "\n");254 output.raw(CLR ++ "\n");255 output.raw(CLR ++ "\n");256 }257258 259 output.raw(CLR ++ "\n");260261 262 output.raw(CLR);263 if (mode == .packages) {264 output.p(" {s}[i] profile [s] system [t] try [c] copy [up/down] nav [esc] exit{s}\n", .{ DIM, RST });265 } else {266 output.p(" {s}[i] insert [up/down] nav [esc] exit{s}\n", .{ DIM, RST });267 }268 269 var pad: usize = 14;270 while (pad < WIDGET_LINES) : (pad += 1) {271 output.raw(CLR ++ "\n");272 }273}274275fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {276 _ = gpa;277 278 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };279 for (tools) |tool| {280 var child = std.process.spawn(io, .{281 .argv = &.{tool},282 .stdin = .pipe,283 .stdout = .ignore,284 .stderr = .ignore,285 }) catch continue;286 if (child.stdin) |stdin| {287 stdin.writeStreamingAll(io, text) catch {};288 stdin.close(io);289 }290 _ = child.wait(io) catch {};291 return;292 }293}294295fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {296 var tio = try std.posix.tcgetattr(fd);297 saved.* = tio;298 if (comptime builtin.os.tag == .linux) {299 tio.lflag.ECHO = false;300 tio.lflag.ICANON = false;301 tio.lflag.ISIG = false;302 tio.lflag.IEXTEN = false;303 tio.iflag.ICRNL = false;304 tio.iflag.IXON = false;305 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;306 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;307 }308 309 try std.posix.tcsetattr(fd, .FLUSH, tio);310}311312fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {313 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};314}