From f21598bdbb07a5b699fc9706d66f2763aaca0fe5 Mon Sep 17 00:00:00 2001 From: Jeremie Fraeys Date: Tue, 10 Feb 2026 20:26:18 -0500 Subject: [PATCH] Simplify manwhere to minimal version - remove interactive mode, descriptions, verbose flags. Keep only -s section filter. Output plain names for piping to fzf. --- .gitignore | 25 ++ build.zig | 21 +- src/display.zig | 164 ------------- src/main.zig | 31 +-- src/parser.zig | 65 +----- src/search.zig | 599 +++++++++++++----------------------------------- src/types.zig | 2 - 7 files changed, 218 insertions(+), 689 deletions(-) create mode 100644 .gitignore delete mode 100644 src/display.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24355f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Zig +.zig-cache/ +zig-out/ + +# Compiled binaries +*.o +*.exe +*.so +*.dylib +*.dll + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Project specific +manwhere +manwhere-debug diff --git a/build.zig b/build.zig index 7f3be41..afcf537 100644 --- a/build.zig +++ b/build.zig @@ -106,12 +106,25 @@ pub fn build(b: *std.Build) void { std.debug.print("Error: Failed to construct install path\n", .{}); return; }; - defer b.allocator.free(install_path); + // Note: NOT freed - used by build system - const install_exe = b.addInstallArtifact(release_exe, .{ - .dest_dir = .{ .override = .{ .custom = install_path } }, + // Create install directory if it doesn't exist + const mkdir_cmd = b.addSystemCommand(&.{ + "mkdir", "-p", install_path, }); - release_step.dependOn(&install_exe.step); + release_step.dependOn(&mkdir_cmd.step); + + // Build the release executable + const release_build = b.addInstallArtifact(release_exe, .{}); + release_build.step.dependOn(&mkdir_cmd.step); + release_step.dependOn(&release_build.step); + + // Copy to final destination + const copy_cmd = b.addSystemCommand(&.{ + "cp", "zig-out/bin/manwhere", install_path, + }); + copy_cmd.step.dependOn(&release_build.step); + release_step.dependOn(©_cmd.step); // Test step - run all unit tests const run_unit_tests = b.addRunArtifact(unit_tests); diff --git a/src/display.zig b/src/display.zig deleted file mode 100644 index 73b9836..0000000 --- a/src/display.zig +++ /dev/null @@ -1,164 +0,0 @@ -const std = @import("std"); -const types = @import("types.zig"); - -// Following Zig 0.15.1 writergate changes - buffer goes in the interface -var stdout_buffer: [4096]u8 = undefined; -var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); -const stdout = &stdout_writer.interface; - -pub fn displayManEntry(entry: types.ManEntry) !void { - try stdout.print("{s} {s}({s}) - {s}\n", .{ entry.getSectionMarker(), entry.name, entry.section, entry.description }); - if (entry.path) |path| { - try stdout.print(" -> {s}\n", .{path}); - } - // Don't forget to flush! - per writergate guidelines - try stdout.flush(); -} - -pub fn displaySearchResults( - entries: types.ManEntryList, - config: types.SearchConfig, - duration_ms: f64, -) !void { - if (config.verbose) { - if (config.target_sections) |sections| { - // e.g., join sections for display - var joined: [32]u8 = undefined; - var pos: usize = 0; - for (sections, 0..) |s, i| { - if (i != 0) { - joined[pos] = ','; - pos += 1; - } - // Copy the string into the buffer - @memcpy(joined[pos .. pos + s.len], s); - pos += s.len; - } - try stdout.print( - "[=] Found {d} entries matching '{s}' in section(s) {s}\n", - .{ entries.items.len, config.keyword, joined[0..pos] }, - ); - try stdout.print("[=] Found {d} entries matching '{s}'\n", .{ entries.items.len, config.keyword }); - } - try stdout.print("\n", .{}); - } else { - if (config.target_sections) |sections| { - // Join sections for display - var joined: [32]u8 = undefined; - var pos: usize = 0; - for (sections, 0..) |s, i| { - if (i != 0) { - joined[pos] = ','; - pos += 1; - } - @memcpy(joined[pos .. pos + s.len], s); - pos += s.len; - } - try stdout.print("[=] Found {d} entries matching '{s}' in section {s}\n\n", .{ entries.items.len, config.keyword, joined[0..pos] }); - } else { - try stdout.print("[=] Found {d} entries matching '{s}'\n\n", .{ entries.items.len, config.keyword }); - } - } - try stdout.flush(); // Flush header - - // Iterate over entries - for (entries.items) |entry| { - try displayManEntry(entry); - } - - if (config.verbose) { - try stdout.print("───────────────────────────────────────────────\n", .{}); - try stdout.print("[=] Completed in {d:.1}ms\n", .{duration_ms}); - try stdout.flush(); // Flush footer - } -} - -pub fn displaySearchStart(config: types.SearchConfig) !void { - if (config.verbose) { - if (config.target_sections) |sections| { - // Join sections for display (same pattern as displaySearchResults) - var joined: [32]u8 = undefined; - var pos: usize = 0; - for (sections, 0..) |s, i| { - if (i != 0) { - joined[pos] = ','; - pos += 1; - } - @memcpy(joined[pos .. pos + s.len], s); - pos += s.len; - } - try stdout.print("[=] Searching for '{s}' in man pages database (section {s})...\n", .{ config.keyword, joined[0..pos] }); - } else { - try stdout.print("[=] Searching for '{s}' in man pages database...\n", .{config.keyword}); - } - try stdout.flush(); // Flush search start message - } -} - -// ============================================================================ -// TESTS -// ============================================================================ - -test "displayManEntry - basic entry" { - const entry = types.ManEntry{ - .name = "ls", - .section = "1", - .description = "list directory contents", - .path = null, - }; - // Verify function doesn't panic - try displayManEntry(entry); -} - -test "displaySearchResults - non-verbose no sections" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var entries = types.ManEntryList.init(allocator); - defer { - for (entries.items) |entry| { - allocator.free(entry.name); - allocator.free(entry.section); - allocator.free(entry.description); - } - entries.deinit(); - } - - try entries.append(types.ManEntry{ - .name = try allocator.dupe(u8, "ls"), - .section = try allocator.dupe(u8, "1"), - .description = try allocator.dupe(u8, "list files"), - .path = null, - }); - - const config = types.SearchConfig{ - .keyword = "ls", - .target_sections = null, - .show_paths = false, - .verbose = false, - }; - - try displaySearchResults(entries, config, 100.0); -} - -test "displaySearchStart - non-verbose" { - const config = types.SearchConfig{ - .keyword = "test", - .target_sections = null, - .show_paths = false, - .verbose = false, - }; - // Should not output anything in non-verbose mode - try displaySearchStart(config); -} - -test "displaySearchStart - verbose" { - const config = types.SearchConfig{ - .keyword = "sleep", - .target_sections = null, - .show_paths = false, - .verbose = true, - }; - try displaySearchStart(config); -} diff --git a/src/main.zig b/src/main.zig index 7f94ee9..cc9762f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,53 +2,44 @@ const std = @import("std"); const types = @import("types.zig"); const parser = @import("parser.zig"); const search = @import("search.zig"); -const display = @import("display.zig"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - // Following Zig 0.15.1 writergate changes - buffer goes in the interface - var stderr_buffer: [1024]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + var stderr_buf: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); const stderr = &stderr_writer.interface; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); - // Parse command line arguments const config = parser.parseArgs(allocator, args) catch |err| switch (err) { error.HelpRequested => return, error.InvalidOption, error.InvalidSection, error.MultipleKeywords, error.NoKeyword => std.process.exit(1), else => return err, }; - // Display search start message - try display.displaySearchStart(config); - - // Perform the search - const start_time = std.time.nanoTimestamp(); - var entries = search.searchManPages(config.keyword, config.target_sections, config.show_paths, config.verbose, allocator, stderr) catch |err| { + var entries = search.searchManPages(config.keyword, config.target_sections, allocator, stderr) catch |err| { try stderr.print("[X] Search failed: {}\n", .{err}); - try stderr.flush(); // Don't forget to flush! return; }; defer { for (entries.items) |entry| { - // Free all the allocated strings from parseManLine allocator.free(entry.name); allocator.free(entry.section); allocator.free(entry.description); - if (entry.path) |path| allocator.free(path); } entries.deinit(allocator); } - // Calculate duration - const end_time = std.time.nanoTimestamp(); - const duration_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0; - - // Display results - try display.displaySearchResults(entries, config, duration_ms); + // Print results (simple name-only format for piping) + var stdout_buf: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_writer.interface; + for (entries.items) |entry| { + try stdout.print("{s}\n", .{entry.name}); + } + try stdout.flush(); } diff --git a/src/parser.zig b/src/parser.zig index 23939f8..d44ddc6 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -8,17 +8,12 @@ pub const HELP_TEXT = \\ \\OPTIONS: \\ -h, --help Show this help message - \\ -v, --verbose Show timing and extra details - \\ --paths Show file paths (slower) \\ -s, --section NUM Only show results from section NUM (1-9) - \\ Can be specified multiple times \\ \\EXAMPLES: \\ manwhere sleep # find all man pages mentioning "sleep" - \\ manwhere -s 1 sleep # find only commands (section 1) mentioning "sleep" - \\ manwhere -s 1 -s 3 sleep # find in sections 1 and 3 - \\ manwhere -v --paths ssl # detailed search with paths and timing - \\ manwhere --section 3 printf # find only library functions (section 3) + \\ manwhere sleep | fzf | xargs man # pipe to fzf for interactive selection + \\ manwhere -s 1 sleep # find only in section 1 \\ ; @@ -31,8 +26,6 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf var keyword: []const u8 = ""; var sections_list = std.ArrayList([]const u8){}; defer sections_list.deinit(allocator); - var show_paths = false; - var verbose = false; var i: usize = 1; while (i < args.len) : (i += 1) { @@ -41,35 +34,20 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { print("{s}", .{HELP_TEXT}); return error.HelpRequested; - } else if (std.mem.eql(u8, arg, "--paths")) { - show_paths = true; - } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--verbose")) { - verbose = true; } else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--section")) { - // Next argument should be the section number if (i + 1 >= args.len) { - print("[X] Option {s} requires a section number (1-9)\n", .{arg}); + print("[X] Option {s} requires a section number\n", .{arg}); return error.InvalidOption; } i += 1; const section_arg = args[i]; - - // Validate section number - if (section_arg.len == 0 or section_arg.len > 4) { - print("[X] Invalid section '{s}'. Use 1-9 or subsections like 3ssl\n", .{section_arg}); + if (section_arg.len == 0 or section_arg[0] < '1' or section_arg[0] > '9') { + print("[X] Invalid section '{s}'\n", .{section_arg}); return error.InvalidSection; } - - // Basic validation - should start with a digit 1-9 - if (section_arg[0] < '1' or section_arg[0] > '9') { - print("[X] Invalid section '{s}'. Section must be 1-9\n", .{section_arg}); - return error.InvalidSection; - } - - // Add section to list try sections_list.append(allocator, section_arg); } else if (std.mem.startsWith(u8, arg, "-")) { - print("[X] Unknown option: {s}\nUse -h or --help for usage information\n", .{arg}); + print("[X] Unknown option: {s}\n", .{arg}); return error.InvalidOption; } else if (keyword.len == 0) { keyword = arg; @@ -80,11 +58,10 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf } if (keyword.len == 0) { - print("[X] No keyword provided\n{s}", .{HELP_TEXT}); + print("[X] No keyword provided\n", .{}); return error.NoKeyword; } - // Convert ArrayList to owned slice, or null if empty const target_sections = if (sections_list.items.len > 0) try sections_list.toOwnedSlice(allocator) else @@ -93,8 +70,6 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf return types.SearchConfig{ .keyword = keyword, .target_sections = target_sections, - .show_paths = show_paths, - .verbose = verbose, }; } @@ -112,32 +87,6 @@ test "parseArgs - basic keyword" { try std.testing.expectEqualStrings("sleep", config.keyword); try std.testing.expect(config.target_sections == null); - try std.testing.expect(!config.show_paths); - try std.testing.expect(!config.verbose); -} - -test "parseArgs - with verbose flag" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "-v", "ls" }; - const config = try parseArgs(allocator, args); - - try std.testing.expectEqualStrings("ls", config.keyword); - try std.testing.expect(config.verbose); -} - -test "parseArgs - with paths flag" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "--paths", "printf" }; - const config = try parseArgs(allocator, args); - - try std.testing.expectEqualStrings("printf", config.keyword); - try std.testing.expect(config.show_paths); } test "parseArgs - with section flag" { diff --git a/src/search.zig b/src/search.zig index f5bceba..e071e9b 100644 --- a/src/search.zig +++ b/src/search.zig @@ -1,332 +1,118 @@ const std = @import("std"); const types = @import("types.zig"); -// Fast man page discovery by directly scanning filesystem -const ManSearcher = struct { +const MAN_DIRS = [_][]const u8{ + "/usr/share/man", + "/usr/local/share/man", + "/opt/homebrew/share/man", + "/opt/local/share/man", + "/usr/X11R6/man", + "/usr/pkg/man", +}; + +fn parseManFilename(filename: []const u8) ?struct { name: []const u8, section: []const u8 } { + var base_name = filename; + for ([_][]const u8{ ".gz", ".bz2", ".xz", ".lz", ".Z" }) |ext| { + if (std.mem.endsWith(u8, base_name, ext)) { + base_name = base_name[0 .. base_name.len - ext.len]; + break; + } + } + + if (std.mem.lastIndexOf(u8, base_name, ".")) |dot_pos| { + if (dot_pos > 0 and dot_pos < base_name.len - 1) { + const name = base_name[0..dot_pos]; + const section = base_name[dot_pos + 1 ..]; + if (section.len > 0 and section.len <= 4) { + return .{ .name = name, .section = section }; + } + } + } + + return null; +} + +fn scanManSection( allocator: std.mem.Allocator, - entries: types.ManEntryList, + entries: *types.ManEntryList, + base_dir: []const u8, + section: []const u8, + keyword: []const u8, +) !void { + var path_buffer: [512]u8 = undefined; + const section_path = try std.fmt.bufPrint(path_buffer[0..], "{s}/man{s}", .{ base_dir, section }); - const Self = @This(); + var dir = std.fs.cwd().openDir(section_path, .{ .iterate = true }) catch return; + defer dir.close(); - // Common man page directories in priority order - const MAN_DIRS = [_][]const u8{ - "/usr/share/man", // Standard Linux/macOS - "/usr/local/share/man", // Local installations - "/opt/homebrew/share/man", // Homebrew on macOS - "/opt/local/share/man", // MacPorts on macOS - "/usr/X11R6/man", // X11 on some systems - "/usr/pkg/man", // pkgsrc on NetBSD - }; + var iterator = dir.iterate(); + while (try iterator.next()) |entry| { + if (entry.kind != .file) continue; - // File extensions to look for (compressed and uncompressed) - const EXTENSIONS = [_][]const u8{ "", ".gz", ".bz2", ".xz", ".lz", ".Z" }; + if (parseManFilename(entry.name)) |parsed| { + if (!std.mem.eql(u8, parsed.section, section)) continue; + if (!keywordMatches(parsed.name, keyword)) continue; - pub fn init(allocator: std.mem.Allocator) Self { - return Self{ - .allocator = allocator, - .entries = .{}, + try entries.append(allocator, .{ + .name = try allocator.dupe(u8, parsed.name), + .section = try allocator.dupe(u8, parsed.section), + .description = try allocator.dupe(u8, ""), + .path = null, + }); + } + } +} + +pub fn searchManPages( + keyword: []const u8, + target_sections: ?[]const []const u8, + allocator: std.mem.Allocator, + stderr: *std.Io.Writer, +) !types.ManEntryList { + var result: types.ManEntryList = .{}; + errdefer { + for (result.items) |entry| { + allocator.free(entry.name); + allocator.free(entry.section); + allocator.free(entry.description); + } + result.deinit(allocator); + } + + const sections = if (target_sections) |sects| + sects + else + &[_][]const u8{ "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + + var path_buffer: [512]u8 = undefined; + + for (MAN_DIRS) |man_dir| { + var base_dir = std.fs.cwd().openDir(man_dir, .{}) catch |err| switch (@as(anyerror, err)) { + error.FileNotFound, error.NotDir, error.AccessDenied => continue, + else => return err, }; - } + defer base_dir.close(); - pub fn deinit(self: *Self) void { - for (self.entries.items) |entry| { - self.allocator.free(entry.name); - self.allocator.free(entry.section); - self.allocator.free(entry.description); - if (entry.path) |path| self.allocator.free(path); - } - self.entries.deinit(self.allocator); - } + for (sections) |section| { + const man_section_path = try std.fmt.bufPrint(path_buffer[0..], "man{s}", .{section}); + _ = base_dir.openDir(man_section_path, .{}) catch |err| switch (@as(anyerror, err)) { + error.FileNotFound, error.NotDir => continue, + error.AccessDenied => continue, + else => return err, + }; - // Extract man page name and section from filename - fn parseManFilename(filename: []const u8) ?struct { name: []const u8, section: []const u8 } { - // Handle compressed extensions first - var base_name = filename; - for (EXTENSIONS[1..]) |ext| { // Skip empty extension - if (std.mem.endsWith(u8, base_name, ext)) { - base_name = base_name[0 .. base_name.len - ext.len]; - break; - } - } - - // Find the last dot to separate name from section - if (std.mem.lastIndexOf(u8, base_name, ".")) |dot_pos| { - if (dot_pos > 0 and dot_pos < base_name.len - 1) { - const name = base_name[0..dot_pos]; - const section = base_name[dot_pos + 1 ..]; - - // Basic validation - section should be reasonable - if (section.len > 0 and section.len <= 4) { - return .{ .name = name, .section = section }; - } - } - } - - return null; - } - - // Get description from man file header (first line parsing) - fn extractDescription(self: Self, file_path: []const u8) []const u8 { - // Try to read the first few lines to extract description - const file = std.fs.cwd().openFile(file_path, .{}) catch return ""; - defer file.close(); - - var buffer: [1024]u8 = undefined; - const bytes_read = file.read(&buffer) catch return ""; - - if (bytes_read == 0) return ""; - - const content = buffer[0..bytes_read]; - - // For compressed files, we'd need to decompress first - // For now, return empty description for compressed files - if (std.mem.indexOf(u8, file_path, ".gz") != null or - std.mem.indexOf(u8, file_path, ".bz2") != null or - std.mem.indexOf(u8, file_path, ".xz") != null) - { - return ""; - } - - // Look for NAME section and extract description - var lines = std.mem.splitScalar(u8, content, '\n'); - var in_name_section = false; - - while (lines.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \t\r\n"); - - if (std.mem.eql(u8, trimmed, ".SH NAME") or - std.mem.eql(u8, trimmed, ".SH \"NAME\"")) - { - in_name_section = true; - continue; - } - - if (in_name_section) { - if (std.mem.startsWith(u8, trimmed, ".SH")) { - break; // End of NAME section - } - - // Look for the description line (usually contains \-) - if (std.mem.indexOf(u8, trimmed, "\\-")) |dash_pos| { - const desc_start = dash_pos + 2; - if (desc_start < trimmed.len) { - const desc = std.mem.trim(u8, trimmed[desc_start..], " \t"); - return self.allocator.dupe(u8, desc) catch ""; - } - } - } - } - - return ""; - } - - // Scan a single man directory section (e.g., man1, man2, etc.) - fn scanManSection(self: *Self, base_dir: []const u8, section: []const u8, show_paths: bool) !void { - var path_buffer: [512]u8 = undefined; - const section_path = try std.fmt.bufPrint(path_buffer[0..], "{s}/man{s}", .{ base_dir, section }); - - var dir = std.fs.cwd().openDir(section_path, .{ .iterate = true }) catch return; - defer dir.close(); - - var iterator = dir.iterate(); - while (try iterator.next()) |entry| { - if (entry.kind != .file) continue; - - // Parse the filename to get name and section - if (parseManFilename(entry.name)) |parsed| { - // Verify section matches directory - if (!std.mem.eql(u8, parsed.section, section)) continue; - - const full_path = if (show_paths) blk: { - var full_path_buf: [768]u8 = undefined; - const full_path_str = try std.fmt.bufPrint(full_path_buf[0..], "{s}/{s}", .{ section_path, entry.name }); - break :blk try self.allocator.dupe(u8, full_path_str); - } else null; - - // Extract description (simplified - just use empty for now) - // In a production implementation, you'd want to parse the man file header - const description = if (show_paths) - self.extractDescription(full_path.?) - else - try self.allocator.dupe(u8, ""); - - const man_entry = types.ManEntry{ - .name = try self.allocator.dupe(u8, parsed.name), - .section = try self.allocator.dupe(u8, parsed.section), - .description = description, - .path = full_path, - }; - - try self.entries.append(self.allocator, man_entry); - } - } - } - - // Main search function - much faster than man -k - // In the searchManPages function of ManSearcher: - pub fn searchManPages( - self: *Self, - keyword: []const u8, - target_sections: ?[]const []const u8, - show_paths: bool, - verbose: bool, - debug_writer: *std.Io.Writer, - ) !void { - if (verbose) { - try debug_writer.print("[=] Scanning man page directories directly...\n", .{}); - try debug_writer.flush(); - } - - // Sections to scan - handle multiple sections - const sections = if (target_sections) |sects| - sects // Use provided sections array - else - &[_][]const u8{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "1m", "3c", "3x", "n" }; - - for (MAN_DIRS) |man_dir| { - // Check if directory exists - var dir = std.fs.cwd().openDir(man_dir, .{}) catch |err| switch (@as(anyerror, err)) { + scanManSection(allocator, &result, man_dir, section, keyword) catch |err| switch (@as(anyerror, err)) { error.FileNotFound, error.NotDir, error.AccessDenied => continue, else => return err, }; - dir.close(); - - if (verbose) { - try debug_writer.print("[=] Scanning {s}...\n", .{man_dir}); - try debug_writer.flush(); - } - - for (sections) |section| { - self.scanManSection(man_dir, section, show_paths) catch |err| switch (@as(anyerror, err)) { - error.FileNotFound, error.NotDir, error.AccessDenied => continue, - else => return err, - }; - } - } - - // Filter by keyword (case-insensitive search in name) - var filtered_entries = types.ManEntryList{}; - defer { - // Move entries to self.entries and clean up filtered_entries - self.entries.deinit(self.allocator); - self.entries = filtered_entries; - } - - for (self.entries.items) |entry| { - // Simple case-insensitive substring search - var name_lower: [256]u8 = undefined; - var keyword_lower: [256]u8 = undefined; - - if (entry.name.len < name_lower.len and keyword.len < keyword_lower.len) { - _ = std.ascii.lowerString(name_lower[0..entry.name.len], entry.name); - _ = std.ascii.lowerString(keyword_lower[0..keyword.len], keyword); - - if (std.mem.indexOf(u8, name_lower[0..entry.name.len], keyword_lower[0..keyword.len]) != null) { - try filtered_entries.append(self.allocator, entry); - } else { - // Clean up unused entry - self.allocator.free(entry.name); - self.allocator.free(entry.section); - self.allocator.free(entry.description); - if (entry.path) |path| self.allocator.free(path); - } - } } } -}; -// Updated search function that uses direct filesystem scanning -pub fn searchManPagesFast( - keyword: []const u8, - target_section: ?[]const []const u8, - show_paths: bool, - verbose: bool, - allocator: std.mem.Allocator, - stderr: *std.Io.Writer, // Post-writergate: concrete type -) !types.ManEntryList { - var searcher = ManSearcher.init(allocator); - defer searcher.deinit(); - - // Create debug writer for verbose output - using stderr for debug messages - try searcher.searchManPages(keyword, target_section, show_paths, verbose, stderr); - - if (searcher.entries.items.len == 0) { - if (target_section) |sections| { - // Join sections for display + if (result.items.len == 0) { + if (target_sections) |sects| { var joined: [32]u8 = undefined; var pos: usize = 0; - for (sections, 0..) |s, i| { - if (i != 0) { - joined[pos] = ','; - pos += 1; - } - @memcpy(joined[pos .. pos + s.len], s); - pos += s.len; - } - try stderr.print("[X] No matches found for '{s}' in section {s}\n", .{ keyword, joined[0..pos] }); - } else { - try stderr.print("[X] No matches found for '{s}'\n", .{keyword}); - } - try stderr.flush(); // Don't forget to flush! - } - - // Transfer ownership of entries - var result: types.ManEntryList = .{}; - try result.appendSlice(allocator, searcher.entries.items); - - // Clear the searcher entries so deinit doesn't free them - searcher.entries.clearRetainingCapacity(); - - return result; -} - -// Original implementation as fallback using child process -fn searchManPagesOriginal( - keyword: []const u8, - target_section: ?[]const []const u8, - show_paths: bool, - verbose: bool, - allocator: std.mem.Allocator, - stderr: *std.io.Writer, -) !types.ManEntryList { - _ = show_paths; - _ = verbose; - - var child = std.process.Child.init(&[_][]const u8{ "man", "-k", keyword }, allocator); - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Ignore; - - try child.spawn(); - - // In 0.15.1, ArrayList is unmanaged - var output_list = std.ArrayList(u8){}; - defer output_list.deinit(allocator); - - // Read from child stdout using the new reader interface - var stdout_buffer: [4096]u8 = undefined; - var stdout_reader_wrapper = child.stdout.?.reader(&stdout_buffer); - const reader = &stdout_reader_wrapper.interface; - - // Read all data - use readAll or read in a loop - var temp_buffer: [4096]u8 = undefined; - while (true) { - const bytes_read = reader.takeByte() catch |err| switch (err) { - error.EndOfStream => break, - else => return err, - }; - if (bytes_read == 0) break; - try output_list.appendSlice(allocator, temp_buffer[0..bytes_read]); - } - - std.debug.print("ouptut_list len: {d}\n", .{output_list.items.len}); - - const term = try child.wait(); - if ((term != .Exited or term.Exited != 0) and output_list.items.len == 0) { - if (target_section) |sections| { - // Join sections for display - var joined: [32]u8 = undefined; - var pos: usize = 0; - for (sections, 0..) |s, i| { + for (sects, 0..) |s, i| { if (i != 0) { joined[pos] = ','; pos += 1; @@ -339,150 +125,81 @@ fn searchManPagesOriginal( try stderr.print("[X] No matches found for '{s}'\n", .{keyword}); } try stderr.flush(); - return .{}; } - var entries: types.ManEntryList = .{}; - errdefer { - for (entries.items) |entry| { - allocator.free(entry.name); - allocator.free(entry.section); - allocator.free(entry.description); - if (entry.path) |path| allocator.free(path); - } - entries.deinit(allocator); - } - - // Process output line by line - var line_iterator = std.mem.splitScalar(u8, output_list.items, '\n'); - while (line_iterator.next()) |raw_line| { - if (raw_line.len == 0) continue; - - var cleaned_buffer: [4096]u8 = undefined; - var cleaned_len: usize = 0; - - for (raw_line) |byte| { - if (cleaned_len >= cleaned_buffer.len - 1) break; - if (byte >= 32 and byte <= 126) { - cleaned_buffer[cleaned_len] = byte; - cleaned_len += 1; - } else if (byte == '\t') { - cleaned_buffer[cleaned_len] = ' '; - cleaned_len += 1; - } else if (byte >= 128) { - cleaned_buffer[cleaned_len] = '?'; - cleaned_len += 1; - } - } - - // const cleaned_line = cleaned_buffer[0..cleaned_len]; - // if (cleaned_line.len == 0) continue; - // - // _ = cleaned_line; // Your parsing logic here - } - - return entries; + return result; } -// Main search function with fallback strategy -pub fn searchManPages( - keyword: []const u8, - target_section: ?[]const []const u8, - show_paths: bool, - verbose: bool, - allocator: std.mem.Allocator, - stderr: *std.Io.Writer, // Post-writergate: concrete type -) !types.ManEntryList { - // Try fast method first, fallback to man -k if needed - return searchManPagesFast(keyword, target_section, show_paths, verbose, allocator, stderr) catch |err| { - if (verbose) { - try stderr.print("[W] Fast search failed ({}), falling back to man -k...\n", .{err}); - try stderr.flush(); // Don't forget to flush! - } +fn keywordMatches(name: []const u8, keyword: []const u8) bool { + if (name.len > 256 or keyword.len > 256) return false; - // Fallback to original man -k implementation - return searchManPagesOriginal(keyword, target_section, show_paths, verbose, allocator, stderr); - }; + var name_lower: [256]u8 = undefined; + var keyword_lower: [256]u8 = undefined; + + _ = std.ascii.lowerString(name_lower[0..name.len], name); + _ = std.ascii.lowerString(keyword_lower[0..keyword.len], keyword); + + return std.mem.indexOf(u8, name_lower[0..name.len], keyword_lower[0..keyword.len]) != null; } // ============================================================================ // TESTS // ============================================================================ -test "ManSearcher - basic initialization" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - // Test the types ManEntryList uses - var entries = types.ManEntryList.init(allocator); - defer { - for (entries.items) |entry| { - allocator.free(entry.name); - allocator.free(entry.section); - allocator.free(entry.description); - } - entries.deinit(); - } - - try std.testing.expectEqual(@as(usize, 0), entries.items.len); +test "parseManFilename - basic" { + const result = parseManFilename("ls.1"); + try std.testing.expect(result != null); + try std.testing.expectEqualStrings("ls", result.?.name); + try std.testing.expectEqualStrings("1", result.?.section); } -test "ManEntry - full lifecycle" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const entry = types.ManEntry{ - .name = try allocator.dupe(u8, "grep"), - .section = try allocator.dupe(u8, "1"), - .description = try allocator.dupe(u8, "global regular expression print"), - .path = try allocator.dupe(u8, "/usr/share/man/man1/grep.1.gz"), - }; - defer { - allocator.free(entry.name); - allocator.free(entry.section); - allocator.free(entry.description); - if (entry.path) |path| allocator.free(path); - } - - try std.testing.expectEqualStrings("grep", entry.name); - try std.testing.expectEqualStrings("1", entry.section); - try std.testing.expectEqualStrings("global regular expression print", entry.description); - try std.testing.expect(entry.path != null); - try std.testing.expectEqualStrings("/usr/share/man/man1/grep.1.gz", entry.path.?); +test "parseManFilename - with gz extension" { + const result = parseManFilename("ls.1.gz"); + try std.testing.expect(result != null); + try std.testing.expectEqualStrings("ls", result.?.name); + try std.testing.expectEqualStrings("1", result.?.section); } -test "ManEntryList - append multiple" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var entries = types.ManEntryList.init(allocator); - defer { - for (entries.items) |entry| { - allocator.free(entry.name); - allocator.free(entry.section); - allocator.free(entry.description); - } - entries.deinit(); - } - - try entries.append(types.ManEntry{ - .name = try allocator.dupe(u8, "ls"), - .section = try allocator.dupe(u8, "1"), - .description = try allocator.dupe(u8, "list"), - .path = null, - }); - - try entries.append(types.ManEntry{ - .name = try allocator.dupe(u8, "cat"), - .section = try allocator.dupe(u8, "1"), - .description = try allocator.dupe(u8, "concatenate"), - .path = null, - }); - - try std.testing.expectEqual(@as(usize, 2), entries.items.len); - try std.testing.expectEqualStrings("ls", entries.items[0].name); - try std.testing.expectEqualStrings("cat", entries.items[1].name); +test "parseManFilename - library function" { + const result = parseManFilename("printf.3"); + try std.testing.expect(result != null); + try std.testing.expectEqualStrings("printf", result.?.name); + try std.testing.expectEqualStrings("3", result.?.section); +} + +test "parseManFilename - subsection" { + const result = parseManFilename("SSL_new.3ssl"); + try std.testing.expect(result != null); + try std.testing.expectEqualStrings("SSL_new", result.?.name); + try std.testing.expectEqualStrings("3ssl", result.?.section); +} + +test "parseManFilename - no extension" { + const result = parseManFilename("ls"); + try std.testing.expect(result == null); +} + +test "keywordMatches - exact match" { + try std.testing.expect(keywordMatches("ls", "ls")); +} + +test "keywordMatches - case insensitive" { + try std.testing.expect(keywordMatches("LS", "ls")); + try std.testing.expect(keywordMatches("ls", "LS")); +} + +test "keywordMatches - partial match" { + try std.testing.expect(keywordMatches("perlsec", "sec")); + try std.testing.expect(keywordMatches("ls", "l")); +} + +test "keywordMatches - no match" { + try std.testing.expect(!keywordMatches("ls", "cat")); + try std.testing.expect(!keywordMatches("ls", "xyz")); +} + +test "keywordMatches - too long strings" { + const long_name = "a" ** 300; + try std.testing.expect(!keywordMatches(long_name, "ls")); + try std.testing.expect(!keywordMatches("ls", long_name)); } diff --git a/src/types.zig b/src/types.zig index 058d0a8..644a0bb 100644 --- a/src/types.zig +++ b/src/types.zig @@ -50,8 +50,6 @@ pub const ManEntryList = std.ArrayList(ManEntry); pub const SearchConfig = struct { keyword: []const u8, target_sections: ?[]const []const u8, - show_paths: bool, - verbose: bool, }; // ============================================================================