use crate::humanize::Lang;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Context {
GitConflict,
NodeNeedsInstall,
ManyImages,
GitRepo,
Generic,
}
pub fn detect_context(cwd: &Path) -> Context {
let git = cwd.join(".git");
if git.is_dir() {
if git.join("MERGE_HEAD").exists()
|| git.join("rebase-merge").exists()
|| git.join("rebase-apply").exists()
{
return Context::GitConflict;
}
}
if cwd.join("package.json").is_file() && !cwd.join("node_modules").exists() {
return Context::NodeNeedsInstall;
}
if looks_like_photo_folder(cwd) {
return Context::ManyImages;
}
if git.is_dir() {
return Context::GitRepo;
}
Context::Generic
}
fn looks_like_photo_folder(cwd: &Path) -> bool {
let exts = ["jpg", "jpeg", "png", "heic", "gif", "webp"];
let mut images = 0usize;
let mut total = 0usize;
if let Ok(entries) = std::fs::read_dir(cwd) {
for e in entries.flatten().take(200) {
if e.path().is_file() {
total += 1;
if let Some(ext) = e.path().extension().and_then(|x| x.to_str()) {
if exts.contains(&ext.to_lowercase().as_str()) {
images += 1;
}
}
}
}
}
total >= 8 && images * 2 >= total
}
fn context_offer(ctx: Context, lang: Lang) -> Option<String> {
let s = match (ctx, lang) {
(Context::GitConflict, Lang::Fr) => {
"Je vois un conflit git en cours ici. Tu veux que je t'aide à le régler ? → sparrow fix"
}
(Context::GitConflict, Lang::En) => {
"I see a git conflict in progress here. Want me to help resolve it? → sparrow fix"
}
(Context::NodeNeedsInstall, Lang::Fr) => {
"Ce projet n'est pas installé (pas de node_modules). Je m'en occupe ? → sparrow fix"
}
(Context::NodeNeedsInstall, Lang::En) => {
"This project isn't installed (no node_modules). Want me to handle it? → sparrow fix"
}
(Context::ManyImages, Lang::Fr) => {
"Beaucoup de photos ici. Je peux les ranger par année. → sparrow idees grand-mere"
}
(Context::ManyImages, Lang::En) => {
"Lots of photos here. I can sort them by year. → sparrow ideas grandparent"
}
(Context::GitRepo, Lang::Fr) => {
"Tu es dans un projet de code. Un bug à corriger ? → sparrow fix · Comprendre le code ? → sparrow explique"
}
(Context::GitRepo, Lang::En) => {
"You're in a code project. A bug to fix? → sparrow fix · Understand the code? → sparrow explain"
}
(Context::Generic, _) => return None,
};
Some(s.to_string())
}
pub fn welcome_text(cwd: &Path, lang: Lang) -> String {
let mut out = String::new();
let header = match lang {
Lang::Fr => "🐦 Salut ! Qu'est-ce qu'on règle aujourd'hui ?",
Lang::En => "🐦 Hi! What are we sorting out today?",
};
out.push_str(header);
out.push_str("\n\n");
if let Some(offer) = context_offer(detect_context(cwd), lang) {
out.push_str(" ");
out.push_str(&offer);
out.push_str("\n\n");
}
let body = match lang {
Lang::Fr => {
" Décris ton problème avec tes mots :\n\
\x20 · sparrow fix \"mon site ne s'affiche plus\"\n\
\x20 · sparrow explique \"ce message d'erreur\"\n\
\x20 · sparrow idees (tout ce que tu peux faire)\n\
\x20 · sparrow whatis token (c'est quoi ce mot ?)\n\n\
Et si ça ne te plaît pas : sparrow annule remet tout comme avant."
}
Lang::En => {
" Describe your problem in your own words:\n\
\x20 · sparrow fix \"my site won't load\"\n\
\x20 · sparrow explain \"this error message\"\n\
\x20 · sparrow ideas (everything you can do)\n\
\x20 · sparrow whatis token (what's this word?)\n\n\
And if you don't like it: sparrow undo puts everything back."
}
};
out.push_str(body);
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn tmp(name: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!(
"sparrow-welcome-{name}-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn detects_node_needs_install() {
let d = tmp("node");
fs::write(d.join("package.json"), "{}").unwrap();
assert_eq!(detect_context(&d), Context::NodeNeedsInstall);
let _ = fs::remove_dir_all(d);
}
#[test]
fn detects_git_conflict() {
let d = tmp("conflict");
fs::create_dir_all(d.join(".git")).unwrap();
fs::write(d.join(".git").join("MERGE_HEAD"), "x").unwrap();
assert_eq!(detect_context(&d), Context::GitConflict);
let _ = fs::remove_dir_all(d);
}
#[test]
fn detects_photo_folder() {
let d = tmp("photos");
for i in 0..10 {
fs::write(d.join(format!("img{i}.jpg")), "x").unwrap();
}
assert_eq!(detect_context(&d), Context::ManyImages);
let _ = fs::remove_dir_all(d);
}
#[test]
fn empty_dir_is_generic() {
let d = tmp("empty");
assert_eq!(detect_context(&d), Context::Generic);
let _ = fs::remove_dir_all(d);
}
#[test]
fn welcome_text_is_warm_and_jargon_free() {
let d = tmp("welcome");
let text = welcome_text(&d, Lang::Fr);
assert!(text.contains("Qu'est-ce qu'on règle"));
assert!(text.contains("sparrow fix"));
assert!(text.contains("sparrow annule"));
for bad in ["tier", "tokens", "0.0.0.0", "[Tool"] {
assert!(!text.contains(bad), "welcome leaked `{bad}`");
}
let _ = fs::remove_dir_all(d);
}
}