123const std = @import("std");4const types = @import("types.zig");5const output = @import("output.zig");6const api = @import("api.zig");7const rawterm = @import("rawterm.zig");89const WIDGET_LINES: usize = 18;10const RESULT_ROWS: usize = 5;11const STDIN_FD = rawterm.STDIN_FD;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 environ: *const std.process.Environ.Map,42) !?types.NixPackage {43 44 45 46 if (!stdinIsTty(io)) return runPlain(gpa, io, initial_query, mode, environ);4748 var saved: std.posix.termios = undefined;49 try rawterm.rawModeEnable(STDIN_FD, &saved);50 defer rawterm.rawModeDisable(STDIN_FD, saved);5152 var arena = std.heap.ArenaAllocator.init(gpa);53 defer arena.deinit();5455 var query_buf: [256]u8 = undefined;56 var query_len: usize = @min(initial_query.len, query_buf.len - 1);57 @memcpy(query_buf[0..query_len], initial_query[0..query_len]);5859 var packages: []types.NixPackage = &.{};60 var opts: []api.NixOption = &.{};61 var selection: usize = 0;6263 64 65 66 var expanded = false;67 var meta_arena = std.heap.ArenaAllocator.init(gpa);68 defer meta_arena.deinit();69 var meta: ?api.PkgMeta = null;70 var meta_attr: []const u8 = "";7172 73 _ = arena.reset(.free_all);74 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts, environ);7576 77 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, .collapsed);78 output.flush();7980 81 82 83 84 var chosen: ?types.NixPackage = null;85 var query_dirty = false;86 var query_changed_at = std.Io.Clock.awake.now(io);87 loop: while (true) {88 89 90 var timeout_ms: i32 = -1;91 if (query_dirty) {92 const remaining = DEBOUNCE_MS - elapsedMs(io, query_changed_at);93 timeout_ms = if (remaining <= 0) 0 else @intCast(remaining);94 }9596 var pfd = [_]std.posix.pollfd{.{ .fd = STDIN_FD, .events = std.posix.POLL.IN, .revents = 0 }};97 const ready = std.posix.poll(&pfd, timeout_ms) catch 0;9899 if (ready == 0) {100 101 if (!(query_dirty and elapsedMs(io, query_changed_at) >= DEBOUNCE_MS)) continue;102 query_dirty = false;103 104 105 output.cursorUp(WIDGET_LINES);106 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, true, .collapsed);107 output.flush();108109 _ = arena.reset(.free_all);110 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts, environ);111 112 113 114 selection = 0;115 meta = null;116 117 } else {118 var buf: [4]u8 = undefined;119 const n = std.posix.read(STDIN_FD, &buf) catch break;120 if (n == 0) break;121122 123 124 125 126 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {127 switch (buf[2]) {128 'A' => if (selection > 0) { selection -= 1; }, 129 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 130 else => {},131 }132 } else switch (buf[0]) {133 0x1b => break :loop, 134 '\t' => expanded = !expanded, 135 '\r', '\n' => { 136 if (mode == .packages and packages.len > selection) {137 chosen = packages[selection];138 }139 break :loop;140 },141 127, 8 => { 142 if (query_len > 0) {143 query_len -= 1;144 query_dirty = true;145 query_changed_at = std.Io.Clock.awake.now(io);146 selection = 0;147 }148 },149 else => |ch| {150 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {151 query_buf[query_len] = ch;152 query_len += 1;153 query_dirty = true;154 query_changed_at = std.Io.Clock.awake.now(io);155 selection = 0;156 }157 },158 }159 }160161 162 163 164 165 const count = resultCount(packages, opts, mode);166 const sel = if (count == 0) 0 else @min(selection, count - 1);167 var detail: Detail = .collapsed;168 if (expanded and mode == .packages and count > 0) {169 const cur = packages[sel].attr;170 if (meta == null or !std.mem.eql(u8, meta_attr, cur)) {171 output.cursorUp(WIDGET_LINES);172 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, .loading);173 output.flush();174 _ = meta_arena.reset(.free_all);175 meta_attr = meta_arena.allocator().dupe(u8, cur) catch "";176 meta = api.packageMeta(meta_arena.allocator(), io, cur);177 }178 detail = if (meta) |mm| Detail{ .meta = mm } else Detail.unavailable;179 }180 output.cursorUp(WIDGET_LINES);181 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, detail);182 output.flush();183 }184185 186 output.cursorUp(WIDGET_LINES);187 var i: usize = 0;188 while (i < WIDGET_LINES) : (i += 1) {189 output.raw(output.C_CLEAR_EOL ++ "\n");190 }191 output.cursorUp(WIDGET_LINES);192 output.flush();193194 195 196 if (chosen) |pkg| return dupePackage(gpa, pkg);197 return null;198}199200fn dupePackage(gpa: std.mem.Allocator, pkg: types.NixPackage) types.NixPackage {201 return .{202 .attr = gpa.dupe(u8, pkg.attr) catch "",203 .pname = gpa.dupe(u8, pkg.pname) catch "",204 .version = gpa.dupe(u8, pkg.version) catch "",205 .description = gpa.dupe(u8, pkg.description) catch "",206 };207}208209pub const Mode = enum { packages, options };210211fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption, environ: *const std.process.Environ.Map) []types.NixPackage {212 213 214 if (std.mem.trim(u8, query, " \t").len == 0) {215 opts.* = &.{};216 return &.{};217 }218 switch (mode) {219 .packages => {220 const packages: []types.NixPackage = api.searchPackages(arena, io, environ, query) catch &.{};221 sanitizeDescriptions(arena, packages);222 return packages;223 },224 .options => {225 const options: []api.NixOption = api.searchOptions(arena, io, query) catch &.{};226 sanitizeOptionDescriptions(arena, options);227 opts.* = options;228 return &.{};229 },230 }231}232233234235236237238239fn sanitizeDescriptions(arena: std.mem.Allocator, packages: []types.NixPackage) void {240 for (packages) |*pkg| {241 pkg.description = output.sanitize(arena, pkg.description) catch pkg.description;242 }243}244245fn sanitizeOptionDescriptions(arena: std.mem.Allocator, options: []api.NixOption) void {246 for (options) |*opt| {247 opt.description = output.sanitize(arena, opt.description) catch opt.description;248 }249}250251fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {252 return switch (mode) {253 .packages => packages.len,254 .options => opts.len,255 };256}257258fn stdinIsTty(io: std.Io) bool {259 return std.Io.File.stdin().isTty(io) catch false;260}261262263264fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, environ: *const std.process.Environ.Map) !?types.NixPackage {265 266 if (std.mem.trim(u8, query, " \t").len == 0) {267 output.searchEmptyQuery();268 output.flush();269 return null;270 }271 var arena = std.heap.ArenaAllocator.init(gpa);272 defer arena.deinit();273 var opts: []api.NixOption = &.{};274 const packages = doSearch(arena.allocator(), io, query, mode, &opts, environ);275 const count = resultCount(packages, opts, mode);276 const mode_str = if (mode == .packages) "nixpkgs" else "options";277 output.searchPlainHeader(mode_str, query, count);278 if (count == 0) {279 output.searchPlainNoResults();280 output.flush();281 return null;282 }283 switch (mode) {284 285 286 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),287 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),288 }289 output.flush();290 return null;291}292293294295296fn keyHint(comptime label: []const u8) []const u8 {297 return output.C_DIM ++ "[" ++ output.C_RESET ++ output.C_PINK ++ label ++ output.C_RESET ++ output.C_DIM ++ "]" ++ output.C_RESET;298}299300fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode, searching: bool, detail: Detail) void {301 const count = resultCount(packages, opts, mode);302 const CLR = output.C_CLEAR_EOL;303 const RST = output.C_RESET;304 const PKN = output.C_PINK;305 const BLD = output.C_BOLD;306 const DIM = output.C_DIM;307 const CYN = output.C_CYAN;308309 310 311 const sel = if (count == 0) 0 else @min(selection, count - 1);312313 314 315 output.raw(CLR);316 const mode_str = if (mode == .packages) "nixpkgs" else "options";317 if (searching) {318 output.p("{s}:: {s}search {s} {s} {s}searching...{s}\n", .{319 PKN, RST, mode_str, query, DIM, RST,320 });321 } else {322 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{323 PKN, RST, mode_str, query, DIM, count, RST,324 });325 }326327 328 output.raw(CLR ++ "\n");329330 331 const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;332 var i: usize = 0;333 while (i < RESULT_ROWS) : (i += 1) {334 const idx = scroll + i;335 output.raw(CLR);336 if (idx < count) {337 const selected = (idx == sel);338 switch (mode) {339 .packages => {340 const pkg = packages[idx];341 const marker = if (selected) PKN ++ "> " ++ RST else " ";342 const name_pre = if (selected) PKN ++ BLD else "";343 const name_suf = if (selected) RST else "";344 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, CYN, pkg.version, RST, pkg.description });345 },346 .options => {347 const opt = opts[idx];348 const marker = if (selected) PKN ++ "> " ++ RST else " ";349 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });350 },351 }352 } else {353 output.raw("\n");354 }355 }356357 358 output.raw(CLR ++ "\n");359360 361 362 363 if (count > 0 and mode == .packages) {364 const pkg = packages[sel];365 output.raw(CLR);366 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });367 switch (detail) {368 .collapsed => {369 output.raw(CLR);370 output.p(" {s}\n", .{pkg.description});371 output.raw(CLR ++ "\n");372 output.raw(CLR);373 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });374 },375 .loading => {376 output.raw(CLR);377 output.p(" {s}loading details...{s}\n", .{ DIM, RST });378 output.raw(CLR ++ "\n");379 output.raw(CLR);380 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });381 },382 .unavailable => {383 output.raw(CLR);384 output.p(" {s}details unavailable{s}\n", .{ DIM, RST });385 output.raw(CLR ++ "\n");386 output.raw(CLR);387 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });388 },389 .meta => |m| {390 const home = if (m.homepage.len > 0) m.homepage else "—";391 const lic = if (m.license.len > 0) m.license else "—";392 output.raw(CLR);393 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "home", RST, CYN, home, RST });394 output.raw(CLR);395 output.p(" {s}{s:<8}{s}{s}\n", .{ DIM, "license", RST, lic });396 output.raw(CLR);397 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "attr", RST, CYN, pkg.attr, RST });398 },399 }400 } else if (count > 0 and mode == .options) {401 const opt = opts[sel];402 output.raw(CLR);403 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });404 output.raw(CLR);405 output.p(" {s}\n", .{opt.description});406 output.raw(CLR ++ "\n");407 output.raw(CLR ++ "\n");408 } else {409 output.raw(CLR ++ "\n");410 output.raw(CLR ++ "\n");411 output.raw(CLR ++ "\n");412 output.raw(CLR ++ "\n");413 }414415 416 output.raw(CLR ++ "\n");417418 419 output.raw(CLR);420 if (mode == .packages) {421 output.p(" {s} install {s} info {s}[up/down]{s} nav {s} cancel\n", .{422 keyHint("enter"), keyHint("tab"), DIM, RST, keyHint("esc"),423 });424 } else {425 output.p(" {s}[up/down]{s} nav {s} cancel\n", .{ DIM, RST, keyHint("esc") });426 }427 428 var pad: usize = 14;429 while (pad < WIDGET_LINES) : (pad += 1) {430 output.raw(CLR ++ "\n");431 }432}433434435436