Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/libs/helper_functions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ const releases_url = "https://api.github.com/repos/{s}/releases";
const branches_url = "https://api.github.com/repos/{s}/branches";
const tar_file_url = "https://github.com/{s}/archive/refs/tags/{s}.tar.gz";

const in = std.fs.File.stdin();
var in: std.fs.File = undefined;

pub fn set_file_stdin() void {
in = std.fs.File.stdin();
}

pub fn semver_parse_range_based(input: []const u8) !struct {
min: types.semver,
Expand Down Expand Up @@ -192,8 +196,22 @@ pub fn read_string(allocator: std.mem.Allocator) !u32 {
pub fn read_integer() !usize {
var buf: [25]u8 = undefined;
var reader = in.reader(&buf);
const num_str = reader.interface.takeDelimiterExclusive('\n') catch return error.UnexpectedEos;
return try std.fmt.parseInt(usize, num_str, 10);
const num_str_cr = reader.interface.takeDelimiterExclusive('\n') catch |err| {
std.debug.print("ERROR in takeDelimiterExclusive: {s}\n", .{@errorName(err)});
return error.UnexpectedEos;
};

// Windows terminal compatibility issues
// Trim the carriage return
const num_str = std.mem.trim(u8, num_str_cr, " \r\n\t");

std.debug.print("\n", .{});
const parsed_int = std.fmt.parseInt(usize, num_str, 10) catch |err| {
std.debug.print("Failed to parse '{s}': {s}\n", .{ num_str, @errorName(err) });
return error.InvalidNumber;
};

return parsed_int;
}

pub fn fetch_versions(repo: types.repository, allocator: std.mem.Allocator) ![][]const u8 {
Expand Down
11 changes: 10 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const program_manager = @import("./programs/install.zig");
const types = @import("types.zig");
const hfs = @import("./libs/helper_functions.zig");
const init = @import("./libs/init.zig");
const package_search = @import("./packages/search.zig");

inline fn eql(x: []const u8, y: []const u8) bool {
return std.mem.eql(u8, x, y);
Expand Down Expand Up @@ -35,6 +36,7 @@ pub fn main() !void {
// if (deinit_status == .leak) @panic("Memory got leaked.");
// }

hfs.set_file_stdin();
const allocator = std.heap.c_allocator;

// arguments
Expand Down Expand Up @@ -91,6 +93,10 @@ pub fn main() !void {
}
// ====================================================
try program_manager.install_app(repo, allocator);
} else if (eql(args[1], "search")) {
const query = if (args.len > 2) args[2] else null;
const filter = if (args.len > 3) args[3] else null;
try package_search.search_packages(allocator, query, filter);
} else if (eql(args[1], "remove")) {
try remove_package.remove_dependency(allocator, args[2]);
} else if (eql(args[1], "update")) {
Expand Down Expand Up @@ -142,8 +148,11 @@ pub fn main() !void {
return;
}
try info_package.info(repo, allocator);
} else if (eql(args[1], "search")) {
const query = if (args.len > 2) args[2] else null;
const filter = if (args.len > 3) args[3] else null;
try package_search.search_packages(allocator, query, filter);
} else display.err.unknown_argument(args[2]),

// args[0] args[1] args[2] args[3]
// zigp something something_else yet_something
else => display.err.unknown_argument(args[2]),
Expand Down
252 changes: 252 additions & 0 deletions src/packages/search.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
const std = @import("std");
const http = std.http;
const json = std.json;
const ansi = @import("../libs/ansi_codes.zig");

fn month_num_to_month_name(month: u8) []const u8 {
return switch (month) {
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
else => "unknown month",
};
}

fn print_special_date_time(date: []const u8) struct {
year: []const u8,
month: []const u8,
day: []const u8,
hour: []const u8,
minutes: []const u8,
seconds: []const u8,
} {
var my_iter = std.mem.splitAny(u8, date, "-:ZT");
return .{
.year = my_iter.next().?,
.month = my_iter.next().?,
.day = if (my_iter.next()) |day| if (day[0] == '0') day[1..] else day else unreachable,
.hour = my_iter.next().?,
.minutes = my_iter.next().?,
.seconds = my_iter.next().?,
};
}

// package response from zigistry
const Package = struct {
name: []const u8,
full_name: []const u8,
description: []const u8,
stargazers_count: u32,
forks_count: u32,
license: ?[]const u8,
topics: []const []const u8,
repo_from: []const u8,
zig_minimum_version: []const u8,
has_build_zig: bool,
has_build_zig_zon: bool,
updated_at: []const u8,
};

pub fn search_packages(allocator: std.mem.Allocator, query: ?[]const u8, filter: ?[]const u8) !void {
const actual_query = if (query != null and query.?.len > 0 and !std.mem.eql(u8, query.?, "*")) query else null;
const actual_filter = if (filter != null and filter.?.len > 0) filter else null;

if (actual_query == null and actual_filter == null) {
std.debug.print("Error: No valid search parameters provided.\n", .{});
return error.InvalidSearchParameters;
}

var url_buffer: [512]u8 = undefined;
const url = try build_search_url(&url_buffer, actual_query, actual_filter);

// std.debug.print("Searching: {s}\n\n", .{url});

// Create HTTP client
var client = http.Client{ .allocator = allocator };
defer client.deinit();

var body = std.Io.Writer.Allocating.init(allocator);
const bodywriter: *std.Io.Writer = &body.writer;
defer body.deinit();
const response = try client.fetch(.{
.location = .{ .url = url },
.method = .GET,
.response_writer = bodywriter,
});

if (response.status != .ok) {
std.debug.print("API request failed with status: {s}\n", .{@tagName(response.status)});
return;
}

const packages = try parse_packages(allocator, body.written());
defer free_packages(allocator, packages);

print_packages(packages);
}

fn free_packages(allocator: std.mem.Allocator, packages: []Package) void {
for (packages) |pkg| {
allocator.free(pkg.name);
allocator.free(pkg.full_name);
allocator.free(pkg.description);
if (pkg.license) |license| allocator.free(license);
for (pkg.topics) |topic| allocator.free(topic);
allocator.free(pkg.topics);
allocator.free(pkg.repo_from);
allocator.free(pkg.zig_minimum_version);
allocator.free(pkg.updated_at);
}
allocator.free(packages);
}

fn build_search_url(buffer: []u8, query: ?[]const u8, filter: ?[]const u8) ![]const u8 {
const base_url = "https://zigistry-api.hf.space/api/searchPackages";

if (query != null and filter != null) {
// Both provided: ?q=query&filter=filter
return std.fmt.bufPrint(buffer, "{s}?q={s}&filter={s}", .{ base_url, query.?, filter.? });
} else if (query != null) {
// Only query provided: ?q=query
return std.fmt.bufPrint(buffer, "{s}?q={s}", .{ base_url, query.? });
} else if (filter != null) {
// Only filter provided: ?filter=filter
return std.fmt.bufPrint(buffer, "{s}?filter={s}", .{ base_url, filter.? });
} else {
// Neither
return error.NoSearchParameters;
}
}

// Parse JSON response into Package array
fn parse_packages(allocator: std.mem.Allocator, body: []const u8) ![]Package {
var parsed = try json.parseFromSlice(json.Value, allocator, body, .{});
defer parsed.deinit();

if (parsed.value != .array) {
return error.InvalidJSON;
}

const packages_array = parsed.value.array;
var packages = try allocator.alloc(Package, packages_array.items.len);

for (packages_array.items, 0..) |item, i| {
if (item != .object) continue;

const obj = item.object;
packages[i] = Package{
.name = try allocator.dupe(u8, get_string(obj, "name") orelse "unknown"),
.full_name = try allocator.dupe(u8, get_string(obj, "full_name") orelse "unknown"),
.description = try allocator.dupe(u8, get_string(obj, "description") orelse ""),
.stargazers_count = get_u32(obj, "stargazers_count") orelse 0,
.forks_count = get_u32(obj, "forks_count") orelse 0,
.license = if (get_string(obj, "license")) |license|
try allocator.dupe(u8, license)
else
null,
.topics = try parse_topics(allocator, obj),
.repo_from = try allocator.dupe(u8, get_string(obj, "repo_from") orelse "unknown"),
.zig_minimum_version = try allocator.dupe(u8, get_string(obj, "zig_minimum_version") orelse "unknown"),
.has_build_zig = get_bool(obj, "has_build_zig") orelse false,
.has_build_zig_zon = get_bool(obj, "has_build_zig_zon") orelse false,
.updated_at = try allocator.dupe(u8, get_string(obj, "updated_at") orelse "unknown"),
};
}

return packages;
}

// JSON object to sting
fn get_string(obj: json.ObjectMap, key: []const u8) ?[]const u8 {
if (obj.get(key)) |value| {
if (value == .string) {
return value.string;
}
}
return null;
}

// JSON object to u32
fn get_u32(obj: json.ObjectMap, key: []const u8) ?u32 {
if (obj.get(key)) |value| {
if (value == .integer) {
return @intCast(value.integer);
}
}
return null;
}

// JSON object to bool
fn get_bool(obj: json.ObjectMap, key: []const u8) ?bool {
if (obj.get(key)) |value| {
if (value == .bool) {
return value.bool;
}
}
return null;
}

// Parse topics array
fn parse_topics(allocator: std.mem.Allocator, obj: json.ObjectMap) ![]const []const u8 {
if (obj.get("topics")) |value| {
if (value == .array) {
const topics_array = value.array;
var topics = try allocator.alloc([]const u8, topics_array.items.len);

for (topics_array.items, 0..) |topic, i| {
if (topic == .string) {
topics[i] = try allocator.dupe(u8, topic.string);
} else {
topics[i] = try allocator.dupe(u8, "");
}
}
return topics;
}
}
return &[0][]const u8{};
}

// Print packages in a nice format
fn print_packages(packages: []const Package) void {
if (packages.len == 0) {
std.debug.print("No packages found.\n", .{});
return;
}

std.debug.print(ansi.BRIGHT_GREEN ++ "Found {s}{d}{s} package(s):{s}\n\n", .{ ansi.UNDERLINE, packages.len, ansi.RESET ++ ansi.BRIGHT_GREEN, ansi.RESET });

for (packages, 0..) |pkg, i| {
std.debug.print(ansi.BRIGHT_YELLOW ++ "---{s} Package {d}{s} ---{s}\n", .{ ansi.RESET ++ ansi.BRIGHT_CYAN, i + 1, ansi.BRIGHT_YELLOW, ansi.RESET });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Name{s}: {s}\n", .{ ansi.RESET, pkg.name });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Full Name{s}: {s}\n", .{ ansi.RESET, pkg.full_name });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Description{s}: {s}\n", .{ ansi.RESET, pkg.description });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Stars{s}: {d}, {s}Forks{s}: {d}\n", .{ ansi.RESET, pkg.stargazers_count, ansi.BRIGHT_GREEN ++ ansi.BOLD, ansi.RESET, pkg.forks_count });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "License{s}: {s}\n", .{ ansi.RESET, pkg.license orelse "None" });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Source{s}: {s}\n", .{ ansi.RESET, pkg.repo_from });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Zig Version{s}: {s}\n", .{ ansi.RESET, pkg.zig_minimum_version });
std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Build.zig{s}: {}, {s}Build.zig.zon{s}: {}\n", .{ ansi.RESET, pkg.has_build_zig, ansi.BRIGHT_GREEN ++ ansi.BOLD, ansi.RESET, pkg.has_build_zig_zon });

if (pkg.topics.len > 0) {
std.debug.print("{s}Topics{s}: ", .{ ansi.BRIGHT_GREEN ++ ansi.BOLD, ansi.RESET });
for (pkg.topics, 0..) |topic, j| {
if (j > 0) std.debug.print(", ", .{});
std.debug.print(ansi.BRIGHT_CYAN ++ ansi.BOLD ++ ansi.UNDERLINE ++ "#{s}" ++ ansi.RESET, .{topic});
}
std.debug.print("\n", .{});
}

const data = print_special_date_time(pkg.updated_at);

std.debug.print(ansi.BRIGHT_GREEN ++ ansi.BOLD ++ "Updated{s}: {s}th of {s} {s} at {s}:{s}:{s}\n", .{ ansi.RESET, data.day, month_num_to_month_name(std.fmt.parseInt(u8, data.month, 10) catch 13), data.year, data.hour, data.minutes, data.seconds });
std.debug.print("\n", .{});
}
}