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 84 85 86 selection = 0;87 output.cursorUp(WIDGET_LINES);88 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);89 output.flush();90 }91 continue;92 }9394 var buf: [4]u8 = undefined;95 const n = std.posix.read(STDIN_FD, &buf) catch break;96 if (n == 0) break;9798 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {99 switch (buf[2]) {100 'A' => if (selection > 0) { selection -= 1; }, 101 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 102 else => {},103 }104 } else switch (buf[0]) {105 'k' => if (selection > 0) { selection -= 1; },106 'j' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; },107 0x1b, 'q' => break :loop,108 'i', '\r' => {109 if (mode == .packages and packages.len > selection) {110 chosen = .{ .package = packages[selection], .action = .profile };111 }112 break :loop;113 },114 's' => {115 if (mode == .packages and packages.len > selection) {116 chosen = .{ .package = packages[selection], .action = .system };117 }118 break :loop;119 },120 't' => {121 if (mode == .packages and packages.len > selection) {122 chosen = .{ .package = packages[selection], .action = .profile };123 124 }125 break :loop;126 },127 'c' => {128 129 if (mode == .packages and packages.len > selection) {130 copyToClipboard(gpa, io, packages[selection].attr);131 }132 },133 127 => { 134 if (query_len > 0) {135 query_len -= 1;136 query_dirty = true;137 query_changed_at = std.Io.Clock.awake.now(io);138 selection = 0;139 }140 },141 else => |ch| {142 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {143 query_buf[query_len] = ch;144 query_len += 1;145 query_dirty = true;146 query_changed_at = std.Io.Clock.awake.now(io);147 selection = 0;148 }149 },150 }151152 153 154 output.cursorUp(WIDGET_LINES);155 renderWidget(query_buf[0..query_len], packages, opts, selection, mode);156 output.flush();157 }158159 160 output.cursorUp(WIDGET_LINES);161 var i: usize = 0;162 while (i < WIDGET_LINES) : (i += 1) {163 output.raw(output.C_CLEAR_EOL ++ "\n");164 }165 output.cursorUp(WIDGET_LINES);166 output.flush();167168 return chosen;169}170171pub const Mode = enum { packages, options };172173fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption) []types.NixPackage {174 switch (mode) {175 .packages => return api.searchPackages(arena, io, query) catch &.{},176 .options => {177 opts.* = api.searchOptions(arena, io, query) catch &.{};178 return &.{};179 },180 }181}182183fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {184 return switch (mode) {185 .packages => packages.len,186 .options => opts.len,187 };188}189190fn stdinIsTty(io: std.Io) bool {191 return std.Io.File.stdin().isTty(io) catch false;192}193194195196fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {197 198 if (std.mem.trim(u8, query, " \t").len == 0) {199 output.searchEmptyQuery();200 output.flush();201 return null;202 }203 var arena = std.heap.ArenaAllocator.init(gpa);204 defer arena.deinit();205 var opts: []api.NixOption = &.{};206 const packages = doSearch(arena.allocator(), io, query, mode, &opts);207 const count = resultCount(packages, opts, mode);208 const mode_str = if (mode == .packages) "nixpkgs" else "options";209 output.searchPlainHeader(mode_str, query, count);210 if (count == 0) {211 output.searchPlainNoResults();212 output.flush();213 return null;214 }215 switch (mode) {216 217 218 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),219 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),220 }221 output.flush();222 return null;223}224225226227228fn keyHint(comptime label: []const u8) []const u8 {229 return output.C_DIM ++ "[" ++ output.C_RESET ++ output.C_PINK ++ label ++ output.C_RESET ++ output.C_DIM ++ "]" ++ output.C_RESET;230}231232fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode) void {233 const count = resultCount(packages, opts, mode);234 const CLR = output.C_CLEAR_EOL;235 const RST = output.C_RESET;236 const PKN = output.C_PINK;237 const BLD = output.C_BOLD;238 const DIM = output.C_DIM;239 const CYN = output.C_CYAN;240241 242 243 const sel = if (count == 0) 0 else @min(selection, count - 1);244245 246 output.raw(CLR);247 const mode_str = if (mode == .packages) "nixpkgs" else "options";248 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{249 PKN, RST, mode_str, query, DIM, count, RST,250 });251252 253 output.raw(CLR ++ "\n");254255 256 const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;257 var i: usize = 0;258 while (i < RESULT_ROWS) : (i += 1) {259 const idx = scroll + i;260 output.raw(CLR);261 if (idx < count) {262 const selected = (idx == sel);263 switch (mode) {264 .packages => {265 const pkg = packages[idx];266 const marker = if (selected) PKN ++ "> " ++ RST else " ";267 const name_pre = if (selected) PKN ++ BLD else "";268 const name_suf = if (selected) RST else "";269 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, CYN, pkg.version, RST, pkg.description });270 },271 .options => {272 const opt = opts[idx];273 const marker = if (selected) PKN ++ "> " ++ RST else " ";274 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });275 },276 }277 } else {278 output.raw("\n");279 }280 }281282 283 output.raw(CLR ++ "\n");284285 286 if (count > 0 and mode == .packages) {287 const pkg = packages[sel];288 output.raw(CLR);289 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });290 output.raw(CLR);291 output.p(" {s}\n", .{pkg.description});292 output.raw(CLR ++ "\n");293 output.raw(CLR);294 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });295 } else if (count > 0 and mode == .options) {296 const opt = opts[sel];297 output.raw(CLR);298 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });299 output.raw(CLR);300 output.p(" {s}\n", .{opt.description});301 output.raw(CLR ++ "\n");302 output.raw(CLR ++ "\n");303 } else {304 output.raw(CLR ++ "\n");305 output.raw(CLR ++ "\n");306 output.raw(CLR ++ "\n");307 output.raw(CLR ++ "\n");308 }309310 311 output.raw(CLR ++ "\n");312313 314 output.raw(CLR);315 if (mode == .packages) {316 output.p(" {s} profile {s} system {s} try {s} copy {s}[up/down]{s} nav {s} exit\n", .{317 keyHint("i"), keyHint("s"), keyHint("t"), keyHint("c"), DIM, RST, keyHint("esc"),318 });319 } else {320 output.p(" {s} insert {s}[up/down]{s} nav {s} exit\n", .{ keyHint("i"), DIM, RST, keyHint("esc") });321 }322 323 var pad: usize = 14;324 while (pad < WIDGET_LINES) : (pad += 1) {325 output.raw(CLR ++ "\n");326 }327}328329fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {330 _ = gpa;331 332 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };333 for (tools) |tool| {334 var child = std.process.spawn(io, .{335 .argv = &.{tool},336 .stdin = .pipe,337 .stdout = .ignore,338 .stderr = .ignore,339 }) catch continue;340 if (child.stdin) |stdin| {341 stdin.writeStreamingAll(io, text) catch {};342 stdin.close(io);343 }344 _ = child.wait(io) catch {};345 return;346 }347}348349fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {350 var tio = try std.posix.tcgetattr(fd);351 saved.* = tio;352 if (comptime builtin.os.tag == .linux) {353 tio.lflag.ECHO = false;354 tio.lflag.ICANON = false;355 tio.lflag.ISIG = false;356 tio.lflag.IEXTEN = false;357 tio.iflag.ICRNL = false;358 tio.iflag.IXON = false;359 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;360 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;361 }362 363 try std.posix.tcsetattr(fd, .FLUSH, tio);364}365366fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {367 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};368}