commit_wizard/adapters/prompt/
tty.rs

1use 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}