123const std = @import("std");4const types = @import("types.zig");5const output = @import("output.zig");6const api = @import("api.zig");78const WIDGET_LINES: usize = 18;9const RESULT_ROWS: usize = 5;10const STDIN_FD = std.posix.STDIN_FILENO;111213141516const DEBOUNCE_MS: i64 = 500;17181920fn elapsedMs(io: std.Io, since: std.Io.Timestamp) i64 {21 const ns = @max(0, since.untilNow(io, .awake).toNanoseconds());22 return @intCast(@divFloor(ns, 1_000_000));23}2425262728const Detail = union(enum) {29 collapsed,30 loading,31 unavailable,32 meta: api.PkgMeta,33};3435pub fn run(36 gpa: std.mem.Allocator,37 io: std.Io,38 initial_query: []const u8,39 mode: Mode,40 environ: *const std.process.Environ.Map,41) !?types.NixPackage {42 43 44 45 if (!stdinIsTty(io)) return runPlain(gpa, io, initial_query, mode, environ);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, environ);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.NixPackage = 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, environ);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 122 123 124 125 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {126 switch (buf[2]) {127 'A' => if (selection > 0) { selection -= 1; }, 128 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 129 else => {},130 }131 } else switch (buf[0]) {132 0x1b => break :loop, 133 '\t' => expanded = !expanded, 134 '\r', '\n' => { 135 if (mode == .packages and packages.len > selection) {136 chosen = packages[selection];137 }138 break :loop;139 },140 127, 8 => { 141 if (query_len > 0) {142 query_len -= 1;143 query_dirty = true;144 query_changed_at = std.Io.Clock.awake.now(io);145 selection = 0;146 }147 },148 else => |ch| {149 if (ch >= 32 and ch < 127 and query_len < query_buf.len - 1) {150 query_buf[query_len] = ch;151 query_len += 1;152 query_dirty = true;153 query_changed_at = std.Io.Clock.awake.now(io);154 selection = 0;155 }156 },157 }158 }159160 161 162 163 164 const count = resultCount(packages, opts, mode);165 const sel = if (count == 0) 0 else @min(selection, count - 1);166 var detail: Detail = .collapsed;167 if (expanded and mode == .packages and count > 0) {168 const cur = packages[sel].attr;169 if (meta == null or !std.mem.eql(u8, meta_attr, cur)) {170 output.cursorUp(WIDGET_LINES);171 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, .loading);172 output.flush();173 _ = meta_arena.reset(.free_all);174 meta_attr = meta_arena.allocator().dupe(u8, cur) catch "";175 meta = api.packageMeta(meta_arena.allocator(), io, cur);176 }177 detail = if (meta) |mm| Detail{ .meta = mm } else Detail.unavailable;178 }179 output.cursorUp(WIDGET_LINES);180 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false, detail);181 output.flush();182 }183184 185 output.cursorUp(WIDGET_LINES);186 var i: usize = 0;187 while (i < WIDGET_LINES) : (i += 1) {188 output.raw(output.C_CLEAR_EOL ++ "\n");189 }190 output.cursorUp(WIDGET_LINES);191 output.flush();192193 194 195 if (chosen) |pkg| return dupePackage(gpa, pkg);196 return null;197}198199fn dupePackage(gpa: std.mem.Allocator, pkg: types.NixPackage) types.NixPackage {200 return .{201 .attr = gpa.dupe(u8, pkg.attr) catch "",202 .pname = gpa.dupe(u8, pkg.pname) catch "",203 .version = gpa.dupe(u8, pkg.version) catch "",204 .description = gpa.dupe(u8, pkg.description) catch "",205 };206}207208pub const Mode = enum { packages, options };209210fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption, environ: *const std.process.Environ.Map) []types.NixPackage {211 212 213 if (std.mem.trim(u8, query, " \t").len == 0) {214 opts.* = &.{};215 return &.{};216 }217 switch (mode) {218 .packages => {219 const packages: []types.NixPackage = api.searchPackages(arena, io, environ, query) catch &.{};220 sanitizeDescriptions(arena, packages);221 return packages;222 },223 .options => {224 const options: []api.NixOption = api.searchOptions(arena, io, query) catch &.{};225 sanitizeOptionDescriptions(arena, options);226 opts.* = options;227 return &.{};228 },229 }230}231232233234235236237238fn sanitizeDescriptions(arena: std.mem.Allocator, packages: []types.NixPackage) void {239 for (packages) |*pkg| {240 pkg.description = output.sanitize(arena, pkg.description) catch pkg.description;241 }242}243244fn sanitizeOptionDescriptions(arena: std.mem.Allocator, options: []api.NixOption) void {245 for (options) |*opt| {246 opt.description = output.sanitize(arena, opt.description) catch opt.description;247 }248}249250fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {251 return switch (mode) {252 .packages => packages.len,253 .options => opts.len,254 };255}256257fn stdinIsTty(io: std.Io) bool {258 return std.Io.File.stdin().isTty(io) catch false;259}260261262263fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, environ: *const std.process.Environ.Map) !?types.NixPackage {264 265 if (std.mem.trim(u8, query, " \t").len == 0) {266 output.searchEmptyQuery();267 output.flush();268 return null;269 }270 var arena = std.heap.ArenaAllocator.init(gpa);271 defer arena.deinit();272 var opts: []api.NixOption = &.{};273 const packages = doSearch(arena.allocator(), io, query, mode, &opts, environ);274 const count = resultCount(packages, opts, mode);275 const mode_str = if (mode == .packages) "nixpkgs" else "options";276 output.searchPlainHeader(mode_str, query, count);277 if (count == 0) {278 output.searchPlainNoResults();279 output.flush();280 return null;281 }282 switch (mode) {283 284 285 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),286 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),287 }288 output.flush();289 return null;290}291292293294295fn keyHint(comptime label: []const u8) []const u8 {296 return output.C_DIM ++ "[" ++ output.C_RESET ++ output.C_PINK ++ label ++ output.C_RESET ++ output.C_DIM ++ "]" ++ output.C_RESET;297}298299fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode, searching: bool, detail: Detail) void {300 const count = resultCount(packages, opts, mode);301 const CLR = output.C_CLEAR_EOL;302 const RST = output.C_RESET;303 const PKN = output.C_PINK;304 const BLD = output.C_BOLD;305 const DIM = output.C_DIM;306 const CYN = output.C_CYAN;307308 309 310 const sel = if (count == 0) 0 else @min(selection, count - 1);311312 313 314 output.raw(CLR);315 const mode_str = if (mode == .packages) "nixpkgs" else "options";316 if (searching) {317 output.p("{s}:: {s}search {s} {s} {s}searching...{s}\n", .{318 PKN, RST, mode_str, query, DIM, RST,319 });320 } else {321 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{322 PKN, RST, mode_str, query, DIM, count, RST,323 });324 }325326 327 output.raw(CLR ++ "\n");328329 330 const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;331 var i: usize = 0;332 while (i < RESULT_ROWS) : (i += 1) {333 const idx = scroll + i;334 output.raw(CLR);335 if (idx < count) {336 const selected = (idx == sel);337 switch (mode) {338 .packages => {339 const pkg = packages[idx];340 const marker = if (selected) PKN ++ "> " ++ RST else " ";341 const name_pre = if (selected) PKN ++ BLD else "";342 const name_suf = if (selected) RST else "";343 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, CYN, pkg.version, RST, pkg.description });344 },345 .options => {346 const opt = opts[idx];347 const marker = if (selected) PKN ++ "> " ++ RST else " ";348 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });349 },350 }351 } else {352 output.raw("\n");353 }354 }355356 357 output.raw(CLR ++ "\n");358359 360 361 362 if (count > 0 and mode == .packages) {363 const pkg = packages[sel];364 output.raw(CLR);365 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });366 switch (detail) {367 .collapsed => {368 output.raw(CLR);369 output.p(" {s}\n", .{pkg.description});370 output.raw(CLR ++ "\n");371 output.raw(CLR);372 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });373 },374 .loading => {375 output.raw(CLR);376 output.p(" {s}loading details...{s}\n", .{ DIM, RST });377 output.raw(CLR ++ "\n");378 output.raw(CLR);379 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });380 },381 .unavailable => {382 output.raw(CLR);383 output.p(" {s}details unavailable{s}\n", .{ DIM, RST });384 output.raw(CLR ++ "\n");385 output.raw(CLR);386 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });387 },388 .meta => |m| {389 const home = if (m.homepage.len > 0) m.homepage else "—";390 const lic = if (m.license.len > 0) m.license else "—";391 output.raw(CLR);392 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "home", RST, CYN, home, RST });393 output.raw(CLR);394 output.p(" {s}{s:<8}{s}{s}\n", .{ DIM, "license", RST, lic });395 output.raw(CLR);396 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "attr", RST, CYN, pkg.attr, RST });397 },398 }399 } else if (count > 0 and mode == .options) {400 const opt = opts[sel];401 output.raw(CLR);402 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });403 output.raw(CLR);404 output.p(" {s}\n", .{opt.description});405 output.raw(CLR ++ "\n");406 output.raw(CLR ++ "\n");407 } else {408 output.raw(CLR ++ "\n");409 output.raw(CLR ++ "\n");410 output.raw(CLR ++ "\n");411 output.raw(CLR ++ "\n");412 }413414 415 output.raw(CLR ++ "\n");416417 418 output.raw(CLR);419 if (mode == .packages) {420 output.p(" {s} install {s} info {s}[up/down]{s} nav {s} cancel\n", .{421 keyHint("enter"), keyHint("tab"), DIM, RST, keyHint("esc"),422 });423 } else {424 output.p(" {s}[up/down]{s} nav {s} cancel\n", .{ DIM, RST, keyHint("esc") });425 }426 427 var pad: usize = 14;428 while (pad < WIDGET_LINES) : (pad += 1) {429 output.raw(CLR ++ "\n");430 }431}432433fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {434 var tio = try std.posix.tcgetattr(fd);435 saved.* = tio;436 437 438 439 440 tio.lflag.ECHO = false;441 tio.lflag.ICANON = false;442 tio.lflag.ISIG = false;443 tio.lflag.IEXTEN = false;444 tio.iflag.ICRNL = false;445 tio.iflag.IXON = false;446 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;447 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;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}