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 var saved: std.posix.termios = undefined;20 try rawModeEnable(STDIN_FD, &saved);21 defer rawModeDisable(STDIN_FD, saved);2223 var arena = std.heap.ArenaAllocator.init(gpa);24 defer arena.deinit();2526 var query_buf: [256]u8 = undefined;27 var query_len: usize = @min(initial_query.len, query_buf.len - 1);28 @memcpy(query_buf[0..query_len], initial_query[0..query_len]);2930 var packages: []types.NixPackage = &.{};31 var opts: []api.NixOption = &.{};32 var selection: usize = 0;3334 35 _ = arena.reset(.free_all);36 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);3738 39 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);40 output.flush();4142 43 var chosen: ?types.SearchResult = null;44 loop: while (true) {45 var buf: [4]u8 = undefined;46 const n = std.posix.read(STDIN_FD, &buf) catch break;47 if (n == 0) break;4849 var needs_search = false;5051 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {52 switch (buf[2]) {53 'A' => if (selection > 0) { selection -= 1; }, 54 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 55 else => {},56 }57 } else switch (buf[0]) {58 'k' => if (selection > 0) { selection -= 1; },59 'j' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; },60 0x1b, 'q' => break :loop,61 'i', '\r' => {62 if (mode == .packages and packages.len > selection) {63 chosen = .{ .package = packages[selection], .action = .profile };64 }65 break :loop;66 },67 's' => {68 if (mode == .packages and packages.len > selection) {69 chosen = .{ .package = packages[selection], .action = .system };70 }71 break :loop;72 },73 't' => {74 if (mode == .packages and packages.len > selection) {75 chosen = .{ .package = packages[selection], .action = .profile };76 77 }78 break :loop;79 },80 'c' => {81 82 if (mode == .packages and packages.len > selection) {83 copyToClipboard(gpa, io, packages[selection].attr);84 }85 },86 127 => { 87 if (query_len > 0) {88 query_len -= 1;89 needs_search = true;90 selection = 0;91 }92 },93 else => |ch| {94 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {95 query_buf[query_len] = ch;96 query_len += 1;97 needs_search = true;98 selection = 0;99 }100 },101 }102103 if (needs_search) {104 _ = arena.reset(.free_all);105 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);106 }107108 109 output.cursorUp(WIDGET_LINES);110 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);111 output.flush();112 }113114 115 output.cursorUp(WIDGET_LINES);116 var i: usize = 0;117 while (i < WIDGET_LINES) : (i += 1) {118 output.raw(output.C_CLEAR_EOL ++ "\n");119 }120 output.cursorUp(WIDGET_LINES);121 output.flush();122123 return chosen;124}125126pub const Mode = enum { packages, options };127128fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption) []types.NixPackage {129 switch (mode) {130 .packages => return api.searchPackages(arena, io, query) catch &.{},131 .options => {132 opts.* = api.searchOptions(arena, io, query) catch &.{};133 return &.{};134 },135 }136}137138fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {139 return switch (mode) {140 .packages => packages.len,141 .options => opts.len,142 };143}144145fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {146 const count = resultCount(packages, opts, mode);147 const CLR = output.C_CLEAR_EOL;148 const RST = output.C_RESET;149 const PKN = output.C_PINK;150 const BLD = output.C_BOLD;151 const DIM = output.C_DIM;152153 154 output.raw(CLR);155 const mode_str = if (mode == .packages) "nixpkgs" else "options";156 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{157 PKN, RST, mode_str, query, DIM, count, RST,158 });159160 161 output.raw(CLR ++ "\n");162163 164 var i: usize = 0;165 while (i < RESULT_ROWS) : (i += 1) {166 output.raw(CLR);167 if (i < count) {168 const selected = (i == selection);169 switch (mode) {170 .packages => {171 const pkg = packages[i];172 const marker = if (selected) PKN ++ "> " ++ RST else " ";173 const name_pre = if (selected) PKN ++ BLD else "";174 const name_suf = if (selected) RST else "";175 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, DIM, pkg.version, RST, pkg.description });176 },177 .options => {178 const opt = opts[i];179 const marker = if (selected) PKN ++ "> " ++ RST else " ";180 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });181 },182 }183 } else {184 output.raw("\n");185 }186 }187188 189 output.raw(CLR ++ "\n");190191 192 if (count > 0 and mode == .packages) {193 const pkg = packages[selection];194 output.raw(CLR);195 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, DIM, pkg.version, RST });196 output.raw(CLR);197 output.p(" {s}\n", .{pkg.description});198 output.raw(CLR ++ "\n");199 output.raw(CLR);200 output.p(" {s}attr {s}{s}\n", .{ DIM, pkg.attr, RST });201 } else if (count > 0 and mode == .options) {202 const opt = opts[selection];203 output.raw(CLR);204 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });205 output.raw(CLR);206 output.p(" {s}\n", .{opt.description});207 output.raw(CLR ++ "\n");208 output.raw(CLR ++ "\n");209 } else {210 output.raw(CLR ++ "\n");211 output.raw(CLR ++ "\n");212 output.raw(CLR ++ "\n");213 output.raw(CLR ++ "\n");214 }215216 217 output.raw(CLR ++ "\n");218219 220 output.raw(CLR);221 if (mode == .packages) {222 output.p(" {s}[i] profile [s] system [t] try [c] copy [up/down] nav [esc] exit{s}\n", .{ DIM, RST });223 } else {224 output.p(" {s}[i] insert [up/down] nav [esc] exit{s}\n", .{ DIM, RST });225 }226 227 var pad: usize = 14;228 while (pad < WIDGET_LINES) : (pad += 1) {229 output.raw(CLR ++ "\n");230 }231}232233fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {234 _ = gpa;235 236 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };237 for (tools) |tool| {238 var child = std.process.spawn(io, .{239 .argv = &.{tool},240 .stdin = .pipe,241 .stdout = .ignore,242 .stderr = .ignore,243 }) catch continue;244 if (child.stdin) |stdin| {245 stdin.writeStreamingAll(io, text) catch {};246 stdin.close(io);247 }248 _ = child.wait(io) catch {};249 return;250 }251}252253fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {254 var tio = try std.posix.tcgetattr(fd);255 saved.* = tio;256 if (comptime builtin.os.tag == .linux) {257 tio.lflag.ECHO = false;258 tio.lflag.ICANON = false;259 tio.lflag.ISIG = false;260 tio.lflag.IEXTEN = false;261 tio.iflag.ICRNL = false;262 tio.iflag.IXON = false;263 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;264 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;265 }266 267 try std.posix.tcsetattr(fd, .FLUSH, tio);268}269270fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {271 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};272}