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;121314151617const DEBOUNCE_MS: i64 = 500;18192021fn elapsedMs(io: std.Io, since: std.Io.Timestamp) i64 {22 const ns = @max(0, since.untilNow(io, .awake).toNanoseconds());23 return @intCast(@divFloor(ns, 1_000_000));24}2526272829const Detail = union(enum) {30 collapsed,31 loading,32 unavailable,33 meta: api.PkgMeta,34};3536pub fn run(37 gpa: std.mem.Allocator,38 io: std.Io,39 initial_query: []const u8,40 mode: Mode,41) !?types.SearchResult {42 43 44 45 if (!stdinIsTty(io)) return runPlain(gpa, io, initial_query, mode);4647 var saved: std.posix.termios = undefined;48 try rawModeEnable(STDIN_FD, &saved);49 defer rawModeDisable(STDIN_FD, saved);5051 var arena = std.heap.ArenaAllocator.init(gpa);52 defer arena.deinit();5354 var query_buf: [256]u8 = undefined;55 var query_len: usize = @min(initial_query.len, query_buf.len - 1);56 @memcpy(query_buf[0..query_len], initial_query[0..query_len]);5758 var packages: []types.NixPackage = &.{};59 var opts: []api.NixOption = &.{};60 var selection: usize = 0;6162 63 64 65 var expanded = false;66 var meta_arena = std.heap.ArenaAllocator.init(gpa);67 defer meta_arena.deinit();68 var meta: ?api.PkgMeta = null;69 var meta_attr: []const u8 = "";7071 72 _ = arena.reset(.free_all);73 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);7475 76 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, .collapsed);77 output.flush();7879 80 81 82 83 var chosen: ?types.SearchResult = null;84 var query_dirty = false;85 var query_changed_at = std.Io.Clock.awake.now(io);86 loop: while (true) {87 88 89 var timeout_ms: i32 = -1;90 if (query_dirty) {91 const remaining = DEBOUNCE_MS - elapsedMs(io, query_changed_at);92 timeout_ms = if (remaining <= 0) 0 else @intCast(remaining);93 }9495 var pfd = [_]std.posix.pollfd{.{ .fd = STDIN_FD, .events = std.posix.POLL.IN, .revents = 0 }};96 const ready = std.posix.poll(&pfd, timeout_ms) catch 0;9798 if (ready == 0) {99 100 if (!(query_dirty and elapsedMs(io, query_changed_at) >= DEBOUNCE_MS)) continue;101 query_dirty = false;102 103 104 output.cursorUp(WIDGET_LINES);105 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, true, .collapsed);106 output.flush();107108 _ = arena.reset(.free_all);109 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);110 111 112 113 selection = 0;114 meta = null;115 116 } else {117 var buf: [4]u8 = undefined;118 const n = std.posix.read(STDIN_FD, &buf) catch break;119 if (n == 0) break;120121 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {122 switch (buf[2]) {123 'A' => if (selection > 0) { selection -= 1; }, 124 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 125 else => {},126 }127 } else switch (buf[0]) {128 'k' => if (selection > 0) { selection -= 1; },129 'j' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; },130 0x1b, 'q' => break :loop,131 '\t' => expanded = !expanded, 132 'i', '\r' => {133 if (mode == .packages and packages.len > selection) {134 chosen = .{ .package = packages[selection], .action = .profile };135 }136 break :loop;137 },138 's' => {139 if (mode == .packages and packages.len > selection) {140 chosen = .{ .package = packages[selection], .action = .system };141 }142 break :loop;143 },144 't' => {145 if (mode == .packages and packages.len > selection) {146 chosen = .{ .package = packages[selection], .action = .profile };147 148 }149 break :loop;150 },151 'c' => {152 153 if (mode == .packages and packages.len > selection) {154 copyToClipboard(gpa, io, packages[selection].attr);155 }156 },157 127 => { 158 if (query_len > 0) {159 query_len -= 1;160 query_dirty = true;161 query_changed_at = std.Io.Clock.awake.now(io);162 selection = 0;163 }164 },165 else => |ch| {166 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {167 query_buf[query_len] = ch;168 query_len += 1;169 query_dirty = true;170 query_changed_at = std.Io.Clock.awake.now(io);171 selection = 0;172 }173 },174 }175 }176177 178 179 180 181 const count = resultCount(packages, opts, mode);182 const sel = if (count == 0) 0 else @min(selection, count - 1);183 var detail: Detail = .collapsed;184 if (expanded and mode == .packages and count > 0) {185 const cur = packages[sel].attr;186 if (meta == null or !std.mem.eql(u8, meta_attr, cur)) {187 output.cursorUp(WIDGET_LINES);188 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, .loading);189 output.flush();190 _ = meta_arena.reset(.free_all);191 meta_attr = meta_arena.allocator().dupe(u8, cur) catch "";192 meta = api.packageMeta(meta_arena.allocator(), io, cur);193 }194 detail = if (meta) |mm| Detail{ .meta = mm } else Detail.unavailable;195 }196 output.cursorUp(WIDGET_LINES);197 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, detail);198 output.flush();199 }200201 202 output.cursorUp(WIDGET_LINES);203 var i: usize = 0;204 while (i < WIDGET_LINES) : (i += 1) {205 output.raw(output.C_CLEAR_EOL ++ "\n");206 }207 output.cursorUp(WIDGET_LINES);208 output.flush();209210 return chosen;211}212213pub const Mode = enum { packages, options };214215fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption) []types.NixPackage {216 217 218 if (std.mem.trim(u8, query, " \t").len == 0) {219 opts.* = &.{};220 return &.{};221 }222 switch (mode) {223 .packages => return api.searchPackages(arena, io, query) catch &.{},224 .options => {225 opts.* = api.searchOptions(arena, io, query) catch &.{};226 return &.{};227 },228 }229}230231fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {232 return switch (mode) {233 .packages => packages.len,234 .options => opts.len,235 };236}237238fn stdinIsTty(io: std.Io) bool {239 return std.Io.File.stdin().isTty(io) catch false;240}241242243244fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {245 246 if (std.mem.trim(u8, query, " \t").len == 0) {247 output.searchEmptyQuery();248 output.flush();249 return null;250 }251 var arena = std.heap.ArenaAllocator.init(gpa);252 defer arena.deinit();253 var opts: []api.NixOption = &.{};254 const packages = doSearch(arena.allocator(), io, query, mode, &opts);255 const count = resultCount(packages, opts, mode);256 const mode_str = if (mode == .packages) "nixpkgs" else "options";257 output.searchPlainHeader(mode_str, query, count);258 if (count == 0) {259 output.searchPlainNoResults();260 output.flush();261 return null;262 }263 switch (mode) {264 265 266 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),267 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),268 }269 output.flush();270 return null;271}272273274275276fn keyHint(comptime label: []const u8) []const u8 {277 return output.C_DIM ++ "[" ++ output.C_RESET ++ output.C_PINK ++ label ++ output.C_RESET ++ output.C_DIM ++ "]" ++ output.C_RESET;278}279280fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode, searching: bool, detail: Detail) void {281 const count = resultCount(packages, opts, mode);282 const CLR = output.C_CLEAR_EOL;283 const RST = output.C_RESET;284 const PKN = output.C_PINK;285 const BLD = output.C_BOLD;286 const DIM = output.C_DIM;287 const CYN = output.C_CYAN;288289 290 291 const sel = if (count == 0) 0 else @min(selection, count - 1);292293 294 295 output.raw(CLR);296 const mode_str = if (mode == .packages) "nixpkgs" else "options";297 if (searching) {298 output.p("{s}:: {s}search {s} {s} {s}searching...{s}\n", .{299 PKN, RST, mode_str, query, DIM, RST,300 });301 } else {302 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{303 PKN, RST, mode_str, query, DIM, count, RST,304 });305 }306307 308 output.raw(CLR ++ "\n");309310 311 const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;312 var i: usize = 0;313 while (i < RESULT_ROWS) : (i += 1) {314 const idx = scroll + i;315 output.raw(CLR);316 if (idx < count) {317 const selected = (idx == sel);318 switch (mode) {319 .packages => {320 const pkg = packages[idx];321 const marker = if (selected) PKN ++ "> " ++ RST else " ";322 const name_pre = if (selected) PKN ++ BLD else "";323 const name_suf = if (selected) RST else "";324 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, CYN, pkg.version, RST, pkg.description });325 },326 .options => {327 const opt = opts[idx];328 const marker = if (selected) PKN ++ "> " ++ RST else " ";329 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });330 },331 }332 } else {333 output.raw("\n");334 }335 }336337 338 output.raw(CLR ++ "\n");339340 341 342 343 if (count > 0 and mode == .packages) {344 const pkg = packages[sel];345 output.raw(CLR);346 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });347 switch (detail) {348 .collapsed => {349 output.raw(CLR);350 output.p(" {s}\n", .{pkg.description});351 output.raw(CLR ++ "\n");352 output.raw(CLR);353 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });354 },355 .loading => {356 output.raw(CLR);357 output.p(" {s}loading details...{s}\n", .{ DIM, RST });358 output.raw(CLR ++ "\n");359 output.raw(CLR);360 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });361 },362 .unavailable => {363 output.raw(CLR);364 output.p(" {s}details unavailable{s}\n", .{ DIM, RST });365 output.raw(CLR ++ "\n");366 output.raw(CLR);367 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });368 },369 .meta => |m| {370 const home = if (m.homepage.len > 0) m.homepage else "—";371 const lic = if (m.license.len > 0) m.license else "—";372 output.raw(CLR);373 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "home", RST, CYN, home, RST });374 output.raw(CLR);375 output.p(" {s}{s:<8}{s}{s}\n", .{ DIM, "license", RST, lic });376 output.raw(CLR);377 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "attr", RST, CYN, pkg.attr, RST });378 },379 }380 } else if (count > 0 and mode == .options) {381 const opt = opts[sel];382 output.raw(CLR);383 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });384 output.raw(CLR);385 output.p(" {s}\n", .{opt.description});386 output.raw(CLR ++ "\n");387 output.raw(CLR ++ "\n");388 } else {389 output.raw(CLR ++ "\n");390 output.raw(CLR ++ "\n");391 output.raw(CLR ++ "\n");392 output.raw(CLR ++ "\n");393 }394395 396 output.raw(CLR ++ "\n");397398 399 output.raw(CLR);400 if (mode == .packages) {401 output.p(" {s} profile {s} system {s} try {s} copy {s} info {s} exit\n", .{402 keyHint("i"), keyHint("s"), keyHint("t"), keyHint("c"), keyHint("tab"), keyHint("esc"),403 });404 } else {405 output.p(" {s} insert {s}[up/down]{s} nav {s} exit\n", .{ keyHint("i"), DIM, RST, keyHint("esc") });406 }407 408 var pad: usize = 14;409 while (pad < WIDGET_LINES) : (pad += 1) {410 output.raw(CLR ++ "\n");411 }412}413414fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {415 _ = gpa;416 417 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };418 for (tools) |tool| {419 var child = std.process.spawn(io, .{420 .argv = &.{tool},421 .stdin = .pipe,422 .stdout = .ignore,423 .stderr = .ignore,424 }) catch continue;425 if (child.stdin) |stdin| {426 stdin.writeStreamingAll(io, text) catch {};427 stdin.close(io);428 }429 _ = child.wait(io) catch {};430 return;431 }432}433434fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {435 var tio = try std.posix.tcgetattr(fd);436 saved.* = tio;437 if (comptime builtin.os.tag == .linux) {438 tio.lflag.ECHO = false;439 tio.lflag.ICANON = false;440 tio.lflag.ISIG = false;441 tio.lflag.IEXTEN = false;442 tio.iflag.ICRNL = false;443 tio.iflag.IXON = false;444 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;445 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;446 }447 448 try std.posix.tcsetattr(fd, .FLUSH, tio);449}450451fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {452 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};453}