From 166fef2400adfd44096120a65f99930e3788b3cf Mon Sep 17 00:00:00 2001 From: NGnius Date: Fri, 4 Mar 2022 20:37:59 -0500 Subject: [PATCH] Overhaul REPL cli --- Cargo.lock | 32 ++++ Cargo.toml | 1 + mps-interpreter/src/interpretor.rs | 15 +- src/repl.rs | 235 ++++++++++++++++++++++------- 4 files changed, 218 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e299edb..53ab099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 2799255..2613cda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs index 183d04a..9c8a109 100644 --- a/mps-interpreter/src/interpretor.rs +++ b/mps-interpreter/src/interpretor.rs @@ -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>(error: T, line: usize) -> MpsError { + let mut err = error.into(); + err.set_line(line); + err } /// Builder function to add the standard statements of MPS. diff --git a/src/repl.rs b/src/repl.rs index 0e699d9..6ec3da2 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -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, + statement_buf: Vec, writer: ChannelWriter, in_literal: Option, bracket_depth: usize, curly_depth: usize, + history: Vec, + selected_history: usize, + current_line: Vec, + 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(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::().as_bytes()) + .expect("Failed to write to MPS interpreter"); + execute(); + state.statement_buf.clear(); + } + } + '\n' => { + let statement = state.statement_buf.iter().collect::(); + 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::().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::(); + 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::(); 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::(); + 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)]