use color_eyre::{eyre::Report, Result};
use inquire::{validator::Validation, Confirm, CustomUserError, InquireError, Select, Text};
#[cfg(test)]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(test)]
use std::sync::{LazyLock, Mutex};
#[cfg(test)]
static MOCK_ENABLED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false));
#[cfg(test)]
static MOCK_INPUT: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
#[cfg(test)]
static MOCK_CONFIRM: LazyLock<Mutex<Option<bool>>> = LazyLock::new(|| Mutex::new(None));
#[cfg(test)]
static MOCK_SELECT: LazyLock<Mutex<Option<usize>>> = LazyLock::new(|| Mutex::new(None));
#[cfg(test)]
pub fn enable_mocking() {
MOCK_ENABLED.store(true, Ordering::SeqCst);
}
#[cfg(test)]
pub fn disable_mocking() {
MOCK_ENABLED.store(false, Ordering::SeqCst);
}
#[cfg(test)]
pub fn set_mock_input(value: Option<String>) {
if let Ok(mut lock) = MOCK_INPUT.lock() {
*lock = value;
}
}
#[cfg(test)]
pub fn set_mock_confirm(value: Option<bool>) {
if let Ok(mut lock) = MOCK_CONFIRM.lock() {
*lock = value;
}
}
#[cfg(test)]
pub fn set_mock_select(value: Option<usize>) {
if let Ok(mut lock) = MOCK_SELECT.lock() {
*lock = value;
}
}
pub fn input_with_default(prompt: &str, default: &str) -> color_eyre::Result<String> {
#[cfg(test)]
{
if MOCK_ENABLED.load(Ordering::SeqCst) {
if let Ok(mut lock) = MOCK_INPUT.lock() {
if let Some(mock_value) = lock.take() {
return Ok(mock_value);
}
}
}
}
let result = Text::new(prompt)
.with_default(default)
.with_help_message("Enter a value or press Enter to use the default")
.prompt();
match result {
Ok(value) => Ok(value),
Err(InquireError::OperationCanceled) => Err(Report::msg("Operation canceled by user")),
Err(err) => Err(Report::msg(format!("Input error: {err}"))),
}
}
#[allow(dead_code)]
pub fn input_required(prompt: &str) -> color_eyre::Result<String> {
#[cfg(test)]
{
if MOCK_ENABLED.load(Ordering::SeqCst) {
if let Ok(mut lock) = MOCK_INPUT.lock() {
if let Some(mock_value) = lock.take() {
return Ok(mock_value);
}
}
}
}
let result = Text::new(prompt)
.with_help_message("This field is required")
.prompt();
match result {
Ok(value) => Ok(value),
Err(InquireError::OperationCanceled) => Err(Report::msg("Operation canceled by user")),
Err(err) => Err(Report::msg(format!("Input error: {err}"))),
}
}
pub fn confirm(prompt: &str, default: bool) -> color_eyre::Result<bool> {
#[cfg(test)]
{
if MOCK_ENABLED.load(Ordering::SeqCst) {
if let Ok(mut lock) = MOCK_CONFIRM.lock() {
if let Some(mock_value) = lock.take() {
return Ok(mock_value);
}
}
}
}
let result = Confirm::new(prompt)
.with_default(default)
.with_help_message("Press y for yes, n for no")
.prompt();
match result {
Ok(value) => Ok(value),
Err(InquireError::OperationCanceled) => Err(Report::msg("Operation canceled by user")),
Err(err) => Err(Report::msg(format!("Confirmation error: {err}"))),
}
}
pub fn select(prompt: &str, options: &[&str]) -> color_eyre::Result<usize> {
#[cfg(test)]
{
if MOCK_ENABLED.load(Ordering::SeqCst) {
if let Ok(mut lock) = MOCK_SELECT.lock() {
if let Some(mock_value) = lock.take() {
return Ok(mock_value);
}
}
}
}
let result = Select::new(prompt, options.to_vec())
.with_help_message("Use arrow keys to navigate, Enter to select")
.prompt();
match result {
Ok(value) => {
match options.iter().position(|&item| item == value) {
Some(index) => Ok(index),
None => Err(Report::msg("Selected value not found in options list")),
}
}
Err(InquireError::OperationCanceled) => Err(Report::msg("Operation canceled by user")),
Err(err) => Err(Report::msg(format!("Selection error: {err}"))),
}
}
#[allow(dead_code)]
pub fn project_name(prompt: &str) -> Result<String> {
#[cfg(test)]
{
if MOCK_ENABLED.load(Ordering::SeqCst) {
if let Ok(mut lock) = MOCK_INPUT.lock() {
if let Some(mock_value) = lock.take() {
if mock_value.is_empty() {
return Err(Report::msg("Project name cannot be empty"));
}
if mock_value.contains(|c: char| !c.is_alphanumeric() && c != '_' && c != '-') {
return Err(Report::msg(
"Project name must contain only alphanumeric characters, '-', or '_'",
));
}
if mock_value.chars().next().is_none_or(|c| !c.is_alphabetic()) {
return Err(Report::msg("Project name must start with a letter"));
}
return Ok(mock_value);
}
}
}
}
let validator = |input: &str| -> Result<Validation, CustomUserError> {
if input.is_empty() {
return Ok(Validation::Invalid("Project name cannot be empty".into()));
}
if input.contains(|c: char| !c.is_alphanumeric() && c != '_' && c != '-') {
return Ok(Validation::Invalid(
"Project name must contain only alphanumeric characters, '-', or '_'".into(),
));
}
if input.chars().next().is_none_or(|c| !c.is_alphabetic()) {
return Ok(Validation::Invalid(
"Project name must start with a letter".into(),
));
}
Ok(Validation::Valid)
};
let result = Text::new(prompt)
.with_validator(validator)
.with_help_message(
"Must start with a letter and contain only alphanumeric characters, '-', or '_'",
)
.prompt();
match result {
Ok(value) => Ok(value),
Err(InquireError::OperationCanceled) => Err(Report::msg("Operation canceled by user")),
Err(err) => Err(Report::msg(format!("Input error: {err}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use color_eyre::Result;
use pretty_assertions::assert_eq;
struct MockGuard;
impl MockGuard {
fn new() -> Self {
enable_mocking();
Self
}
}
impl Drop for MockGuard {
fn drop(&mut self) {
disable_mocking();
}
}
#[test]
fn test_input_with_default_mocked() -> Result<()> {
let _guard = MockGuard::new();
set_mock_input(Some("test input".to_string()));
let result = input_with_default("Test prompt", "default")?;
assert_eq!(result, "test input");
Ok(())
}
#[test]
fn test_input_required_mocked() -> Result<()> {
let _guard = MockGuard::new();
set_mock_input(Some("required input".to_string()));
let result = input_required("Test prompt")?;
assert_eq!(result, "required input");
Ok(())
}
#[test]
fn test_confirm_mocked() -> Result<()> {
let _guard = MockGuard::new();
set_mock_confirm(Some(true));
let result = confirm("Test prompt", false)?;
assert!(result);
Ok(())
}
#[test]
fn test_select_mocked() -> Result<()> {
let _guard = MockGuard::new();
set_mock_select(Some(1));
let result = select("Test prompt", &["Option 1", "Option 2"])?;
assert_eq!(result, 1);
Ok(())
}
#[test]
fn test_project_name_validation() -> Result<()> {
let _guard = MockGuard::new();
set_mock_input(Some("valid-project".to_string()));
let result = project_name("Project name")?;
assert_eq!(result, "valid-project");
set_mock_input(Some("".to_string()));
let empty_result = project_name("Project name");
assert!(empty_result.is_err());
set_mock_input(Some("invalid@project".to_string()));
let invalid_chars_result = project_name("Project name");
assert!(invalid_chars_result.is_err());
set_mock_input(Some("1invalid".to_string()));
let invalid_start_result = project_name("Project name");
assert!(invalid_start_result.is_err());
Ok(())
}
}