Overhaul REPL cli

This commit is contained in:
NGnius 2022-03-04 20:37:59 -05:00
parent 4581fe8fe9
commit 166fef2400
4 changed files with 218 additions and 65 deletions

32
Cargo.lock generated
View file

@ -363,6 +363,21 @@ dependencies = [
"memchr 2.4.1", "memchr 2.4.1",
] ]
[[package]]
name = "console"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"regex 1.5.4",
"terminal_size",
"unicode-width",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.3" version = "0.8.3"
@ -600,6 +615,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.30" version = "0.8.30"
@ -1081,6 +1102,7 @@ name = "mps"
version = "0.6.0" version = "0.6.0"
dependencies = [ dependencies = [
"clap 3.1.2", "clap 3.1.2",
"console",
"mps-interpreter", "mps-interpreter",
"mps-player", "mps-player",
] ]
@ -2242,6 +2264,16 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"

View file

@ -23,6 +23,7 @@ mps-interpreter = { version = "0.6.0", path = "./mps-interpreter" }
# external # external
clap = { version = "3.0", features = ["derive"] } clap = { version = "3.0", features = ["derive"] }
# termios = { version = "^0.3"} # termios = { version = "^0.3"}
console = { version = "0.15" }
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
mps-player = { version = "0.6.0", path = "./mps-player", default-features = false } mps-player = { version = "0.6.0", path = "./mps-player", default-features = false }

View file

@ -92,7 +92,7 @@ where
is_stmt_done = true; is_stmt_done = true;
} }
next_item next_item
.map(|item| item.map_err(|e| error_with_ctx(e.into(), self.tokenizer.current_line()))) .map(|item| item.map_err(|e| error_with_ctx(e, self.tokenizer.current_line())))
} else { } else {
/*if self.tokenizer.end_of_file() { /*if self.tokenizer.end_of_file() {
return None; return None;
@ -102,7 +102,7 @@ where
let token_result = self let token_result = self
.tokenizer .tokenizer
.next_statement(&mut self.buffer) .next_statement(&mut self.buffer)
.map_err(|e| error_with_ctx(e.into(), self.tokenizer.current_line())); .map_err(|e| error_with_ctx(e, self.tokenizer.current_line()));
match token_result { match token_result {
Ok(_) => {} Ok(_) => {}
Err(x) => return Some(Err(x)), Err(x) => return Some(Err(x)),
@ -124,11 +124,11 @@ where
is_stmt_done = true; is_stmt_done = true;
} }
next_item.map(|item| { next_item.map(|item| {
item.map_err(|e| error_with_ctx(e.into(), self.tokenizer.current_line())) item.map_err(|e| error_with_ctx(e, self.tokenizer.current_line()))
}) })
} }
Err(e) => { Err(e) => {
Some(Err(e).map_err(|e| error_with_ctx(e.into(), self.tokenizer.current_line()))) Some(Err(e).map_err(|e| error_with_ctx(e, self.tokenizer.current_line())))
} }
} }
}; };
@ -139,9 +139,10 @@ where
} }
} }
fn error_with_ctx(mut error: MpsError, line: usize) -> MpsError { fn error_with_ctx<T: std::convert::Into<MpsError>>(error: T, line: usize) -> MpsError {
error.set_line(line); let mut err = error.into();
error err.set_line(line);
err
} }
/// Builder function to add the standard statements of MPS. /// Builder function to add the standard statements of MPS.

View file

@ -1,6 +1,8 @@
//! Read, Execute, Print Loop functionality //! Read, Execute, Print Loop functionality
use std::io::{self, Read, Stdin, Write}; use std::io::{self, Write};
use console::{Term, Key};
use mps_interpreter::MpsRunner; use mps_interpreter::MpsRunner;
use mps_player::{MpsController, MpsPlayer}; use mps_player::{MpsController, MpsPlayer};
@ -9,33 +11,40 @@ use super::channel_io::{channel_io, ChannelWriter};
use super::cli::CliArgs; use super::cli::CliArgs;
struct ReplState { struct ReplState {
stdin: Stdin, terminal: Term,
line_number: usize, line_number: usize,
statement_buf: Vec<u8>, statement_buf: Vec<char>,
writer: ChannelWriter, writer: ChannelWriter,
in_literal: Option<char>, in_literal: Option<char>,
bracket_depth: usize, bracket_depth: usize,
curly_depth: usize, curly_depth: usize,
history: Vec<String>,
selected_history: usize,
current_line: Vec<char>,
cursor_rightward_position: usize,
} }
impl ReplState { impl ReplState {
fn new(chan_writer: ChannelWriter) -> Self { fn new(chan_writer: ChannelWriter, term: Term) -> Self {
Self { Self {
stdin: io::stdin(), terminal: term,
line_number: 0, line_number: 0,
statement_buf: Vec::new(), statement_buf: Vec::new(),
writer: chan_writer, writer: chan_writer,
in_literal: None, in_literal: None,
bracket_depth: 0, bracket_depth: 0,
curly_depth: 0, curly_depth: 0,
history: Vec::new(),
selected_history: 0,
current_line: Vec::new(),
cursor_rightward_position: 0,
} }
} }
} }
pub fn repl(args: CliArgs) { pub fn repl(args: CliArgs) {
/*let mut terminal = termios::Termios::from_fd(0 /* stdin */).unwrap(); let term = Term::stdout();
terminal.c_lflag &= !termios::ICANON; // no echo and canonical mode term.set_title("mps");
termios::tcsetattr(0, termios::TCSANOW, &mut terminal).unwrap();*/
let (writer, reader) = channel_io(); let (writer, reader) = channel_io();
let volume = args.volume.clone(); let volume = args.volume.clone();
let player_builder = move || { let player_builder = move || {
@ -47,9 +56,9 @@ pub fn repl(args: CliArgs) {
} }
player player
}; };
let mut state = ReplState::new(writer); let mut state = ReplState::new(writer, term);
if let Some(playlist_file) = &args.playlist { if let Some(playlist_file) = &args.playlist {
println!("Playlist mode (output: `{}`)", playlist_file); writeln!(state.terminal, "Playlist mode (output: `{}`)", playlist_file).expect("Failed to write to terminal output");
let mut player = player_builder(); let mut player = player_builder();
let mut playlist_writer = let mut playlist_writer =
io::BufWriter::new(std::fs::File::create(playlist_file).unwrap_or_else(|_| { io::BufWriter::new(std::fs::File::create(playlist_file).unwrap_or_else(|_| {
@ -71,7 +80,7 @@ pub fn repl(args: CliArgs) {
.expect("Failed to flush playlist to file"); .expect("Failed to flush playlist to file");
}); });
} else { } else {
println!("Playback mode (output: audio device)"); writeln!(state.terminal, "Playback mode (output: audio device)").expect("Failed to write to terminal output");
let ctrl = MpsController::create_repl(player_builder); let ctrl = MpsController::create_repl(player_builder);
read_loop(&args, &mut state, || { read_loop(&args, &mut state, || {
if args.wait { if args.wait {
@ -96,28 +105,31 @@ pub fn repl(args: CliArgs) {
} }
fn read_loop<F: FnMut()>(args: &CliArgs, state: &mut ReplState, mut execute: F) -> ! { fn read_loop<F: FnMut()>(args: &CliArgs, state: &mut ReplState, mut execute: F) -> ! {
let mut read_buf: [u8; 1] = [0]; prompt(state, args);
prompt(&mut state.line_number, args);
loop { loop {
let mut read_count = 0; match state.terminal.read_key().expect("Failed to read terminal input") {
//read_buf[0] = 0; Key::Char(read_c) => {
while read_count == 0 { if state.cursor_rightward_position == 0 {
// TODO: enable raw mode (char by char) reading of stdin write!(state.terminal, "{}", read_c).expect("Failed to write to terminal output");
read_count = state state.statement_buf.push(read_c);
.stdin state.current_line.push(read_c);
.read(&mut read_buf) } else {
.expect("Failed to read stdin"); write!(state.terminal, "{}", read_c).expect("Failed to write to terminal output");
for i in state.current_line.len() - state.cursor_rightward_position .. state.current_line.len() {
write!(state.terminal, "{}", state.current_line[i]).expect("Failed to write to terminal output");
} }
//println!("Read {}", read_buf[0]); state.terminal.move_cursor_left(state.cursor_rightward_position).expect("Failed to write to terminal output");
state.statement_buf.push(read_buf[0]); state.statement_buf.insert(state.statement_buf.len() - state.cursor_rightward_position, read_c);
match read_buf[0] as char { state.current_line.insert(state.current_line.len() - state.cursor_rightward_position, read_c);
}
match read_c {
'"' | '`' => { '"' | '`' => {
if let Some(c) = state.in_literal { if let Some(c) = state.in_literal {
if c == read_buf[0] as char { if c == read_c {
state.in_literal = None; state.in_literal = None;
} }
} else { } else {
state.in_literal = Some(read_buf[0] as char); state.in_literal = Some(read_c);
} }
} }
'(' => state.bracket_depth += 1, '(' => state.bracket_depth += 1,
@ -128,39 +140,146 @@ fn read_loop<F: FnMut()>(args: &CliArgs, state: &mut ReplState, mut execute: F)
if state.in_literal.is_none() { if state.in_literal.is_none() {
state state
.writer .writer
.write(state.statement_buf.as_slice()) .write(state.statement_buf.iter().collect::<String>().as_bytes())
.expect("Failed to write to MPS interpreter"); .expect("Failed to write to MPS interpreter");
execute(); execute();
state.statement_buf.clear(); state.statement_buf.clear();
} }
} }
'\n' => { '\n' => {
let statement_result = std::str::from_utf8(state.statement_buf.as_slice()); let statement = state.statement_buf.iter().collect::<String>();
if statement_result.is_ok() && statement_result.unwrap().trim().starts_with('?') { let statement_result = statement.trim();
if statement_result.starts_with('?') {
//println!("Got {}", statement_result.unwrap()); //println!("Got {}", statement_result.unwrap());
repl_commands(statement_result.unwrap().trim()); repl_commands(statement_result);
state.statement_buf.clear(); state.statement_buf.clear();
} else if state.bracket_depth == 0 && state.in_literal.is_none() && state.curly_depth == 0 { } else if state.bracket_depth == 0 && state.in_literal.is_none() && state.curly_depth == 0 {
state.statement_buf.push(b';'); state.statement_buf.push(';');
state state
.writer .writer
.write(state.statement_buf.as_slice()) .write(state.statement_buf.iter().collect::<String>().as_bytes())
.expect("Failed to write to MPS interpreter"); .expect("Failed to write to MPS interpreter");
execute(); execute();
state.statement_buf.clear(); state.statement_buf.clear();
} }
prompt(&mut state.line_number, args); prompt(state, args);
} }
_ => {} _ => {}
} }
},
Key::Backspace => {
if let Some(c) = state.statement_buf.pop() {
match c {
'\n' | '\r' => {
// another line, cannot backspace that far
state.statement_buf.push(c);
}
_ => {
state.current_line.pop();
state.terminal.move_cursor_left(1).expect("Failed to write to terminal output");
write!(state.terminal, " ").expect("Failed to write to terminal output");
state.terminal.flush().expect("Failed to flush terminal output");
state.terminal.move_cursor_left(1).expect("Failed to write to terminal output");
}
}
}
},
Key::Enter => {
state.terminal.write_line("").expect("Failed to write to terminal output");
let statement = state.statement_buf.iter().collect::<String>();
let statement_result = statement.trim();
if statement_result.starts_with('?') {
//println!("Got {}", statement_result.unwrap());
repl_commands(statement_result);
state.statement_buf.clear();
} else if state.bracket_depth == 0 && state.in_literal.is_none() && state.curly_depth == 0 {
state.statement_buf.push(';');
let complete_statement = state.statement_buf.iter().collect::<String>();
state
.writer
.write(complete_statement.as_bytes())
.expect("Failed to write to MPS interpreter");
execute();
state.statement_buf.clear();
}
state.statement_buf.push('\n');
state.cursor_rightward_position = 0;
// history
let last_line = state.current_line.iter().collect::<String>();
state.current_line.clear();
if !last_line.is_empty() && ((!state.history.is_empty() && state.history[state.history.len()-1] != last_line) || state.history.is_empty()) {
state.history.push(last_line);
}
state.selected_history = 0;
prompt(state, args);
},
Key::ArrowUp => {
if state.selected_history != state.history.len() {
state.selected_history += 1;
display_history_line(state, args);
}
},
Key::ArrowDown => {
if state.selected_history > 1 {
state.selected_history -= 1;
display_history_line(state, args);
} else if state.selected_history == 1 {
state.selected_history = 0;
state.line_number -= 1;
state.terminal.clear_line().expect("Failed to write to terminal output");
prompt(state, args);
// clear stale input buffer
state.statement_buf.clear();
state.current_line.clear();
state.in_literal = None;
state.bracket_depth = 0;
state.curly_depth = 0;
}
},
Key::ArrowLeft => {
if state.current_line.len() > state.cursor_rightward_position {
state.terminal.move_cursor_left(1).expect("Failed to write to terminal output");
state.cursor_rightward_position += 1;
}
},
Key::ArrowRight => {
if state.cursor_rightward_position != 0 {
state.terminal.move_cursor_right(1).expect("Failed to write to terminal output");
state.cursor_rightward_position -= 1;
}
},
_ => continue
}
//println!("Read {}", read_buf[0]);
} }
} }
#[inline(always)] #[inline(always)]
fn prompt(line: &mut usize, args: &CliArgs) { fn prompt(state: &mut ReplState, args: &CliArgs) {
print!("{}{}", line, args.prompt); write!(state.terminal, "{}{}", state.line_number, args.prompt).expect("Failed to write to terminal output");
*line += 1; state.line_number += 1;
std::io::stdout().flush().expect("Failed to flush stdout"); state.terminal.flush().expect("Failed to flush terminal output");
}
#[inline(always)]
fn display_history_line(state: &mut ReplState, args: &CliArgs) {
// get historical line
state.line_number -= 1;
state.terminal.clear_line().expect("Failed to write to terminal output");
prompt(state, args);
let new_statement = state.history[state.history.len() - state.selected_history].trim();
state.terminal.write(new_statement.as_bytes()).expect("Failed to write to terminal output");
// clear stale input buffer
state.statement_buf.clear();
state.current_line.clear();
state.in_literal = None;
state.bracket_depth = 0;
state.curly_depth = 0;
state.statement_buf.extend(new_statement.chars());
state.current_line.extend(new_statement.chars());
} }
#[inline(always)] #[inline(always)]