main.rs
143 lignes · 4698 octets
//! `screen-select` : génère/complète le plan des écrans à coder pour une UI //! custom consommant rustwarden-core, et recoche le réalisé (`--check`). mod catalogue; mod check; mod generate; mod section; mod security; mod todo; use std::io::{self, Write}; use std::path::Path; const PLAN_PATH: &str = "docs/plans/ui-screens.md"; fn main() -> io::Result<()> { if std::env::args().any(|a| a == "--check") { run_check() } else { run_wizard() } } /// Lit une ligne au clavier (chaîne vide si EOF). fn prompt(msg: &str) -> io::Result<String> { print!("{msg}"); io::stdout().flush()?; let mut line = String::new(); io::stdin().read_line(&mut line)?; Ok(line.trim().to_string()) } fn run_wizard() -> io::Result<()> { println!("== screen-select : écrans à coder ==\n"); // 1. Sélection des fonctionnalités (les "always" sont incluses d'office). let choosable: Vec<&catalogue::Functionality> = catalogue::all().iter().filter(|f| !f.always).collect(); println!("Fonctionnalités activées ? (numéros séparés par des virgules)\n"); for (i, f) in choosable.iter().enumerate() { println!(" {:>2}. {}", i + 1, f.label); } let raw = prompt("\nSélection : ")?; let picked: Vec<usize> = raw .split(',') .filter_map(|s| s.trim().parse::<usize>().ok()) .filter(|n| *n >= 1 && *n <= choosable.len()) .map(|n| n - 1) .collect(); let mut selected: Vec<&catalogue::Functionality> = catalogue::all().iter().filter(|f| f.always).collect(); for i in picked { selected.push(choosable[i]); } // 2. Entrées optionnelles. let design_path = prompt("Chemin d'un design.md (charte graphique, vide si aucun) : ")?; let design_md = if design_path.is_empty() { None } else { Some(std::fs::read_to_string(&design_path)?) }; let layout = prompt("Structure de page (ex. 'navbar: gauche; 2 colonnes', vide si aucune) : ")?; let layout = if layout.is_empty() { None } else { Some(layout) }; let project = std::env::current_dir()? .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_else(|| "projet".into()); let inputs = generate::PlanInputs { project: &project, selected, design_md, layout, }; let desired = generate::rows_for(&inputs.selected); // 3. Fusion si un plan existe déjà (préserve statuts + routes éditées). let path = Path::new(PLAN_PATH); let content = if path.exists() { let existing = std::fs::read_to_string(path)?; let prev = todo::parse(&existing); let merged = todo::merge(&prev, &desired); // conserve l'en-tête existant, remplace le tableau… let mut c = todo::replace_table(&existing, &merged); // …puis régénère les sections sécurité (idempotent, suit la sélection courante). c = section::upsert(&c, security::GUIDES_ID, &security::guides_body()); c = section::upsert( &c, security::PER_SCREEN_ID, &security::reference_body(&inputs.selected), ); c } else { generate::render_plan(&inputs, &desired) }; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, content)?; println!( "\n✓ Plan écrit dans {PLAN_PATH} ({} écrans).", desired.len() ); println!( " Contrôlez le réalisé : cargo run -p rustwarden-core --bin screen-select -- --check" ); Ok(()) } fn run_check() -> io::Result<()> { let path = Path::new(PLAN_PATH); if !path.exists() { eprintln!("Aucun plan à {PLAN_PATH} — lancez d'abord `screen-select` sans --check."); std::process::exit(1); } let content = std::fs::read_to_string(path)?; let mut rows = todo::parse(&content); let haystack = check::collect_sources(&std::env::current_dir()?); check::mark(&mut rows, &haystack); let updated = todo::replace_table(&content, &rows); std::fs::write(path, updated)?; let done = rows.iter().filter(|r| r.done).count(); let missing: Vec<&todo::TodoRow> = rows.iter().filter(|r| !r.done).collect(); println!("Couverture : {}/{} écrans réalisés.", done, rows.len()); if missing.is_empty() { println!("✓ Tous les écrans des fonctionnalités activées sont câblés."); } else { println!("Écrans manquants :"); for r in missing { println!(" ☐ {} — {} ({})", r.functionality, r.screen, r.route); } } Ok(()) }
GitRust