muss/src/repl.rs

351 lines
16 KiB
Rust
Raw Normal View History

2022-01-03 22:53:57 +00:00
//! Read, Execute, Print Loop functionality
2022-03-05 01:37:59 +00:00
use std::io::{self, Write};
use console::{Term, Key};
2022-01-03 22:53:57 +00:00
use mps_interpreter::MpsRunner;
use mps_player::{MpsController, MpsPlayer};
2022-01-03 22:53:57 +00:00
2022-01-04 01:15:19 +00:00
use super::channel_io::{channel_io, ChannelWriter};
use super::cli::CliArgs;
2022-01-04 01:15:19 +00:00
struct ReplState {
2022-03-05 01:37:59 +00:00
terminal: Term,
2022-01-04 01:15:19 +00:00
line_number: usize,
2022-03-05 01:37:59 +00:00
statement_buf: Vec<char>,
2022-01-04 01:15:19 +00:00
writer: ChannelWriter,
in_literal: Option<char>,
bracket_depth: usize,
curly_depth: usize,
2022-03-05 01:37:59 +00:00
history: Vec<String>,
selected_history: usize,
current_line: Vec<char>,
cursor_rightward_position: usize,
2022-01-04 01:15:19 +00:00
}
impl ReplState {
2022-03-05 01:37:59 +00:00
fn new(chan_writer: ChannelWriter, term: Term) -> Self {
2022-01-04 01:15:19 +00:00
Self {
2022-03-05 01:37:59 +00:00
terminal: term,
2022-01-04 01:15:19 +00:00
line_number: 0,
statement_buf: Vec::new(),
writer: chan_writer,
in_literal: None,
bracket_depth: 0,
curly_depth: 0,
2022-03-05 01:37:59 +00:00
history: Vec::new(),
selected_history: 0,
current_line: Vec::new(),
cursor_rightward_position: 0,
2022-01-04 01:15:19 +00:00
}
}
}
2022-01-03 22:53:57 +00:00
pub fn repl(args: CliArgs) {
2022-03-05 01:37:59 +00:00
let term = Term::stdout();
term.set_title("mps");
2022-01-04 01:15:19 +00:00
let (writer, reader) = channel_io();
2022-02-03 22:04:01 +00:00
let volume = args.volume.clone();
2022-01-03 22:53:57 +00:00
let player_builder = move || {
let runner = MpsRunner::with_stream(reader);
2022-01-31 14:22:36 +00:00
2022-02-03 22:04:01 +00:00
let player = MpsPlayer::new(runner).unwrap();
if let Some(vol) = volume {
player.set_volume(vol);
}
player
2022-01-03 22:53:57 +00:00
};
2022-03-05 01:37:59 +00:00
let mut state = ReplState::new(writer, term);
2022-01-03 22:53:57 +00:00
if let Some(playlist_file) = &args.playlist {
2022-03-05 01:37:59 +00:00
writeln!(state.terminal, "Playlist mode (output: `{}`)", playlist_file).expect("Failed to write to terminal output");
2022-01-03 22:53:57 +00:00
let mut player = player_builder();
2022-01-31 14:22:36 +00:00
let mut playlist_writer =
io::BufWriter::new(std::fs::File::create(playlist_file).unwrap_or_else(|_| {
panic!("Abort: Cannot create writeable file `{}`", playlist_file)
}));
2022-01-04 01:15:19 +00:00
read_loop(&args, &mut state, || {
match player.save_m3u8(&mut playlist_writer) {
Ok(_) => {}
Err(e) => {
error_prompt(e, &args);
// consume any further errors (this shouldn't actually write anything)
while let Err(e) = player.save_m3u8(&mut playlist_writer) {
error_prompt(e, &args);
}
2022-01-24 21:12:29 +00:00
}
2022-01-03 22:53:57 +00:00
}
playlist_writer
.flush()
.expect("Failed to flush playlist to file");
2022-01-04 01:15:19 +00:00
});
2022-01-03 22:53:57 +00:00
} else {
2022-03-05 01:37:59 +00:00
writeln!(state.terminal, "Playback mode (output: audio device)").expect("Failed to write to terminal output");
2022-01-03 22:53:57 +00:00
let ctrl = MpsController::create_repl(player_builder);
2022-01-04 01:15:19 +00:00
read_loop(&args, &mut state, || {
if args.wait {
match ctrl.wait_for_empty() {
Ok(_) => {}
Err(e) => error_prompt(e, &args),
2022-01-04 01:15:19 +00:00
}
} else {
// consume all incoming errors
let mut had_err = true;
while had_err {
let mut new_had_err = false;
for e in ctrl.check_ack() {
error_prompt(e, &args);
new_had_err = true;
}
had_err = new_had_err;
2022-01-04 01:15:19 +00:00
}
2022-01-03 22:53:57 +00:00
}
2022-01-04 01:15:19 +00:00
});
}
}
fn read_loop<F: FnMut()>(args: &CliArgs, state: &mut ReplState, mut execute: F) -> ! {
2022-03-05 01:37:59 +00:00
prompt(state, args);
2022-01-04 01:15:19 +00:00
loop {
2022-03-05 01:37:59 +00:00
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 {
2022-03-05 01:37:59 +00:00
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);
}
2022-03-05 01:37:59 +00:00
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);
}
_ => {}
}
2022-03-05 01:37:59 +00:00
},
Key::Backspace => {
if state.cursor_rightward_position == 0 {
if let Some(c) = state.statement_buf.pop() {
// re-sync syntax tracking
match c {
'"' | '`' => {
if let Some(c2) = state.in_literal {
if c == c2 {
state.in_literal = None;
}
} else {
state.in_literal = Some(c);
}
},
'(' => if state.bracket_depth != 0 { state.bracket_depth -= 1 },
')' => state.bracket_depth += 1,
'{' => if state.curly_depth != 0 { state.curly_depth -= 1 },
'}' => state.curly_depth += 1,
_ => {},
}
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");
}
2022-03-05 01:37:59 +00:00
}
}
} else {
if state.current_line.len() != state.cursor_rightward_position {
// if not at start of line
let removed_char = state.current_line.remove(state.current_line.len()-state.cursor_rightward_position-1);
state.statement_buf.remove(state.statement_buf.len()-state.cursor_rightward_position-1);
// re-sync unclosed syntax tracking
match removed_char {
'"' | '`' => {
if let Some(c2) = state.in_literal {
if removed_char == c2 {
state.in_literal = None;
}
} else {
state.in_literal = Some(removed_char);
}
},
'(' => if state.bracket_depth != 0 { state.bracket_depth -= 1 },
')' => state.bracket_depth += 1,
'{' => if state.curly_depth != 0 { state.curly_depth -= 1 },
'}' => state.curly_depth += 1,
_ => {},
}
// re-print end of line to remove character in middle
state.terminal.move_cursor_left(1).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");
2022-03-05 01:37:59 +00:00
}
write!(state.terminal, " ").expect("Failed to write to terminal output");
state.terminal.move_cursor_left(state.cursor_rightward_position + 1).expect("Failed to write to terminal output");
2022-03-05 01:37:59 +00:00
}
}
2022-03-05 01:37:59 +00:00
},
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());
2022-03-05 01:37:59 +00:00
repl_commands(statement_result);
state.statement_buf.clear();
} else if state.bracket_depth == 0 && state.in_literal.is_none() && state.curly_depth == 0 {
2022-03-05 01:37:59 +00:00
state.statement_buf.push(';');
let complete_statement = state.statement_buf.iter().collect::<String>();
state
.writer
2022-03-05 01:37:59 +00:00
.write(complete_statement.as_bytes())
.expect("Failed to write to MPS interpreter");
execute();
state.statement_buf.clear();
}
2022-03-05 01:37:59 +00:00
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
2022-01-03 22:53:57 +00:00
}
2022-03-05 01:37:59 +00:00
//println!("Read {}", read_buf[0]);
2022-01-03 22:53:57 +00:00
}
}
#[inline(always)]
2022-03-05 01:37:59 +00:00
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());
2022-01-03 22:53:57 +00:00
}
#[inline(always)]
fn error_prompt(error: mps_player::PlaybackError, args: &CliArgs) {
eprintln!("E{}{}", args.prompt, error.message());
}
fn repl_commands(command_str: &str) {
2022-01-29 21:13:52 +00:00
let words: Vec<&str> = command_str.split(' ').map(|s| s.trim()).collect();
match words[0] {
2022-01-04 02:22:22 +00:00
"?help" => println!("{}", super::help::HELP_STRING),
"?function" | "?functions" => println!("{}", super::help::FUNCTIONS),
"?filter" | "?filters" => println!("{}", super::help::FILTERS),
2022-01-25 15:03:27 +00:00
"?sort" | "?sorter" | "?sorters" => println!("{}", super::help::SORTERS),
"?proc" | "?procedure" | "?procedures" => println!("{}", super::help::PROCEDURES),
_ => println!("Unknown command, try ?help"),
}
}