1use std::fmt::Display;
31
32use bon::Builder;
33use getset::CopyGetters;
34
35pub use self::answer_prompt_error::AnswerPromptError;
36pub use self::confirm::Confirm;
37pub use self::confirmation::Confirmation;
38pub use self::prompt_user_error::PromptUserError;
39pub use self::scripted_answer::ScriptedAnswer;
40pub use self::scripted_user::ScriptedUser;
41pub use self::select::Select;
42pub use self::text::Text;
43use crate::cancellation::Cancellation;
44use crate::context::Interactivity;
45use crate::event::prompt::{PendingAnswer, PromptRequest};
46use crate::output::Output;
47
48mod answer_prompt_error;
50mod confirm;
52mod confirmation;
54mod prompt_user_error;
56mod scripted_answer;
58mod scripted_user;
60mod select;
62mod text;
64
65#[derive(Clone, Debug, Builder, CopyGetters)]
100pub struct Prompt {
101 output: Output,
103
104 #[builder(default)]
106 cancellation: Cancellation,
107
108 #[builder(default)]
110 #[getset(get_copy = "pub")]
111 interactivity: Interactivity,
112}
113
114impl Prompt {
115 pub async fn confirm(
139 &self,
140 question: impl Into<Confirm>,
141 ) -> Result<Confirmation, PromptUserError> {
142 let question = question.into();
143 let text = question.question().clone();
144 let (request, pending) = PromptRequest::confirm(question);
145
146 self.ask(text, request, pending).await
147 }
148
149 pub async fn select<T: Display>(
174 &self,
175 question: impl Into<String>,
176 options: impl IntoIterator<Item = T>,
177 ) -> Result<T, PromptUserError> {
178 let question = question.into();
179 let mut options = options.into_iter().collect::<Vec<_>>();
180 let count = options.len();
181
182 if options.is_empty() {
183 return Err(PromptUserError::MissingOptions { question });
184 }
185
186 let select = Select::new(question.clone(), options.iter().map(ToString::to_string));
187 let (request, pending) = PromptRequest::select(select);
188 let index = self.ask(question.clone(), request, pending).await?;
189
190 if index < count {
191 Ok(options.swap_remove(index))
192 } else {
193 Err(PromptUserError::UnknownOption {
194 question,
195 index,
196 count,
197 })
198 }
199 }
200
201 pub async fn text(&self, question: impl Into<Text>) -> Result<String, PromptUserError> {
224 let question = question.into();
225 let text = question.question().clone();
226 let (request, pending) = PromptRequest::text(question);
227
228 self.ask(text, request, pending).await
229 }
230
231 async fn ask<T>(
247 &self,
248 question: String,
249 request: PromptRequest,
250 pending: PendingAnswer<T>,
251 ) -> Result<T, PromptUserError> {
252 match self.interactivity {
253 Interactivity::Interactive => {}
254 Interactivity::NonInteractive => return Err(PromptUserError::AbsentUser { question }),
255 }
256
257 let sent = tokio::select! {
258 biased;
259
260 () = self.cancellation.cancelled() => {
261 return Err(PromptUserError::CancelledPrompt { question });
262 }
263 sent = self.output.prompt(request) => sent,
264 };
265 if let Err(source) = sent {
266 return Err(PromptUserError::UndeliverablePrompt { question, source });
267 }
268
269 let answer = tokio::select! {
270 biased;
271
272 () = self.cancellation.cancelled() => {
273 return Err(PromptUserError::CancelledPrompt { question });
274 }
275 answer = pending.wait() => answer,
276 };
277
278 answer.map_err(|source| PromptUserError::UnansweredPrompt { question, source })
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 #![allow(clippy::missing_panics_doc)]
287
288 use super::*;
289 use crate::event::{Event, EventReceiver, event_channel};
290
291 fn interactive() -> (Prompt, EventReceiver) {
293 let (sender, receiver) = event_channel();
294 let prompt = Prompt::builder()
295 .output(Output::new(sender))
296 .interactivity(Interactivity::Interactive)
297 .build();
298
299 (prompt, receiver)
300 }
301
302 async fn next_request(receiver: &mut EventReceiver) -> Option<PromptRequest> {
304 while let Some(event) = receiver.recv().await {
305 match event {
306 Event::Prompt(request) => return Some(*request),
307 Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Process(_) => {}
308 }
309 }
310
311 None
312 }
313
314 fn is_absent_user(error: &PromptUserError) -> bool {
316 match error {
317 PromptUserError::AbsentUser { .. } => true,
318 PromptUserError::CancelledPrompt { .. } => false,
319 PromptUserError::UnansweredPrompt { .. } => false,
320 PromptUserError::UndeliverablePrompt { .. } => false,
321 PromptUserError::MissingOptions { .. } => false,
322 PromptUserError::UnknownOption { .. } => false,
323 }
324 }
325
326 fn is_cancelled(error: &PromptUserError) -> bool {
328 match error {
329 PromptUserError::CancelledPrompt { .. } => true,
330 PromptUserError::AbsentUser { .. } => false,
331 PromptUserError::UnansweredPrompt { .. } => false,
332 PromptUserError::UndeliverablePrompt { .. } => false,
333 PromptUserError::MissingOptions { .. } => false,
334 PromptUserError::UnknownOption { .. } => false,
335 }
336 }
337
338 fn is_dropped(error: &PromptUserError) -> bool {
340 match error {
341 PromptUserError::UnansweredPrompt { source, .. } => match source {
342 AnswerPromptError::DroppedRequest => true,
343 AnswerPromptError::ClosedInput => false,
344 AnswerPromptError::UnscriptedPrompt => false,
345 AnswerPromptError::UnusableTerminal { .. } => false,
346 },
347 PromptUserError::AbsentUser { .. } => false,
348 PromptUserError::CancelledPrompt { .. } => false,
349 PromptUserError::UndeliverablePrompt { .. } => false,
350 PromptUserError::MissingOptions { .. } => false,
351 PromptUserError::UnknownOption { .. } => false,
352 }
353 }
354
355 fn is_undeliverable(error: &PromptUserError) -> bool {
357 match error {
358 PromptUserError::UndeliverablePrompt { .. } => true,
359 PromptUserError::AbsentUser { .. } => false,
360 PromptUserError::CancelledPrompt { .. } => false,
361 PromptUserError::UnansweredPrompt { .. } => false,
362 PromptUserError::MissingOptions { .. } => false,
363 PromptUserError::UnknownOption { .. } => false,
364 }
365 }
366
367 #[tokio::test]
368 async fn confirm_sends_the_question_with_its_default() {
369 let (prompt, mut receiver) = interactive();
370 let question = Confirm::new("Release?").with_default(Confirmation::No);
371 let asked = question.clone();
372 let command = tokio::spawn(async move { prompt.confirm(asked).await });
373
374 let request = next_request(&mut receiver).await;
375 command.abort();
376
377 assert_eq!(
378 request.and_then(|request| match request {
379 PromptRequest::Confirm { question, .. } => Some(question),
380 PromptRequest::Select { .. } | PromptRequest::Text { .. } => None,
381 }),
382 Some(question)
383 );
384 }
385
386 #[tokio::test]
387 async fn confirm_with_a_cancelled_token_returns_an_error() {
388 let cancellation = Cancellation::new();
389 let (sender, _receiver) = event_channel();
390 let prompt = Prompt::builder()
391 .output(Output::new(sender))
392 .cancellation(cancellation.clone())
393 .interactivity(Interactivity::Interactive)
394 .build();
395 cancellation.cancel();
396
397 let error = prompt.confirm("Release?").await.expect_err("should fail");
398
399 assert!(is_cancelled(&error));
400 }
401
402 #[tokio::test]
403 async fn confirm_with_a_cancelled_token_sends_no_request() {
404 let cancellation = Cancellation::new();
405 let (sender, mut receiver) = event_channel();
406 let prompt = Prompt::builder()
407 .output(Output::new(sender))
408 .cancellation(cancellation.clone())
409 .interactivity(Interactivity::Interactive)
410 .build();
411 cancellation.cancel();
412
413 drop(prompt.confirm("Release?").await);
414 drop(prompt);
415
416 assert!(next_request(&mut receiver).await.is_none());
417 }
418
419 #[tokio::test]
420 async fn confirm_with_a_closed_channel_returns_an_error() {
421 let (prompt, receiver) = interactive();
422 drop(receiver);
423
424 let error = prompt.confirm("Release?").await.expect_err("should fail");
425
426 assert!(is_undeliverable(&error));
427 }
428
429 #[tokio::test]
430 async fn confirm_with_a_dropped_request_returns_an_error() {
431 let (prompt, mut receiver) = interactive();
432 let presenter = tokio::spawn(async move { drop(next_request(&mut receiver).await) });
433
434 let error = prompt.confirm("Release?").await.expect_err("should fail");
435 presenter.await.expect("should join");
436
437 assert!(is_dropped(&error));
438 }
439
440 #[tokio::test]
441 async fn confirm_with_an_answer_returns_it() {
442 let (prompt, mut receiver) = interactive();
443 let presenter = tokio::spawn(async move {
444 match next_request(&mut receiver).await {
445 Some(PromptRequest::Confirm { reply, .. }) => reply.answer(Confirmation::Yes),
446 Some(PromptRequest::Select { .. } | PromptRequest::Text { .. }) | None => {}
447 }
448 });
449
450 let answer = prompt.confirm("Release?").await.expect("should answer");
451 presenter.await.expect("should join");
452
453 assert_eq!(answer, Confirmation::Yes);
454 }
455
456 #[tokio::test]
457 async fn confirm_with_cancellation_while_it_waits_abandons_the_reply() {
458 let cancellation = Cancellation::new();
459 let (sender, mut receiver) = event_channel();
460 let prompt = Prompt::builder()
461 .output(Output::new(sender))
462 .cancellation(cancellation.clone())
463 .interactivity(Interactivity::Interactive)
464 .build();
465 let command = tokio::spawn(async move { prompt.confirm("Release?").await });
466 let request = next_request(&mut receiver).await;
467 cancellation.cancel();
468
469 drop(command.await.expect("should join"));
470
471 assert_eq!(
472 request.map(|request| match request {
473 PromptRequest::Confirm { reply, .. } => reply.is_abandoned(),
474 PromptRequest::Select { reply, .. } => reply.is_abandoned(),
475 PromptRequest::Text { reply, .. } => reply.is_abandoned(),
476 }),
477 Some(true)
478 );
479 }
480
481 #[tokio::test]
482 async fn confirm_with_cancellation_while_it_waits_returns_an_error() {
483 let cancellation = Cancellation::new();
484 let (sender, mut receiver) = event_channel();
485 let prompt = Prompt::builder()
486 .output(Output::new(sender))
487 .cancellation(cancellation.clone())
488 .interactivity(Interactivity::Interactive)
489 .build();
490 let command = tokio::spawn(async move { prompt.confirm("Release?").await });
491 let _request = next_request(&mut receiver).await;
492 cancellation.cancel();
493
494 let error = command
495 .await
496 .expect("should join")
497 .expect_err("should fail");
498
499 assert!(is_cancelled(&error));
500 }
501
502 #[tokio::test]
503 async fn confirm_without_a_user_returns_an_error() {
504 let (sender, _receiver) = event_channel();
505 let prompt = Prompt::builder().output(Output::new(sender)).build();
506
507 let error = prompt.confirm("Release?").await.expect_err("should fail");
508
509 assert!(is_absent_user(&error));
510 }
511
512 #[tokio::test]
513 async fn confirm_without_a_user_sends_no_request() {
514 let (sender, mut receiver) = event_channel();
515 let prompt = Prompt::builder().output(Output::new(sender)).build();
516
517 drop(prompt.confirm("Release?").await);
518 drop(prompt);
519
520 assert!(next_request(&mut receiver).await.is_none());
521 }
522
523 #[test]
524 fn interactivity_with_defaults_is_non_interactive() {
525 let (sender, _receiver) = event_channel();
526
527 let prompt = Prompt::builder().output(Output::new(sender)).build();
528
529 assert_eq!(prompt.interactivity(), Interactivity::NonInteractive);
530 }
531
532 #[tokio::test]
533 async fn select_sends_the_options_as_their_display_text() {
534 let (prompt, mut receiver) = interactive();
535 let command = tokio::spawn(async move { prompt.select("Port", [80, 443]).await });
536
537 let request = next_request(&mut receiver).await;
538 command.abort();
539
540 assert_eq!(
541 request.and_then(|request| match request {
542 PromptRequest::Select { question, .. } => Some(question),
543 PromptRequest::Confirm { .. } | PromptRequest::Text { .. } => None,
544 }),
545 Some(Select::new("Port", ["80", "443"]))
546 );
547 }
548
549 #[tokio::test]
550 async fn select_with_an_answer_returns_the_option_at_that_index() {
551 let (prompt, mut receiver) = interactive();
552 let presenter = tokio::spawn(async move {
553 match next_request(&mut receiver).await {
554 Some(PromptRequest::Select { reply, .. }) => reply.answer(1),
555 Some(PromptRequest::Confirm { .. } | PromptRequest::Text { .. }) | None => {}
556 }
557 });
558
559 let answer = prompt
560 .select("Port", [80, 443])
561 .await
562 .expect("should answer");
563 presenter.await.expect("should join");
564
565 assert_eq!(answer, 443);
566 }
567
568 #[tokio::test]
569 async fn select_with_an_index_beyond_the_options_returns_an_error() {
570 let (prompt, mut receiver) = interactive();
571 let presenter = tokio::spawn(async move {
572 match next_request(&mut receiver).await {
573 Some(PromptRequest::Select { reply, .. }) => reply.answer(2),
574 Some(PromptRequest::Confirm { .. } | PromptRequest::Text { .. }) | None => {}
575 }
576 });
577
578 let error = prompt
579 .select("Port", [80, 443])
580 .await
581 .expect_err("should fail");
582 presenter.await.expect("should join");
583
584 assert_eq!(
585 error.to_string(),
586 "the answer to the prompt `Port` names option 2, and the prompt has 2 options"
587 );
588 }
589
590 #[tokio::test]
591 async fn select_without_a_user_returns_an_error() {
592 let (sender, _receiver) = event_channel();
593 let prompt = Prompt::builder().output(Output::new(sender)).build();
594
595 let error = prompt
596 .select("Port", [80, 443])
597 .await
598 .expect_err("should fail");
599
600 assert!(is_absent_user(&error));
601 }
602
603 #[tokio::test]
604 async fn select_without_options_returns_an_error() {
605 let (prompt, _receiver) = interactive();
606
607 let error = prompt
608 .select("Port", Vec::<u16>::new())
609 .await
610 .expect_err("should fail");
611
612 assert_eq!(
613 error.to_string(),
614 "the prompt `Port` has no options to select from"
615 );
616 }
617
618 #[tokio::test]
619 async fn text_with_an_answer_returns_it() {
620 let (prompt, mut receiver) = interactive();
621 let presenter = tokio::spawn(async move {
622 match next_request(&mut receiver).await {
623 Some(PromptRequest::Text { reply, .. }) => reply.answer("Fix the race".to_owned()),
624 Some(PromptRequest::Confirm { .. } | PromptRequest::Select { .. }) | None => {}
625 }
626 });
627
628 let answer = prompt.text("Title").await.expect("should answer");
629 presenter.await.expect("should join");
630
631 assert_eq!(answer, "Fix the race");
632 }
633
634 #[tokio::test]
635 async fn text_without_a_user_returns_an_error() {
636 let (sender, _receiver) = event_channel();
637 let prompt = Prompt::builder().output(Output::new(sender)).build();
638
639 let error = prompt.text("Title").await.expect_err("should fail");
640
641 assert!(is_absent_user(&error));
642 }
643
644 #[test]
645 fn trait_send() {
646 fn assert_send<T: Send>() {}
647 assert_send::<Prompt>();
648 }
649
650 #[test]
651 fn trait_sync() {
652 fn assert_sync<T: Sync>() {}
653 assert_sync::<Prompt>();
654 }
655
656 #[test]
657 fn trait_unpin() {
658 fn assert_unpin<T: Unpin>() {}
659 assert_unpin::<Prompt>();
660 }
661}