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 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 rawModeEnable(STDIN_FD, &saved);50 defer 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 => return api.searchPackages(arena, io, environ, query) catch &.{},220 .options => {221 opts.* = api.searchOptions(arena, io, query) catch &.{};222 return &.{};223 },224 }225}226227fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {228 return switch (mode) {229 .packages => packages.len,230 .options => opts.len,231 };232}233234fn stdinIsTty(io: std.Io) bool {235 return std.Io.File.stdin().isTty(io) catch false;236}237238239240fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, environ: *const std.process.Environ.Map) !?types.NixPackage {241 242 if (std.mem.trim(u8, query, " \t").len == 0) {243 output.searchEmptyQuery();244 output.flush();245 return null;246 }247 var arena = std.heap.ArenaAllocator.init(gpa);248 defer arena.deinit();249 var opts: []api.NixOption = &.{};250 const packages = doSearch(arena.allocator(), io, query, mode, &opts, environ);251 const count = resultCount(packages, opts, mode);252 const mode_str = if (mode == .packages) "nixpkgs" else "options";253 output.searchPlainHeader(mode_str, query, count);254 if (count == 0) {255 output.searchPlainNoResults();256 output.flush();257 return null;258 }259 switch (mode) {260 261 262 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),263 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),264 }265 output.flush();266 return null;267}268269270271272fn keyHint(comptime label: []const u8) []const u8 {273 return output.C_DIM ++ "[" ++ output.C_RESET ++ output.C_PINK ++ label ++ output.C_RESET ++ output.C_DIM ++ "]" ++ output.C_RESET;274}275276fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode, searching: bool, detail: Detail) void {277 const count = resultCount(packages, opts, mode);278 const CLR = output.C_CLEAR_EOL;279 const RST = output.C_RESET;280 const PKN = output.C_PINK;281 const BLD = output.C_BOLD;282 const DIM = output.C_DIM;283 const CYN = output.C_CYAN;284285 286 287 const sel = if (count == 0) 0 else @min(selection, count - 1);288289 290 291 output.raw(CLR);292 const mode_str = if (mode == .packages) "nixpkgs" else "options";293 if (searching) {294 output.p("{s}:: {s}search {s} {s} {s}searching...{s}\n", .{295 PKN, RST, mode_str, query, DIM, RST,296 });297 } else {298 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{299 PKN, RST, mode_str, query, DIM, count, RST,300 });301 }302303 304 output.raw(CLR ++ "\n");305306 307 const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;308 var i: usize = 0;309 while (i < RESULT_ROWS) : (i += 1) {310 const idx = scroll + i;311 output.raw(CLR);312 if (idx < count) {313 const selected = (idx == sel);314 switch (mode) {315 .packages => {316 const pkg = packages[idx];317 const marker = if (selected) PKN ++ "> " ++ RST else " ";318 const name_pre = if (selected) PKN ++ BLD else "";319 const name_suf = if (selected) RST else "";320 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, CYN, pkg.version, RST, pkg.description });321 },322 .options => {323 const opt = opts[idx];324 const marker = if (selected) PKN ++ "> " ++ RST else " ";325 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });326 },327 }328 } else {329 output.raw("\n");330 }331 }332333 334 output.raw(CLR ++ "\n");335336 337 338 339 if (count > 0 and mode == .packages) {340 const pkg = packages[sel];341 output.raw(CLR);342 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });343 switch (detail) {344 .collapsed => {345 output.raw(CLR);346 output.p(" {s}\n", .{pkg.description});347 output.raw(CLR ++ "\n");348 output.raw(CLR);349 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });350 },351 .loading => {352 output.raw(CLR);353 output.p(" {s}loading details...{s}\n", .{ DIM, RST });354 output.raw(CLR ++ "\n");355 output.raw(CLR);356 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });357 },358 .unavailable => {359 output.raw(CLR);360 output.p(" {s}details unavailable{s}\n", .{ DIM, RST });361 output.raw(CLR ++ "\n");362 output.raw(CLR);363 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });364 },365 .meta => |m| {366 const home = if (m.homepage.len > 0) m.homepage else "—";367 const lic = if (m.license.len > 0) m.license else "—";368 output.raw(CLR);369 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "home", RST, CYN, home, RST });370 output.raw(CLR);371 output.p(" {s}{s:<8}{s}{s}\n", .{ DIM, "license", RST, lic });372 output.raw(CLR);373 output.p(" {s}{s:<8}{s}{s}{s}{s}\n", .{ DIM, "attr", RST, CYN, pkg.attr, RST });374 },375 }376 } else if (count > 0 and mode == .options) {377 const opt = opts[sel];378 output.raw(CLR);379 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });380 output.raw(CLR);381 output.p(" {s}\n", .{opt.description});382 output.raw(CLR ++ "\n");383 output.raw(CLR ++ "\n");384 } else {385 output.raw(CLR ++ "\n");386 output.raw(CLR ++ "\n");387 output.raw(CLR ++ "\n");388 output.raw(CLR ++ "\n");389 }390391 392 output.raw(CLR ++ "\n");393394 395 output.raw(CLR);396 if (mode == .packages) {397 output.p(" {s} install {s} info {s}[up/down]{s} nav {s} cancel\n", .{398 keyHint("enter"), keyHint("tab"), DIM, RST, keyHint("esc"),399 });400 } else {401 output.p(" {s}[up/down]{s} nav {s} cancel\n", .{ DIM, RST, keyHint("esc") });402 }403 404 var pad: usize = 14;405 while (pad < WIDGET_LINES) : (pad += 1) {406 output.raw(CLR ++ "\n");407 }408}409410fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {411 var tio = try std.posix.tcgetattr(fd);412 saved.* = tio;413 if (comptime builtin.os.tag == .linux) {414 tio.lflag.ECHO = false;415 tio.lflag.ICANON = false;416 tio.lflag.ISIG = false;417 tio.lflag.IEXTEN = false;418 tio.iflag.ICRNL = false;419 tio.iflag.IXON = false;420 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;421 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;422 }423 424 try std.posix.tcsetattr(fd, .FLUSH, tio);425}426427fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {428 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};429}