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()) 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() bool {151 _ = std.posix.tcgetattr(STDIN_FD) catch return false;152 return true;153}154155156157fn 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;179}180181fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {182 const count = resultCount(packages, opts, mode);183 const CLR = output.C_CLEAR_EOL;184 const RST = output.C_RESET;185 const PKN = output.C_PINK;186 const BLD = output.C_BOLD;187 const DIM = output.C_DIM;188189 190 output.raw(CLR);191 const mode_str = if (mode == .packages) "nixpkgs" else "options";192 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{193 PKN, RST, mode_str, query, DIM, count, RST,194 });195196 197 output.raw(CLR ++ "\n");198199 200 var i: usize = 0;201 while (i < RESULT_ROWS) : (i += 1) {202 output.raw(CLR);203 if (i < count) {204 const selected = (i == selection);205 switch (mode) {206 .packages => {207 const pkg = packages[i];208 const marker = if (selected) PKN ++ "> " ++ RST else " ";209 const name_pre = if (selected) PKN ++ BLD else "";210 const name_suf = if (selected) RST else "";211 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, DIM, pkg.version, RST, pkg.description });212 },213 .options => {214 const opt = opts[i];215 const marker = if (selected) PKN ++ "> " ++ RST else " ";216 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });217 },218 }219 } else {220 output.raw("\n");221 }222 }223224 225 output.raw(CLR ++ "\n");226227 228 if (count > 0 and mode == .packages) {229 const pkg = packages[selection];230 output.raw(CLR);231 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, DIM, pkg.version, RST });232 output.raw(CLR);233 output.p(" {s}\n", .{pkg.description});234 output.raw(CLR ++ "\n");235 output.raw(CLR);236 output.p(" {s}attr {s}{s}\n", .{ DIM, pkg.attr, RST });237 } else if (count > 0 and mode == .options) {238 const opt = opts[selection];239 output.raw(CLR);240 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });241 output.raw(CLR);242 output.p(" {s}\n", .{opt.description});243 output.raw(CLR ++ "\n");244 output.raw(CLR ++ "\n");245 } else {246 output.raw(CLR ++ "\n");247 output.raw(CLR ++ "\n");248 output.raw(CLR ++ "\n");249 output.raw(CLR ++ "\n");250 }251252 253 output.raw(CLR ++ "\n");254255 256 output.raw(CLR);257 if (mode == .packages) {258 output.p(" {s}[i] profile [s] system [t] try [c] copy [up/down] nav [esc] exit{s}\n", .{ DIM, RST });259 } else {260 output.p(" {s}[i] insert [up/down] nav [esc] exit{s}\n", .{ DIM, RST });261 }262 263 var pad: usize = 14;264 while (pad < WIDGET_LINES) : (pad += 1) {265 output.raw(CLR ++ "\n");266 }267}268269fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {270 _ = gpa;271 272 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };273 for (tools) |tool| {274 var child = std.process.spawn(io, .{275 .argv = &.{tool},276 .stdin = .pipe,277 .stdout = .ignore,278 .stderr = .ignore,279 }) catch continue;280 if (child.stdin) |stdin| {281 stdin.writeStreamingAll(io, text) catch {};282 stdin.close(io);283 }284 _ = child.wait(io) catch {};285 return;286 }287}288289fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {290 var tio = try std.posix.tcgetattr(fd);291 saved.* = tio;292 if (comptime builtin.os.tag == .linux) {293 tio.lflag.ECHO = false;294 tio.lflag.ICANON = false;295 tio.lflag.ISIG = false;296 tio.lflag.IEXTEN = false;297 tio.iflag.ICRNL = false;298 tio.iflag.IXON = false;299 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;300 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;301 }302 303 try std.posix.tcsetattr(fd, .FLUSH, tio);304}305306fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {307 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};308}