use crate::cli::printer::PRINTER;
use crate::lv;
use crate::{Atomic, Context as _};
pub(crate) static PROMPT_LEVEL: Atomic<u8, lv::Prompt> =
Atomic::new_u8(lv::Prompt::Interactive as u8);
type AnswerRecv = oneshot::Receiver<cu::Result<Option<cu::ZString>>>;
#[cfg_attr(feature = "coroutine", doc = "```rust,no_run")]
#[cfg_attr(not(feature = "coroutine"), doc = "```rust,ignore")]
#[inline(always)]
#[must_use = "prompt() returns a builder; you must call run() or co_run() to start the prompt"]
pub fn prompt(
message: impl Into<String>,
) -> PromptBuilder<cu::ZString, Cancellable, impl FnMut(&mut String) -> cu::Result<bool>> {
PromptBuilder::new(message)
}
#[inline(always)]
#[must_use = "yesno() returns a builder; you must call run() or co_run() to start the prompt"]
pub fn yesno(
message: impl Into<String>,
) -> PromptBuilder<bool, DefaultIfCancel, impl FnMut(&mut String) -> cu::Result<bool>> {
let mut message = message.into();
message.push_str(" [y/n]");
PromptBuilder::<bool, _, _>::new(message).if_cancel(false)
}
#[doc(hidden)]
pub trait PromptCancelConfig {}
#[doc(hidden)]
pub struct Cancellable;
#[doc(hidden)]
pub struct DefaultIfCancel;
#[doc(hidden)]
pub struct BailIfCancel;
impl PromptCancelConfig for Cancellable {}
impl PromptCancelConfig for DefaultIfCancel {}
impl PromptCancelConfig for BailIfCancel {}
pub struct PromptBuilder<
TOutput,
TCancel: PromptCancelConfig,
TValidate: FnMut(&mut String) -> cu::Result<bool>,
> {
message: String,
is_password: bool,
trim_trailing_whitespace: bool,
validator: TValidate,
cancel_type: TCancel,
cancel_value: Option<TOutput>,
}
impl<TOutput> PromptBuilder<TOutput, Cancellable, fn(&mut String) -> cu::Result<bool>> {
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
fn new(message: impl Into<String>) -> Self {
PromptBuilder {
message: message.into(),
is_password: false,
trim_trailing_whitespace: true,
validator: empty_validator,
cancel_type: Cancellable,
cancel_value: None,
}
}
}
impl<TCancel: PromptCancelConfig, TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<cu::ZString, TCancel, TValidate>
{
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn password(mut self) -> Self {
self.is_password = true;
self
}
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn trim_trailing_whitespace(mut self, trim: bool) -> Self {
self.trim_trailing_whitespace = trim;
self
}
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn validate_with<F>(self, validator: F) -> PromptBuilder<cu::ZString, TCancel, F>
where
F: FnMut(&mut String) -> cu::Result<bool>,
{
PromptBuilder {
message: self.message,
is_password: self.is_password,
trim_trailing_whitespace: self.trim_trailing_whitespace,
validator,
cancel_type: self.cancel_type,
cancel_value: self.cancel_value,
}
}
}
impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<cu::ZString, Cancellable, TValidate>
{
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn yesno(mut self) -> PromptBuilder<bool, Cancellable, TValidate> {
self.message.push_str(" [y/n]");
PromptBuilder {
message: self.message,
is_password: self.is_password,
trim_trailing_whitespace: self.trim_trailing_whitespace,
validator: self.validator,
cancel_type: Cancellable,
cancel_value: None,
}
}
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn if_cancel(
self,
default: impl Into<String>,
) -> PromptBuilder<cu::ZString, DefaultIfCancel, TValidate> {
PromptBuilder {
message: self.message,
is_password: self.is_password,
trim_trailing_whitespace: self.trim_trailing_whitespace,
validator: self.validator,
cancel_type: DefaultIfCancel,
cancel_value: Some(default.into().into()),
}
}
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn or_cancel(mut self) -> PromptBuilder<cu::ZString, BailIfCancel, TValidate> {
self.message.push_str(" (Ctrl-C to cancel)");
PromptBuilder {
message: self.message,
is_password: self.is_password,
trim_trailing_whitespace: self.trim_trailing_whitespace,
validator: self.validator,
cancel_type: BailIfCancel,
cancel_value: None,
}
}
pub fn run(self) -> cu::Result<Option<cu::ZString>> {
check_prompt_level(false)?;
run_prompt_loop(
self.message,
self.is_password,
self.trim_trailing_whitespace,
self.validator,
)
}
#[cfg(feature = "coroutine")]
pub async fn co_run(self) -> cu::Result<Option<cu::ZString>> {
check_prompt_level(false)?;
co_run_prompt_loop(
self.message,
self.is_password,
self.trim_trailing_whitespace,
self.validator,
)
.await
}
}
impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<cu::ZString, DefaultIfCancel, TValidate>
{
pub fn run(self) -> cu::Result<cu::ZString> {
check_prompt_level(false)?;
let result = run_prompt_loop(
self.message,
self.is_password,
self.trim_trailing_whitespace,
self.validator,
)?
.unwrap_or(self.cancel_value.unwrap());
Ok(result)
}
#[cfg(feature = "coroutine")]
pub async fn co_run(self) -> cu::Result<cu::ZString> {
check_prompt_level(false)?;
let result = co_run_prompt_loop(
self.message,
self.is_password,
self.trim_trailing_whitespace,
self.validator,
)
.await?
.unwrap_or(self.cancel_value.unwrap());
Ok(result)
}
}
impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<cu::ZString, BailIfCancel, TValidate>
{
pub fn run(self) -> cu::Result<cu::ZString> {
check_prompt_level(false)?;
match run_prompt_loop(
self.message,
self.is_password,
self.trim_trailing_whitespace,
self.validator,
)? {
Some(result) => Ok(result),
None => crate::bail!("operation cancelled by user"),
}
}
#[cfg(feature = "coroutine")]
pub async fn co_run(self) -> cu::Result<cu::ZString> {
check_prompt_level(false)?;
match co_run_prompt_loop(
self.message,
self.is_password,
self.trim_trailing_whitespace,
self.validator,
)
.await?
{
Some(result) => Ok(result),
None => crate::bail!("operation cancelled by user"),
}
}
}
impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<bool, Cancellable, TValidate>
{
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn if_cancel(self, default: bool) -> PromptBuilder<bool, DefaultIfCancel, TValidate> {
PromptBuilder {
message: self.message,
is_password: self.is_password,
trim_trailing_whitespace: self.trim_trailing_whitespace,
validator: self.validator,
cancel_type: DefaultIfCancel,
cancel_value: Some(default),
}
}
#[inline(always)]
#[must_use = "you must call run() or co_run() to start the prompt"]
pub fn or_cancel(mut self) -> PromptBuilder<bool, BailIfCancel, TValidate> {
self.message.push_str(" (Ctrl-C to cancel)");
PromptBuilder {
message: self.message,
is_password: self.is_password,
trim_trailing_whitespace: self.trim_trailing_whitespace,
validator: self.validator,
cancel_type: BailIfCancel,
cancel_value: None,
}
}
pub fn run(self) -> cu::Result<Option<bool>> {
if check_prompt_level(true)? {
return Ok(Some(true));
}
run_yesno_loop(self.message, self.is_password)
}
#[cfg(feature = "coroutine")]
pub async fn co_run(self) -> cu::Result<Option<bool>> {
if check_prompt_level(true)? {
return Ok(Some(true));
}
co_run_yesno_loop(self.message, self.is_password).await
}
}
impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<bool, BailIfCancel, TValidate>
{
pub fn run(self) -> cu::Result<bool> {
if check_prompt_level(true)? {
return Ok(true);
}
match run_yesno_loop(self.message, self.is_password)? {
Some(result) => Ok(result),
None => crate::bail!("operation cancelled by user"),
}
}
#[cfg(feature = "coroutine")]
pub async fn co_run(self) -> cu::Result<bool> {
if check_prompt_level(true)? {
return Ok(true);
}
match co_run_yesno_loop(self.message, self.is_password).await? {
Some(result) => Ok(result),
None => crate::bail!("operation cancelled by user"),
}
}
}
impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
PromptBuilder<bool, DefaultIfCancel, TValidate>
{
pub fn run(self) -> cu::Result<bool> {
if check_prompt_level(true)? {
return Ok(true);
}
Ok(run_yesno_loop(self.message, self.is_password)?.unwrap_or(self.cancel_value.unwrap()))
}
#[cfg(feature = "coroutine")]
pub async fn co_run(self) -> cu::Result<bool> {
if check_prompt_level(true)? {
return Ok(true);
}
Ok(co_run_yesno_loop(self.message, self.is_password)
.await?
.unwrap_or(self.cancel_value.unwrap()))
}
}
fn run_yesno_loop(message: String, is_password: bool) -> cu::Result<Option<bool>> {
let mut answer = false;
let _ = cu::some!(run_prompt_loop(
message,
is_password,
false, |x| {
match parse_yesno(x) {
Some(x) => {
answer = x;
Ok(true)
}
None => {
cu::hint!("please enter yes or no");
Ok(false)
}
}
}
)?);
Ok(Some(answer))
}
#[cfg(feature = "coroutine")]
async fn co_run_yesno_loop(message: String, is_password: bool) -> cu::Result<Option<bool>> {
let mut answer = false;
let _ = cu::some!(
co_run_prompt_loop(
message,
is_password,
false, |x| {
match parse_yesno(x) {
Some(x) => {
answer = x;
Ok(true)
}
None => {
cu::hint!("please enter yes or no");
Ok(false)
}
}
}
)
.await?
);
Ok(Some(answer))
}
#[inline]
fn parse_yesno(x: &mut str) -> Option<bool> {
x.make_ascii_lowercase();
match x.trim() {
"y" | "yes" => Some(true),
"n" | "no" => Some(false),
_ => None,
}
}
#[inline(always)]
fn run_prompt_loop<F: FnMut(&mut String) -> cu::Result<bool>>(
message: String,
is_password: bool,
trim_trailing_whitespace: bool,
mut validator: F,
) -> cu::Result<Option<cu::ZString>> {
loop {
let result = do_show_prompt(&message, is_password)?;
let result = cu::check!(result.recv(), "failed to receive answer to prompt")?;
let result = cu::check!(result, "an error occured while processing a prompt")?;
let mut result = cu::some!(result);
if trim_trailing_whitespace {
let len = result.trim_end().len();
result.truncate(len);
}
if validator(&mut result)? {
return Ok(Some(result));
}
}
}
#[inline(always)]
#[cfg(feature = "coroutine")]
async fn co_run_prompt_loop<F: FnMut(&mut String) -> cu::Result<bool>>(
message: String,
is_password: bool,
trim_trailing_whitespace: bool,
mut validator: F,
) -> cu::Result<Option<cu::ZString>> {
loop {
let result = do_show_prompt(&message, is_password)?;
let result = cu::check!(result.await, "failed to receive answer to prompt")?;
let result = cu::check!(result, "an error occured while processing a prompt")?;
let mut result = cu::some!(result);
if trim_trailing_whitespace {
let len = result.trim_end().len();
result.truncate(len);
}
if validator(&mut result)? {
return Ok(Some(result));
}
}
}
fn do_show_prompt(message: &str, is_password: bool) -> cu::Result<AnswerRecv> {
if let Ok(mut printer) = PRINTER.lock()
&& let Some(printer) = printer.as_mut()
{
Ok(printer.show_prompt(message, is_password))
} else {
crate::bail!("prompt failed: failed to lock global printer");
}
}
fn check_prompt_level(is_yesno: bool) -> crate::Result<bool> {
if is_yesno {
match PROMPT_LEVEL.get() {
lv::Prompt::YesOrInteractive | lv::Prompt::YesOrBlock => return Ok(true),
lv::Prompt::Interactive => return Ok(false),
lv::Prompt::Block => {}
}
} else {
if !matches!(
PROMPT_LEVEL.get(),
lv::Prompt::YesOrBlock | lv::Prompt::Block
) {
return Ok(false);
}
}
crate::bail!("prompt not allowed with --non-interactive");
}
#[inline(always)]
fn empty_validator(_: &mut String) -> cu::Result<bool> {
Ok(true)
}