tl_cli/ui/
mod.rs

1use anyhow::Result;
2use inquire::InquireError;
3
4mod spinner;
5mod theme;
6
7pub use spinner::Spinner;
8pub use theme::Style;
9
10/// Check if the inquire error is a user cancellation/interruption.
11const fn is_prompt_cancelled(err: &InquireError) -> bool {
12    matches!(
13        err,
14        InquireError::OperationCanceled | InquireError::OperationInterrupted
15    )
16}
17
18/// Wraps a function that uses interactive prompts and handles user cancellation gracefully.
19///
20/// If the user cancels the prompt (Ctrl+C or Escape), this function prints a newline
21/// to clean up the terminal and returns `Ok(())` instead of propagating the error.
22pub fn handle_prompt_cancellation<F>(f: F) -> Result<()>
23where
24    F: FnOnce() -> Result<()>,
25{
26    match f() {
27        Ok(()) => Ok(()),
28        Err(e)
29            if e.downcast_ref::<InquireError>()
30                .is_some_and(is_prompt_cancelled) =>
31        {
32            println!();
33            Ok(())
34        }
35        Err(e) => Err(e),
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn test_handle_prompt_cancellation_ok() {
45        let result = handle_prompt_cancellation(|| Ok(()));
46        assert!(result.is_ok());
47    }
48
49    #[test]
50    fn test_handle_prompt_cancellation_operation_canceled() {
51        let result = handle_prompt_cancellation(|| Err(InquireError::OperationCanceled.into()));
52        assert!(result.is_ok());
53    }
54
55    #[test]
56    fn test_handle_prompt_cancellation_operation_interrupted() {
57        let result = handle_prompt_cancellation(|| Err(InquireError::OperationInterrupted.into()));
58        assert!(result.is_ok());
59    }
60
61    #[test]
62    fn test_handle_prompt_cancellation_other_error() {
63        let result = handle_prompt_cancellation(|| Err(anyhow::anyhow!("Some other error")));
64        let Err(err) = result else {
65            panic!("expected an error");
66        };
67        assert!(err.to_string().contains("Some other error"));
68    }
69
70    #[test]
71    fn test_is_prompt_cancelled_operation_canceled() {
72        assert!(is_prompt_cancelled(&InquireError::OperationCanceled));
73    }
74
75    #[test]
76    fn test_is_prompt_cancelled_operation_interrupted() {
77        assert!(is_prompt_cancelled(&InquireError::OperationInterrupted));
78    }
79
80    #[test]
81    fn test_is_prompt_cancelled_other_error() {
82        let err = InquireError::Custom("test".into());
83        assert!(!is_prompt_cancelled(&err));
84    }
85}