gshell 1.0.0

gshell is a shell for people who live in the terminal. It pairs familiar Unix behavior with a tighter core, fast interaction, and an interface built to stay out of the way.
Documentation
use nu_ansi_term::Color;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PromptMode {
    Internal,
    Starship,
    #[default]
    Auto,
}

impl PromptMode {
    pub fn parse(value: &str) -> Self {
        match value.trim().to_ascii_lowercase().as_str() {
            "internal" => Self::Internal,
            "starship" => Self::Starship,
            "auto" => Self::Auto,
            _ => Self::Auto,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PromptConfig {
    mode: PromptMode,
    starship_binary: String,
}

impl Default for PromptConfig {
    fn default() -> Self {
        Self {
            mode: PromptMode::Auto,
            starship_binary: "starship".to_string(),
        }
    }
}

impl PromptConfig {
    pub fn new(mode: PromptMode) -> Self {
        Self {
            mode,
            ..Self::default()
        }
    }

    pub fn from_env() -> Self {
        let mut config = Self::default();

        if let Ok(mode) = std::env::var("GSHELL_PROMPT") {
            config.mode = PromptMode::parse(&mode);
        }

        if let Ok(binary) = std::env::var("GSHELL_STARSHIP_BIN")
            && !binary.trim().is_empty()
        {
            config.starship_binary = binary;
        }

        config
    }

    pub fn mode(&self) -> PromptMode {
        self.mode
    }

    pub fn starship_binary(&self) -> &str {
        &self.starship_binary
    }

    pub fn with_starship_binary(mut self, binary: impl Into<String>) -> Self {
        self.starship_binary = binary.into();
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HighlighterConfig {
    command_color: Color,
    builtin_color: Color,
    argument_color: Color,
    flag_color: Color,
    hint_color: Color,
    operator_color: Color,
    redirect_color: Color,
}

impl Default for HighlighterConfig {
    fn default() -> Self {
        Self {
            command_color: Color::Cyan,
            builtin_color: Color::Cyan,
            argument_color: Color::Green,
            flag_color: Color::Blue,
            hint_color: Color::DarkGray,
            operator_color: Color::Purple,
            redirect_color: Color::Purple,
        }
    }
}

impl HighlighterConfig {
    pub fn from_env() -> Self {
        let mut config = Self::default();

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_COMMAND")
            && let Some(color) = parse_color(&value)
        {
            config.command_color = color;
        }

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_BUILTIN")
            && let Some(color) = parse_color(&value)
        {
            config.builtin_color = color;
        }

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_ARGUMENT")
            && let Some(color) = parse_color(&value)
        {
            config.argument_color = color;
        }

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_FLAG")
            && let Some(color) = parse_color(&value)
        {
            config.flag_color = color;
        }

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_HINT")
            && let Some(color) = parse_color(&value)
        {
            config.hint_color = color;
        }

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_OPERATOR")
            && let Some(color) = parse_color(&value)
        {
            config.operator_color = color;
        }

        if let Ok(value) = std::env::var("GSHELL_HIGHLIGHT_REDIRECT")
            && let Some(color) = parse_color(&value)
        {
            config.redirect_color = color;
        }

        config
    }

    pub fn command_color(&self) -> Color {
        self.command_color
    }

    pub fn builtin_color(&self) -> Color {
        self.builtin_color
    }

    pub fn operator_color(&self) -> Color {
        self.operator_color
    }

    pub fn argument_color(&self) -> Color {
        self.argument_color
    }

    pub fn hint_color(&self) -> Color {
        self.hint_color
    }

    pub fn flag_color(&self) -> Color {
        self.flag_color
    }

    pub fn redirect_color(&self) -> Color {
        self.redirect_color
    }
}

fn parse_color(value: &str) -> Option<Color> {
    let value = value.trim();

    if let Some(hex) = value.strip_prefix('#').or(Some(value))
        && hex.len() == 6
        && hex.bytes().all(|byte| byte.is_ascii_hexdigit())
    {
        let red = u8::from_str_radix(&hex[0..2], 16).ok()?;
        let green = u8::from_str_radix(&hex[2..4], 16).ok()?;
        let blue = u8::from_str_radix(&hex[4..6], 16).ok()?;
        return Some(Color::Rgb(red, green, blue));
    }

    match value.to_ascii_lowercase().as_str() {
        "black" => Some(Color::Black),
        "red" => Some(Color::Red),
        "green" => Some(Color::Green),
        "yellow" => Some(Color::Yellow),
        "blue" => Some(Color::Blue),
        "purple" | "magenta" => Some(Color::Purple),
        "cyan" => Some(Color::Cyan),
        "white" => Some(Color::White),
        "dark_gray" | "dark-gray" | "darkgray" => Some(Color::DarkGray),
        "light_red" | "light-red" | "lightred" => Some(Color::LightRed),
        "light_green" | "light-green" | "lightgreen" => Some(Color::LightGreen),
        "light_yellow" | "light-yellow" | "lightyellow" => Some(Color::LightYellow),
        "light_blue" | "light-blue" | "lightblue" => Some(Color::LightBlue),
        "light_purple" | "light-purple" | "lightpurple" => Some(Color::LightPurple),
        "light_cyan" | "light-cyan" | "lightcyan" => Some(Color::LightCyan),
        "light_gray" | "light-gray" | "lightgray" => Some(Color::LightGray),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use nu_ansi_term::Color;

    use super::parse_color;

    #[test]
    fn parses_hex_colors() {
        assert_eq!(parse_color("#31748f"), Some(Color::Rgb(0x31, 0x74, 0x8f)));
        assert_eq!(parse_color("eb6f92"), Some(Color::Rgb(0xeb, 0x6f, 0x92)));
    }

    #[test]
    fn parses_named_colors() {
        assert_eq!(parse_color("blue"), Some(Color::Blue));
        assert_eq!(parse_color("light-purple"), Some(Color::LightPurple));
    }

    #[test]
    fn rejects_invalid_colors() {
        assert_eq!(parse_color("rose-pine"), None);
        assert_eq!(parse_color("#12345"), None);
    }
}