gitpp 0.7.3

Git Personal Parallel Manager — manage 100+ Git repos with one command
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{Context, Editor, Helper};
use std::borrow::Cow;

pub struct GitppHelper {
    commands: Vec<String>,
}

impl GitppHelper {
    pub fn new() -> Self {
        GitppHelper {
            commands: vec![
                "clone".to_string(),
                "pull".to_string(),
                "push".to_string(),
                "status".to_string(),
                "diff".to_string(),
                "fetch".to_string(),
                "branch".to_string(),
                "switch".to_string(),
                "stash list".to_string(),
                "gc".to_string(),
                "help".to_string(),
                "exit".to_string(),
                "quit".to_string(),
            ],
        }
    }
}

impl Completer for GitppHelper {
    type Candidate = Pair;

    fn complete(
        &self,
        line: &str,
        pos: usize,
        _ctx: &Context<'_>,
    ) -> rustyline::Result<(usize, Vec<Pair>)> {
        let input = &line[..pos];
        let mut candidates = Vec::new();

        for cmd in &self.commands {
            if cmd.starts_with(input) {
                candidates.push(Pair {
                    display: cmd.clone(),
                    replacement: cmd.clone(),
                });
            }
        }

        Ok((0, candidates))
    }
}

impl Hinter for GitppHelper {
    type Hint = String;

    fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option<String> {
        if pos < line.len() {
            return None;
        }

        for cmd in &self.commands {
            if cmd.starts_with(line) && cmd != line {
                return Some(cmd[line.len()..].to_string());
            }
        }

        None
    }
}

impl Highlighter for GitppHelper {
    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
        if self.commands.iter().any(|c| c == line) {
            Cow::Owned(format!("\x1b[1;36m{line}\x1b[0m"))
        } else {
            Cow::Borrowed(line)
        }
    }

    fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
        true
    }
}

impl Validator for GitppHelper {}

impl Helper for GitppHelper {}

pub fn run_interactive_mode() -> rustyline::Result<Vec<String>> {
    let helper = GitppHelper::new();
    let mut rl = Editor::new()?;
    rl.set_helper(Some(helper));

    let history_file = dirs::home_dir()
        .map(|mut path| {
            path.push(".gitpp_history");
            path
        })
        .unwrap_or_else(|| std::path::PathBuf::from(".gitpp_history"));

    let _ = rl.load_history(&history_file);

    println!("\x1b[1;36mgitpp\x1b[0m - Git Personal Parallel Manager");
    println!(
        "Type '\x1b[1;33mhelp\x1b[0m' for available commands, '\x1b[1;33mexit\x1b[0m' to quit\n"
    );

    loop {
        let readline = rl.readline("\x1b[1;36mgitpp>\x1b[0m ");
        match readline {
            Ok(line) => {
                let trimmed = line.trim();
                if trimmed.is_empty() {
                    continue;
                }

                rl.add_history_entry(trimmed)?;

                match trimmed {
                    "exit" | "quit" => {
                        println!("Goodbye!");
                        break;
                    }
                    "help" | "?" => {
                        show_help();
                    }
                    cmd => {
                        let parts: Vec<String> = cmd.split_whitespace().map(String::from).collect();
                        let _ = rl.save_history(&history_file);
                        return Ok(parts);
                    }
                }
            }
            Err(ReadlineError::Interrupted) => {
                println!("^C");
                continue;
            }
            Err(ReadlineError::Eof) => {
                println!("^D");
                break;
            }
            Err(err) => {
                eprintln!("Error: {err:?}");
                break;
            }
        }
    }

    let _ = rl.save_history(&history_file);
    Ok(vec![])
}

fn show_help() {
    println!("\n\x1b[1;36mAvailable Commands:\x1b[0m");
    println!("  \x1b[1;33mclone\x1b[0m (clo, cl)    Clone all enabled repositories");
    println!("  \x1b[1;33mpull\x1b[0m (pul, pl)     Pull all enabled repositories");
    println!("  \x1b[1;33mpush\x1b[0m (pus, ps)     Push all enabled repositories");
    println!("  \x1b[1;33mstatus\x1b[0m (st)        Show uncommitted changes");
    println!("  \x1b[1;33mdiff\x1b[0m (di)          Show diff summary");
    println!("  \x1b[1;33mfetch\x1b[0m (fe)         Fetch from remote");
    println!("  \x1b[1;33mbranch\x1b[0m (br)        Show current branch");
    println!("  \x1b[1;33mswitch\x1b[0m (sw)        Switch to default branch (requires Git 2.23+)");
    println!("  \x1b[1;33mstash list\x1b[0m (sl)    List stashed changes");
    println!(
        "  \x1b[1;33mgc\x1b[0m                 Run garbage collection (I/O heavy, use -j to limit)"
    );
    println!("  \x1b[1;33mhelp\x1b[0m, \x1b[1;33m?\x1b[0m            Show this help message");
    println!("  \x1b[1;33mexit\x1b[0m, \x1b[1;33mquit\x1b[0m         Exit interactive mode");
    println!("\n\x1b[1;36mOptions:\x1b[0m");
    println!("  \x1b[1;33m-j N\x1b[0m, \x1b[1;33m--jobs N\x1b[0m     Max parallel jobs (default: from gitpp.yaml, or 20)");
    println!("\n\x1b[1;36mTips:\x1b[0m");
    println!("  - Use \x1b[1;33mTab\x1b[0m for auto-completion");
    println!("  - Use \x1b[1;33m↑/↓\x1b[0m arrows for command history");
    println!();
}