use crate::cli::PetAction;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PetForm {
Ecat,
Smile,
Joy,
Cry,
Depressed,
Terrified,
Angry,
Coquettish,
Cute,
Pleasing,
Knocking,
Facepalm,
Tongue,
}
impl PetForm {
pub(crate) const ALL: [PetForm; 13] = [
PetForm::Ecat,
PetForm::Smile,
PetForm::Joy,
PetForm::Cry,
PetForm::Depressed,
PetForm::Terrified,
PetForm::Angry,
PetForm::Coquettish,
PetForm::Cute,
PetForm::Pleasing,
PetForm::Knocking,
PetForm::Facepalm,
PetForm::Tongue,
];
pub(crate) fn slug(self) -> &'static str {
match self {
PetForm::Ecat => "ecat",
PetForm::Smile => "smile",
PetForm::Joy => "joy",
PetForm::Cry => "cry",
PetForm::Depressed => "depressed",
PetForm::Terrified => "terrified",
PetForm::Angry => "angry",
PetForm::Coquettish => "coquettish",
PetForm::Cute => "cute",
PetForm::Pleasing => "pleasing",
PetForm::Knocking => "knocking",
PetForm::Facepalm => "facepalm",
PetForm::Tongue => "tongue",
}
}
pub(crate) fn name_zh(self) -> &'static str {
match self {
PetForm::Ecat => "主形象",
PetForm::Smile => "笑",
PetForm::Joy => "欢乐",
PetForm::Cry => "哭",
PetForm::Depressed => "沮丧",
PetForm::Terrified => "惊恐",
PetForm::Angry => "愤怒",
PetForm::Coquettish => "撒娇",
PetForm::Cute => "卖萌",
PetForm::Pleasing => "讨好",
PetForm::Knocking => "敲打",
PetForm::Facepalm => "捂脸",
PetForm::Tongue => "吐舌头",
}
}
pub(crate) fn name_en(self) -> &'static str {
match self {
PetForm::Ecat => "Ecat",
PetForm::Smile => "Smile",
PetForm::Joy => "Joy",
PetForm::Cry => "Cry",
PetForm::Depressed => "Depressed",
PetForm::Terrified => "Terrified",
PetForm::Angry => "Angry",
PetForm::Coquettish => "Coquettish",
PetForm::Cute => "Cute",
PetForm::Pleasing => "Pleasing",
PetForm::Knocking => "Knocking",
PetForm::Facepalm => "Facepalm",
PetForm::Tongue => "Tongue Out",
}
}
pub(crate) fn shortcode(self) -> Option<&'static str> {
match self {
PetForm::Ecat => None,
PetForm::Smile => Some(":emoji-smile:"),
PetForm::Joy => Some(":emoji-joy:"),
PetForm::Cry => Some(":emoji-cry:"),
PetForm::Depressed => Some(":emoji-depressed:"),
PetForm::Terrified => Some(":emoji-terrified:"),
PetForm::Angry => Some(":emoji-angry:"),
PetForm::Coquettish => Some(":emoji-coquettish:"),
PetForm::Cute => Some(":emoji-cute:"),
PetForm::Pleasing => Some(":emoji-pleasing:"),
PetForm::Knocking => Some(":emoji-knocking:"),
PetForm::Facepalm => Some(":emoji-facepalm:"),
PetForm::Tongue => Some(":emoji-tongue:"),
}
}
pub(crate) fn svg(self) -> &'static str {
match self {
PetForm::Ecat => include_str!("../assets/pet/ecat.svg"),
PetForm::Smile => include_str!("../assets/pet/smile.svg"),
PetForm::Joy => include_str!("../assets/pet/joy.svg"),
PetForm::Cry => include_str!("../assets/pet/cry.svg"),
PetForm::Depressed => include_str!("../assets/pet/depressed.svg"),
PetForm::Terrified => include_str!("../assets/pet/terrified.svg"),
PetForm::Angry => include_str!("../assets/pet/angry.svg"),
PetForm::Coquettish => include_str!("../assets/pet/coquettish.svg"),
PetForm::Cute => include_str!("../assets/pet/cute.svg"),
PetForm::Pleasing => include_str!("../assets/pet/pleasing.svg"),
PetForm::Knocking => include_str!("../assets/pet/knocking.svg"),
PetForm::Facepalm => include_str!("../assets/pet/facepalm.svg"),
PetForm::Tongue => include_str!("../assets/pet/tongue.svg"),
}
}
pub(crate) fn parse(raw: &str) -> Option<Self> {
let bare = raw.trim().trim_matches(':');
let bare = bare.strip_prefix("emoji-").unwrap_or(bare);
let lower = bare.to_ascii_lowercase();
Self::ALL.iter().copied().find(|form| {
form.slug() == lower
|| form.name_en().to_ascii_lowercase() == lower
|| form.name_zh() == bare
|| (matches!(form, PetForm::Ecat)
&& matches!(lower.as_str(), "main" | "default" | "一只猫"))
})
}
}
pub(crate) fn run(action: PetAction) -> Result<(), String> {
match action {
PetAction::List => {
list();
Ok(())
}
PetAction::Show { form } => {
let form = PetForm::parse(&form).ok_or_else(|| {
format!("unknown pet form '{form}'; run `ecat pet list` to see the forms")
})?;
print!("{}", form.svg());
Ok(())
}
PetAction::Export { dir } => export(Path::new(&dir)),
}
}
fn list() {
println!(
"{} {} {} SHORTCODE",
pad("FORM", 12),
pad("中文", 10),
pad("ENGLISH", 12)
);
for form in PetForm::ALL {
println!(
"{} {} {} {}",
pad(form.slug(), 12),
pad(form.name_zh(), 10),
pad(form.name_en(), 12),
form.shortcode().unwrap_or("-")
);
}
}
fn pad(text: &str, width: usize) -> String {
let used: usize = text.chars().map(|c| if is_wide(c) { 2 } else { 1 }).sum();
format!("{text}{}", " ".repeat(width.saturating_sub(used)))
}
fn is_wide(c: char) -> bool {
matches!(
c as u32,
0x1100..=0x115F
| 0x2E80..=0xA4CF
| 0xAC00..=0xD7A3
| 0xF900..=0xFAFF
| 0xFE30..=0xFE4F
| 0xFF00..=0xFF60
| 0xFFE0..=0xFFE6
| 0x20000..=0x3FFFD
)
}
fn export(dir: &Path) -> Result<(), String> {
fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
for form in PetForm::ALL {
let path = dir.join(format!("{}.svg", form.slug()));
fs::write(&path, form.svg()).map_err(|e| format!("write {}: {e}", path.display()))?;
println!("wrote {}", path.display());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn twelve_expressions_plus_main() {
assert_eq!(PetForm::ALL.len(), 13);
assert_eq!(PetForm::ALL[0], PetForm::Ecat);
}
#[test]
fn slugs_are_unique() {
let mut slugs: Vec<_> = PetForm::ALL.iter().map(|f| f.slug()).collect();
slugs.sort_unstable();
slugs.dedup();
assert_eq!(slugs.len(), PetForm::ALL.len());
}
#[test]
fn parse_accepts_slug_shortcode_and_names() {
for form in PetForm::ALL {
assert_eq!(PetForm::parse(form.slug()), Some(form));
assert_eq!(PetForm::parse(form.name_zh()), Some(form));
assert_eq!(PetForm::parse(form.name_en()), Some(form));
if let Some(code) = form.shortcode() {
assert_eq!(PetForm::parse(code), Some(form));
assert_eq!(PetForm::parse(code.trim_matches(':')), Some(form));
}
}
assert_eq!(PetForm::parse(":emoji-smile:"), Some(PetForm::Smile));
assert_eq!(PetForm::parse("main"), Some(PetForm::Ecat));
assert_eq!(PetForm::parse("一只猫"), Some(PetForm::Ecat));
assert_eq!(PetForm::parse("nope"), None);
}
#[test]
fn every_svg_is_vector() {
for form in PetForm::ALL {
let svg = form.svg();
assert!(svg.starts_with("<svg"), "{} is not an SVG", form.slug());
assert!(
!svg.contains("data:image"),
"{} still embeds a raster image",
form.slug()
);
assert!(svg.contains("<title>"), "{} lacks a title", form.slug());
}
}
#[test]
fn assets_match_docs() {
let docs = Path::new(env!("CARGO_MANIFEST_DIR")).join("../docs");
for form in PetForm::ALL {
let rel = if form == PetForm::Ecat {
"e-cat.svg".to_string()
} else {
format!("pet-expressions/{}.svg", form.slug())
};
let on_disk = fs::read_to_string(docs.join(rel)).unwrap();
assert_eq!(on_disk, form.svg(), "asset drift for {}", form.slug());
}
}
}