use crate::cli::UserInterface;
use crate::core::app_errors::{GenerationError, Result};
use fluent::{FluentArgs, FluentBundle, FluentResource};
use futures::{FutureExt, future::LocalBoxFuture};
use std::fmt;
#[doc(alias = "translate")]
#[doc(alias = "i18n")]
pub fn fallback_translation(
bundle: &FluentBundle<FluentResource>,
key: &str,
fallback: &str,
args: Option<&FluentArgs>,
) -> String {
crate::core::i18n::get_translation(bundle, key, args).unwrap_or_else(|_| fallback.to_owned())
}
#[doc(alias = "prompt")]
#[doc(alias = "input loop")]
pub async fn prompt_loop<T, E, F, H>(
ui: &mut dyn UserInterface,
prompt: &str,
parse_fn: F,
mut on_err: H,
) -> Result<T>
where
F: Fn(&str) -> std::result::Result<T, E>,
H: for<'a> FnMut(&'a mut dyn UserInterface, &'a E) -> LocalBoxFuture<'a, ()>,
E: fmt::Display + 'static,
{
const MAX_INPUT_FAILURES: usize = 10;
let mut state = PromptState::new();
loop {
let input = match ui.prompt(prompt).await {
Ok(s) => {
state.reset();
s
}
Err(e) => {
state.register_failure();
let retry_msg = state.retry_message(MAX_INPUT_FAILURES, &e);
if let Err(_e) = ui.print(&retry_msg).await {}
if state.reached_limit(MAX_INPUT_FAILURES) {
return Err(state.input_failure_error(MAX_INPUT_FAILURES, &e));
}
continue;
}
};
match parse_fn(&input) {
Ok(v) => break Ok(v),
Err(e) => on_err(ui, &e).await,
}
}
}
struct PromptState {
failures: usize,
}
impl PromptState {
fn new() -> Self {
Self { failures: 0 }
}
fn reset(&mut self) {
self.failures = 0;
}
fn register_failure(&mut self) {
self.failures = self.failures.saturating_add(1);
}
fn reached_limit(&self, max_limit: usize) -> bool {
self.failures >= max_limit
}
fn retry_message(&self, max_limit: usize, err: &dyn fmt::Display) -> String {
format!(
"Failed to read input (attempt {}/{max_limit}): {err}",
self.failures
)
}
fn input_failure_error(&self, max_limit: usize, err: &dyn fmt::Display) -> GenerationError {
GenerationError::InputFailure(format!(
"Consecutive input errors exceeded maximum attempts ({max_limit}). Last error: {err}"
))
}
}
#[doc(alias = "ask")]
#[doc(alias = "yes no")]
pub async fn ask_user_yes_no(
ui: &mut dyn UserInterface,
bundle: &FluentBundle<FluentResource>,
message: &str,
) -> Result<bool> {
if message.is_empty() {
return Ok(false);
}
let invalid_msg = fallback_translation(
bundle,
"error_invalid_input",
"Invalid input. Please enter yes or no.",
None,
);
let ans = prompt_loop(ui, message, parse_yes_no_input, {
move |ui, _error| {
let msg = invalid_msg.clone();
async move {
ui.print(&msg).await.ok();
}
.boxed_local()
}
})
.await?;
Ok(ans)
}
#[doc(alias = "parse")]
#[doc(alias = "yes no")]
pub fn parse_yes_no_input(s: &str) -> std::result::Result<bool, GenerationError> {
match s.trim().to_lowercase().as_str() {
"y" | "yes" | "はい" | "ja" => Ok(true),
"n" | "no" | "いいえ" | "nein" => Ok(false),
_ => Err(GenerationError::InvalidInput),
}
}