yoo 0.2.0

A tiny developer companion that starts coding sessions with good vibes.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RunOptions {
    pub fast: bool,
    pub no_art: bool,
    pub plain: bool,
    pub name: Option<String>,
    pub theme: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
    Run(RunOptions),
    Init,
    ConfigPath,
    Help,
    Version,
}

pub fn parse(arguments: &[String]) -> Result<Command, String> {
    if arguments.is_empty() {
        return Ok(Command::Run(RunOptions::default()));
    }

    if matches!(arguments[0].as_str(), "help" | "--help" | "-h") {
        return require_standalone(arguments, Command::Help);
    }

    if matches!(arguments[0].as_str(), "init" | "--init") {
        return require_standalone(arguments, Command::Init);
    }

    if matches!(arguments[0].as_str(), "config" | "--config") {
        return require_standalone(arguments, Command::ConfigPath);
    }

    if matches!(arguments[0].as_str(), "--version" | "-V") {
        return require_standalone(arguments, Command::Version);
    }

    let mut options = RunOptions::default();
    let mut index = 0;

    while index < arguments.len() {
        match arguments[index].as_str() {
            "--fast" => options.fast = true,
            "--no-art" => options.no_art = true,
            "--plain" => options.plain = true,
            "--name" => {
                index += 1;
                options.name = Some(required_value(arguments, index, "--name")?);
            }
            "--theme" => {
                index += 1;
                options.theme = Some(required_value(arguments, index, "--theme")?);
            }
            value if value.starts_with('-') => return Err(format!("unknown option `{value}`")),
            value => return Err(format!("unknown command `{value}`")),
        }

        index += 1;
    }

    Ok(Command::Run(options))
}

fn require_standalone(arguments: &[String], command: Command) -> Result<Command, String> {
    if arguments.len() == 1 {
        Ok(command)
    } else {
        Err(format!(
            "`{}` cannot be combined with other options",
            arguments[0]
        ))
    }
}

fn required_value(arguments: &[String], index: usize, flag: &str) -> Result<String, String> {
    let value = arguments
        .get(index)
        .ok_or_else(|| format!("{flag} needs a value"))?
        .trim()
        .to_owned();

    if value.is_empty() {
        return Err(format!("{flag} cannot be empty"));
    }

    Ok(value)
}

pub fn help_text() -> &'static str {
    r#"yoo — a tiny developer companion

USAGE:
  yoo [OPTIONS]
  yoo <COMMAND>

COMMANDS:
  init              Create a starter config file
  config            Print the config file location
  help              Print this help message

OPTIONS:
  --fast            Skip the typewriter animation
  --no-art          Hide the ASCII logo for this run
  --plain           Disable ANSI colours for this run
  --name <NAME>     Use a name for this run only
  --theme <THEME>   neon, ocean, or mono for this run only
  -h, --help        Print help
  -V, --version     Print version

EXAMPLES:
  yoo
  yoo --fast --theme ocean
  yoo --name Nihit --no-art
  yoo init
"#
}

#[cfg(test)]
mod tests {
    use super::*;

    fn values(items: &[&str]) -> Vec<String> {
        items.iter().map(|item| (*item).to_owned()).collect()
    }

    #[test]
    fn no_arguments_runs_with_defaults() {
        assert_eq!(parse(&[]), Ok(Command::Run(RunOptions::default())));
    }

    #[test]
    fn parses_run_options() {
        let arguments = values(&["--fast", "--no-art", "--name", "Nihit", "--theme", "ocean"]);
        let command = parse(&arguments).expect("options should parse");

        assert_eq!(
            command,
            Command::Run(RunOptions {
                fast: true,
                no_art: true,
                plain: false,
                name: Some("Nihit".to_owned()),
                theme: Some("ocean".to_owned()),
            })
        );
    }

    #[test]
    fn rejects_unknown_options() {
        let arguments = values(&["--turbo"]);
        assert!(parse(&arguments).is_err());
    }

    #[test]
    fn init_must_be_standalone() {
        let arguments = values(&["init", "--fast"]);
        assert!(parse(&arguments).is_err());
    }
}