commit_wizard/adapters/prompt/
tty.rs1use anyhow::Result;
2use dialoguer::{Confirm, Input, Select};
3
4use crate::{domain::commit::CommitType, ports::prompt::PromptPort};
5
6#[derive(Default)]
7pub struct TtyPrompt;
8
9impl PromptPort for TtyPrompt {
10 fn ask_type(&self) -> Result<CommitType> {
11 let types = [
12 ("feat", CommitType::Feat),
13 ("fix", CommitType::Fix),
14 ("docs", CommitType::Docs),
15 ("chore", CommitType::Chore),
16 ("refactor", CommitType::Refactor),
17 ("test", CommitType::Test),
18 ("perf", CommitType::Perf),
19 ("build", CommitType::Build),
20 ("ci", CommitType::Ci),
21 ("style", CommitType::Style),
22 ];
23 let labels: Vec<&str> = types.iter().map(|(s, _)| *s).collect();
24
25 let idx = Select::new()
26 .with_prompt("Type")
27 .items(&labels)
28 .default(0)
29 .interact()?;
30
31 Ok(types[idx].1)
32 }
33
34 fn ask_scope(&self) -> Result<Option<String>> {
35 let s: String = Input::new()
36 .with_prompt("Scope (optional)")
37 .allow_empty(true)
38 .interact_text()?;
39 let s = s.trim().to_string();
40 Ok(if s.is_empty() { None } else { Some(s) })
41 }
42
43 fn ask_summary(&self) -> Result<String> {
44 let summary: String = Input::new()
45 .with_prompt("Summary (max 72 chars)")
46 .validate_with(|input: &String| -> Result<(), &str> {
47 let trimmed = input.trim();
48 if trimmed.is_empty() {
49 return Err("summary cannot be empty");
50 }
51 if trimmed.chars().count() > 72 {
52 return Err("summary too long (max 72 chars)");
53 }
54 Ok(())
55 })
56 .interact_text()?;
57 Ok(summary.trim().to_string())
58 }
59
60 fn ask_body(&self) -> Result<Option<String>> {
61 let body: String = Input::new()
62 .with_prompt("Body (optional, single line for now)")
63 .allow_empty(true)
64 .interact_text()?;
65 let body = body.trim().to_string();
66 Ok(if body.is_empty() { None } else { Some(body) })
67 }
68
69 fn confirm_breaking(&self) -> Result<bool> {
70 let yes = Confirm::new()
71 .with_prompt("Breaking change?")
72 .default(false)
73 .interact()?;
74 Ok(yes)
75 }
76}