whi 0.1.0

Magically simple PATH management - improved which with PATH manipulation
use std::env;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorWhen {
    Auto,
    Never,
    Always,
}

#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct Args {
    pub names: Vec<String>,
    pub all: bool,
    pub full: bool,
    pub follow_symlinks: bool,
    pub print0: bool,
    pub quiet: bool,
    pub silent: bool,
    pub one: bool,
    pub show_nonexec: bool,
    pub path_override: Option<String>,
    pub color: ColorWhen,
    pub stat: bool,
    pub index: bool,
    pub move_indices: Option<(usize, usize)>,
    pub swap_indices: Option<(usize, usize)>,
    pub prefer_target: Option<(String, usize)>,
    pub init_shell: Option<String>,
}

impl Args {
    #[allow(clippy::too_many_lines)]
    pub fn parse() -> Result<Self, String> {
        let mut args = Args {
            names: Vec::new(),
            all: false,
            full: false,
            follow_symlinks: false,
            print0: false,
            quiet: false,
            silent: false,
            one: false,
            show_nonexec: false,
            path_override: None,
            color: ColorWhen::Auto,
            stat: false,
            index: false,
            move_indices: None,
            swap_indices: None,
            prefer_target: None,
            init_shell: None,
        };

        let args_vec: Vec<String> = env::args().skip(1).collect();

        // Handle 'init' subcommand early
        if let Some(idx) = args_vec.iter().position(|a| a == "init") {
            if idx + 1 >= args_vec.len() {
                return Err("init requires a shell argument (bash, zsh, fish)".to_string());
            }
            args.init_shell = Some(args_vec[idx + 1].clone());
            return Ok(args);
        }

        let mut expect_path = false;
        let mut expect_color = false;
        let mut expect_move_from = false;
        let mut expect_swap_from = false;
        let mut expect_prefer_name = false;
        let mut move_from: Option<usize> = None;
        let mut swap_from: Option<usize> = None;
        let mut prefer_name: Option<String> = None;

        for arg in args_vec {
            if expect_path {
                args.path_override = Some(arg);
                expect_path = false;
                continue;
            }

            if expect_color {
                args.color = Self::parse_color(&arg)?;
                expect_color = false;
                continue;
            }

            if expect_move_from {
                let from = arg
                    .parse::<usize>()
                    .map_err(|_| format!("Invalid move source index: {arg}"))?;
                move_from = Some(from);
                expect_move_from = false;
                continue;
            }

            if let Some(from) = move_from {
                let to = arg
                    .parse::<usize>()
                    .map_err(|_| format!("Invalid move destination index: {arg}"))?;
                args.move_indices = Some((from, to));
                move_from = None;
                continue;
            }

            if expect_swap_from {
                let idx1 = arg
                    .parse::<usize>()
                    .map_err(|_| format!("Invalid swap first index: {arg}"))?;
                swap_from = Some(idx1);
                expect_swap_from = false;
                continue;
            }

            if let Some(idx1) = swap_from {
                let idx2 = arg
                    .parse::<usize>()
                    .map_err(|_| format!("Invalid swap second index: {arg}"))?;
                args.swap_indices = Some((idx1, idx2));
                swap_from = None;
                continue;
            }

            if expect_prefer_name {
                prefer_name = Some(arg);
                expect_prefer_name = false;
                continue;
            }

            if let Some(name) = prefer_name {
                let idx = arg
                    .parse::<usize>()
                    .map_err(|_| format!("Invalid prefer index: {arg}"))?;
                args.prefer_target = Some((name, idx));
                prefer_name = None;
                continue;
            }

            Self::process_arg(
                &mut args,
                &arg,
                &mut expect_path,
                &mut expect_color,
                &mut expect_move_from,
                &mut expect_swap_from,
                &mut expect_prefer_name,
            )?;
        }

        if expect_path {
            return Err("--path requires a value".to_string());
        }
        if expect_color {
            return Err("--color requires a value".to_string());
        }
        if expect_move_from || move_from.is_some() {
            return Err("--move requires two indices: FROM TO".to_string());
        }
        if expect_swap_from || swap_from.is_some() {
            return Err("--swap requires two indices: IDX1 IDX2".to_string());
        }
        if expect_prefer_name || prefer_name.is_some() {
            return Err("--prefer requires NAME and INDEX".to_string());
        }

        Ok(args)
    }

    fn parse_color(val: &str) -> Result<ColorWhen, String> {
        match val {
            "auto" => Ok(ColorWhen::Auto),
            "never" => Ok(ColorWhen::Never),
            "always" => Ok(ColorWhen::Always),
            _ => Err(format!("Invalid color value: {val}")),
        }
    }

    fn process_arg(
        args: &mut Args,
        arg: &str,
        expect_path: &mut bool,
        expect_color: &mut bool,
        expect_move_from: &mut bool,
        expect_swap_from: &mut bool,
        expect_prefer_name: &mut bool,
    ) -> Result<(), String> {
        match arg {
            "-a" | "--all" => args.all = true,
            "-f" | "--full" => args.full = true,
            "-i" | "--index" => args.index = true,
            "-l" | "-L" | "--follow-symlinks" => args.follow_symlinks = true,
            "--move" => *expect_move_from = true,
            "--swap" => *expect_swap_from = true,
            "--prefer" => *expect_prefer_name = true,
            "-o" | "--one" => args.one = true,
            "-0" | "--print0" => args.print0 = true,
            "-q" | "--quiet" => args.quiet = true,
            "-s" | "--stat" => args.stat = true,
            "--silent" => args.silent = true,
            "--show-nonexec" => args.show_nonexec = true,
            "--path" => *expect_path = true,
            "--color" => *expect_color = true,
            "-h" | "--help" => {
                print_help();
                std::process::exit(0);
            }
            s if s.starts_with("--path=") => {
                args.path_override = Some(s.trim_start_matches("--path=").to_string());
            }
            s if s.starts_with("--color=") => {
                let val = s.trim_start_matches("--color=");
                args.color = Self::parse_color(val)?;
            }
            s if s.starts_with('-') && !s.starts_with("--") && s.len() > 2 => {
                Self::parse_combined_flags(args, s)?;
            }
            s if s.starts_with('-') => {
                return Err(format!("Unknown option: {s}"));
            }
            _ => args.names.push(arg.to_string()),
        }
        Ok(())
    }

    fn parse_combined_flags(args: &mut Args, s: &str) -> Result<(), String> {
        // Handle combined short flags like -af, -ail, -aifL
        for ch in s[1..].chars() {
            match ch {
                'a' => args.all = true,
                'f' => args.full = true,
                'i' => args.index = true,
                'l' | 'L' => args.follow_symlinks = true,
                'o' => args.one = true,
                's' => args.stat = true,
                '0' => args.print0 = true,
                'q' => args.quiet = true,
                'h' => {
                    print_help();
                    std::process::exit(0);
                }
                _ => return Err(format!("Unknown flag: -{ch}")),
            }
        }
        Ok(())
    }
}

fn print_help() {
    println!(
        "whi - magically simple PATH management

USAGE:
    whi [FLAGS] [OPTIONS] <NAME>...
    whi [FLAGS] [OPTIONS]            # names from stdin
    whi --move <FROM> <TO>           # reorder PATH
    whi --swap <IDX1> <IDX2>         # swap PATH entries
    whi --prefer <NAME> <INDEX>      # prefer executable at INDEX
    whi init <SHELL>                 # output shell integration

FLAGS:
    -a, --all              Show all PATH matches (default: only winner)
    -f, --full             Show all matches + full PATH listing (implies -a)
    -i, --index            Show PATH index next to each entry
    -l, -L, --follow-symlinks
                           Resolve and show canonical targets
    -s, --stat             Include inode/device/mtime/size metadata
    -0, --print0           NUL-separated output for xargs
    -q, --quiet            Suppress non-fatal stderr warnings
        --silent           Print nothing to stderr, use exit codes only
    -o, --one              Only print the first match per name
        --show-nonexec     Also list files that exist but aren't executable
    -h, --help             Print help information

PATH MANIPULATION:
        --move <FROM> <TO> Move PATH entry from index FROM to index TO
        --swap <IDX1> <IDX2>
                           Swap PATH entries at indices IDX1 and IDX2
        --prefer <NAME> <INDEX>
                           Make executable NAME at INDEX win by moving it
                           just before the current winner (minimal change)

OPTIONS:
        --path <PATH>      Override environment PATH string
        --color <WHEN>     Colorize output: auto, never, always [default: auto]

SHELL INTEGRATION:
    whi init bash       Output bash integration code
    whi init zsh        Output zsh integration code
    whi init fish       Output fish integration code

    Add to your shell config:
        bash/zsh: eval \"$(whi init bash)\" or eval \"$(whi init zsh)\"
        fish:     whi init fish | source

    Provides shell commands to manipulate PATH in current shell:
        whim 10 1      # Move PATH entry 10 to position 1
        whis 10 41     # Swap PATH entries 10 and 41
        whip cargo 50  # Make cargo at index 50 the winner
        whia cargo     # Show all cargo matches with indices (whi -ia)
        whii           # Show all PATH entries with indices (whi -i)

EXIT CODES:
    0  All names found
    1  At least one not found
    2  Usage error
    3  I/O or environment error"
    );
}