terminal_menu/
lib.rs

1//! Display simple menus on the terminal!
2//! [Examples](https://gitlab.com/xamn/terminal-menu-rs/tree/master/examples)
3
4mod fancy_menu;
5mod utils;
6
7use std::sync::{Arc, RwLock, RwLockWriteGuard};
8use std::thread;
9use std::time::Duration;
10use crossterm::style::Color;
11
12pub type TerminalMenu = Arc<RwLock<TerminalMenuStruct>>;
13
14enum TMIKind {
15    Label,
16    Button,
17    BackButton,
18    Scroll   { values: Vec<String>, selected: usize },
19    List     { values: Vec<String>, selected: usize },
20    String   { value: String, allow_empty: bool },
21    Password { value: String, allow_empty: bool },
22    Numeric  { value:  f64, step: Option<f64>, min: Option<f64>, max: Option<f64> },
23    Submenu(TerminalMenu),
24}
25pub struct TerminalMenuItem {
26    name: String,
27    kind: TMIKind,
28    color: crossterm::style::Color,
29}
30
31
32
33/// Make a label terminal-menu item.
34/// Can't be selected.
35/// Useful for example as a title, separator, or help text.
36/// # Example
37/// ```
38/// use terminal_menu::{menu, label, list};
39/// let menu = menu(vec![
40///     label("This is my menu:"),
41///     list("This is my menu items name", vec!["foo", "bar", "baz"])
42/// ]);
43/// ```
44pub fn label<T: Into<String>>(text: T) -> TerminalMenuItem {
45    TerminalMenuItem {
46        name: text.into(),
47        kind: TMIKind::Label,
48        color: Color::Reset
49    }
50}
51
52/// Make a button terminal-menu item.
53/// Exits the menu with all the parent menus when pressed.
54/// # Example
55/// ```
56/// use terminal_menu::{menu, button, run, mut_menu};
57/// let my_menu = menu(vec![
58///     button("Alice"),
59///     button("Bob")
60/// ]);
61/// run(&my_menu);
62/// println!("Selected Button: {}", mut_menu(&my_menu).selected_item_name());
63/// ```
64pub fn button<T: Into<String>>(name: T) -> TerminalMenuItem {
65    TerminalMenuItem {
66        name: name.into(),
67        kind: TMIKind::Button,
68        color: Color::Reset
69    }
70}
71
72/// Make a back button terminal-menu item.
73/// Returns to the previous menu (or exits when there is none) when pressed.
74/// # Example
75/// ```
76/// use terminal_menu::{menu, back_button, submenu};
77/// let menu = menu(vec![
78///     submenu("Submenus Name", vec![
79///         back_button("Back")
80///     ]),
81///     back_button("Exit"),
82/// ]);
83/// ```
84pub fn back_button<T: Into<String>>(name: T) -> TerminalMenuItem {
85    TerminalMenuItem {
86        name: name.into(),
87        kind: TMIKind::BackButton,
88        color: Color::Reset
89    }
90}
91
92/// Make a terminal-menu item from which you can select a value from a selection.
93/// All values are dispalyed all the time.
94/// # Example
95/// ```
96/// use terminal_menu::{menu, scroll, run, mut_menu};
97/// let menu = menu(vec![
98///     scroll("My Scrolls Name", vec![
99///         "First Option",
100///         "Second Option",
101///         "Third Option"
102///     ])
103/// ]);
104/// run(&menu);
105/// println!("My Scrolls Value: {}", mut_menu(&menu).selection_value("My Scrolls Name"));
106/// ```
107pub fn scroll<T: Into<String>, T2: IntoIterator>(name: T, values: T2) -> TerminalMenuItem where T2::Item: Into<String> {
108    let values: Vec<String> = values.into_iter().map(|a| a.into()).collect();
109    if values.is_empty() {
110        panic!("values cannot be empty");
111    }
112    TerminalMenuItem {
113        name: name.into(),
114        kind: TMIKind::Scroll {
115            values,
116            selected: 0
117        },
118        color: Color::Reset
119    }
120}
121
122/// Make a terminal-menu item from which you can select a value from a selection.
123/// Only the selected value is visible.
124/// # Example
125/// ```
126/// use terminal_menu::{menu, list, run, mut_menu};
127/// let menu = menu(vec![
128///     list("My Lists Name", vec![
129///         "First Option",
130///         "Second Option",
131///         "Third Option"
132///     ])
133/// ]);
134/// run(&menu);
135/// println!("My Lists Value: {}", mut_menu(&menu).selection_value("My Lists Name"));
136/// ```
137pub fn list<T: Into<String>, T2: IntoIterator>(name: T, values: T2) -> TerminalMenuItem where T2::Item: Into<String> {
138    let values: Vec<String> = values.into_iter().map(|a| a.into()).collect();
139    if values.is_empty() {
140        panic!("values cannot be empty");
141    }
142    TerminalMenuItem {
143        name: name.into(),
144        kind: TMIKind::List {
145            values,
146            selected: 0
147        },
148        color: Color::Reset
149    }
150}
151
152/// Make a terminal-menu item which you can enter a string of characters to.
153/// Empty strings may be enabled with a flag.
154/// # Example
155/// ```
156/// use terminal_menu::{menu, string, run, mut_menu};
157/// let menu = menu(vec![
158///     string("My Strings Name", "Default Value", /* allow empty string */ false)
159/// ]);
160/// run(&menu);
161/// println!("My Strings Value: {}", mut_menu(&menu).selection_value("My Strings Name"));
162/// ```
163pub fn string<T: Into<String>, T2: Into<String>>(name: T, default: T2, allow_empty: bool) -> TerminalMenuItem {
164    TerminalMenuItem {
165        name: name.into(),
166        kind: TMIKind::String { value: default.into(), allow_empty },
167        color: Color::Reset,
168    }
169}
170
171/// Make a terminal-menu item which you can enter a string of hidden characters to. For example a password.
172/// Empty strings may be enabled with a flag.
173/// # Example
174/// ```
175/// use terminal_menu::{menu, string, run, mut_menu};
176/// let menu = menu(vec![
177///     password("My Password", "pass", /* allow empty string */ false)
178/// ]);
179/// run(&menu);
180/// println!("My Strings Value: {}", mut_menu(&menu).selection_value("My Password"));
181/// ```
182pub fn password<T: Into<String>, T2: Into<String>>(name: T, default: T2, allow_empty: bool) -> TerminalMenuItem {
183    TerminalMenuItem {
184        name: name.into(),
185        kind: TMIKind::Password { value: default.into(), allow_empty },
186        color: Color::Reset,
187    }
188}
189
190/// Make a terminal-menu item from which you can select a number between specified bounds.
191/// # Example
192/// ```
193/// use terminal_menu::{menu, numeric, run, mut_menu};
194/// let menu = menu(vec![
195///     numeric("My Numerics Name",
196///         0.0,  //default
197///         Some(0.5),  //step (optional)
198///         Some(-5.0), //minimum (optional)
199///         Some(10.0)  //maximum (optional)
200///     )
201/// ]);
202/// run(&menu);
203/// println!("My Numerics Value: {}", mut_menu(&menu).numeric_value("My Numerics Name"))
204/// ```
205pub fn numeric<T: Into<String>>(name: T, default: f64, step: Option<f64>, min: Option<f64>, max: Option<f64>) -> TerminalMenuItem {
206    if !utils::value_valid(default, step, min, max) {
207        panic!("invalid default value");
208    }
209    TerminalMenuItem {
210        name: name.into(),
211        kind: TMIKind::Numeric {
212            value: default,
213            step,
214            min,
215            max
216        },
217        color: Color::Reset
218    }
219}
220
221/// Make a terminal-menu submenu item.
222/// It is basically a menu inside a menu.
223/// # Example
224/// ```
225/// use terminal_menu::{menu, submenu, list, button, back_button, run, mut_menu};
226/// let menu = menu(vec![
227///     submenu("My Submenus Name", vec![
228///         list("List", vec!["First", "Second", "Third"]),
229///         back_button("Back"),
230///         button("Exit")
231///     ]),
232/// ]);
233/// run(&menu);
234/// println!("{}",
235///     mut_menu(&menu)
236///     .get_submenu("My Submenus Name")
237///     .selection_value("List"));
238/// ```
239pub fn submenu<T: Into<String> + Clone>(name: T, items: Vec<TerminalMenuItem>) -> TerminalMenuItem {
240    let menu = menu(items);
241    menu.write().unwrap().name = Some(name.clone().into());
242    TerminalMenuItem {
243        name: name.into(),
244        kind: TMIKind::Submenu(menu),
245        color: Color::Reset
246    }
247}
248
249impl TerminalMenuItem {
250
251    /// Get the name of the terminal-menu item.
252    pub fn name(&self) -> &str {
253        &self.name
254    }
255
256    /// Set a color to print the item in.
257    /// # Example
258    /// ```
259    /// use terminal_menu::{menu, label, scroll};
260    /// use crossterm::style::Color;
261    /// let menu = menu(vec![
262    ///     label("Colorize me").colorize(Color::Magenta),
263    ///     scroll("Me too!", vec!["foo", "bar"]).colorize(Color::Green)
264    /// ]);
265    /// ```
266    pub fn colorize(mut self, color: Color) -> Self {
267        self.color = color;
268        self
269    }
270
271}
272
273pub(crate) enum PrintState {
274    None,
275    Big
276}
277
278pub struct TerminalMenuStruct {
279    name: Option<String>,
280    pub items: Vec<TerminalMenuItem>,
281    selected: usize,
282    active: bool,
283    exited: bool,
284
285    longest_name: usize,
286    exit: Option<String>,
287    canceled: bool,
288    printed: PrintState,
289}
290impl TerminalMenuStruct {
291
292    /// Returns the name of the selected menu item.
293    /// # Example
294    /// ```
295    /// use terminal_menu::{menu, button, run, mut_menu};
296    /// let my_menu = menu(vec![
297    ///     button("a"),
298    ///     button("b"),
299    /// ]);
300    /// run(&my_menu);
301    /// println!("selected item name: {}", mut_menu(&my_menu).selected_item_name()); //"a" or "b"
302    /// ```
303    pub fn selected_item_name(&self) -> &str {
304        &self.items[self.selected].name
305    }
306
307    /// Returns the selected item as an index of the items vec.
308    /// # Example
309    /// ```
310    /// use terminal_menu::{menu, button, run, mut_menu};
311    /// let my_menu = menu(vec![
312    ///     button("a"),
313    ///     button("b"),
314    /// ]);
315    /// run(&my_menu);
316    /// println!("selected item index: {}", mut_menu(&my_menu).selected_item_index()); // 0 or 1
317    /// ```
318    pub fn selected_item_index(&self) -> usize {
319        self.selected
320    }
321
322    fn index_of(&self, name: &str) -> usize {
323        self.items.iter().position(|a| a.name == name).expect("No item with the given name")
324    }
325
326    /// Set the selected item with a name.
327    /// # Example
328    /// ```
329    /// use terminal_menu::{TerminalMenu, menu, button, mut_menu};
330    /// let my_menu: TerminalMenu = menu(vec![
331    ///     button("item"),
332    ///     button("other item")
333    /// ]);
334    /// mut_menu(&my_menu).set_selected_item_with_name("item");
335    /// ```
336    pub fn set_selected_item_with_name(&mut self, item: &str) {
337        self.selected = self.index_of(item);
338    }
339
340    /// Set the selected item with an index of the items vec.
341    /// # Example
342    /// ```
343    /// use terminal_menu::{TerminalMenu, menu, button, mut_menu};
344    /// let my_menu: TerminalMenu = menu(vec![
345    ///     button("item"),
346    ///     button("other item")
347    /// ]);
348    /// mut_menu(&my_menu).set_selected_item_with_index(1); //index 1 = other item
349    /// ```
350    pub fn set_selected_item_with_index(&mut self, item: usize) {
351        if item >= self.items.len() {
352            panic!("index out of bounds");
353        }
354        self.selected = item;
355    }
356
357    /// Returns the value of the specified scroll, list, or string item.
358    /// # Example
359    /// ```
360    /// use terminal_menu::{TerminalMenu, menu, scroll, run, mut_menu};
361    /// let my_menu: TerminalMenu = menu(vec![
362    ///     scroll("item", vec!["val1", "val2"])
363    /// ]);
364    /// run(&my_menu);
365    /// println!("item value: {}", mut_menu(&my_menu).selection_value("item"));
366    /// ```
367    pub fn selection_value(&self, name: &str) -> &str {
368        match &self.items[self.index_of(name)].kind {
369            TMIKind::Scroll { values, selected } |
370            TMIKind::List   { values, selected } => {
371                &values[*selected]
372            }
373            TMIKind::String { value, .. } | TMIKind::Password { value, .. } => value,
374            _ => panic!("item wrong kind")
375        }
376    }
377
378    /// Returns the value of the specified numeric item.
379    /// # Example
380    /// ```
381    /// use terminal_menu::{TerminalMenu, menu, scroll, run, numeric, mut_menu};
382    /// let my_menu: TerminalMenu = menu(vec![
383    ///     numeric("item", 0.0, None, None, None)
384    /// ]);
385    /// run(&my_menu);
386    /// println!("item value: {}", mut_menu(&my_menu).numeric_value("item"));
387    /// ```
388    pub fn numeric_value(&self, name: &str) -> f64 {
389        match self.items[self.index_of(name)].kind {
390            TMIKind::Numeric { value, .. } => value,
391            _ => panic!("item wrong kind")
392        }
393    }
394
395    /// Returns the specified submenu.
396    /// # Example
397    /// ```
398    /// use terminal_menu::{TerminalMenu, menu, run, submenu, scroll, mut_menu};
399    /// let my_menu: TerminalMenu = menu(vec![
400    ///     submenu("sub",vec![
401    ///         scroll("item", vec!["winnie", "the", "pooh"])
402    ///     ])
403    /// ]);
404    /// run(&my_menu);
405    /// println!("{}", mut_menu(&my_menu).get_submenu("sub").selection_value("item"));
406    /// ```
407    pub fn get_submenu(&mut self, name: &str) -> RwLockWriteGuard<TerminalMenuStruct> {
408        for item in &self.items {
409            if item.name == name {
410                if let TMIKind::Submenu(submenu) = &item.kind {
411                    return submenu.write().unwrap();
412                }
413            }
414        }
415        panic!("Item not found or is wrong kind");
416    }
417
418    /// Returns the menu (or submenu) which was active on deactivation.
419    pub fn get_latest_menu_name(&mut self) -> Option<&str> {
420        match &self.exit {
421            None => None,
422            Some(a) => Some(a)
423        }
424    }
425
426    /// Returns true if menu was exited with 'q' or esc
427    /// # Example
428    /// ```
429    /// use terminal_menu::{menu, button, run, mut_menu};
430    /// let menu = menu(vec![
431    ///     button("button")
432    /// ]);
433    /// run(&menu);
434    ///
435    /// // true if esc, false if button
436    /// println!("{}", mut_menu(&menu).canceled());
437    /// ```
438    pub fn canceled(&self) -> bool {
439        self.canceled
440    }
441
442}
443
444/// Create a terminal-menu. See the examples for more.
445/// # Example
446/// ```
447/// use terminal_menu::*;
448/// let my_menu = menu(vec![
449///     label("label"),
450///     button("button"),
451///     scroll("scroll", vec!["a", "b", "c"])
452/// ]);
453/// run(&my_menu);
454/// {
455///     let mm = mut_menu(&my_menu);
456///     println!("{}", mm.selection_value("scroll"));
457///     println!("{}", mm.selected_item_name());
458/// }
459/// ```
460pub fn menu(items: Vec<TerminalMenuItem>) -> TerminalMenu {
461    for i in 0..items.len() {
462        if let TMIKind::Label = items[i].kind {
463        } else {
464            return Arc::new(RwLock::new(TerminalMenuStruct {
465                name: None,
466                items,
467                selected: i,
468                active: false,
469                exited: true,
470
471                longest_name: 0,
472                exit: None,
473                canceled: false,
474                printed: PrintState::None,
475            }))
476        }
477    }
478    panic!("no selectable items");
479}
480
481/// Returns true if the menu has exited.
482pub fn has_exited(menu: &TerminalMenu) -> bool {
483    menu.read().unwrap().exited
484}
485
486/// Get a mutable instance of the menu.
487/// Works only if has_exited(&menu) is true.
488/// # Example
489/// ```
490/// use terminal_menu::{menu, numeric, string, run, has_exited, mut_menu};
491/// let mut my_menu = menu(vec![
492///     numeric("Charlie", 46.5, None, Some(32332.2), None)
493/// ]);
494/// run(&my_menu);
495///
496/// //stuff
497///
498/// {
499///     let mut mutable_menu = mut_menu(&my_menu);
500///     println!("Selected Item: {}", mutable_menu.selected_item_name());
501///     mutable_menu.items.push(string("new item", "def", false));
502/// }
503///
504/// run(&my_menu);
505///
506/// ```
507pub fn mut_menu(menu: &TerminalMenu) -> RwLockWriteGuard<TerminalMenuStruct> {
508    if !has_exited(menu) {
509        panic!("Cannot call mutable_instance if has_exited() is not true");
510    }
511    menu.write().unwrap()
512}
513
514/// Activate (open) the menu.
515/// Menu will deactivate when deactivated manually or button items are pressed.
516/// # Example
517/// ```
518/// use terminal_menu::{TerminalMenu, menu, activate, wait_for_exit};
519/// let my_menu = menu(vec![
520///     list("galadriel", vec!["frodo", "bilbo"])
521///     numeric("boo", 4.67, Some(3.0), None, None)
522/// ]);
523/// activate(&my_menu);
524///
525/// //do something here
526///
527/// wait_for_exit(&my_menu);
528///```
529pub fn activate(menu: &TerminalMenu) {
530    let menu = menu.clone();
531        thread::spawn(move || {
532            fancy_menu::run(menu.clone())
533        });
534}
535
536/// Wait for menu to exit.
537/// # Example
538/// ```
539/// use terminal_menu::{TerminalMenu, menu, activate, wait_for_exit};
540/// let my_menu = menu(vec![
541///     list("galadriel", vec!["frodo", "bilbo"])
542///     numeric("boo", 4.67, Some(3.0), None, None)
543/// ]);
544/// activate(&my_menu);
545///
546/// //do something here
547///
548/// wait_for_exit(&my_menu);
549///```
550pub fn wait_for_exit(menu: &TerminalMenu) {
551    loop {
552        thread::sleep(Duration::from_millis(10));
553        if has_exited(menu) {
554            break;
555        }
556    }
557}
558
559/// Activate the menu and wait for it to exit.
560/// # Example
561/// ```
562/// use terminal_menu::{TerminalMenu, menu, run};
563/// let my_menu = menu(vec![
564///     list("galadriel", vec!["frodo", "bilbo"])
565///     numeric("boo", 4.67, Some(3.0), None, None)
566/// ]);
567/// run(&my_menu);
568/// ```
569pub fn run(menu: &TerminalMenu) {
570        fancy_menu::run(menu.clone());
571}