From 50244b7eb92e2eb8f654abff1e1e49fc7791aa54 Mon Sep 17 00:00:00 2001 From: Jeremie Fraeys Date: Tue, 10 Feb 2026 18:05:51 -0500 Subject: [PATCH] Convert manwhere to submodule --- .gitmodules | 3 + scripts/manwhere | 1 + scripts/manwhere/README.md | 162 ---------- scripts/manwhere/build.zig | 139 --------- scripts/manwhere/src/display.zig | 164 ----------- scripts/manwhere/src/main.zig | 54 ---- scripts/manwhere/src/parser.zig | 203 ------------- scripts/manwhere/src/search.zig | 488 ------------------------------- scripts/manwhere/src/types.zig | 168 ----------- 9 files changed, 4 insertions(+), 1378 deletions(-) create mode 100644 .gitmodules create mode 160000 scripts/manwhere delete mode 100644 scripts/manwhere/README.md delete mode 100644 scripts/manwhere/build.zig delete mode 100644 scripts/manwhere/src/display.zig delete mode 100644 scripts/manwhere/src/main.zig delete mode 100644 scripts/manwhere/src/parser.zig delete mode 100644 scripts/manwhere/src/search.zig delete mode 100644 scripts/manwhere/src/types.zig diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bfd0cd6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "scripts/manwhere"] + path = scripts/manwhere + url = https://git.jfraeys.com/jfraeysd/manwhere.git diff --git a/scripts/manwhere b/scripts/manwhere new file mode 160000 index 0000000..26eb50d --- /dev/null +++ b/scripts/manwhere @@ -0,0 +1 @@ +Subproject commit 26eb50d38f31ad0f791242e84481f57c4d2326e9 diff --git a/scripts/manwhere/README.md b/scripts/manwhere/README.md deleted file mode 100644 index ec77314..0000000 --- a/scripts/manwhere/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# manwhere - -A fast, simple command-line tool to search man pages by keyword, written in Zig. - -## Features - -- **Fast keyword search** using `man -k` (apropos) -- **Section filtering** to search specific man page sections (1-9) -- **Path resolution** to show actual file locations -- **Verbose mode** with timing information -- **POSIX compatible** - works on Linux and macOS -- **Simple and reliable** - no complex dependencies - -## Project Structure - -The project is organized into logical modules for maintainability: - -``` -src/ -├── main.zig # Main program entry point -├── types.zig # Data structures and types -├── parser.zig # Command line argument parsing -├── search.zig # Man page search functionality -├── display.zig # Output formatting and display -└── performance.zig # Essential performance optimizations -``` - -### Module Responsibilities - -- **`types.zig`**: Contains `ManEntry`, `ManEntryList`, and `SearchConfig` structures -- **`parser.zig`**: Handles command line argument parsing, validation, and help text -- **`search.zig`**: Manages man page searching, parsing, and path resolution -- **`display.zig`**: Formats and displays search results and progress information -- **`performance.zig`**: Essential optimizations for fast string operations and I/O -- **`main.zig`**: Orchestrates the program flow and coordinates between modules - -## Performance Features - -The tool is optimized for speed while maintaining simplicity: - -- **Early filtering**: Section filtering happens during parsing for better performance -- **Efficient I/O**: Optimized buffer sizes and reading strategies -- **Fast string operations**: Optimized string comparisons for common cases -- **Memory efficiency**: Exponential growth strategy for collections -- **POSIX optimization**: Common man page directories checked first - -## Usage - -### Basic Search -```bash -manwhere sleep # Find all man pages mentioning "sleep" -``` - -### Section Filtering -```bash -manwhere -s 1 sleep # Find only commands (section 1) -manwhere --section 3 sleep # Find only library functions (section 3) -``` - -### Verbose Mode -```bash -manwhere -v sleep # Show timing and processing details -manwhere --verbose sleep # Same as above -``` - -### Path Resolution -```bash -manwhere --paths sleep # Show file paths (slower but more detailed) -``` - -### Combined Options -```bash -manwhere -v --paths -s 1 sleep # Verbose, with paths, section 1 only -``` - -### Help -```bash -manwhere -h # Show help message -manwhere --help # Same as above -``` - -## Man Page Sections - -- **1**: Commands (user commands) -- **2**: System calls -- **3**: Library functions -- **4**: Device files -- **5**: File formats -- **6**: Games -- **7**: Miscellaneous -- **8**: System administration -- **9**: Kernel routines - -## Building - -### Prerequisites -- Zig 0.15.1 or later - -### Build Commands -```bash -# Debug build (good for development) -zig build - -# Release build (maximum performance) -zig build -Doptimize=ReleaseFast - -# Clean build -zig build clean -``` - -### Install to ~/.local/bin -```bash -zig build release -``` - -## Performance - -The tool is designed to be fast and efficient: - -- **Typical search time**: 700-800ms for common keywords -- **Section filtering**: 20-30% faster than searching all sections -- **Path resolution**: Adds ~200-300ms for detailed results -- **Memory usage**: ~8-12MB for typical searches -- **Scalability**: Handles large result sets efficiently - -## POSIX Compatibility - -Works reliably on: -- **Linux**: Standard `/usr/share/man` and `/usr/local/share/man` -- **macOS**: Standard paths plus Homebrew (`/opt/homebrew/share/man`) and MacPorts (`/opt/local/share/man`) -- **Other Unix-like systems**: Standard man page directory structures - -## Error Handling - -The program provides clear error messages for: -- Invalid command line options -- Missing required arguments -- Invalid section numbers -- Multiple keywords (not supported) -- Search failures - -## Why Simple and Fast? - -This tool prioritizes: -1. **Reliability** - Works consistently across POSIX systems -2. **Speed** - Essential optimizations without complexity -3. **Simplicity** - Easy to understand, maintain, and debug -4. **POSIX compatibility** - No platform-specific dependencies -5. **Performance** - Fast enough for daily use without over-engineering - -## Contributing - -The simple structure makes it easy to: -- Add new search features -- Implement different output formats -- Add support for additional platforms -- Extend the man page section detection -- Optimize performance further - -## License - -This project is open source. See the source code for license details. diff --git a/scripts/manwhere/build.zig b/scripts/manwhere/build.zig deleted file mode 100644 index 7f3be41..0000000 --- a/scripts/manwhere/build.zig +++ /dev/null @@ -1,139 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // ============================================================================ - // MODULES - // ============================================================================ - - // Main module (uses standard optimize option) - const root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // Debug module (always Debug optimization) - const debug_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = .Debug, - }); - - // Release module (always ReleaseFast optimization) - const release_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = .ReleaseFast, - }); - - // Test module (uses standard optimize option) - const test_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // ============================================================================ - // EXECUTABLES - // ============================================================================ - - // Main executable - const exe = b.addExecutable(.{ - .name = "manwhere", - .root_module = root_module, - }); - b.installArtifact(exe); - - // Debug executable - const debug_exe = b.addExecutable(.{ - .name = "manwhere-debug", - .root_module = debug_module, - }); - - // Release executable - const release_exe = b.addExecutable(.{ - .name = "manwhere", - .root_module = release_module, - }); - - // Check executable (for compilation check) - const check_exe = b.addExecutable(.{ - .name = "check-compile", - .root_module = root_module, - }); - - // Test executable - const unit_tests = b.addTest(.{ - .root_module = test_module, - }); - - // ============================================================================ - // BUILD STEPS - // ============================================================================ - - // Default run step - const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| run_cmd.addArgs(args); - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); - - // Debug build step - const debug_step = b.step("debug", "Build with Debug optimization"); - b.installArtifact(debug_exe); - debug_step.dependOn(b.getInstallStep()); - - // Debug run step - const debug_run_cmd = b.addRunArtifact(debug_exe); - if (b.args) |args| debug_run_cmd.addArgs(args); - debug_run_cmd.step.dependOn(&debug_exe.step); - const debug_run_step = b.step("debug-run", "Run the app with Debug optimization"); - debug_run_step.dependOn(&debug_run_cmd.step); - - // Release step - build optimized binary and install to ~/.local/bin - const release_step = b.step("release", "Build optimized binary and install to ~/.local/bin/"); - - // Get home directory from environment - const home = std.process.getEnvVarOwned(b.allocator, "HOME") catch { - std.debug.print("Error: HOME environment variable not set\n", .{}); - return; - }; - defer b.allocator.free(home); - - const install_path = std.fs.path.join(b.allocator, &.{ home, ".local", "bin" }) catch { - std.debug.print("Error: Failed to construct install path\n", .{}); - return; - }; - defer b.allocator.free(install_path); - - const install_exe = b.addInstallArtifact(release_exe, .{ - .dest_dir = .{ .override = .{ .custom = install_path } }, - }); - release_step.dependOn(&install_exe.step); - - // Test step - run all unit tests - const run_unit_tests = b.addRunArtifact(unit_tests); - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_unit_tests.step); - - // Check step - compilation check without output - const check_step = b.step("check", "Check code formatting and compilation"); - check_step.dependOn(&check_exe.step); - - // Format step - format source code - const fmt_step = b.step("fmt", "Format source code"); - const fmt_cmd = b.addFmt(.{ - .paths = &.{ "src", "build.zig" }, - .check = false, - }); - fmt_step.dependOn(&fmt_cmd.step); - - // Clean step - remove build artifacts - const clean_step = b.step("clean", "Remove build artifacts"); - const clean_cmd = b.addSystemCommand(&.{ - "rm", "-rf", ".zig-cache", "zig-out", - }); - clean_step.dependOn(&clean_cmd.step); -} diff --git a/scripts/manwhere/src/display.zig b/scripts/manwhere/src/display.zig deleted file mode 100644 index 73b9836..0000000 --- a/scripts/manwhere/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/scripts/manwhere/src/main.zig b/scripts/manwhere/src/main.zig deleted file mode 100644 index 7f94ee9..0000000 --- a/scripts/manwhere/src/main.zig +++ /dev/null @@ -1,54 +0,0 @@ -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); - 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| { - 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); -} diff --git a/scripts/manwhere/src/parser.zig b/scripts/manwhere/src/parser.zig deleted file mode 100644 index 23939f8..0000000 --- a/scripts/manwhere/src/parser.zig +++ /dev/null @@ -1,203 +0,0 @@ -const std = @import("std"); -const types = @import("types.zig"); -const print = std.debug.print; - -pub const HELP_TEXT = - \\Usage: manwhere [OPTIONS] - \\Find man pages containing a keyword. - \\ - \\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) - \\ -; - -pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConfig { - if (args.len < 2) { - print("{s}", .{HELP_TEXT}); - return error.NoKeyword; - } - - 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) { - const arg = args[i]; - - 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}); - 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}); - 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}); - return error.InvalidOption; - } else if (keyword.len == 0) { - keyword = arg; - } else { - print("[X] Multiple keywords not supported\n", .{}); - return error.MultipleKeywords; - } - } - - if (keyword.len == 0) { - print("[X] No keyword provided\n{s}", .{HELP_TEXT}); - 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 - null; - - return types.SearchConfig{ - .keyword = keyword, - .target_sections = target_sections, - .show_paths = show_paths, - .verbose = verbose, - }; -} - -// ============================================================================ -// TESTS -// ============================================================================ - -test "parseArgs - basic keyword" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "sleep" }; - const config = try parseArgs(allocator, args); - - 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" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "-s", "1", "ls" }; - const config = try parseArgs(allocator, args); - - try std.testing.expectEqualStrings("ls", config.keyword); - try std.testing.expect(config.target_sections != null); - try std.testing.expectEqual(@as(usize, 1), config.target_sections.?.len); - try std.testing.expectEqualStrings("1", config.target_sections.?[0]); -} - -test "parseArgs - with multiple sections" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "-s", "1", "-s", "3", "test" }; - const config = try parseArgs(allocator, args); - - try std.testing.expectEqualStrings("test", config.keyword); - try std.testing.expect(config.target_sections != null); - try std.testing.expectEqual(@as(usize, 2), config.target_sections.?.len); - try std.testing.expectEqualStrings("1", config.target_sections.?[0]); - try std.testing.expectEqualStrings("3", config.target_sections.?[1]); -} - -test "parseArgs - error no keyword" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{"manwhere"}; - const result = parseArgs(allocator, args); - - try std.testing.expectError(error.NoKeyword, result); -} - -test "parseArgs - error invalid option" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "--invalid", "ls" }; - const result = parseArgs(allocator, args); - - try std.testing.expectError(error.InvalidOption, result); -} - -test "parseArgs - error multiple keywords" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = &[_][:0]u8{ "manwhere", "ls", "cat" }; - const result = parseArgs(allocator, args); - - try std.testing.expectError(error.MultipleKeywords, result); -} diff --git a/scripts/manwhere/src/search.zig b/scripts/manwhere/src/search.zig deleted file mode 100644 index f5bceba..0000000 --- a/scripts/manwhere/src/search.zig +++ /dev/null @@ -1,488 +0,0 @@ -const std = @import("std"); -const types = @import("types.zig"); - -// Fast man page discovery by directly scanning filesystem -const ManSearcher = struct { - allocator: std.mem.Allocator, - entries: types.ManEntryList, - - const Self = @This(); - - // 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 - }; - - // File extensions to look for (compressed and uncompressed) - const EXTENSIONS = [_][]const u8{ "", ".gz", ".bz2", ".xz", ".lz", ".Z" }; - - pub fn init(allocator: std.mem.Allocator) Self { - return Self{ - .allocator = allocator, - .entries = .{}, - }; - } - - 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); - } - - // 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)) { - 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 - 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| { - 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(); - 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; -} - -// 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! - } - - // Fallback to original man -k implementation - return searchManPagesOriginal(keyword, target_section, show_paths, verbose, allocator, stderr); - }; -} - -// ============================================================================ -// 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 "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 "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); -} diff --git a/scripts/manwhere/src/types.zig b/scripts/manwhere/src/types.zig deleted file mode 100644 index 058d0a8..0000000 --- a/scripts/manwhere/src/types.zig +++ /dev/null @@ -1,168 +0,0 @@ -const std = @import("std"); - -pub const Allocator = std.mem.Allocator; - -pub const ManEntry = struct { - name: []const u8, - section: []const u8, - description: []const u8, - path: ?[]const u8 = null, - - pub fn getSectionMarker(self: ManEntry) []const u8 { - if (self.section.len == 0) return "[?] Unknown"; - // Fast path for common sections - return switch (self.section[0]) { - '1' => "[*] Command", - '2' => "[S] Syscall", - '3' => blk: { - // Check for specific 3xxx subsections (order matters - longer first) - if (std.mem.startsWith(u8, self.section, "3ssl")) break :blk "[L] OpenSSL"; - if (std.mem.startsWith(u8, self.section, "3tcl")) break :blk "[L] Tcl API"; - if (std.mem.startsWith(u8, self.section, "3pm")) break :blk "[L] Perl"; - if (std.mem.startsWith(u8, self.section, "3p")) break :blk "[L] POSIX"; - if (std.mem.startsWith(u8, self.section, "3x")) break :blk "[L] X11/ncurses"; - break :blk "[L] C Library"; - }, - '4' => "[D] Device", - '5' => "[F] Format", - '6' => "[G] Game", - '7' => "[M] Misc", - '8' => "[A] Admin", - '9' => "[K] Kernel", - else => "[?] Unknown", - }; - } - - pub fn matchesSection(self: ManEntry, target_sections: ?[]const []const u8) bool { - if (target_sections == null) return true; - for (target_sections.?) |section| { - if (self.section.len == 0) continue; - if (std.mem.eql(u8, self.section, section)) return true; - if (section.len == 1 and std.mem.startsWith(u8, self.section, section)) return true; - } - return false; - } -}; - -// Use modern ArrayList instead of custom implementation -pub const ManEntryList = std.ArrayList(ManEntry); - -pub const SearchConfig = struct { - keyword: []const u8, - target_sections: ?[]const []const u8, - show_paths: bool, - verbose: bool, -}; - -// ============================================================================ -// TESTS -// ============================================================================ - -test "ManEntry.getSectionMarker - commands" { - const entry = ManEntry{ - .name = "ls", - .section = "1", - .description = "list directory contents", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[*] Command", marker); -} - -test "ManEntry.getSectionMarker - syscalls" { - const entry = ManEntry{ - .name = "open", - .section = "2", - .description = "open file", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[S] Syscall", marker); -} - -test "ManEntry.getSectionMarker - C library" { - const entry = ManEntry{ - .name = "printf", - .section = "3", - .description = "format and print data", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[L] C Library", marker); -} - -test "ManEntry.getSectionMarker - OpenSSL" { - const entry = ManEntry{ - .name = "SSL_new", - .section = "3ssl", - .description = "create SSL structure", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[L] OpenSSL", marker); -} - -test "ManEntry.getSectionMarker - POSIX" { - const entry = ManEntry{ - .name = "pthread_create", - .section = "3p", - .description = "create thread", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[L] POSIX", marker); -} - -test "ManEntry.getSectionMarker - kernel" { - const entry = ManEntry{ - .name = "kmalloc", - .section = "9", - .description = "kernel memory alloc", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[K] Kernel", marker); -} - -test "ManEntry.getSectionMarker - unknown" { - const entry = ManEntry{ - .name = "unknown", - .section = "0", - .description = "invalid section", - .path = null, - }; - const marker = entry.getSectionMarker(); - try std.testing.expectEqualStrings("[?] Unknown", marker); -} - -test "ManEntry.matchesSection - no target" { - const entry = ManEntry{ - .name = "ls", - .section = "1", - .description = "list", - .path = null, - }; - try std.testing.expect(entry.matchesSection(null)); -} - -test "ManEntry.matchesSection - exact match" { - const entry = ManEntry{ - .name = "ls", - .section = "1", - .description = "list", - .path = null, - }; - const targets = &[_][]const u8{"1"}; - try std.testing.expect(entry.matchesSection(targets)); -} - -test "ManEntry.matchesSection - no match" { - const entry = ManEntry{ - .name = "ls", - .section = "1", - .description = "list", - .path = null, - }; - const targets = &[_][]const u8{"3"}; - try std.testing.expect(!entry.matchesSection(targets)); -}