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;12131415const DEBOUNCE_MS: i64 = 250;16171819fn elapsedMs(io: std.Io, since: std.Io.Timestamp) i64 {20 const ns = @max(0, since.untilNow(io, .awake).toNanoseconds());21 return @intCast(@divFloor(ns, 1_000_000));22}2324pub fn run(25 gpa: std.mem.Allocator,26 io: std.Io,27 initial_query: []const u8,28 mode: Mode,29) !?types.SearchResult {30 31 32 33 if (!stdinIsTty(io)) return runPlain(gpa, io, initial_query, mode);3435 var saved: std.posix.termios = undefined;36 try rawModeEnable(STDIN_FD, &saved);37 defer rawModeDisable(STDIN_FD, saved);3839 var arena = std.heap.ArenaAllocator.init(gpa);40 defer arena.deinit();4142 var query_buf: [256]u8 = undefined;43 var query_len: usize = @min(initial_query.len, query_buf.len - 1);44 @memcpy(query_buf[0..query_len], initial_query[0..query_len]);4546 var packages: []types.NixPackage = &.{};47 var opts: []api.NixOption = &.{};48 var selection: usize = 0;4950 51 _ = arena.reset(.free_all);52 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);5354 55 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);56 output.flush();5758 59 60 61 62 var chosen: ?types.SearchResult = null;63 var query_dirty = false;64 var query_changed_at = std.Io.Clock.awake.now(io);65 loop: while (true) {66 67 68 var timeout_ms: i32 = -1;69 if (query_dirty) {70 const remaining = DEBOUNCE_MS - elapsedMs(io, query_changed_at);71 timeout_ms = if (remaining <= 0) 0 else @intCast(remaining);72 }7374 var pfd = [_]std.posix.pollfd{.{ .fd = STDIN_FD, .events = std.posix.POLL.IN, .revents = 0 }};75 const ready = std.posix.poll(&pfd, timeout_ms) catch 0;7677 if (ready == 0) {78 79 if (query_dirty and elapsedMs(io, query_changed_at) >= DEBOUNCE_MS) {80 query_dirty = false;81 _ = arena.reset(.free_all);82 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);83 output.cursorUp(WIDGET_LINES);84 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);85 output.flush();86 }87 continue;88 }8990 var buf: [4]u8 = undefined;91 const n = std.posix.read(STDIN_FD, &buf) catch break;92 if (n == 0) break;9394 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {95 switch (buf[2]) {96 'A' => if (selection > 0) { selection -= 1; }, 97 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 98 else => {},99 }100 } else switch (buf[0]) {101 'k' => if (selection > 0) { selection -= 1; },102 'j' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; },103 0x1b, 'q' => break :loop,104 'i', '\r' => {105 if (mode == .packages and packages.len > selection) {106 chosen = .{ .package = packages[selection], .action = .profile };107 }108 break :loop;109 },110 's' => {111 if (mode == .packages and packages.len > selection) {112 chosen = .{ .package = packages[selection], .action = .system };113 }114 break :loop;115 },116 't' => {117 if (mode == .packages and packages.len > selection) {118 chosen = .{ .package = packages[selection], .action = .profile };119 120 }121 break :loop;122 },123 'c' => {124 125 if (mode == .packages and packages.len > selection) {126 copyToClipboard(gpa, io, packages[selection].attr);127 }128 },129 127 => { 130 if (query_len > 0) {131 query_len -= 1;132 query_dirty = true;133 query_changed_at = std.Io.Clock.awake.now(io);134 selection = 0;135 }136 },137 else => |ch| {138 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {139 query_buf[query_len] = ch;140 query_len += 1;141 query_dirty = true;142 query_changed_at = std.Io.Clock.awake.now(io);143 selection = 0;144 }145 },146 }147148 149 150 output.cursorUp(WIDGET_LINES);151 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);152 output.flush();153 }154155 156 output.cursorUp(WIDGET_LINES);157 var i: usize = 0;158 while (i < WIDGET_LINES) : (i += 1) {159 output.raw(output.C_CLEAR_EOL ++ "\n");160 }161 output.cursorUp(WIDGET_LINES);162 output.flush();163164 return chosen;165}166167pub const Mode = enum { packages, options };168169fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption) []types.NixPackage {170 switch (mode) {171 .packages => return api.searchPackages(arena, io, query) catch &.{},172 .options => {173 opts.* = api.searchOptions(arena, io, query) catch &.{};174 return &.{};175 },176 }177}178179fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {180 return switch (mode) {181 .packages => packages.len,182 .options => opts.len,183 };184}185186fn stdinIsTty(io: std.Io) bool {187 return std.Io.File.stdin().isTty(io) catch false;188}189190191192fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {193 194 if (std.mem.trim(u8, query, " \t").len == 0) {195 output.searchEmptyQuery();196 output.flush();197 return null;198 }199 var arena = std.heap.ArenaAllocator.init(gpa);200 defer arena.deinit();201 var opts: []api.NixOption = &.{};202 const packages = doSearch(arena.allocator(), io, query, mode, &opts);203 const count = resultCount(packages, opts, mode);204 const mode_str = if (mode == .packages) "nixpkgs" else "options";205 output.searchPlainHeader(mode_str, query, count);206 if (count == 0) {207 output.searchPlainNoResults();208 output.flush();209 return null;210 }211 switch (mode) {212 213 214 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),215 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),216 }217 output.flush();218 return null;219}220221fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {222 const count = resultCount(packages, opts, mode);223 const CLR = output.C_CLEAR_EOL;224 const RST = output.C_RESET;225 const PKN = output.C_PINK;226 const BLD = output.C_BOLD;227 const DIM = output.C_DIM;228229 230 output.raw(CLR);231 const mode_str = if (mode == .packages) "nixpkgs" else "options";232 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{233 PKN, RST, mode_str, query, DIM, count, RST,234 });235236 237 output.raw(CLR ++ "\n");238239 240 const scroll: usize = if (selection >= RESULT_ROWS) selection - RESULT_ROWS + 1 else 0;241 var i: usize = 0;242 while (i < RESULT_ROWS) : (i += 1) {243 const idx = scroll + i;244 output.raw(CLR);245 if (idx < count) {246 const selected = (idx == selection);247 switch (mode) {248 .packages => {249 const pkg = packages[idx];250 const marker = if (selected) PKN ++ "> " ++ RST else " ";251 const name_pre = if (selected) PKN ++ BLD else "";252 const name_suf = if (selected) RST else "";253 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, DIM, pkg.version, RST, pkg.description });254 },255 .options => {256 const opt = opts[idx];257 const marker = if (selected) PKN ++ "> " ++ RST else " ";258 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });259 },260 }261 } else {262 output.raw("\n");263 }264 }265266 267 output.raw(CLR ++ "\n");268269 270 if (count > 0 and mode == .packages) {271 const pkg = packages[selection];272 output.raw(CLR);273 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, DIM, pkg.version, RST });274 output.raw(CLR);275 output.p(" {s}\n", .{pkg.description});276 output.raw(CLR ++ "\n");277 output.raw(CLR);278 output.p(" {s}attr {s}{s}\n", .{ DIM, pkg.attr, RST });279 } else if (count > 0 and mode == .options) {280 const opt = opts[selection];281 output.raw(CLR);282 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });283 output.raw(CLR);284 output.p(" {s}\n", .{opt.description});285 output.raw(CLR ++ "\n");286 output.raw(CLR ++ "\n");287 } else {288 output.raw(CLR ++ "\n");289 output.raw(CLR ++ "\n");290 output.raw(CLR ++ "\n");291 output.raw(CLR ++ "\n");292 }293294 295 output.raw(CLR ++ "\n");296297 298 output.raw(CLR);299 if (mode == .packages) {300 output.p(" {s}[i] profile [s] system [t] try [c] copy [up/down] nav [esc] exit{s}\n", .{ DIM, RST });301 } else {302 output.p(" {s}[i] insert [up/down] nav [esc] exit{s}\n", .{ DIM, RST });303 }304 305 var pad: usize = 14;306 while (pad < WIDGET_LINES) : (pad += 1) {307 output.raw(CLR ++ "\n");308 }309}310311fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {312 _ = gpa;313 314 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };315 for (tools) |tool| {316 var child = std.process.spawn(io, .{317 .argv = &.{tool},318 .stdin = .pipe,319 .stdout = .ignore,320 .stderr = .ignore,321 }) catch continue;322 if (child.stdin) |stdin| {323 stdin.writeStreamingAll(io, text) catch {};324 stdin.close(io);325 }326 _ = child.wait(io) catch {};327 return;328 }329}330331fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {332 var tio = try std.posix.tcgetattr(fd);333 saved.* = tio;334 if (comptime builtin.os.tag == .linux) {335 tio.lflag.ECHO = false;336 tio.lflag.ICANON = false;337 tio.lflag.ISIG = false;338 tio.lflag.IEXTEN = false;339 tio.iflag.ICRNL = false;340 tio.iflag.IXON = false;341 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;342 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;343 }344 345 try std.posix.tcsetattr(fd, .FLUSH, tio);346}347348fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {349 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};350}