prompts/
utils.rs

1//! Utility functions and enums for prompts
2//!
3//! Public in case you want to implement your own custom prompts
4
5use crossterm::{
6    event::{KeyCode, KeyEvent, KeyModifiers},
7    style::{style, Color, PrintStyledContent},
8};
9use std::cmp;
10
11/// Figures that are used for the prompts
12pub enum Figures {
13    ArrowUp,
14    ArrowDown,
15    ArrowLeft,
16    ArrowRight,
17    RadioOn,
18    RadioOff,
19    Tick,
20    Cross,
21    Ellipsis,
22    PointerSmall,
23    Line,
24    Pointer,
25}
26impl Figures {
27    #[cfg(windows)]
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            Figures::ArrowUp => "↑",
31            Figures::ArrowDown => "↓",
32            Figures::ArrowLeft => "←",
33            Figures::ArrowRight => "→",
34            Figures::RadioOn => "(*)",
35            Figures::RadioOff => "( )",
36            Figures::Tick => "√",
37            Figures::Cross => "×",
38            Figures::Ellipsis => "...",
39            Figures::PointerSmall => "»",
40            Figures::Line => "─",
41            Figures::Pointer => ">",
42        }
43    }
44    #[cfg(not(windows))]
45    pub fn as_str(&self) -> &'static str {
46        match self {
47            Figures::ArrowUp => "↑",
48            Figures::ArrowDown => "↓",
49            Figures::ArrowLeft => "←",
50            Figures::ArrowRight => "→",
51            Figures::RadioOn => "◉",
52            Figures::RadioOff => "◯",
53            Figures::Tick => "✔",
54            Figures::Cross => "✖",
55            Figures::Ellipsis => "…",
56            Figures::PointerSmall => "›",
57            Figures::Line => "─",
58            Figures::Pointer => "❯",
59        }
60    }
61}
62
63/// Internal state of a prompt
64#[derive(Eq, PartialEq, Debug)]
65pub enum PromptState {
66    /// Prompt was just created (and has not yet been displayed for the first time)
67    Created,
68    /// Prompt is running/displaying
69    Running,
70    /// Prompt input needs validation
71    Validate,
72    /// The prompt was aborted by the user
73    Aborted,
74    /// The prompt completed successfully
75    Success,
76}
77impl Default for PromptState {
78    fn default() -> PromptState {
79        PromptState::Created
80    }
81}
82impl PromptState {
83    pub fn is_done(&self) -> bool {
84        *self == PromptState::Aborted || *self == PromptState::Success
85    }
86}
87
88/// Should we abort on this event
89///
90/// Returns true on CTRL+c, CTRL+z and ESC
91pub fn is_abort_event(event: KeyEvent) -> bool {
92    match event {
93        KeyEvent {
94            modifiers: KeyModifiers::CONTROL,
95            code: KeyCode::Char('c'),
96        } => true,
97        KeyEvent {
98            modifiers: KeyModifiers::CONTROL,
99            code: KeyCode::Char('d'),
100        } => true,
101        KeyEvent {
102            modifiers,
103            code: KeyCode::Esc,
104        } if modifiers == KeyModifiers::empty() => true,
105        _ => false,
106    }
107}
108
109/// Prints a cross, a tick or a question mark depending on prompt state
110pub fn print_state_icon(state: &PromptState) -> PrintStyledContent<&'static str> {
111    PrintStyledContent(match state {
112        PromptState::Aborted => style(Figures::Cross.as_str()).with(Color::Red),
113        PromptState::Success => style(Figures::Tick.as_str()).with(Color::Green),
114        _ => style("?").with(Color::Magenta),
115    })
116}
117
118/// Prints a pointer or ellipsis depending on prompt state
119pub fn print_input_icon(state: &PromptState) -> PrintStyledContent<String> {
120    PrintStyledContent(
121        style(match state {
122            PromptState::Aborted => "".to_string(),
123            PromptState::Success => format!("{} ", Figures::Ellipsis.as_str()),
124            _ => format!("{} ", Figures::PointerSmall.as_str()),
125        })
126        .with(Color::Grey),
127    )
128}
129
130/// Returns start and end-index for showing a limited amount of items
131///
132/// Used for SelectPrompt and AutocompletePrompt
133pub fn calc_entries(current: usize, total: usize, limit: usize) -> (usize, usize) {
134    let start_index = cmp::min(
135        total.checked_sub(limit).unwrap_or(0),
136        current.checked_sub(limit / 2).unwrap_or(0),
137    );
138    let end_index = cmp::min(start_index + limit, total);
139    (start_index, end_index)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    #[cfg(windows)]
148    fn write_all() {
149        let figures = [
150            Figures::ArrowUp,
151            Figures::ArrowDown,
152            Figures::ArrowLeft,
153            Figures::ArrowRight,
154            Figures::RadioOn,
155            Figures::RadioOff,
156            Figures::Tick,
157            Figures::Cross,
158            Figures::Ellipsis,
159            Figures::PointerSmall,
160            Figures::Line,
161            Figures::Pointer,
162        ];
163        let mut s = String::from("");
164        for figure in figures.iter() {
165            s.push_str(figure.as_str());
166        }
167        assert_eq!(s, "↑↓←→(*)( )√×...»─>");
168    }
169
170    #[test]
171    #[cfg(not(windows))]
172    fn write_all() {
173        let figures = [
174            Figures::ArrowUp,
175            Figures::ArrowDown,
176            Figures::ArrowLeft,
177            Figures::ArrowRight,
178            Figures::RadioOn,
179            Figures::RadioOff,
180            Figures::Tick,
181            Figures::Cross,
182            Figures::Ellipsis,
183            Figures::PointerSmall,
184            Figures::Line,
185            Figures::Pointer,
186        ];
187        let mut s = String::from("");
188        for figure in figures.iter() {
189            s.push_str(figure.as_str());
190        }
191        assert_eq!(s, "↑↓←→◉◯✔✖…›─❯");
192    }
193}