1pub mod emojis;
10
11use rand::prelude::IndexedRandom;
12use serde_json::{json, Value};
13use std::fs;
14use std::path::Path;
15
16pub const PLACEHOLDER: &str = "🐾";
19
20pub const MAIN_EMOJI: &str = "🦊";
22
23pub fn load_avatars() -> Value {
24 let path = Path::new(".fur/avatars.json");
25 if path.exists() {
26 let content = fs::read_to_string(path).unwrap_or_default();
27 serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
28 } else {
29 json!({})
30 }
31}
32
33pub fn save_avatars(avatars: &Value) {
34 let path = Path::new(".fur/avatars.json");
35 if let Ok(serialized) = serde_json::to_string_pretty(avatars) {
36 let _ = fs::write(path, serialized);
37 }
38}
39
40pub fn resolve_avatar(avatars: &Value, key: &str) -> (String, String) {
41 if let Some(emoji) = avatars.get(key).and_then(|v| v.as_str()) {
43 return (key.to_string(), emoji.to_string());
44 }
45
46 if let Some((name, _)) = avatars
48 .as_object()
49 .and_then(|map| map.iter().find(|(_, v)| v.as_str() == Some(key)))
50 {
51 return (name.clone(), key.to_string());
52 }
53
54 (key.to_string(), PLACEHOLDER.to_string()) }
56
57pub fn is_bot_name(name: &str) -> bool {
64 let n = name.to_lowercase();
65
66 const SUBSTRING_MARKERS: [&str; 6] = ["gpt", "claude", "gemini", "bard", "grok", "copilot"];
67
68 if SUBSTRING_MARKERS.iter().any(|m| n.contains(m)) {
69 return true;
70 }
71
72 const TOKEN_MARKERS: [&str; 7] = ["ai", "bot", "llm", "agent", "assistant", "model", "llama"];
73
74 n.split(|c: char| !c.is_alphanumeric())
75 .any(|tok| TOKEN_MARKERS.contains(&tok))
76}
77
78pub fn get_random_emoji_for_name(name: &str) -> String {
81 if is_bot_name(name) {
82 return "🤖".to_string();
83 }
84
85 if let Some(emoji) = role_emoji(name) {
86 return emoji.to_string();
87 }
88
89 let pool = ["👤", "🧑", "👔", "🙂"];
90 pool.choose(&mut rand::rng()).unwrap_or(&"👤").to_string()
91}
92
93fn role_emoji(name: &str) -> Option<&'static str> {
95 const ROLES: &[(&str, &str)] = &[
96 ("me", MAIN_EMOJI),
97 ("self", MAIN_EMOJI),
98 ("user", MAIN_EMOJI),
99 ("boss", "💼"),
100 ("manager", "💼"),
101 ("hr", "📋"),
102 ("therapist", "🛋"),
103 ("lawyer", "⚖"),
104 ("doctor", "🩺"),
105 ("client", "🤝"),
106 ("team", "👥"),
107 ("cat", "🐱"),
108 ("dog", "🐶"),
109 ];
110
111 let key = name.trim().to_lowercase();
112
113 ROLES
114 .iter()
115 .find(|(candidate, _)| key == *candidate)
116 .map(|(_, emoji)| *emoji)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn recognises_models() {
125 assert!(is_bot_name("gpt5"));
126 assert!(is_bot_name("claude"));
127 assert!(is_bot_name("my-ai"));
128 assert!(is_bot_name("research agent"));
129 }
130
131 #[test]
132 fn does_not_mistake_people_for_bots() {
133 assert!(!is_bot_name("claire"));
135 assert!(!is_bot_name("raina"));
136 assert!(!is_bot_name("robotics_lab"));
137 assert!(!is_bot_name("management"));
138 }
139
140 #[test]
141 fn roles_get_stable_emoji() {
142 assert_eq!(get_random_emoji_for_name("hr"), "📋");
143 assert_eq!(get_random_emoji_for_name("me"), MAIN_EMOJI);
144 }
145}