Skip to main content

pistonite_cu/cli/
prompt.rs

1use crate::cli::printer::PRINTER;
2use crate::lv;
3use crate::{Atomic, Context as _};
4
5pub(crate) static PROMPT_LEVEL: Atomic<u8, lv::Prompt> =
6    Atomic::new_u8(lv::Prompt::Interactive as u8);
7
8type AnswerRecv = oneshot::Receiver<cu::Result<Option<cu::ZString>>>;
9
10/// # Prompting
11/// The `prompt` feature allows displaying prompts in the console to accept
12/// user input. The prompts are thread-safe and synchronized with the printer.
13/// When a prompt is active, outputs to the console will be buffered inside the printer.
14/// Progress bars will also be paused.
15///
16/// The `--yes`, `--non-interactive`, and `--interactive` CLI flags are enabled
17/// with the `prompt` feature, see [Command Line Interface](mod@crate::cli) for more information.
18///
19/// The [`PromptBuilder`](crate::cli::PromptBuilder) is the main struct used to configure the prompt.
20/// The entry point to configuration are `cu::prompt()` and [`cu::yesno()`]. The default configuration
21/// are as follows:
22/// - `cu::prompt(message)`: No validation, returns `None` if user presses `Ctrl-C`
23/// - `cu::yesno(messsage)`: Accepts case-insensitive `y`, `n`, `yes` or `no`, appends `" [y/n]"`
24///   to the input message, and pressing `Ctrl-C` is the same as answering "no".
25///
26/// Call `.run()` to show the prompt and wait for the input:
27/// ```rust,no_run
28/// # use pistonite_cu as cu;
29/// match cu::prompt("please enter your name").run()? {
30///     None => cu::info!("cancelled"), // pressed Ctrl-C
31///     Some(name) => cu::info!("user entered: {name}"),
32/// }
33/// # cu::Ok(())
34/// ```
35///
36/// # Macros
37/// The [`cu::prompt!`] or the [`cu::yesno!`] macro are shorthands to show prompt for the most
38/// common scenarios. They also accept format args:
39/// - `cu::prompt!(format_args)`: No validation, **bails if user presses `Ctrl-C`**
40/// - `cu::yesno!(format_args)`: Same behavior as `cu::yesno(message)`
41///
42/// These are designed to "just work" with `?` in 99% of the cases.
43///
44/// # Builder Methods
45/// For advanced configurations, refer to the following builder methods
46/// - [`password`](PromptBuilder::password): Hides the input when typing in console.
47///   Also will force reading from terminal if stdin is not terminal.
48/// - [`trim_trailing_whitespace`](PromptBuilder::trim_trailing_whitespace):
49///   If disabled, trailing whitespaces are not trimmed. The output will still not contain
50///   CRLF or LF at the end. (Default is enabled - will trim whitespace).
51/// - [`validate_with`](PromptBuilder::validate_with): attaches a validation function. The prompt
52///   will loop until the validation passed or an error occurs.
53/// - [`if_cancel`](PromptBuilder::if_cancel): Set a default value to return if `Ctrl-C`
54///   is pressed. This will make the prompt not return `Option`, but the inner value directly.
55///   (Note the default value does not go through the validator)
56/// - [`or_cancel`](PromptBuilder::or_cancel): Return a "canceled by user" error if `Ctrl-C` is pressed.
57///   This also appends "(Ctrl-C to cancel)" to the prompt message.
58///
59/// Example:
60/// ```rust,no_run
61/// # use pistonite_cu as cu;
62/// let expected = "rust";
63/// let answer: cu::ZString = cu::prompt(
64///     format!("what's your favorite programming language? please answer {}", expected)
65/// ).validate_with(
66///     |answer| {
67///         if answer == expected {
68///             return Ok(true);
69///         }
70///         if answer == "javascript" {
71///             cu::bail!("that's not good");
72///         }
73///         cu::error!("try again");
74///         Ok(false)
75///     }
76/// )
77/// .if_cancel("javascript")
78/// .run()?;
79/// if answer == "javascript" {
80///     cu::info!("hey, did you press Ctrl-C?");
81/// } else {
82///     cu::info!("your answer: {answer}");
83/// }
84/// # cu::Ok(())
85/// ```
86///
87/// # Async
88/// Use [`.co_run().await?`](PromptBuilder::co_run) instead of `.run()` in async
89/// contexts to wait for the answer using the async runtime instead of blocking the current
90/// thread.
91///
92/// When using the macros, add `async` to the beginning of the input
93#[cfg_attr(feature = "coroutine", doc = "```rust,no_run")]
94#[cfg_attr(not(feature = "coroutine"), doc = "```rust,ignore")]
95/// # use pistonite_cu as cu;
96/// # async fn main_() -> cu::Result<()> {
97/// let answer = cu::prompt!(async "how are you today?")?;
98/// cu::info!("you: {answer}");
99/// # Ok(()) }
100/// ```
101///
102/// # Multiple Prompts
103/// If multiple prompts are requested (for example from many threads), they are put into a FIFO
104/// queue. Only one prompt will be displayed to the user at a time.
105///
106/// # Interaction with Progress Bars
107/// If there are both progress bars and a prompt active, the prompt is displayed below
108/// the progress bars. This lines up with the common expectation that prompts are shown
109/// at the bottom of the output. Progress bars will also be paused - otherwise the user
110/// will be typing their answer all over in the animated area.
111///
112/// This brings one interesting use case, which is printing other messages related
113/// to the prompt before the prompt. For example:
114/// ```rust,no_run
115/// # use pistonite_cu as cu;
116/// // ... imagine a progress bar is active here
117/// cu::warn!(
118///     "this is an important warning about the prompt! read this carefully when answering!"
119/// );
120/// let _ = cu::prompt!("please enter important information")?;
121/// # cu::Ok(())
122/// ```
123///
124/// The warning message is guaranteed to be printed before the prompt. However, because
125/// there are progress bars active, the warning message and the prompt will be separated
126/// by the progress bar. The user might miss the important message.
127///
128/// ```text
129/// W] this is an important warning about the prompt! read this carefully when answering!
130///  ][42/100] progress running...
131/// !] please enter important information
132/// -:
133/// ```
134///
135/// The way to address this is to print any message related to the prompt within the prompt
136/// macro:
137/// ```rust,no_run
138/// # use pistonite_cu as cu;
139/// // ... imagine a progress bar is active here
140/// let _ = cu::prompt!(r"this is an important warning about the prompt! read this carefully when answering!
141/// please enter important information")?;
142/// # cu::Ok(())
143/// ```
144/// output:
145/// ```text
146///  ][42/100] progress running...
147/// !] this is an important warning about the prompt! read this carefully when answering!
148///  | please enter important information
149/// -:
150/// ```
151#[inline(always)]
152#[must_use = "prompt() returns a builder; you must call run() or co_run() to start the prompt"]
153pub fn prompt(
154    message: impl Into<String>,
155) -> PromptBuilder<cu::ZString, Cancellable, impl FnMut(&mut String) -> cu::Result<bool>> {
156    PromptBuilder::new(message)
157}
158
159/// Show a Yes/No prompt
160///
161/// *Note: Use the [`cu::yesno!`] macro if the default configuration is all you need*
162///
163/// Return `true` if the answer is Yes. Return an error if prompt is not allowed.
164///
165/// If `-y` is specified from the command line, then the prompt will not show,
166/// and `true` will be returned immediately.
167///
168/// If user does not answer `y` or `n`, the prompt will show again, until
169/// user makes a decision.
170/// ```rust,no_run
171/// # use pistonite_cu as cu;
172/// if cu::yesno("do you want to continue?").run()? {
173///     cu::info!("user picked yes");
174/// }
175/// # cu::Ok(())
176/// ```
177#[inline(always)]
178#[must_use = "yesno() returns a builder; you must call run() or co_run() to start the prompt"]
179pub fn yesno(
180    message: impl Into<String>,
181) -> PromptBuilder<bool, DefaultIfCancel, impl FnMut(&mut String) -> cu::Result<bool>> {
182    let mut message = message.into();
183    message.push_str(" [y/n]");
184    PromptBuilder::<bool, _, _>::new(message).if_cancel(false)
185}
186
187// marker traits
188#[doc(hidden)]
189pub trait PromptCancelConfig {}
190#[doc(hidden)]
191pub struct Cancellable;
192#[doc(hidden)]
193pub struct DefaultIfCancel;
194#[doc(hidden)]
195pub struct BailIfCancel;
196impl PromptCancelConfig for Cancellable {}
197impl PromptCancelConfig for DefaultIfCancel {}
198impl PromptCancelConfig for BailIfCancel {}
199
200/// See [`Prompting`](fn@crate::prompt)
201pub struct PromptBuilder<
202    TOutput,
203    TCancel: PromptCancelConfig,
204    TValidate: FnMut(&mut String) -> cu::Result<bool>,
205> {
206    message: String,
207    is_password: bool,
208    trim_trailing_whitespace: bool,
209    validator: TValidate,
210    cancel_type: TCancel,
211    cancel_value: Option<TOutput>,
212}
213
214impl<TOutput> PromptBuilder<TOutput, Cancellable, fn(&mut String) -> cu::Result<bool>> {
215    #[inline(always)]
216    #[must_use = "you must call run() or co_run() to start the prompt"]
217    fn new(message: impl Into<String>) -> Self {
218        PromptBuilder {
219            message: message.into(),
220            is_password: false,
221            trim_trailing_whitespace: true,
222            validator: empty_validator,
223            cancel_type: Cancellable,
224            cancel_value: None,
225        }
226    }
227}
228
229impl<TCancel: PromptCancelConfig, TValidate: FnMut(&mut String) -> cu::Result<bool>>
230    PromptBuilder<cu::ZString, TCancel, TValidate>
231{
232    /// Prompt for password. The input will be hidden.
233    ///
234    /// # Example
235    ///
236    /// ```rust,no_run
237    /// # use pistonite_cu as cu;
238    /// # fn main() -> cu::Result<()> {
239    /// match cu::prompt("enter your password:").password().run()? {
240    ///     Some(password) => cu::info!("user entered: {password}"),
241    ///     None => cu::error!("cancelled"),
242    /// }
243    /// # Ok(()) }
244    /// ```
245    #[inline(always)]
246    #[must_use = "you must call run() or co_run() to start the prompt"]
247    pub fn password(mut self) -> Self {
248        self.is_password = true;
249        self
250    }
251
252    /// Configure whether to trim trailing whitespace from the input.
253    ///
254    /// By default, trailing whitespace is trimmed from user input. Set this to `false`
255    /// to preserve trailing whitespace. Note that CRLF/LF line endings are always removed.
256    ///
257    /// # Example
258    ///
259    /// ```rust,no_run
260    /// # use pistonite_cu as cu;
261    /// # fn main() -> cu::Result<()> {
262    /// let input = cu::prompt("enter text:").trim_trailing_whitespace(false).run()?;
263    /// // trailing spaces will be preserved
264    /// # Ok(()) }
265    /// ```
266    #[inline(always)]
267    #[must_use = "you must call run() or co_run() to start the prompt"]
268    pub fn trim_trailing_whitespace(mut self, trim: bool) -> Self {
269        self.trim_trailing_whitespace = trim;
270        self
271    }
272
273    /// Add a validation function to the prompt.
274    /// The prompt will be looped until a validation function passes.
275    ///
276    /// The validation function takes a `&mut String`,
277    /// and returns `cu::Result<bool>`, where:
278    /// - `Ok(true)` means the validation passed.
279    /// - `Ok(false)` means the validation failed. The function can optionally
280    ///   print some kind of error or hint message
281    /// - `Err` means there is an error, the error will be propagated to the prompt call.
282    ///
283    /// The validation function can be a `FnMut` closure, which means
284    /// it can double as a result parsing function if needed
285    ///
286    /// ```rust,no_run
287    /// # use pistonite_cu as cu;
288    /// let mut index: i32 = 0;
289    /// cu::prompt("select a number between 0 and 5")
290    ///     .validate_with(
291    ///         |answer| {
292    ///             let number = match cu::parse::<i32>(answer) {
293    ///                 Err(e) => {
294    ///                     cu::error!("{e}");
295    ///                     cu::hint!("please ensure you are entering a number");
296    ///                     return Ok(false);
297    ///                 }
298    ///                 Ok(x) => x
299    ///             };
300    ///             if number < 0 {
301    ///                 cu::error!("the number you entered is too small");
302    ///                 return Ok(false);
303    ///             }
304    ///             if number > 5 {
305    ///                 cu::error!("the number you entered is too big");
306    ///                 return Ok(false);
307    ///             }
308    ///             index = number;
309    ///             Ok(true)
310    ///         }
311    ///     ).run()?;
312    /// cu::info!("index is {index:?}");
313    /// # cu::Ok(())
314    /// ```
315    #[inline(always)]
316    #[must_use = "you must call run() or co_run() to start the prompt"]
317    pub fn validate_with<F>(self, validator: F) -> PromptBuilder<cu::ZString, TCancel, F>
318    where
319        F: FnMut(&mut String) -> cu::Result<bool>,
320    {
321        PromptBuilder {
322            message: self.message,
323            is_password: self.is_password,
324            trim_trailing_whitespace: self.trim_trailing_whitespace,
325            validator,
326            cancel_type: self.cancel_type,
327            cancel_value: self.cancel_value,
328        }
329    }
330}
331impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
332    PromptBuilder<cu::ZString, Cancellable, TValidate>
333{
334    /// Convert to a yes/no confirmation prompt. The prompt will accept
335    /// "yes", "y", "no", or "n" (case-insensitive) as valid input.
336    ///
337    /// Unlike [`cu::yesno`](fn@crate::yesno), `.prompt().yesno()`
338    /// will let you detect cancellations by pressing `Ctrl-C`.
339    ///
340    /// # Example
341    ///
342    /// ```rust,no_run
343    /// # use pistonite_cu as cu;
344    /// # fn main() -> cu::Result<()> {
345    /// match cu::prompt("delete all files?").yesno().run()? {
346    ///     None => cu::error!("cancelled"),
347    ///     Some(true) => cu::info!("all files are deleted!"),
348    ///     Some(false) => cu::error!("user explicitly answered no"),
349    /// }
350    /// # Ok(()) }
351    /// ```
352    #[inline(always)]
353    #[must_use = "you must call run() or co_run() to start the prompt"]
354    pub fn yesno(mut self) -> PromptBuilder<bool, Cancellable, TValidate> {
355        self.message.push_str(" [y/n]");
356        PromptBuilder {
357            message: self.message,
358            is_password: self.is_password,
359            trim_trailing_whitespace: self.trim_trailing_whitespace,
360            validator: self.validator,
361            cancel_type: Cancellable,
362            cancel_value: None,
363        }
364    }
365
366    /// Set a default value to return if the user cancels the prompt (e.g., Ctrl+C).
367    ///
368    /// Without `if_cancel`, the prompt returns `Option<ZString>` where `None`
369    /// indicates cancellation. With `if_cancel`, the prompt returns `ZString`
370    /// directly, using the provided default value on cancellation.
371    ///
372    /// # Example
373    ///
374    /// ```rust,no_run
375    /// # use pistonite_cu as cu;
376    /// # fn main() -> cu::Result<()> {
377    /// let name: cu::ZString = cu::prompt("enter your name:")
378    ///     .if_cancel("anonymous")
379    ///     .run()?;
380    /// cu::info!("Hello, {name}!");
381    /// # Ok(()) }
382    /// ```
383    ///
384    /// This has the same effect as calling `unwrap_or()` on the resulting
385    /// `Option<ZString>` - Just that the prompt creator has control over
386    /// the outcome of the prompt, rather than the prompt consumer accessing the outcome.
387    #[inline(always)]
388    #[must_use = "you must call run() or co_run() to start the prompt"]
389    pub fn if_cancel(
390        self,
391        default: impl Into<String>,
392    ) -> PromptBuilder<cu::ZString, DefaultIfCancel, TValidate> {
393        PromptBuilder {
394            message: self.message,
395            is_password: self.is_password,
396            trim_trailing_whitespace: self.trim_trailing_whitespace,
397            validator: self.validator,
398            cancel_type: DefaultIfCancel,
399            cancel_value: Some(default.into().into()),
400        }
401    }
402
403    /// Return an error if the user cancels the prompt (e.g., Ctrl+C).
404    ///
405    /// This appends "(Ctrl-C to cancel)" to the prompt message and returns
406    /// an error with message "operation cancelled by user" if the user presses Ctrl+C.
407    ///
408    /// # Example
409    ///
410    /// ```rust,no_run
411    /// # use pistonite_cu as cu;
412    /// # fn main() -> cu::Result<()> {
413    /// let name: cu::ZString = cu::prompt("enter your name:").or_cancel().run()?;
414    /// cu::info!("Hello, {name}!");
415    /// # Ok(()) }
416    /// ```
417    #[inline(always)]
418    #[must_use = "you must call run() or co_run() to start the prompt"]
419    pub fn or_cancel(mut self) -> PromptBuilder<cu::ZString, BailIfCancel, TValidate> {
420        self.message.push_str(" (Ctrl-C to cancel)");
421        PromptBuilder {
422            message: self.message,
423            is_password: self.is_password,
424            trim_trailing_whitespace: self.trim_trailing_whitespace,
425            validator: self.validator,
426            cancel_type: BailIfCancel,
427            cancel_value: None,
428        }
429    }
430
431    /// Show the prompt and block the current thread to wait for input.
432    ///
433    /// If the user presses `Ctrl-C`, `None` will be returned.
434    ///
435    /// # Blocking
436    /// **Do not use this in an async context**, since it will block the runtime,
437    /// and will panic if the thread is currently driving IO.
438    /// Use [`co_run().await?`](`Self::co_run`) instead.
439    pub fn run(self) -> cu::Result<Option<cu::ZString>> {
440        check_prompt_level(false)?;
441        run_prompt_loop(
442            self.message,
443            self.is_password,
444            self.trim_trailing_whitespace,
445            self.validator,
446        )
447    }
448
449    /// Show the prompt and use the async runtime to wait for input.
450    ///
451    /// If the user presses `Ctrl-C`, `None` will be returned.
452    #[cfg(feature = "coroutine")]
453    pub async fn co_run(self) -> cu::Result<Option<cu::ZString>> {
454        check_prompt_level(false)?;
455        co_run_prompt_loop(
456            self.message,
457            self.is_password,
458            self.trim_trailing_whitespace,
459            self.validator,
460        )
461        .await
462    }
463}
464impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
465    PromptBuilder<cu::ZString, DefaultIfCancel, TValidate>
466{
467    /// Show the prompt and block the current thread to wait for input.
468    ///
469    /// If the user presses `Ctrl-C`, the default value set by [`if_cancel`](Self::if_cancel)
470    /// will be returned.
471    ///
472    /// # Blocking
473    /// **Do not use this in an async context**, since it will block the runtime,
474    /// and will panic if the thread is currently driving IO.
475    /// Use [`co_run().await?`](`Self::co_run`) instead.
476    pub fn run(self) -> cu::Result<cu::ZString> {
477        check_prompt_level(false)?;
478        // unwrap: safety from builder
479        let result = run_prompt_loop(
480            self.message,
481            self.is_password,
482            self.trim_trailing_whitespace,
483            self.validator,
484        )?
485        .unwrap_or(self.cancel_value.unwrap());
486        Ok(result)
487    }
488
489    /// Show the prompt and use the async runtime to wait for input.
490    ///
491    /// If the user presses `Ctrl-C`, the default value set by [`if_cancel`](Self::if_cancel)
492    /// will be returned.
493    #[cfg(feature = "coroutine")]
494    pub async fn co_run(self) -> cu::Result<cu::ZString> {
495        check_prompt_level(false)?;
496        // unwrap: safety from builder
497        let result = co_run_prompt_loop(
498            self.message,
499            self.is_password,
500            self.trim_trailing_whitespace,
501            self.validator,
502        )
503        .await?
504        .unwrap_or(self.cancel_value.unwrap());
505        Ok(result)
506    }
507}
508impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
509    PromptBuilder<cu::ZString, BailIfCancel, TValidate>
510{
511    /// Show the prompt and block the current thread to wait for input.
512    ///
513    /// If the user presses `Ctrl-C`, an error with message "operation cancelled by user"
514    /// will be returned.
515    ///
516    /// # Blocking
517    /// **Do not use this in an async context**, since it will block the runtime,
518    /// and will panic if the thread is currently driving IO.
519    /// Use [`co_run().await?`](`Self::co_run`) instead.
520    pub fn run(self) -> cu::Result<cu::ZString> {
521        check_prompt_level(false)?;
522        match run_prompt_loop(
523            self.message,
524            self.is_password,
525            self.trim_trailing_whitespace,
526            self.validator,
527        )? {
528            Some(result) => Ok(result),
529            None => crate::bail!("operation cancelled by user"),
530        }
531    }
532
533    /// Show the prompt and use the async runtime to wait for input.
534    ///
535    /// If the user presses `Ctrl-C`, an error with message "operation cancelled by user"
536    /// will be returned.
537    #[cfg(feature = "coroutine")]
538    pub async fn co_run(self) -> cu::Result<cu::ZString> {
539        check_prompt_level(false)?;
540        match co_run_prompt_loop(
541            self.message,
542            self.is_password,
543            self.trim_trailing_whitespace,
544            self.validator,
545        )
546        .await?
547        {
548            Some(result) => Ok(result),
549            None => crate::bail!("operation cancelled by user"),
550        }
551    }
552}
553impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
554    PromptBuilder<bool, Cancellable, TValidate>
555{
556    /// Set a default value to return if the user cancels the prompt (e.g., Ctrl+C).
557    ///
558    /// Without `if_cancel`, the prompt returns `Option<bool>` where `None`
559    /// indicates cancellation. With `if_cancel`, the prompt returns `bool`
560    /// directly, using the provided default value on cancellation.
561    ///
562    /// Note that [`cu::yesno`](fn@crate::yesno) already calls `.if_cancel(false)`
563    /// internally.
564    ///
565    /// # Example
566    ///
567    /// ```rust,no_run
568    /// # use pistonite_cu as cu;
569    /// # fn main() -> cu::Result<()> {
570    /// if cu::prompt("proceed?").yesno().if_cancel(false).run()? {
571    ///     cu::info!("user answered yes!");
572    /// }
573    /// # Ok(()) }
574    /// ```
575    ///
576    /// This has the same effect as calling `unwrap_or()` on the resulting
577    /// `Option<bool>` - Just that the prompt creator has control over
578    /// the outcome of the prompt, rather than the prompt consumer accessing the outcome.
579    #[inline(always)]
580    #[must_use = "you must call run() or co_run() to start the prompt"]
581    pub fn if_cancel(self, default: bool) -> PromptBuilder<bool, DefaultIfCancel, TValidate> {
582        PromptBuilder {
583            message: self.message,
584            is_password: self.is_password,
585            trim_trailing_whitespace: self.trim_trailing_whitespace,
586            validator: self.validator,
587            cancel_type: DefaultIfCancel,
588            cancel_value: Some(default),
589        }
590    }
591
592    /// Return an error if the user cancels the prompt (e.g., Ctrl+C).
593    ///
594    /// This appends "(Ctrl-C to cancel)" to the prompt message and returns
595    /// an error with message "operation cancelled by user" if the user presses Ctrl+C.
596    ///
597    /// # Example
598    ///
599    /// ```rust,no_run
600    /// # use pistonite_cu as cu;
601    /// # fn main() {
602    /// match cu::prompt("proceed?").yesno().or_cancel().run() {
603    ///     // user answered yes
604    ///     Ok(true) => cu::info!("proceeding..."),
605    ///     // user answered no
606    ///     Ok(false) => cu::warn!("user declined"),
607    ///     // user pressed Ctrl-C, or prompt not allowed
608    ///     Err(e) => cu::error!("{e}"),
609    /// }
610    /// # }
611    /// ```
612    #[inline(always)]
613    #[must_use = "you must call run() or co_run() to start the prompt"]
614    pub fn or_cancel(mut self) -> PromptBuilder<bool, BailIfCancel, TValidate> {
615        self.message.push_str(" (Ctrl-C to cancel)");
616        PromptBuilder {
617            message: self.message,
618            is_password: self.is_password,
619            trim_trailing_whitespace: self.trim_trailing_whitespace,
620            validator: self.validator,
621            cancel_type: BailIfCancel,
622            cancel_value: None,
623        }
624    }
625
626    /// Show the prompt and block the current thread to wait for input.
627    ///
628    /// If the user presses `Ctrl-C`, `None` will be returned.
629    ///
630    /// # Blocking
631    /// **Do not use this in an async context**, since it will block the runtime,
632    /// and will panic if the thread is currently driving IO.
633    /// Use [`co_run().await?`](`Self::co_run`) instead.
634    pub fn run(self) -> cu::Result<Option<bool>> {
635        if check_prompt_level(true)? {
636            return Ok(Some(true));
637        }
638        run_yesno_loop(self.message, self.is_password)
639    }
640
641    /// Show the prompt and use the async runtime to wait for input.
642    ///
643    /// If the user presses `Ctrl-C`, `None` will be returned.
644    #[cfg(feature = "coroutine")]
645    pub async fn co_run(self) -> cu::Result<Option<bool>> {
646        if check_prompt_level(true)? {
647            return Ok(Some(true));
648        }
649        co_run_yesno_loop(self.message, self.is_password).await
650    }
651}
652impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
653    PromptBuilder<bool, BailIfCancel, TValidate>
654{
655    /// Show the prompt and block the current thread to wait for input.
656    ///
657    /// If the user presses `Ctrl-C`, an error with message "operation cancelled by user"
658    /// will be returned.
659    ///
660    /// # Blocking
661    /// **Do not use this in an async context**, since it will block the runtime,
662    /// and will panic if the thread is currently driving IO.
663    /// Use [`co_run().await?`](`Self::co_run`) instead.
664    pub fn run(self) -> cu::Result<bool> {
665        if check_prompt_level(true)? {
666            return Ok(true);
667        }
668        match run_yesno_loop(self.message, self.is_password)? {
669            Some(result) => Ok(result),
670            None => crate::bail!("operation cancelled by user"),
671        }
672    }
673
674    /// Show the prompt and use the async runtime to wait for input.
675    ///
676    /// If the user presses `Ctrl-C`, an error with message "operation cancelled by user"
677    /// will be returned.
678    #[cfg(feature = "coroutine")]
679    pub async fn co_run(self) -> cu::Result<bool> {
680        if check_prompt_level(true)? {
681            return Ok(true);
682        }
683        match co_run_yesno_loop(self.message, self.is_password).await? {
684            Some(result) => Ok(result),
685            None => crate::bail!("operation cancelled by user"),
686        }
687    }
688}
689impl<TValidate: FnMut(&mut String) -> cu::Result<bool>>
690    PromptBuilder<bool, DefaultIfCancel, TValidate>
691{
692    /// Show the prompt and block the current thread to wait for input.
693    ///
694    /// If the user presses `Ctrl-C`, the default value set by [`if_cancel`](Self::if_cancel)
695    /// will be returned.
696    ///
697    /// # Blocking
698    /// **Do not use this in an async context**, since it will block the runtime,
699    /// and will panic if the thread is currently driving IO.
700    /// Use [`co_run().await?`](`Self::co_run`) instead.
701    pub fn run(self) -> cu::Result<bool> {
702        if check_prompt_level(true)? {
703            return Ok(true);
704        }
705        // unwrap: safety from builder
706        Ok(run_yesno_loop(self.message, self.is_password)?.unwrap_or(self.cancel_value.unwrap()))
707    }
708
709    /// Show the prompt and use the async runtime to wait for input.
710    ///
711    /// If the user presses `Ctrl-C`, the default value set by [`if_cancel`](Self::if_cancel)
712    /// will be returned.
713    #[cfg(feature = "coroutine")]
714    pub async fn co_run(self) -> cu::Result<bool> {
715        if check_prompt_level(true)? {
716            return Ok(true);
717        }
718        // unwrap: safety from builder
719        Ok(co_run_yesno_loop(self.message, self.is_password)
720            .await?
721            .unwrap_or(self.cancel_value.unwrap()))
722    }
723}
724
725fn run_yesno_loop(message: String, is_password: bool) -> cu::Result<Option<bool>> {
726    let mut answer = false;
727    let _ = cu::some!(run_prompt_loop(
728        message,
729        is_password,
730        false, // we trim both ends when parsing
731        |x| {
732            match parse_yesno(x) {
733                Some(x) => {
734                    answer = x;
735                    Ok(true)
736                }
737                None => {
738                    cu::hint!("please enter yes or no");
739                    Ok(false)
740                }
741            }
742        }
743    )?);
744    Ok(Some(answer))
745}
746#[cfg(feature = "coroutine")]
747async fn co_run_yesno_loop(message: String, is_password: bool) -> cu::Result<Option<bool>> {
748    let mut answer = false;
749    let _ = cu::some!(
750        co_run_prompt_loop(
751            message,
752            is_password,
753            false, // we trim both ends when parsing
754            |x| {
755                match parse_yesno(x) {
756                    Some(x) => {
757                        answer = x;
758                        Ok(true)
759                    }
760                    None => {
761                        cu::hint!("please enter yes or no");
762                        Ok(false)
763                    }
764                }
765            }
766        )
767        .await?
768    );
769    Ok(Some(answer))
770}
771#[inline]
772fn parse_yesno(x: &mut str) -> Option<bool> {
773    x.make_ascii_lowercase();
774    match x.trim() {
775        "y" | "yes" => Some(true),
776        "n" | "no" => Some(false),
777        _ => None,
778    }
779}
780
781#[inline(always)]
782fn run_prompt_loop<F: FnMut(&mut String) -> cu::Result<bool>>(
783    message: String,
784    is_password: bool,
785    trim_trailing_whitespace: bool,
786    mut validator: F,
787) -> cu::Result<Option<cu::ZString>> {
788    loop {
789        let result = do_show_prompt(&message, is_password)?;
790        let result = cu::check!(result.recv(), "failed to receive answer to prompt")?;
791        let result = cu::check!(result, "an error occured while processing a prompt")?;
792        let mut result = cu::some!(result);
793        if trim_trailing_whitespace {
794            let len = result.trim_end().len();
795            result.truncate(len);
796        }
797        if validator(&mut result)? {
798            return Ok(Some(result));
799        }
800    }
801}
802#[inline(always)]
803#[cfg(feature = "coroutine")]
804async fn co_run_prompt_loop<F: FnMut(&mut String) -> cu::Result<bool>>(
805    message: String,
806    is_password: bool,
807    trim_trailing_whitespace: bool,
808    mut validator: F,
809) -> cu::Result<Option<cu::ZString>> {
810    loop {
811        let result = do_show_prompt(&message, is_password)?;
812        let result = cu::check!(result.await, "failed to receive answer to prompt")?;
813        let result = cu::check!(result, "an error occured while processing a prompt")?;
814        let mut result = cu::some!(result);
815        if trim_trailing_whitespace {
816            let len = result.trim_end().len();
817            result.truncate(len);
818        }
819        if validator(&mut result)? {
820            return Ok(Some(result));
821        }
822    }
823}
824
825fn do_show_prompt(message: &str, is_password: bool) -> cu::Result<AnswerRecv> {
826    if let Ok(mut printer) = PRINTER.lock()
827        && let Some(printer) = printer.as_mut()
828    {
829        Ok(printer.show_prompt(message, is_password))
830    } else {
831        crate::bail!("prompt failed: failed to lock global printer");
832    }
833}
834
835// Ok(true) -> answer Yes
836// Ok(false) -> prompt
837// Err -> prompt not allowed
838fn check_prompt_level(is_yesno: bool) -> crate::Result<bool> {
839    if is_yesno {
840        match PROMPT_LEVEL.get() {
841            // do not even show the prompt if --yes
842            lv::Prompt::YesOrInteractive | lv::Prompt::YesOrBlock => return Ok(true),
843            lv::Prompt::Interactive => return Ok(false),
844            lv::Prompt::Block => {}
845        }
846    } else {
847        if !matches!(
848            PROMPT_LEVEL.get(),
849            lv::Prompt::YesOrBlock | lv::Prompt::Block
850        ) {
851            return Ok(false);
852        }
853    }
854    crate::bail!("prompt not allowed with --non-interactive");
855}
856
857#[inline(always)]
858fn empty_validator(_: &mut String) -> cu::Result<bool> {
859    Ok(true)
860}