├── .gitignore ├── .travis.yml ├── examples ├── input.rs ├── enumerated-list.rs ├── confirm.rs └── list.rs ├── Cargo.toml ├── .editorconfig ├── README.md ├── src ├── lib.rs ├── error.rs ├── choice.rs ├── input.rs ├── confirm.rs └── list.rs └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | -------------------------------------------------------------------------------- /examples/input.rs: -------------------------------------------------------------------------------- 1 | extern crate inquirer; 2 | 3 | fn main() { 4 | let answer = inquirer::input("What is the airspeed velocity of an unladen swallow?"); 5 | println!("You answered: {:?}", answer); 6 | } 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inquirer" 3 | version = "0.1.0" 4 | authors = ["Philip Munksgaard "] 5 | repository = "https://github.com/Munksgaard/inquirer-rs" 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [dependencies] 10 | quick-error = "1.1.0" 11 | termion = "1.0.4" 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inquirer-rs 2 | 3 | This is (the beginnings of) a Rust port of [Inquirer](https://www.npmjs.com/package/inquirer). 4 | 5 | At the moment, this is a very rough prototype, and probably shouldn't be used for anything 6 | critical. It has only been tested on Linux, and only supports the `list` menu. Hopefully, 7 | I'll be able to add more features soon, as well as perform some much needed refactoring. 8 | -------------------------------------------------------------------------------- /examples/enumerated-list.rs: -------------------------------------------------------------------------------- 1 | extern crate inquirer; 2 | 3 | use inquirer::list; 4 | 5 | fn main() { 6 | let choices: Vec<_> = vec!["Red", "Blue", "Green"].into_iter() 7 | .enumerate() 8 | .map(|(index, item)| (item, index)) 9 | .collect(); 10 | 11 | let result = list("Choose an option:", &choices).unwrap(); 12 | 13 | println!("You chose {:?}.", result); 14 | } 15 | -------------------------------------------------------------------------------- /examples/confirm.rs: -------------------------------------------------------------------------------- 1 | extern crate inquirer; 2 | 3 | use inquirer::{confirm, Error}; 4 | 5 | fn main() { 6 | match confirm("Is it working?", false) { 7 | Ok(true) => 8 | println!("It was working!"), 9 | Ok(false) => 10 | println!("It's not working :("), 11 | Err(Error::UserAborted) => { 12 | println!("Pressed Ctrl-C, exiting."); 13 | std::process::exit(1); 14 | } 15 | Err(err) => println!("{:?}", err), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Inquirer – Fancy user interaction on the command line 2 | 3 | #![deny(missing_docs, 4 | missing_debug_implementations, missing_copy_implementations, 5 | trivial_casts, trivial_numeric_casts, 6 | unsafe_code, 7 | unused_import_braces, unused_qualifications)] 8 | 9 | extern crate termion; 10 | #[macro_use] 11 | extern crate quick_error; 12 | 13 | mod choice; 14 | mod error; 15 | mod list; 16 | mod confirm; 17 | mod input; 18 | 19 | pub use choice::Choice; 20 | pub use error::Error; 21 | pub use list::list; 22 | pub use confirm::confirm; 23 | pub use input::input; 24 | -------------------------------------------------------------------------------- /examples/list.rs: -------------------------------------------------------------------------------- 1 | extern crate inquirer; 2 | 3 | use inquirer::{list, Error}; 4 | 5 | fn main() { 6 | #[derive(Debug)] 7 | enum Color { Red, Blue, Green }; 8 | let choices = &[("Red", Color::Red), ("Blue", Color::Blue), ("Green", Color::Green)]; 9 | 10 | match list("Choose your favorite color:", choices) { 11 | Ok(result) => println!("You chose {:?}.", result), 12 | Err(Error::UserAborted) => { 13 | println!("Pressed Ctrl-C, exiting."); 14 | std::process::exit(1); 15 | } 16 | Err(err) => println!("{:?}", err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | quick_error! { 4 | /// Inquirer Error 5 | #[derive(Debug)] 6 | pub enum Error { 7 | /// Error while dealing with file or stdin/stdout 8 | Io(err: io::Error) { 9 | from() 10 | cause(err) 11 | display("I/O error") 12 | description(err.description()) 13 | } 14 | /// Invalid choice 15 | // TODO: Make this a type system error instead 16 | InvalidChoice(option_num: usize) { 17 | display("Option `{}` is not valid", option_num) 18 | description("Invalid choice") 19 | } 20 | /// No more input 21 | NoMoreInput { 22 | display("Didn't get any more input") 23 | } 24 | /// User pressed Ctrl-C 25 | UserAborted { 26 | display("User aborted (pressed Ctrl-C)") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/choice.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | /// An option the user can choose 4 | /// 5 | /// (Since the name "Option" is reserved for the well-known type representing 6 | /// nullability, we are calling this one "Choice".) 7 | pub trait Choice { 8 | /// User visible text 9 | type Text: Display; 10 | /// Internal value representing this choice 11 | type Value; 12 | 13 | /// Get a reference to the text 14 | fn text(&self) -> &Self::Text; 15 | 16 | /// Get a reference to the value of this choice 17 | fn value(&self) -> &Self::Value; 18 | } 19 | 20 | impl<'a> Choice for &'a str { 21 | type Text = &'a str; 22 | type Value = &'a str; 23 | 24 | fn text(&self) -> &Self::Text { 25 | self 26 | } 27 | 28 | fn value(&self) -> &Self::Value { 29 | self 30 | } 31 | } 32 | 33 | impl<'a, T, V> Choice for (T, V) 34 | where T: Display 35 | { 36 | type Text = T; 37 | type Value = V; 38 | 39 | fn text(&self) -> &T { 40 | &self.0 41 | } 42 | 43 | fn value(&self) -> &V { 44 | &self.1 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Philip Munksgaard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Write, stdout, stdin}; 2 | 3 | use termion::input::TermRead; 4 | use termion::{color, style}; 5 | 6 | use error::Error; 7 | 8 | /// Prompt the user to input a string. 9 | /// 10 | /// The return type is a `Result` that contains a `String`. 11 | /// 12 | /// # Examples 13 | /// 14 | /// ```rust,no_run 15 | /// # extern crate inquirer; 16 | /// let answer = inquirer::input("What is the airspeed velocity of an unladen swallow?"); 17 | /// ``` 18 | /// 19 | /// ## Error Handling 20 | /// 21 | /// ```rust,no_run 22 | /// # extern crate inquirer; 23 | /// match inquirer::input("What is the airspeed velocity of an unladen swallow?") { 24 | /// Ok(result) => println!("You chose {:?}.", result), 25 | /// Err(inquirer::Error::UserAborted) => { 26 | /// println!("Pressed Ctrl-C, exiting."); 27 | /// std::process::exit(1); 28 | /// } 29 | /// Err(err) => println!("{:?}", err) 30 | /// } 31 | /// ``` 32 | pub fn input(prompt: &str) -> Result { 33 | let mut stdin = stdin(); 34 | let mut stdout = stdout(); 35 | 36 | try!(write!(stdout, "{}[?] {}{} ", color::Fg(color::Green), style::Reset, prompt)); 37 | 38 | try!(stdout.lock().flush()); 39 | 40 | match TermRead::read_line(&mut stdin) { 41 | Ok(Some(s)) => Ok(s), 42 | Ok(None) => Err(Error::NoMoreInput), 43 | Err(e) => Err(Error::Io(e)), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/confirm.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Write, stdout, stdin}; 2 | 3 | use termion::event::Key; 4 | use termion::input::TermRead; 5 | use termion::raw::IntoRawMode; 6 | use termion::{color, style}; 7 | 8 | use error::Error; 9 | 10 | /// Confirm a selection by answering yes or no. 11 | /// 12 | /// The return type is a `Result` that contains a bool, indicating if the user 13 | /// answered yes or no. The `default` parameter indicates what happens if the 14 | /// user presses Enter. 15 | /// 16 | /// # Examples 17 | /// 18 | /// ```rust,no_run 19 | /// # extern crate inquirer; 20 | /// let answer = inquirer::confirm("Do you want to use inquirer?", true); 21 | /// ``` 22 | /// 23 | /// ## Error Handling 24 | /// 25 | /// ```rust,no_run 26 | /// # extern crate inquirer; 27 | /// match inquirer::confirm("Do you want to use inquirer?", true) { 28 | /// Ok(result) => println!("You chose {:?}.", result), 29 | /// Err(inquirer::Error::UserAborted) => { 30 | /// println!("Pressed Ctrl-C, exiting."); 31 | /// std::process::exit(1); 32 | /// } 33 | /// Err(err) => println!("{:?}", err) 34 | /// } 35 | /// ``` 36 | pub fn confirm(prompt: &str, default: bool) -> Result { 37 | let stdin = stdin(); 38 | let (y, n) = if default { 39 | ('Y', 'n') 40 | } else { 41 | ('y', 'N') 42 | }; 43 | 44 | let mut stdout = try!(stdout().into_raw_mode()); 45 | 46 | try!(write!(stdout, 47 | "{}[?] {}{} ({}/{}) ", 48 | color::Fg(color::Green), 49 | style::Reset, 50 | prompt, 51 | y, 52 | n)); 53 | 54 | try!(stdout.lock().flush()); 55 | 56 | let mut input = stdin.keys(); 57 | 58 | let mut result = default; 59 | 60 | loop { 61 | let next = try!(input.next().ok_or_else(|| Error::NoMoreInput)); 62 | 63 | match try!(next) { 64 | Key::Char('\n') => // Enter: Use the default 65 | break, 66 | Key::Char('y') | Key::Char('Y') => { 67 | result = true; 68 | break; 69 | } 70 | Key::Char('n') | Key::Char('N') => { 71 | result = false; 72 | break; 73 | } 74 | Key::Ctrl('c') => { 75 | try!(write!(stdout, "\n\r")); 76 | return Err(Error::UserAborted); 77 | } 78 | _ => { 79 | // pass 80 | } 81 | } 82 | } 83 | 84 | try!(write!(stdout, "\n\r")); 85 | Ok(result) 86 | } 87 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Write, stdout, stdin}; 2 | 3 | use termion::event::Key; 4 | use termion::input::TermRead; 5 | use termion::raw::IntoRawMode; 6 | use termion::{clear, color, cursor, style}; 7 | 8 | use choice::Choice; 9 | use error::Error; 10 | 11 | /// Render a list the user can select one value from 12 | /// 13 | /// The return type is a `Result` that contains either a reference to the value 14 | /// of the selected choice or a custom error. **Please note:** If the user 15 | /// presses CtrlC, this will result in an `UserAborted` 16 | /// error that the application should handle. 17 | /// 18 | /// # Examples 19 | /// 20 | /// Simple example, using only string slices for options. 21 | /// 22 | /// ```rust,no_run 23 | /// extern crate inquirer; 24 | /// 25 | /// let choices = &["Red", "Blue", "Green"]; 26 | /// let result = inquirer::list("Chose your favorite color:", choices).unwrap(); 27 | /// ``` 28 | /// 29 | /// After choosing the first option, `result` will be `"Red"`. 30 | /// 31 | /// ## Complex types 32 | /// 33 | /// You can also use tuples for options, where the first item is printed to the 34 | /// screen, but the second item is returned as the value of the selection. 35 | /// 36 | /// ```rust,no_run 37 | /// # extern crate inquirer; 38 | /// enum Color { Red, Blue, Green }; 39 | /// let choices = &[("Red", Color::Red), ("Blue", Color::Blue), ("Green", Color::Green)]; 40 | /// let result = inquirer::list("Chose your favorite color:", choices).unwrap(); 41 | /// ``` 42 | /// 43 | /// Here, `result` will be `Red` when you select the first option. 44 | /// 45 | /// ## Error Handling 46 | /// 47 | /// ```rust,no_run 48 | /// # extern crate inquirer; 49 | /// let choices = &["Red", "Blue", "Green"]; 50 | /// 51 | /// match inquirer::list("Choose your favorite color:", choices) { 52 | /// Ok(result) => println!("You chose {:?}.", result), 53 | /// Err(inquirer::Error::UserAborted) => { 54 | /// println!("Pressed Ctrl-C, exiting."); 55 | /// std::process::exit(1); 56 | /// } 57 | /// Err(err) => println!("{:?}", err) 58 | /// } 59 | /// ``` 60 | pub fn list<'c, C, V>(prompt: &str, choices: &'c [C]) -> Result<&'c V, Error> 61 | where C: Choice 62 | { 63 | let stdin = stdin(); 64 | let mut stdout = try!(stdout().into_raw_mode()); 65 | try!(write!(stdout, 66 | "{}{}[?] {}{}\n", 67 | cursor::Hide, 68 | color::Fg(color::Green), 69 | style::Reset, 70 | prompt)); 71 | 72 | for _ in 0..choices.len() - 1 { 73 | try!(write!(stdout, "\n")); 74 | } 75 | 76 | let mut cur: usize = 0; 77 | 78 | let mut input = stdin.keys(); 79 | 80 | loop { 81 | print!("{}", cursor::Up(choices.len() as u16)); 82 | for (i, s) in choices.iter().enumerate() { 83 | try!(write!(stdout, "\n\r{}", clear::CurrentLine)); 84 | 85 | if cur == i { 86 | try!(write!(stdout, "{} > {}{}", style::Bold, s.text(), style::Reset)); 87 | } else { 88 | try!(write!(stdout, " {}", s.text())); 89 | } 90 | } 91 | 92 | try!(stdout.lock().flush()); 93 | 94 | let next = try!(input.next().ok_or_else(|| Error::NoMoreInput)); 95 | 96 | match try!(next) { 97 | Key::Char('\n') => { 98 | // Enter 99 | break; 100 | } 101 | Key::Up if cur != 0 => { 102 | cur -= 1; 103 | } 104 | Key::Down if cur != choices.len() - 1 => { 105 | cur += 1; 106 | } 107 | Key::Ctrl('c') => { 108 | try!(write!(stdout, "\n\r{}", cursor::Show)); 109 | return Err(Error::UserAborted); 110 | } 111 | _ => { 112 | // pass 113 | } 114 | } 115 | } 116 | 117 | try!(write!(stdout, "\n\r{}", cursor::Show)); 118 | 119 | choices.get(cur) 120 | .ok_or_else(|| Error::InvalidChoice(cur)) 121 | .map(|choice| choice.value()) 122 | } 123 | --------------------------------------------------------------------------------