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}