diff --git a/src/interactive.rs b/src/interactive.rs new file mode 100644 index 0000000..89ed63a --- /dev/null +++ b/src/interactive.rs @@ -0,0 +1,69 @@ +use dialoguer::{Confirm, MultiSelect, Input, Select}; +use crate::convention::CommitType; +use crate::error::Result; + +pub struct InteractivePrompter; + +impl InteractivePrompter { + pub fn new() -> Self { Self } + + pub fn select_commit_type(&self, detected_type: &CommitType, all_types: &[CommitType]) -> Result { + println!("Detected commit type: {}", detected_type); + let type_names: Vec = all_types.iter().map(|t| t.to_string()).collect(); + let items: Vec<&str> = type_names.iter().map(|s| s.as_str()).collect(); + println!("\nSelect commit type:"); + let selected = Select::new().with_prompt("Choose type").items(&items).default(0).interact()?; + Ok(all_types[selected].clone()) + } + + pub fn confirm_scope(&self, suggested_scope: &str, available_scopes: &[String]) -> Result> { + println!("\nSuggested scope: {}", suggested_scope); + let mut options = vec!["Use suggested scope", "Enter custom scope", "No scope"]; + options.extend(available_scopes.iter().map(|s| s.as_str())); + let selected = Select::new().with_prompt("Choose scope").items(&options).default(0).interact()?; + match selected { + 0 => Ok(Some(suggested_scope.to_string())), + 1 => { + let input: String = Input::new().with_prompt("Enter custom scope").interact_text()?; + Ok(if input.trim().is_empty() { None } else { Some(input) }) + } + 2 => Ok(None), + _ => { + let idx = selected - 3; + Ok(if idx < available_scopes.len() { Some(available_scopes[idx].clone()) } else { None }) + } + } + } + + pub fn confirm_description(&self, suggested: &str) -> Result { + let input: String = Input::new().with_prompt("Commit description").with_initial_text(suggested).interact_text()?; + Ok(input) + } + + pub fn confirm_commit(&self, commit_message: &str) -> Result { + println!("\nGenerated commit message:\n {}", commit_message); + Confirm::new().with_prompt("Use this commit message?").default(true).interact() + .map_err(|e| crate::error::Error::Other(e.to_string())) + } + + pub fn select_type_interactively(&self, suggestions: &[(CommitType, f64)]) -> Result { + let items: Vec = suggestions.iter().map(|(t, conf)| format!("{} ({:.0}%)", t, conf * 100.0)).collect(); + let selected = MultiSelect::new().with_prompt("Select commit types").items(&items.iter().map(|s| s.as_str()).collect::>()).interact()?; + if selected.is_empty() { return Ok(CommitType::Chore); } + Ok(suggestions[selected[0]].0.clone()) + } + + pub fn show_alternatives(&self, alternatives: &[String]) -> Result> { + if alternatives.is_empty() { return Ok(None); } + let selected = Select::new().with_prompt("Multiple possibilities detected. Choose one:").items(&alternatives.iter().map(|s| s.as_str()).collect::>()).default(0).interact()?; + Ok(Some(selected)) + } +} + +#[cfg(test)] mod tests { + use super::*; + #[test] fn test_interactive_prompter_new() { + let prompter = InteractivePrompter::new(); + assert!(prompter.select_commit_type(&CommitType::Feat, &[CommitType::Feat, CommitType::Fix]).is_ok()); + } +}