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}2526pub fn run(27 gpa: std.mem.Allocator,28 io: std.Io,29 initial_query: []const u8,30 mode: Mode,31) !?types.SearchResult {32 33 34 35 if (!stdinIsTty(io)) return runPlain(gpa, io, initial_query, mode);3637 var saved: std.posix.termios = undefined;38 try rawModeEnable(STDIN_FD, &saved);39 defer rawModeDisable(STDIN_FD, saved);4041 var arena = std.heap.ArenaAllocator.init(gpa);42 defer arena.deinit();4344 var query_buf: [256]u8 = undefined;45 var query_len: usize = @min(initial_query.len, query_buf.len - 1);46 @memcpy(query_buf[0..query_len], initial_query[0..query_len]);4748 var packages: []types.NixPackage = &.{};49 var opts: []api.NixOption = &.{};50 var selection: usize = 0;5152 53 _ = arena.reset(.free_all);54 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);5556 57 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false);58 output.flush();5960 61 62 63 64 var chosen: ?types.SearchResult = null;65 var query_dirty = false;66 var query_changed_at = std.Io.Clock.awake.now(io);67 loop: while (true) {68 69 70 var timeout_ms: i32 = -1;71 if (query_dirty) {72 const remaining = DEBOUNCE_MS - elapsedMs(io, query_changed_at);73 timeout_ms = if (remaining <= 0) 0 else @intCast(remaining);74 }7576 var pfd = [_]std.posix.pollfd{.{ .fd = STDIN_FD, .events = std.posix.POLL.IN, .revents = 0 }};77 const ready = std.posix.poll(&pfd, timeout_ms) catch 0;7879 if (ready == 0) {80 81 if (query_dirty and elapsedMs(io, query_changed_at) >= DEBOUNCE_MS) {82 query_dirty = false;83 84 85 output.cursorUp(WIDGET_LINES);86 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, true);87 output.flush();8889 _ = arena.reset(.free_all);90 packages = doSearch(arena.allocator(), io, query_buf[0..query_len], mode, &opts);91 92 93 94 selection = 0;95 output.cursorUp(WIDGET_LINES);96 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false);97 output.flush();98 }99 continue;100 }101102 var buf: [4]u8 = undefined;103 const n = std.posix.read(STDIN_FD, &buf) catch break;104 if (n == 0) break;105106 if (n >= 3 and buf[0] == 0x1b and buf[1] == '[') {107 switch (buf[2]) {108 'A' => if (selection > 0) { selection -= 1; }, 109 'B' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; }, 110 else => {},111 }112 } else switch (buf[0]) {113 'k' => if (selection > 0) { selection -= 1; },114 'j' => if (selection + 1 < resultCount(packages, opts, mode)) { selection += 1; },115 0x1b, 'q' => break :loop,116 'i', '\r' => {117 if (mode == .packages and packages.len > selection) {118 chosen = .{ .package = packages[selection], .action = .profile };119 }120 break :loop;121 },122 's' => {123 if (mode == .packages and packages.len > selection) {124 chosen = .{ .package = packages[selection], .action = .system };125 }126 break :loop;127 },128 't' => {129 if (mode == .packages and packages.len > selection) {130 chosen = .{ .package = packages[selection], .action = .profile };131 132 }133 break :loop;134 },135 'c' => {136 137 if (mode == .packages and packages.len > selection) {138 copyToClipboard(gpa, io, packages[selection].attr);139 }140 },141 127 => { 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 }159160 161 162 output.cursorUp(WIDGET_LINES);163 renderWidget(query_buf[0..query_len], packages, opts, selection, mode, false);164 output.flush();165 }166167 168 output.cursorUp(WIDGET_LINES);169 var i: usize = 0;170 while (i < WIDGET_LINES) : (i += 1) {171 output.raw(output.C_CLEAR_EOL ++ "\n");172 }173 output.cursorUp(WIDGET_LINES);174 output.flush();175176 return chosen;177}178179pub const Mode = enum { packages, options };180181fn doSearch(arena: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode, opts: *[]api.NixOption) []types.NixPackage {182 183 184 if (std.mem.trim(u8, query, " \t").len == 0) {185 opts.* = &.{};186 return &.{};187 }188 switch (mode) {189 .packages => return api.searchPackages(arena, io, query) catch &.{},190 .options => {191 opts.* = api.searchOptions(arena, io, query) catch &.{};192 return &.{};193 },194 }195}196197fn resultCount(packages: []types.NixPackage, opts: []api.NixOption, mode: Mode) usize {198 return switch (mode) {199 .packages => packages.len,200 .options => opts.len,201 };202}203204fn stdinIsTty(io: std.Io) bool {205 return std.Io.File.stdin().isTty(io) catch false;206}207208209210fn runPlain(gpa: std.mem.Allocator, io: std.Io, query: []const u8, mode: Mode) !?types.SearchResult {211 212 if (std.mem.trim(u8, query, " \t").len == 0) {213 output.searchEmptyQuery();214 output.flush();215 return null;216 }217 var arena = std.heap.ArenaAllocator.init(gpa);218 defer arena.deinit();219 var opts: []api.NixOption = &.{};220 const packages = doSearch(arena.allocator(), io, query, mode, &opts);221 const count = resultCount(packages, opts, mode);222 const mode_str = if (mode == .packages) "nixpkgs" else "options";223 output.searchPlainHeader(mode_str, query, count);224 if (count == 0) {225 output.searchPlainNoResults();226 output.flush();227 return null;228 }229 switch (mode) {230 231 232 .packages => for (packages) |pkg| output.searchPlainPackage(pkg.pname, pkg.version, pkg.description, false),233 .options => for (opts) |opt| output.searchPlainOption(opt.name, opt.type_str),234 }235 output.flush();236 return null;237}238239240241242fn keyHint(comptime label: []const u8) []const u8 {243 return output.C_DIM ++ "[" ++ output.C_RESET ++ output.C_PINK ++ label ++ output.C_RESET ++ output.C_DIM ++ "]" ++ output.C_RESET;244}245246fn renderWidget(query: []const u8, packages: []types.NixPackage, opts: []api.NixOption, selection: usize, mode: Mode, searching: bool) void {247 const count = resultCount(packages, opts, mode);248 const CLR = output.C_CLEAR_EOL;249 const RST = output.C_RESET;250 const PKN = output.C_PINK;251 const BLD = output.C_BOLD;252 const DIM = output.C_DIM;253 const CYN = output.C_CYAN;254255 256 257 const sel = if (count == 0) 0 else @min(selection, count - 1);258259 260 261 output.raw(CLR);262 const mode_str = if (mode == .packages) "nixpkgs" else "options";263 if (searching) {264 output.p("{s}:: {s}search {s} {s} {s}searching...{s}\n", .{265 PKN, RST, mode_str, query, DIM, RST,266 });267 } else {268 output.p("{s}:: {s}search {s} {s} {s}{d} results{s}\n", .{269 PKN, RST, mode_str, query, DIM, count, RST,270 });271 }272273 274 output.raw(CLR ++ "\n");275276 277 const scroll: usize = if (sel >= RESULT_ROWS) sel - RESULT_ROWS + 1 else 0;278 var i: usize = 0;279 while (i < RESULT_ROWS) : (i += 1) {280 const idx = scroll + i;281 output.raw(CLR);282 if (idx < count) {283 const selected = (idx == sel);284 switch (mode) {285 .packages => {286 const pkg = packages[idx];287 const marker = if (selected) PKN ++ "> " ++ RST else " ";288 const name_pre = if (selected) PKN ++ BLD else "";289 const name_suf = if (selected) RST else "";290 output.p(" {s}{s}{s}{s} {s}{s}{s} {s}\n", .{ marker, name_pre, pkg.pname, name_suf, CYN, pkg.version, RST, pkg.description });291 },292 .options => {293 const opt = opts[idx];294 const marker = if (selected) PKN ++ "> " ++ RST else " ";295 output.p(" {s}{s} {s}{s}{s}\n", .{ marker, opt.name, DIM, opt.type_str, RST });296 },297 }298 } else {299 output.raw("\n");300 }301 }302303 304 output.raw(CLR ++ "\n");305306 307 if (count > 0 and mode == .packages) {308 const pkg = packages[sel];309 output.raw(CLR);310 output.p(" {s}{s}{s}{s} {s}{s}{s}\n", .{ PKN, BLD, pkg.pname, RST, CYN, pkg.version, RST });311 output.raw(CLR);312 output.p(" {s}\n", .{pkg.description});313 output.raw(CLR ++ "\n");314 output.raw(CLR);315 output.p(" {s}attr{s} {s}{s}{s}\n", .{ DIM, RST, CYN, pkg.attr, RST });316 } else if (count > 0 and mode == .options) {317 const opt = opts[sel];318 output.raw(CLR);319 output.p(" {s} {s}{s}{s}\n", .{ opt.name, DIM, opt.type_str, RST });320 output.raw(CLR);321 output.p(" {s}\n", .{opt.description});322 output.raw(CLR ++ "\n");323 output.raw(CLR ++ "\n");324 } else {325 output.raw(CLR ++ "\n");326 output.raw(CLR ++ "\n");327 output.raw(CLR ++ "\n");328 output.raw(CLR ++ "\n");329 }330331 332 output.raw(CLR ++ "\n");333334 335 output.raw(CLR);336 if (mode == .packages) {337 output.p(" {s} profile {s} system {s} try {s} copy {s}[up/down]{s} nav {s} exit\n", .{338 keyHint("i"), keyHint("s"), keyHint("t"), keyHint("c"), DIM, RST, keyHint("esc"),339 });340 } else {341 output.p(" {s} insert {s}[up/down]{s} nav {s} exit\n", .{ keyHint("i"), DIM, RST, keyHint("esc") });342 }343 344 var pad: usize = 14;345 while (pad < WIDGET_LINES) : (pad += 1) {346 output.raw(CLR ++ "\n");347 }348}349350fn copyToClipboard(gpa: std.mem.Allocator, io: std.Io, text: []const u8) void {351 _ = gpa;352 353 const tools = [_][]const u8{ "pbcopy", "xclip", "xsel" };354 for (tools) |tool| {355 var child = std.process.spawn(io, .{356 .argv = &.{tool},357 .stdin = .pipe,358 .stdout = .ignore,359 .stderr = .ignore,360 }) catch continue;361 if (child.stdin) |stdin| {362 stdin.writeStreamingAll(io, text) catch {};363 stdin.close(io);364 }365 _ = child.wait(io) catch {};366 return;367 }368}369370fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {371 var tio = try std.posix.tcgetattr(fd);372 saved.* = tio;373 if (comptime builtin.os.tag == .linux) {374 tio.lflag.ECHO = false;375 tio.lflag.ICANON = false;376 tio.lflag.ISIG = false;377 tio.lflag.IEXTEN = false;378 tio.iflag.ICRNL = false;379 tio.iflag.IXON = false;380 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;381 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;382 }383 384 try std.posix.tcsetattr(fd, .FLUSH, tio);385}386387fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {388 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};389}