Load and save CLI history. (#758, #760) r=grisha

This commit is contained in:
Nick Alexander 2018-06-22 15:39:46 -07:00
commit 7f76d53612
4 changed files with 101 additions and 39 deletions

View file

@ -24,7 +24,7 @@ failure = "0.1.1"
failure_derive = "0.1.1" failure_derive = "0.1.1"
getopts = "0.2" getopts = "0.2"
lazy_static = "0.2" lazy_static = "0.2"
linefeed = "0.4" linefeed = "0.5"
log = "0.4" log = "0.4"
tabwriter = "1" tabwriter = "1"
tempfile = "1.1" tempfile = "1.1"

View file

@ -8,11 +8,15 @@
// CONDITIONS OF ANY KIND, either express or implied. See the License for the // CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License. // specific language governing permissions and limitations under the License.
use std::io::stdin; use std::io::{
stdin,
stdout,
Write,
};
use linefeed::{ use linefeed::{
DefaultTerminal, DefaultTerminal,
Reader, Interface,
ReadResult, ReadResult,
Signal, Signal,
}; };
@ -52,7 +56,7 @@ pub enum InputResult {
/// Reads input from `stdin` /// Reads input from `stdin`
pub struct InputReader { pub struct InputReader {
buffer: String, buffer: String,
reader: Option<Reader<DefaultTerminal>>, interface: Option<Interface<DefaultTerminal>>,
in_process_cmd: Option<Command>, in_process_cmd: Option<Command>,
} }
@ -71,27 +75,29 @@ enum UserAction {
impl InputReader { impl InputReader {
/// Constructs a new `InputReader` reading from `stdin`. /// Constructs a new `InputReader` reading from `stdin`.
pub fn new() -> InputReader { pub fn new(interface: Option<Interface<DefaultTerminal>>) -> InputReader {
let r = match Reader::new("mentat") { if let Some(ref interface) = interface {
Ok(mut r) => { // It's fine to fail to load history.
let p = ::history_file_path();
let loaded = interface.load_history(&p);
debug!("history read from {}: {}", p.display(), loaded.is_ok());
let mut r = interface.lock_reader();
// Handle SIGINT (Ctrl-C) // Handle SIGINT (Ctrl-C)
r.set_report_signal(Signal::Interrupt, true); r.set_report_signal(Signal::Interrupt, true);
r.set_word_break_chars(" \t\n!\"#$%&'(){}*+,-./:;<=>?@[\\]^`"); r.set_word_break_chars(" \t\n!\"#$%&'(){}*+,-./:;<=>?@[\\]^`");
Some(r) }
},
Err(_) => None,
};
InputReader{ InputReader{
buffer: String::new(), buffer: String::new(),
reader: r, interface,
in_process_cmd: None, in_process_cmd: None,
} }
} }
/// Returns whether the `InputReader` is reading from a TTY. /// Returns whether the `InputReader` is reading from a TTY.
pub fn is_tty(&self) -> bool { pub fn is_tty(&self) -> bool {
self.reader.is_some() self.interface.is_some()
} }
/// Reads a single command, item, or statement from `stdin`. /// Reads a single command, item, or statement from `stdin`.
@ -179,7 +185,7 @@ impl InputReader {
} }
fn read_line(&mut self, prompt: &str) -> UserAction { fn read_line(&mut self, prompt: &str) -> UserAction {
match self.reader { match self.interface {
Some(ref mut r) => { Some(ref mut r) => {
r.set_prompt(prompt); r.set_prompt(prompt);
r.read_line().ok().map_or(UserAction::Quit, |line| r.read_line().ok().map_or(UserAction::Quit, |line|
@ -191,7 +197,13 @@ impl InputReader {
}) })
}, },
None => self.read_stdin() None => {
print!("{}", prompt);
if stdout().flush().is_err() {
return UserAction::Quit;
}
self.read_stdin()
},
} }
} }
@ -200,13 +212,29 @@ impl InputReader {
match stdin().read_line(&mut s) { match stdin().read_line(&mut s) {
Ok(0) | Err(_) => UserAction::Quit, Ok(0) | Err(_) => UserAction::Quit,
Ok(_) => UserAction::TextInput(s) Ok(_) => {
if s.ends_with("\n") {
let len = s.len() - 1;
s.truncate(len);
}
UserAction::TextInput(s)
},
} }
} }
fn add_history(&mut self, line: String) { fn add_history(&self, line: String) {
if let Some(ref mut r) = self.reader { if let Some(ref interface) = self.interface {
r.add_history(line); interface.add_history(line);
}
self.save_history();
}
pub fn save_history(&self) -> () {
if let Some(ref interface) = self.interface {
let p = ::history_file_path();
// It's okay to fail to save history.
let saved = interface.save_history(&p);
debug!("history saved to {}: {}", p.display(), saved.is_ok());
} }
} }
} }

View file

@ -10,6 +10,10 @@
#![crate_name = "mentat_cli"] #![crate_name = "mentat_cli"]
use std::path::{
PathBuf,
};
#[macro_use] extern crate failure_derive; #[macro_use] extern crate failure_derive;
#[macro_use] extern crate log; #[macro_use] extern crate log;
#[macro_use] extern crate lazy_static; #[macro_use] extern crate lazy_static;
@ -36,6 +40,17 @@ use termion::{
color, color,
}; };
static HISTORY_FILE_PATH: &str = ".mentat_history";
/// The Mentat CLI stores input history in a readline-compatible file like "~/.mentat_history".
/// This accords with main other tools which prefix with "." and suffix with "_history": lein,
/// node_repl, python, and sqlite, at least.
pub(crate) fn history_file_path() -> PathBuf {
let mut p = ::std::env::home_dir().unwrap_or_default();
p.push(::HISTORY_FILE_PATH);
p
}
static BLUE: color::Rgb = color::Rgb(0x99, 0xaa, 0xFF); static BLUE: color::Rgb = color::Rgb(0x99, 0xaa, 0xFF);
static GREEN: color::Rgb = color::Rgb(0x77, 0xFF, 0x99); static GREEN: color::Rgb = color::Rgb(0x77, 0xFF, 0x99);
@ -64,6 +79,7 @@ pub fn run() -> i32 {
opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT"); opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT");
opts.optmulti("i", "import", "Execute an import on startup. Imports are executed before queries.", "PATH"); opts.optmulti("i", "import", "Execute an import on startup. Imports are executed before queries.", "PATH");
opts.optflag("v", "version", "Print version and exit"); opts.optflag("v", "version", "Print version and exit");
opts.optflag("", "no-tty", "Don't try to use a TTY for readline-like input processing");
let matches = match opts.parse(&args[1..]) { let matches = match opts.parse(&args[1..]) {
Ok(m) => m, Ok(m) => m,
@ -121,13 +137,15 @@ pub fn run() -> i32 {
} }
}).collect(); }).collect();
let repl = repl::Repl::new(); let mut repl = match repl::Repl::new(!matches.opt_present("no-tty")) {
if repl.is_ok() { Ok(repl) => repl,
repl.unwrap().run(Some(cmds)); Err(e) => {
println!("{}", e);
} else { return 1
println!("{}", repl.err().unwrap());
} }
};
repl.run(Some(cmds));
0 0
} }

View file

@ -9,13 +9,16 @@
// specific language governing permissions and limitations under the License. // specific language governing permissions and limitations under the License.
use std::io::Write; use std::io::Write;
use std::process;
use failure::{ use failure::{
err_msg, err_msg,
Error, Error,
}; };
use linefeed::{
Interface,
};
use tabwriter::TabWriter; use tabwriter::TabWriter;
use termion::{ use termion::{
@ -185,6 +188,7 @@ fn format_time(duration: Duration) {
/// Executes input and maintains state of persistent items. /// Executes input and maintains state of persistent items.
pub struct Repl { pub struct Repl {
input_reader: InputReader,
path: String, path: String,
store: Store, store: Store,
timer_on: bool, timer_on: bool,
@ -200,19 +204,26 @@ impl Repl {
} }
/// Constructs a new `Repl`. /// Constructs a new `Repl`.
pub fn new() -> Result<Repl, String> { pub fn new(tty: bool) -> Result<Repl, String> {
let interface = if tty {
Some(Interface::new("mentat").map_err(|_| "failed to create tty interface; try --no-tty")?)
} else {
None
};
let input_reader = InputReader::new(interface);
let store = Store::open("").map_err(|e| e.to_string())?; let store = Store::open("").map_err(|e| e.to_string())?;
Ok(Repl { Ok(Repl {
input_reader,
path: "".to_string(), path: "".to_string(),
store: store, store,
timer_on: false, timer_on: false,
}) })
} }
/// Runs the REPL interactively. /// Runs the REPL interactively.
pub fn run(&mut self, startup_commands: Option<Vec<Command>>) { pub fn run(&mut self, startup_commands: Option<Vec<Command>>) {
let mut input = InputReader::new();
if let Some(cmds) = startup_commands { if let Some(cmds) = startup_commands {
for command in cmds.iter() { for command in cmds.iter() {
println!("{}", command.output()); println!("{}", command.output());
@ -221,17 +232,19 @@ impl Repl {
} }
loop { loop {
let res = input.read_input(); let res = self.input_reader.read_input();
match res { match res {
Ok(MetaCommand(cmd)) => { Ok(MetaCommand(cmd)) => {
debug!("read command: {:?}", cmd); debug!("read command: {:?}", cmd);
self.handle_command(cmd); if !self.handle_command(cmd) {
break;
}
}, },
Ok(Empty) | Ok(Empty) |
Ok(More) => (), Ok(More) => (),
Ok(Eof) => { Ok(Eof) => {
if input.is_tty() { if self.input_reader.is_tty() {
println!(); println!();
} }
break; break;
@ -239,6 +252,8 @@ impl Repl {
Err(e) => eprintln!("{}", e.to_string()), Err(e) => eprintln!("{}", e.to_string()),
} }
} }
self.input_reader.save_history();
} }
fn cache(&mut self, attr: String, direction: CacheDirection) { fn cache(&mut self, attr: String, direction: CacheDirection) {
@ -253,7 +268,7 @@ impl Repl {
} }
/// Runs a single command input. /// Runs a single command input.
fn handle_command(&mut self, cmd: Command) { fn handle_command(&mut self, cmd: Command) -> bool {
let should_print_times = self.timer_on && cmd.is_timed(); let should_print_times = self.timer_on && cmd.is_timed();
let mut start = PreciseTime::now(); let mut start = PreciseTime::now();
@ -267,9 +282,8 @@ impl Repl {
self.close(); self.close();
}, },
Command::Exit => { Command::Exit => {
self.close();
eprintln!("Exiting…"); eprintln!("Exiting…");
process::exit(0); return false;
}, },
Command::Help(args) => { Command::Help(args) => {
self.help_command(args); self.help_command(args);
@ -366,6 +380,8 @@ impl Repl {
eprint!(": "); eprint!(": ");
format_time(start.to(end)); format_time(start.to(end));
} }
return true;
} }
fn execute_import<T>(&mut self, path: T) fn execute_import<T>(&mut self, path: T)