yoo 0.6.2

A tiny developer companion for better coding sessions.
use std::{
    io::{self, Write},
    thread,
    time::Duration,
};

const ART: &str = r#"
██╗   ██╗ ██████╗  ██████╗
╚██╗ ██╔╝██╔═══██╗██╔═══██╗
 ╚████╔╝ ██║   ██║██║   ██║
  ╚██╔╝  ██║   ██║██║   ██║
   ██║   ╚██████╔╝╚██████╔╝
   ╚═╝    ╚═════╝  ╚═════╝
"#;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Theme {
    Neon,
    Ocean,
    Mono,
    Dracula,
    TokyoNight,
    Gruvbox,
    Nord,
    RosePine,
    Catppuccin,
}

impl Theme {
    pub fn parse(value: &str) -> Option<Self> {
        match value.trim().to_lowercase().as_str() {
            "neon" => Some(Self::Neon),
            "ocean" => Some(Self::Ocean),
            "mono" => Some(Self::Mono),
            "dracula" => Some(Self::Dracula),
            "tokyo-night" | "tokyonight" => Some(Self::TokyoNight),
            "gruvbox" => Some(Self::Gruvbox),
            "nord" => Some(Self::Nord),
            "rose-pine" | "rosepine" => Some(Self::RosePine),
            "catppuccin" => Some(Self::Catppuccin),
            _ => None,
        }
    }

    pub fn names() -> &'static [&'static str] {
        &[
            "neon",
            "ocean",
            "mono",
            "dracula",
            "tokyo-night",
            "gruvbox",
            "nord",
            "rose-pine",
            "catppuccin",
        ]
    }

    fn accent(self) -> &'static str {
        match self {
            Self::Neon => "95",
            Self::Ocean => "96",
            Self::Mono => "37",
            Self::Dracula => "95",
            Self::TokyoNight => "94",
            Self::Gruvbox => "93",
            Self::Nord => "96",
            Self::RosePine => "95",
            Self::Catppuccin => "94",
        }
    }

    fn muted(self) -> &'static str {
        match self {
            Self::Neon => "92",
            Self::Ocean => "94",
            Self::Mono => "90",
            Self::Dracula => "35",
            Self::TokyoNight => "36",
            Self::Gruvbox => "33",
            Self::Nord => "36",
            Self::RosePine => "35",
            Self::Catppuccin => "36",
        }
    }
}

pub struct Ui {
    theme: Theme,
    use_colour: bool,
    typing_delay: Duration,
}

impl Ui {
    pub fn new(theme: Theme, use_colour: bool, typing_speed_ms: u64) -> Self {
        Self {
            theme,
            use_colour,
            typing_delay: Duration::from_millis(typing_speed_ms),
        }
    }

    pub fn print_art(&self) -> io::Result<()> {
        self.write_line(&self.paint(ART.trim_end(), self.theme.accent(), true))
    }

    pub fn heading(&self, text: &str) -> io::Result<()> {
        self.write_line(&self.paint(text, self.theme.accent(), true))
    }

    pub fn divider(&self) -> io::Result<()> {
        self.write_line(&self.paint(
            "────────────────────────────────────────",
            self.theme.muted(),
            false,
        ))
    }

    pub fn info(&self, icon: &str, label: &str, value: &str) -> io::Result<()> {
        let key = self.paint(&format!("{icon} {label}"), self.theme.accent(), true);
        self.write_line(&format!("{key} {value}"))
    }

    pub fn type_line(&self, text: &str) -> io::Result<()> {
        if self.typing_delay.is_zero() {
            return self.write_line(text);
        }

        let stdout = io::stdout();
        let mut output = stdout.lock();

        for character in text.chars() {
            write!(output, "{character}")?;
            output.flush()?;
            thread::sleep(self.typing_delay);
        }

        writeln!(output)?;
        Ok(())
    }

    pub fn blank_line(&self) -> io::Result<()> {
        self.write_line("")
    }

    fn paint(&self, text: &str, colour: &str, bold: bool) -> String {
        if !self.use_colour {
            return text.to_owned();
        }

        let weight = if bold { "1;" } else { "" };
        format!("\x1b[{weight}{colour}m{text}\x1b[0m")
    }

    fn write_line(&self, text: &str) -> io::Result<()> {
        let stdout = io::stdout();
        let mut output = stdout.lock();
        writeln!(output, "{text}")
    }
}

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

    #[test]
    fn parses_known_themes_and_aliases() {
        assert_eq!(Theme::parse("neon"), Some(Theme::Neon));
        assert_eq!(Theme::parse("OCEAN"), Some(Theme::Ocean));
        assert_eq!(Theme::parse("tokyonight"), Some(Theme::TokyoNight));
        assert_eq!(Theme::parse("rose-pine"), Some(Theme::RosePine));
        assert_eq!(Theme::parse("unknown"), None);
    }
}