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}