menu_rs/
lib.rs

1//! menu_rs is a library for Rust that allows the creation of simple and interactable command-line menus.
2//!
3//! It's very simple to use, you just create a Menu, adds the option you want it to have with the correspondent
4//! action to be run when selected and that's it!
5//! You can use the arrow keys to move through the options, ENTER to select an option and ESC to exit the menu.
6//!
7//! # Example
8//!
9//! ```
10//! use menu_rs::{Menu, MenuOption};
11//!
12//! let my_variable: u32 = 157;
13//!
14//! fn action_1() {
15//!     println!("action 1")
16//! }
17//! fn action_2(val: u32) {
18//!     println!("action 2 with number {}", val)
19//! }
20//! fn action_3(msg: &str, val: f32) {
21//!     println!("action 3 with string {} and float {}", msg, val)
22//! }
23//! fn action_4() {
24//!     println!("action 4")
25//! }
26//!
27//! let menu = Menu::new(vec![
28//!     MenuOption::new("Option 1", action_1).hint("Hint for option 1"),
29//!     MenuOption::new("Option 2", || action_2(42)),
30//!     MenuOption::new("Option 3", || action_3("example", 3.14)),
31//!     MenuOption::new("Option 4", action_4),
32//!     MenuOption::new("Option 5", move || action_2(my_variable)),
33//! ]);
34//!
35//! menu.show();
36//! ```
37
38#![allow(clippy::needless_return)]
39#![allow(clippy::redundant_field_names)]
40
41use console::{Key, Style, Term};
42
43/// A option that can be added to a Menu.
44pub struct MenuOption {
45    label: String,
46    func: Box<dyn FnMut()>,
47    hint: Option<String>,
48}
49
50/// The Menu to be shown in the command line interface.
51pub struct Menu {
52    title: Option<String>,
53    options: Vec<MenuOption>,
54    selected_option: i32,
55    selected_style: Style,
56    normal_style: Style,
57    hint_style: Style,
58}
59
60impl MenuOption {
61    /// Creates a new Menu option that can then be used by a Menu.
62    ///
63    /// # Example
64    ///
65    /// ```
66    /// fn action_example() {}
67    /// let menu_option = MenuOption::new("Option example", action_example);
68    /// ```
69    pub fn new<F>(label: &str, func: F) -> MenuOption
70    where
71        F: FnMut() + 'static,
72    {
73        return MenuOption {
74            label: label.to_owned(),
75            func: Box::new(func),
76            hint: None,
77        };
78    }
79
80    /// Sets the hint label with the given text.
81    ///
82    /// # Example
83    ///
84    /// ```
85    /// fn action_1() {}
86    /// let menu_option_1 = MenuOption::new("Option 1", action_1).hint("Hint example");
87    /// ```
88    pub fn hint(mut self, text: &str) -> MenuOption {
89        self.hint = Some(text.to_owned());
90        return self;
91    }
92}
93
94impl Menu {
95    /// Creates a new interactable Menu.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// fn action_example() {}
101    /// let menu_option = MenuOption::new("Option example", action_example);
102    /// let menu = Menu::new(vec![menu_option]);
103    /// ```
104    ///
105    /// You can use closures to easily use arguments in your functions.
106    ///
107    /// ```
108    /// fn action_example(msg: &str, val: f32) {
109    ///     println!("action 3 with string {} and float {}", msg, val)
110    /// }
111    /// let menu_option = MenuOption::new("Option example", || action_example("example", 3.514));
112    /// let menu = Menu::new(vec![menu_option]);
113    /// ```
114    pub fn new(options: Vec<MenuOption>) -> Menu {
115        return Menu {
116            title: None,
117            options: options,
118            selected_option: 0,
119            normal_style: Style::new(),
120            selected_style: Style::new().on_blue(),
121            hint_style: Style::new().color256(187),
122        };
123    }
124
125    /// Sets a title for the menu.
126    ///
127    /// # Example
128    ///
129    /// ```
130    /// fn action_example() {}
131    /// let menu_option = MenuOption::new("Option example", action_example);
132    /// let menu = Menu::new(vec![menu_option]).title("Title example");
133    /// ```
134    pub fn title(mut self, text: &str) -> Menu {
135        self.title = Some(text.to_owned());
136        return self;
137    }
138
139    /// Shows the menu in the command line interface allowing the user
140    /// to interact with the menu.
141    pub fn show(mut self) {
142        let stdout = Term::buffered_stdout();
143        stdout.hide_cursor().unwrap();
144
145        // clears the screen and shows the menu
146        stdout.clear_screen().unwrap();
147        self.draw_menu(&stdout);
148
149        // runs the menu navigation
150        self.menu_navigation(&stdout);
151
152        // clears the screen and runs the action function before exiting
153        stdout.clear_screen().unwrap();
154        stdout.flush().unwrap();
155
156        // return on exit selection
157        if self.selected_option == -1 {
158            return;
159        }
160
161        // runs the action function
162        let option = &mut self.options[self.selected_option as usize];
163        (option.func)();
164    }
165
166    fn menu_navigation(&mut self, stdout: &Term) {
167        let options_limit_num: i32 = (self.options.len() - 1) as i32;
168        loop {
169            // gets pressed key
170            let key = match stdout.read_key() {
171                Ok(val) => val,
172                Err(_e) => {
173                    println!("Error reading key");
174                    return;
175                }
176            };
177
178            // handles the pressed key
179            match key {
180                Key::ArrowUp => {
181                    self.selected_option = match self.selected_option == 0 {
182                        true => options_limit_num,
183                        false => self.selected_option - 1,
184                    }
185                }
186                Key::ArrowDown => {
187                    self.selected_option = match self.selected_option == options_limit_num {
188                        true => 0,
189                        false => self.selected_option + 1,
190                    }
191                }
192                Key::Escape => {
193                    self.selected_option = -1;
194                    stdout.show_cursor().unwrap();
195                    return;
196                }
197                Key::Enter => {
198                    stdout.show_cursor().unwrap();
199                    return;
200                }
201                // Key::Char(c) => println!("char {}", c),
202                _ => {}
203            }
204
205            // redraws the menu
206            self.draw_menu(stdout);
207        }
208    }
209
210    fn draw_menu(&self, stdout: &Term) {
211        // clears the screen
212        stdout.clear_screen().unwrap();
213
214        // draw title
215        match &self.title {
216            Some(text) => {
217                let title_style = Style::new().bold();
218                let title = title_style.apply_to(text);
219                let title = format!("  {}", title);
220                stdout.write_line(title.as_str()).unwrap()
221            }
222            None => {}
223        };
224
225        // draw the menu to stdout
226        for (i, option) in self.options.iter().enumerate() {
227            let option_idx: usize = self.selected_option as usize;
228            let label_style = match i == option_idx {
229                true => self.selected_style.clone(),
230                false => self.normal_style.clone(),
231            };
232
233            // styles the menu entry
234            let label = label_style.apply_to(option.label.as_str());
235            let hint_str = match &self.options[i].hint {
236                Some(hint) => hint,
237                None => "",
238            };
239            let hint = self.hint_style.apply_to(hint_str);
240
241            // builds and writes the menu entry
242            let line = format!("- {: <25}\t{}", label, hint);
243            stdout.write_line(line.as_str()).unwrap();
244        }
245
246        // draws to terminal
247        stdout.flush().unwrap();
248    }
249}