typetui 0.1.0

A terminal-based typing test.
Documentation
use rand::prelude::IndexedRandom;
use rand::rng;

include!(concat!(env!("OUT_DIR"), "/content.rs"));

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
    Text,
    Code,
}

impl Mode {
    pub fn all() -> &'static [Mode] {
        &[Mode::Text, Mode::Code]
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            Mode::Text => "text",
            Mode::Code => "code",
        }
    }

    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "text" => Some(Mode::Text),
            "code" => Some(Mode::Code),
            _ => None,
        }
    }

    pub fn languages(&self) -> &'static [&'static str] {
        match self {
            Mode::Text => TEXT_LANGUAGES,
            Mode::Code => CODE_LANGUAGES,
        }
    }

    pub fn default_language(&self) -> &'static str {
        match self {
            Mode::Text => TEXT_LANGUAGES.first().copied().unwrap_or("english"),
            Mode::Code => CODE_LANGUAGES.first().copied().unwrap_or("python"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ContentProvider {
    mode: Mode,
    language: String,
}

impl ContentProvider {
    pub fn new(mode: Mode, language: impl Into<String>) -> Self {
        Self {
            mode,
            language: language.into(),
        }
    }

    pub fn generate_text(&self, word_count: usize) -> String {
        match self.mode {
            Mode::Text => self.generate_words(word_count),
            Mode::Code => self.generate_code_snippet(),
        }
    }

    fn generate_words(&self, count: usize) -> String {
        let words = get_word_list(&self.language);
        if words.is_empty() {
            return "the quick brown fox jumps over the lazy dog".to_string();
        }

        let mut rng = rng();
        let selected: Vec<&str> = (0..count)
            .map(|_| *words.choose(&mut rng).unwrap_or(&"the"))
            .collect();

        selected.join(" ")
    }

    pub fn generate_code_snippet(&self) -> String {
        let snippets = get_snippets(&self.language);
        if snippets.is_empty() {
            return "print('hello world')".to_string();
        }

        let mut rng = rng();
        snippets
            .choose(&mut rng)
            .unwrap_or(&"print('hello world')")
            .to_string()
    }

    pub fn available_languages(&self) -> &'static [&'static str] {
        self.mode.languages()
    }

    pub fn validate_language(&self, lang: &str) -> bool {
        self.available_languages().contains(&lang)
    }
}

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

    #[test]
    fn test_mode_from_str() {
        assert_eq!(Mode::from_str("text"), Some(Mode::Text));
        assert_eq!(Mode::from_str("code"), Some(Mode::Code));
        assert_eq!(Mode::from_str("unknown"), None);
    }

    #[test]
    fn test_content_provider_text() {
        let provider = ContentProvider::new(Mode::Text, "english");
        let text = provider.generate_text(10);
        assert!(!text.is_empty());
        let words: Vec<&str> = text.split_whitespace().collect();
        assert_eq!(words.len(), 10);
    }

    #[test]
    fn test_content_provider_code() {
        let provider = ContentProvider::new(Mode::Code, "python");
        let text = provider.generate_text(10);
        assert!(!text.is_empty());
    }
}