clitrans 0.4.1

Yet another command-line translator
#[cfg(feature = "audio")]
use crate::util::audio::play_audio;

use super::{Layout, Result};
use colored::{Color, Colorize};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::io::{self, Write};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Translation {
    pub query:   String,
    pub url:     String,
    pub prons:   Vec<Pronunciation>,
    pub exps:    Vec<Explanation>,
    pub phrases: Vec<(String, Vec<String>)>,
}

impl Translation {
    pub fn new(query: String, url: String) -> Self {
        Self {
            query,
            url,
            prons: Vec::new(),
            exps: Vec::new(),
            phrases: Vec::new(),
        }
    }

    pub fn pronunciations(mut self, prons: Vec<Pronunciation>) -> Self {
        self.prons = prons;
        self
    }
    pub fn explanations(mut self, exps: Vec<Explanation>) -> Self {
        self.exps = exps;
        self
    }
    pub fn phrases(mut self, phrases: Vec<(String, Vec<String>)>) -> Self {
        self.phrases = phrases;
        self
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Pronunciation {
    pub tag:   String,
    pub value: String,
    pub audio: Option<String>,
}

impl Pronunciation {
    pub fn pinyin(value: String) -> Self {
        Self { tag: "CN".to_owned(), value, audio: None }
    }
    pub fn us(value: String) -> Self {
        Self { tag: "US".to_owned(), value, audio: None }
    }
    pub fn uk(value: String) -> Self {
        Self { tag: "UK".to_owned(), value, audio: None }
    }
    pub fn audio(mut self, url: Option<String>) -> Self {
        self.audio = url;
        self
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Explanation {
    pub tag:   ExpTag,
    pub items: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ExpTag {
    Web,
    Machine,
    Phrase,
    Pos(String),
}

impl Translation {
    #[cfg(feature = "audio")]
    pub fn play_audio(&self, tag: &str) -> Result<()> {
        use anyhow::bail;

        match self
            .prons
            .iter()
            .find(|p| p.tag.to_uppercase() == tag.to_uppercase())
            .and_then(|p| p.audio.as_ref())
        {
            Some(url) => play_audio(url),
            None => {
                let possibles = self
                    .prons
                    .iter()
                    .filter_map(|p| p.audio.as_ref().map(|_| &p.tag))
                    .join(", ");
                if possibles.is_empty() {
                    bail!("audio not found")
                } else {
                    bail!(
                        "audio not found for '{}'; possible values: [{}]",
                        tag,
                        possibles
                    )
                };
            }
        }
    }

    pub fn print(&self, layout: &Layout) -> Result<()> {
        self.print_query()?;
        self.print_pronunciations(layout)?;

        let exps: Vec<_> = self
            .exps
            .iter()
            .filter(|exp| !exp.items.is_empty())
            .map(|exp| {
                #[rustfmt::skip]
                let (color, tag) = match &exp.tag {
                    ExpTag::Web     => (Color::Magenta,     "Web."),
                    ExpTag::Machine => (Color::Cyan,        "Machine."),
                    ExpTag::Phrase  => (Color::Green,       "Phrase."),
                    ExpTag::Pos(s)  => (Color::BrightGreen, s.as_str()),
                };
                (color, tag, exp.items.as_slice())
            })
            .collect();
        let indent = exps.iter().map(|&(_, tag, _)| tag.len()).max().unwrap_or(0);

        self.print_explanations(layout, indent, &exps)?;
        self.print_phrases(layout, indent)?;
        self.print_link(indent)?;
        Ok(())
    }

    fn print_query(&self) -> io::Result<()> {
        writeln!(io::stdout().lock(), "{}", self.query)
    }

    fn print_pronunciations(&self, layout: &Layout) -> io::Result<()> {
        if !self.prons.is_empty() && layout.phonetics > 0 {
            let buf = self
                .prons
                .iter()
                .take(layout.phonetics)
                .map(|pron| &pron.value)
                .unique()
                .map(|s| format!("/{}/", s.yellow()))
                .join(", ");
            return writeln!(io::stdout().lock(), "{}", buf);
        }
        Ok(())
    }

    fn print_explanations(
        &self,
        layout: &Layout,
        indent: usize,
        exps: &[(Color, &str, &[String])],
    ) -> io::Result<()> {
        if !exps.is_empty() && layout.explanations > 0 {
            let buf = exps
                .iter()
                .take(layout.explanations)
                .map(|(color, tag, itmes)| {
                    itmes
                        .iter()
                        .enumerate()
                        .map(|(i, item)| {
                            let title = if i == 0 { tag } else { "" };
                            format!(
                                "{:>w$}  {} {}",
                                title.color(*color).italic(),
                                "*".color(*color),
                                item.color(*color),
                                w = indent,
                            )
                        })
                        .join("\n")
                })
                .join("\n\n");
            return writeln!(io::stdout().lock(), "\n{}", buf);
        }
        Ok(())
    }

    fn print_phrases(&self, layout: &Layout, indent: usize) -> io::Result<()> {
        if !self.phrases.is_empty() && layout.phrases > 0 {
            let buf = self
                .phrases
                .iter()
                .filter(|(_, exps)| !exps.is_empty())
                .take(layout.phrases)
                .map(|(phrase, exps)| {
                    format!("{:>w$}  {} {}\n", "", "*".cyan(), phrase.cyan(), w = indent)
                        + &exps
                            .iter()
                            .map(|exp| {
                                format!("{:>w$}    {} {}", "", "-".cyan(), exp.cyan(), w = indent)
                            })
                            .join("\n")
                })
                .join("\n\n");
            return writeln!(io::stdout().lock(), "\n{}\n{}", "Web Phrases:".cyan(), buf);
        }
        Ok(())
    }

    fn print_link(&self, indent: usize) -> io::Result<()> {
        writeln!(
            io::stdout().lock(),
            "\n{}\n{:>w$}  {} {}",
            "Source URL:".blue(),
            "",
            "*".blue(),
            self.url.blue(),
            w = indent,
        )
    }
}