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",
]
[[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]]
name = "core-foundation-sys"
version = "0.8.3"
@ -600,6 +615,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.30"
@ -1081,6 +1102,7 @@ name = "mps"
version = "0.6.0"
dependencies = [
"clap 3.1.2",
"console",
"mps-interpreter",
"mps-player",
]
@ -2242,6 +2264,16 @@ dependencies = [
"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]]
name = "textwrap"
version = "0.11.0"

View file

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

View file

@ -92,7 +92,7 @@ where
is_stmt_done = true;
}
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 {
/*if self.tokenizer.end_of_file() {
return None;
@ -102,7 +102,7 @@ where
let token_result = self
.tokenizer
.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 {
Ok(_) => {}
Err(x) => return Some(Err(x)),
@ -124,11 +124,11 @@ where
is_stmt_done = true;
}
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) => {
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 {
error.set_line(line);
error
fn error_with_ctx<T: std::convert::Into<MpsError>>(error: T, line: usize) -> MpsError {
let mut err = error.into();
err.set_line(line);
err
}
/// Builder function to add the standard statements of MPS.

View file

@ -1,6 +1,8 @@
//! 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_player::{MpsController, MpsPlayer};
@ -9,33 +11,40 @@ use super::channel_io::{channel_io, ChannelWriter};
use super::cli::CliArgs;
struct ReplState {
stdin: Stdin,
terminal: Term,
line_number: usize,
statement_buf: Vec<u8>,
statement_buf: Vec<char>,
writer: ChannelWriter,
in_literal: Option<char>,
bracket_depth: usize,
curly_depth: usize,
history: Vec<String>,
selected_history: usize,
current_line: Vec<char>,
cursor_rightward_position: usize,
}
impl ReplState {
fn new(chan_writer: ChannelWriter) -> Self {
fn new(chan_writer: ChannelWriter, term: Term) -> Self {
Self {
stdin: io::stdin(),
terminal: term,
line_number: 0,
statement_buf: Vec::new(),
writer: chan_writer,
in_literal: None,
bracket_depth: 0,
curly_depth: 0,
history: Vec::new(),
selected_history: 0,
current_line: Vec::new(),
cursor_rightward_position: 0,
}
}
}
pub fn repl(args: CliArgs) {
/*let mut terminal = termios::Termios::from_fd(0 /* stdin */).unwrap();
terminal.c_lflag &= !termios::ICANON; // no echo and canonical mode
termios::tcsetattr(0, termios::TCSANOW, &mut terminal).unwrap();*/
let term = Term::stdout();
term.set_title("mps");
let (writer, reader) = channel_io();
let volume = args.volume.clone();
let player_builder = move || {
@ -47,9 +56,9 @@ pub fn repl(args: CliArgs) {
}
player
};
let mut state = ReplState::new(writer);
let mut state = ReplState::new(writer, term);
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 playlist_writer =
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");
});
} 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);
read_loop(&args, &mut state, || {
if args.wait {
@ -96,71 +105,181 @@ pub fn repl(args: CliArgs) {
}
fn read_loop<F: FnMut()>(args: &CliArgs, state: &mut ReplState, mut execute: F) -> ! {
let mut read_buf: [u8; 1] = [0];
prompt(&mut state.line_number, args);
prompt(state, args);
loop {
let mut read_count = 0;
//read_buf[0] = 0;
while read_count == 0 {
// TODO: enable raw mode (char by char) reading of stdin
read_count = state
.stdin
.read(&mut read_buf)
.expect("Failed to read stdin");
}
//println!("Read {}", read_buf[0]);
state.statement_buf.push(read_buf[0]);
match read_buf[0] as char {
'"' | '`' => {
if let Some(c) = state.in_literal {
if c == read_buf[0] as char {
state.in_literal = None;
}
match state.terminal.read_key().expect("Failed to read terminal input") {
Key::Char(read_c) => {
if state.cursor_rightward_position == 0 {
write!(state.terminal, "{}", read_c).expect("Failed to write to terminal output");
state.statement_buf.push(read_c);
state.current_line.push(read_c);
} else {
state.in_literal = Some(read_buf[0] as char);
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");
}
state.terminal.move_cursor_left(state.cursor_rightward_position).expect("Failed to write to terminal output");
state.statement_buf.insert(state.statement_buf.len() - state.cursor_rightward_position, read_c);
state.current_line.insert(state.current_line.len() - state.cursor_rightward_position, read_c);
}
}
'(' => state.bracket_depth += 1,
')' => if state.bracket_depth != 0 { state.bracket_depth -= 1 },
'{' => state.curly_depth += 1,
'}' => if state.curly_depth != 0 { state.curly_depth -= 1 },
';' => {
if state.in_literal.is_none() {
state
.writer
.write(state.statement_buf.as_slice())
.expect("Failed to write to MPS interpreter");
execute();
state.statement_buf.clear();
match read_c {
'"' | '`' => {
if let Some(c) = state.in_literal {
if c == read_c {
state.in_literal = None;
}
} else {
state.in_literal = Some(read_c);
}
}
'(' => state.bracket_depth += 1,
')' => if state.bracket_depth != 0 { state.bracket_depth -= 1 },
'{' => state.curly_depth += 1,
'}' => if state.curly_depth != 0 { state.curly_depth -= 1 },
';' => {
if state.in_literal.is_none() {
state
.writer
.write(state.statement_buf.iter().collect::<String>().as_bytes())
.expect("Failed to write to MPS interpreter");
execute();
state.statement_buf.clear();
}
}
'\n' => {
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(';');
state
.writer
.write(state.statement_buf.iter().collect::<String>().as_bytes())
.expect("Failed to write to MPS interpreter");
execute();
state.statement_buf.clear();
}
prompt(state, args);
}
_ => {}
}
}
'\n' => {
let statement_result = std::str::from_utf8(state.statement_buf.as_slice());
if statement_result.is_ok() && statement_result.unwrap().trim().starts_with('?') {
},
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.unwrap().trim());
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(b';');
state.statement_buf.push(';');
let complete_statement = state.statement_buf.iter().collect::<String>();
state
.writer
.write(state.statement_buf.as_slice())
.write(complete_statement.as_bytes())
.expect("Failed to write to MPS interpreter");
execute();
state.statement_buf.clear();
}
prompt(&mut state.line_number, args);
}
_ => {}
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)]
fn prompt(line: &mut usize, args: &CliArgs) {
print!("{}{}", line, args.prompt);
*line += 1;
std::io::stdout().flush().expect("Failed to flush stdout");
fn prompt(state: &mut ReplState, args: &CliArgs) {
write!(state.terminal, "{}{}", state.line_number, args.prompt).expect("Failed to write to terminal output");
state.line_number += 1;
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)]