commit
7f76d53612
4 changed files with 101 additions and 39 deletions
|
@ -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"
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue