mentat/tools/cli/src/mentat_cli/input.rs

200 lines
6.5 KiB
Rust

// Copyright 2017 Mozilla
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
use std::io::stdin;
use linefeed::{
DefaultTerminal,
Reader,
ReadResult,
Signal,
};
use self::InputResult::*;
use command_parser::{
Command,
command,
};
use errors as cli;
/// Starting prompt
const DEFAULT_PROMPT: &'static str = "mentat=> ";
/// Prompt when further input is being read
// TODO: Should this actually reflect the current open brace?
const MORE_PROMPT: &'static str = "mentat.> ";
/// Possible results from reading input from `InputReader`
#[derive(Clone, Debug)]
pub enum InputResult {
/// mentat command as input; (name, rest of line)
MetaCommand(Command),
/// An empty line
Empty,
/// Needs more input
More,
/// End of file reached
Eof,
}
/// Reads input from `stdin`
pub struct InputReader {
buffer: String,
reader: Option<Reader<DefaultTerminal>>,
in_process_cmd: Option<Command>,
}
enum UserAction {
// We've received some text that we should interpret as a new command, or
// as part of the current command.
TextInput(String),
// We were interrupted, if we have a current command we should clear it,
// otherwise we should exit. Currently can only be generated by reading from
// a terminal (and not by reading from stdin).
Interrupt,
// We hit the end of the file, there was an error getting user input, or
// something else happened that means we should exit.
Quit,
}
impl InputReader {
/// Constructs a new `InputReader` reading from `stdin`.
pub fn new() -> InputReader {
let r = match Reader::new("mentat") {
Ok(mut r) => {
// Handle SIGINT (Ctrl-C)
r.set_report_signal(Signal::Interrupt, true);
r.set_word_break_chars(" \t\n!\"#$%&'(){}*+,-./:;<=>?@[\\]^`");
Some(r)
},
Err(_) => None,
};
InputReader{
buffer: String::new(),
reader: r,
in_process_cmd: None,
}
}
/// Returns whether the `InputReader` is reading from a TTY.
pub fn is_tty(&self) -> bool {
self.reader.is_some()
}
/// Reads a single command, item, or statement from `stdin`.
/// Returns `More` if further input is required for a complete result.
/// In this case, the input received so far is buffered internally.
pub fn read_input(&mut self) -> Result<InputResult, cli::Error> {
let prompt = if self.in_process_cmd.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT };
let line = match self.read_line(prompt) {
UserAction::TextInput(s) => s,
UserAction::Interrupt if self.in_process_cmd.is_some() => {
self.in_process_cmd = None;
self.buffer.clear();
// Move to the next line, so that our next prompt isn't on top
// of the previous.
println!();
String::new()
},
_ => return Ok(Eof),
};
if !self.buffer.is_empty() {
self.buffer.push('\n');
}
self.buffer.push_str(&line);
if self.buffer.is_empty() {
return Ok(Empty);
}
// if we have a command in process (i.e. an incomplete query or transaction),
// then we already know which type of command it is and so we don't need to parse the
// command again, only the content, which we do later.
// Therefore, we add the newly read in line to the existing command args.
// If there is no in process command, we parse the read in line as a new command.
let cmd = match &self.in_process_cmd {
&Some(Command::Query(ref args)) => {
Ok(Command::Query(args.clone() + " " + &line))
},
&Some(Command::Transact(ref args)) => {
Ok(Command::Transact(args.clone() + " " + &line))
},
_ => {
command(&self.buffer)
},
};
match cmd {
Ok(cmd) => {
match cmd {
Command::Query(_) |
Command::Transact(_) |
Command::QueryExplain(_) if !cmd.is_complete() => {
// A query or transact is complete if it contains a valid EDN.
// if the command is not complete, ask for more from the REPL and remember
// which type of command we've found here.
self.in_process_cmd = Some(cmd);
Ok(More)
},
_ => {
let entry = self.buffer.clone();
self.buffer.clear();
self.add_history(entry);
self.in_process_cmd = None;
Ok(InputResult::MetaCommand(cmd))
}
}
},
Err(e) => {
let entry = self.buffer.clone();
self.buffer.clear();
self.add_history(entry);
self.in_process_cmd = None;
Err(e)
},
}
}
fn read_line(&mut self, prompt: &str) -> UserAction {
match self.reader {
Some(ref mut r) => {
r.set_prompt(prompt);
r.read_line().ok().map_or(UserAction::Quit, |line|
match line {
ReadResult::Input(s) => UserAction::TextInput(s),
ReadResult::Signal(Signal::Interrupt) =>
UserAction::Interrupt,
_ => UserAction::Quit,
})
},
None => self.read_stdin()
}
}
fn read_stdin(&self) -> UserAction {
let mut s = String::new();
match stdin().read_line(&mut s) {
Ok(0) | Err(_) => UserAction::Quit,
Ok(_) => UserAction::TextInput(s)
}
}
fn add_history(&mut self, line: String) {
if let Some(ref mut r) = self.reader {
r.add_history(line);
}
}
}