|
|
|
@@ -17,7 +17,7 @@ use crate::{
|
|
|
|
///
|
|
|
|
///
|
|
|
|
/// Implement this trait to supply a custom front-end (interactive TUI, mock,
|
|
|
|
/// Implement this trait to supply a custom front-end (interactive TUI, mock,
|
|
|
|
/// headless, etc.) to [`CommitWorkflow`](super::CommitWorkflow).
|
|
|
|
/// headless, etc.) to [`CommitWorkflow`](super::CommitWorkflow).
|
|
|
|
pub trait Prompter {
|
|
|
|
pub trait Prompter: Send + Sync {
|
|
|
|
/// Prompt the user to select a commit type
|
|
|
|
/// Prompt the user to select a commit type
|
|
|
|
fn select_commit_type(&self) -> Result<CommitType, Error>;
|
|
|
|
fn select_commit_type(&self) -> Result<CommitType, Error>;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -66,32 +66,67 @@ pub struct RealPrompts;
|
|
|
|
|
|
|
|
|
|
|
|
impl Prompter for RealPrompts {
|
|
|
|
impl Prompter for RealPrompts {
|
|
|
|
fn select_commit_type(&self) -> Result<CommitType, Error> {
|
|
|
|
fn select_commit_type(&self) -> Result<CommitType, Error> {
|
|
|
|
inquire::Select::new("Select commit type:", CommitType::all().to_vec())
|
|
|
|
use inquire::Select;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let options: Vec<_> = CommitType::all()
|
|
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
|
|
.map(|ct| format!("{}: {}", ct, ct.description()))
|
|
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let answer = Select::new("Select commit type:", options)
|
|
|
|
.with_page_size(11)
|
|
|
|
.with_page_size(11)
|
|
|
|
.with_help_message(
|
|
|
|
.with_help_message(
|
|
|
|
"Use arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details.",
|
|
|
|
"Use arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details.",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.with_formatter(&|option| format!("{}: {}", option.value.as_str(), option.value.description()))
|
|
|
|
|
|
|
|
.prompt()
|
|
|
|
.prompt()
|
|
|
|
.map_err(|_| Error::Cancelled)
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract the commit type from the selected option
|
|
|
|
|
|
|
|
let selected_type = answer
|
|
|
|
|
|
|
|
.split(':')
|
|
|
|
|
|
|
|
.next()
|
|
|
|
|
|
|
|
.ok_or_else(|| Error::JjOperation {
|
|
|
|
|
|
|
|
context: "Failed to parse selected commit type".to_string(),
|
|
|
|
|
|
|
|
})?
|
|
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CommitType::all()
|
|
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
|
|
.find(|ct| ct.as_str() == selected_type)
|
|
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
|
|
.ok_or_else(|| Error::JjOperation {
|
|
|
|
|
|
|
|
context: format!("Unknown commit type: {}", selected_type),
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn input_scope(&self) -> Result<Scope, Error> {
|
|
|
|
fn input_scope(&self) -> Result<Scope, Error> {
|
|
|
|
let answer = inquire::Text::new("Enter scope (optional):")
|
|
|
|
use inquire::Text;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let answer = Text::new("Enter scope (optional):")
|
|
|
|
.with_help_message(
|
|
|
|
.with_help_message(
|
|
|
|
"Scope is optional. If provided, it should be a noun representing the section of code affected (e.g., 'api', 'ui', 'config'). Max 30 characters.",
|
|
|
|
"Scope is optional. If provided, it should be a noun representing the section of code affected (e.g., 'api', 'ui', 'config'). Max 30 characters.",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.with_placeholder("Leave empty if no scope")
|
|
|
|
.with_placeholder("Leave empty if no scope")
|
|
|
|
.prompt_skippable()
|
|
|
|
.prompt_skippable()
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
match answer {
|
|
|
|
|
|
|
|
Some(s) if s.trim().is_empty() => Ok(Scope::empty()),
|
|
|
|
// Empty input is valid (no scope)
|
|
|
|
Some(s) => Scope::parse(s.trim()).map_err(|e| Error::InvalidScope(e.to_string())),
|
|
|
|
let answer_str = match answer {
|
|
|
|
None => Ok(Scope::empty()),
|
|
|
|
Some(s) => s,
|
|
|
|
|
|
|
|
None => return Ok(Scope::empty()),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if answer_str.trim().is_empty() {
|
|
|
|
|
|
|
|
return Ok(Scope::empty());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Parse and validate the scope
|
|
|
|
|
|
|
|
Scope::parse(answer_str.trim()).map_err(|e| Error::InvalidScope(e.to_string()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn input_description(&self) -> Result<Description, Error> {
|
|
|
|
fn input_description(&self) -> Result<Description, Error> {
|
|
|
|
|
|
|
|
use inquire::Text;
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
loop {
|
|
|
|
let answer = Text::new("Enter description (required):")
|
|
|
|
let answer = Text::new("Enter description (required):")
|
|
|
|
.with_help_message(
|
|
|
|
.with_help_message(
|
|
|
|
@@ -145,10 +180,13 @@ impl Prompter for RealPrompts {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn input_body(&self) -> Result<Body, Error> {
|
|
|
|
fn input_body(&self) -> Result<Body, Error> {
|
|
|
|
|
|
|
|
use inquire::Editor;
|
|
|
|
|
|
|
|
|
|
|
|
let wants_body = Confirm::new("Add a body?")
|
|
|
|
let wants_body = Confirm::new("Add a body?")
|
|
|
|
.with_default(false)
|
|
|
|
.with_default(false)
|
|
|
|
.prompt()
|
|
|
|
.prompt()
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
|
|
|
|
|
|
|
|
if !wants_body {
|
|
|
|
if !wants_body {
|
|
|
|
return Ok(Body::default());
|
|
|
|
return Ok(Body::default());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -158,11 +196,12 @@ JJ: Body (optional). Markdown is supported.\n\
|
|
|
|
JJ: Wrap prose lines at 72 characters where possible.\n\
|
|
|
|
JJ: Wrap prose lines at 72 characters where possible.\n\
|
|
|
|
JJ: Lines starting with \"JJ:\" will be removed.\n";
|
|
|
|
JJ: Lines starting with \"JJ:\" will be removed.\n";
|
|
|
|
|
|
|
|
|
|
|
|
let raw = inquire::Editor::new("Body:")
|
|
|
|
let raw = Editor::new("Body:")
|
|
|
|
.with_predefined_text(template)
|
|
|
|
.with_predefined_text(template)
|
|
|
|
.with_file_extension(".md")
|
|
|
|
.with_file_extension(".md")
|
|
|
|
.prompt()
|
|
|
|
.prompt()
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
.map_err(|_| Error::Cancelled)?;
|
|
|
|
|
|
|
|
|
|
|
|
let stripped: String = raw
|
|
|
|
let stripped: String = raw
|
|
|
|
.lines()
|
|
|
|
.lines()
|
|
|
|
.filter(|line| !line.starts_with("JJ:"))
|
|
|
|
.filter(|line| !line.starts_with("JJ:"))
|
|
|
|
@@ -173,11 +212,16 @@ JJ: Lines starting with \"JJ:\" will be removed.\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn confirm_apply(&self, message: &str) -> Result<bool, Error> {
|
|
|
|
fn confirm_apply(&self, message: &str) -> Result<bool, Error> {
|
|
|
|
|
|
|
|
use inquire::Confirm;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Show preview
|
|
|
|
println!(
|
|
|
|
println!(
|
|
|
|
"\n📝 Commit Message Preview:\n{}\n",
|
|
|
|
"\n📝 Commit Message Preview:\n{}\n",
|
|
|
|
format_message_box(message)
|
|
|
|
format_message_box(message)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
inquire::Confirm::new("Apply this commit message?")
|
|
|
|
|
|
|
|
|
|
|
|
// Get confirmation
|
|
|
|
|
|
|
|
Confirm::new("Apply this commit message?")
|
|
|
|
.with_default(true)
|
|
|
|
.with_default(true)
|
|
|
|
.with_help_message("Select 'No' to cancel and start over")
|
|
|
|
.with_help_message("Select 'No' to cancel and start over")
|
|
|
|
.prompt()
|
|
|
|
.prompt()
|
|
|
|
|