1use crate::humanize::Lang;
9use std::path::Path;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Context {
15 GitConflict,
17 NodeNeedsInstall,
19 ManyImages,
21 GitRepo,
23 Generic,
25}
26
27pub fn detect_context(cwd: &Path) -> Context {
30 let git = cwd.join(".git");
31 if git.is_dir() {
32 if git.join("MERGE_HEAD").exists()
34 || git.join("rebase-merge").exists()
35 || git.join("rebase-apply").exists()
36 {
37 return Context::GitConflict;
38 }
39 }
40 if cwd.join("package.json").is_file() && !cwd.join("node_modules").exists() {
41 return Context::NodeNeedsInstall;
42 }
43 if looks_like_photo_folder(cwd) {
44 return Context::ManyImages;
45 }
46 if git.is_dir() {
47 return Context::GitRepo;
48 }
49 Context::Generic
50}
51
52fn looks_like_photo_folder(cwd: &Path) -> bool {
54 let exts = ["jpg", "jpeg", "png", "heic", "gif", "webp"];
55 let mut images = 0usize;
56 let mut total = 0usize;
57 if let Ok(entries) = std::fs::read_dir(cwd) {
58 for e in entries.flatten().take(200) {
59 if e.path().is_file() {
60 total += 1;
61 if let Some(ext) = e.path().extension().and_then(|x| x.to_str()) {
62 if exts.contains(&ext.to_lowercase().as_str()) {
63 images += 1;
64 }
65 }
66 }
67 }
68 }
69 total >= 8 && images * 2 >= total
70}
71
72fn context_offer(ctx: Context, lang: Lang) -> Option<String> {
74 let s = match (ctx, lang) {
75 (Context::GitConflict, Lang::Fr) => {
76 "Je vois un conflit git en cours ici. Tu veux que je t'aide à le régler ? → sparrow fix"
77 }
78 (Context::GitConflict, Lang::En) => {
79 "I see a git conflict in progress here. Want me to help resolve it? → sparrow fix"
80 }
81 (Context::NodeNeedsInstall, Lang::Fr) => {
82 "Ce projet n'est pas installé (pas de node_modules). Je m'en occupe ? → sparrow fix"
83 }
84 (Context::NodeNeedsInstall, Lang::En) => {
85 "This project isn't installed (no node_modules). Want me to handle it? → sparrow fix"
86 }
87 (Context::ManyImages, Lang::Fr) => {
88 "Beaucoup de photos ici. Je peux les ranger par année. → sparrow idees grand-mere"
89 }
90 (Context::ManyImages, Lang::En) => {
91 "Lots of photos here. I can sort them by year. → sparrow ideas grandparent"
92 }
93 (Context::GitRepo, Lang::Fr) => {
94 "Tu es dans un projet de code. Un bug à corriger ? → sparrow fix · Comprendre le code ? → sparrow explique"
95 }
96 (Context::GitRepo, Lang::En) => {
97 "You're in a code project. A bug to fix? → sparrow fix · Understand the code? → sparrow explain"
98 }
99 (Context::Generic, _) => return None,
100 };
101 Some(s.to_string())
102}
103
104pub fn welcome_text(cwd: &Path, lang: Lang) -> String {
106 let mut out = String::new();
107 let header = match lang {
108 Lang::Fr => "🐦 Salut ! Qu'est-ce qu'on règle aujourd'hui ?",
109 Lang::En => "🐦 Hi! What are we sorting out today?",
110 };
111 out.push_str(header);
112 out.push_str("\n\n");
113
114 if let Some(offer) = context_offer(detect_context(cwd), lang) {
115 out.push_str(" ");
116 out.push_str(&offer);
117 out.push_str("\n\n");
118 }
119
120 let body = match lang {
121 Lang::Fr => {
122 " Décris ton problème avec tes mots :\n\
123 \x20 · sparrow fix \"mon site ne s'affiche plus\"\n\
124 \x20 · sparrow explique \"ce message d'erreur\"\n\
125 \x20 · sparrow idees (tout ce que tu peux faire)\n\
126 \x20 · sparrow whatis token (c'est quoi ce mot ?)\n\n\
127 Et si ça ne te plaît pas : sparrow annule remet tout comme avant."
128 }
129 Lang::En => {
130 " Describe your problem in your own words:\n\
131 \x20 · sparrow fix \"my site won't load\"\n\
132 \x20 · sparrow explain \"this error message\"\n\
133 \x20 · sparrow ideas (everything you can do)\n\
134 \x20 · sparrow whatis token (what's this word?)\n\n\
135 And if you don't like it: sparrow undo puts everything back."
136 }
137 };
138 out.push_str(body);
139 out
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::fs;
146
147 fn tmp(name: &str) -> std::path::PathBuf {
148 let p = std::env::temp_dir().join(format!(
149 "sparrow-welcome-{name}-{}",
150 std::time::SystemTime::now()
151 .duration_since(std::time::UNIX_EPOCH)
152 .unwrap()
153 .as_nanos()
154 ));
155 fs::create_dir_all(&p).unwrap();
156 p
157 }
158
159 #[test]
160 fn detects_node_needs_install() {
161 let d = tmp("node");
162 fs::write(d.join("package.json"), "{}").unwrap();
163 assert_eq!(detect_context(&d), Context::NodeNeedsInstall);
164 let _ = fs::remove_dir_all(d);
165 }
166
167 #[test]
168 fn detects_git_conflict() {
169 let d = tmp("conflict");
170 fs::create_dir_all(d.join(".git")).unwrap();
171 fs::write(d.join(".git").join("MERGE_HEAD"), "x").unwrap();
172 assert_eq!(detect_context(&d), Context::GitConflict);
173 let _ = fs::remove_dir_all(d);
174 }
175
176 #[test]
177 fn detects_photo_folder() {
178 let d = tmp("photos");
179 for i in 0..10 {
180 fs::write(d.join(format!("img{i}.jpg")), "x").unwrap();
181 }
182 assert_eq!(detect_context(&d), Context::ManyImages);
183 let _ = fs::remove_dir_all(d);
184 }
185
186 #[test]
187 fn empty_dir_is_generic() {
188 let d = tmp("empty");
189 assert_eq!(detect_context(&d), Context::Generic);
190 let _ = fs::remove_dir_all(d);
191 }
192
193 #[test]
194 fn welcome_text_is_warm_and_jargon_free() {
195 let d = tmp("welcome");
196 let text = welcome_text(&d, Lang::Fr);
197 assert!(text.contains("Qu'est-ce qu'on règle"));
198 assert!(text.contains("sparrow fix"));
199 assert!(text.contains("sparrow annule"));
200 for bad in ["tier", "tokens", "0.0.0.0", "[Tool"] {
202 assert!(!text.contains(bad), "welcome leaked `{bad}`");
203 }
204 let _ = fs::remove_dir_all(d);
205 }
206}