From 838e12acf4b943b71d20600bed80106c91f9a9e0 Mon Sep 17 00:00:00 2001 From: irh Date: Fri, 22 Oct 2021 12:53:54 +0200 Subject: [PATCH] Switch to using crossterm instead of termion in the REPL This will allow for the REPL to be used on Windows, but has the side-effect of making the REPL untestable via stdin, due to https://github.com/crossterm-rs/crossterm/issues/396 --- Cargo.lock | 129 ++++++++++++++++++------- src/cli/Cargo.toml | 2 +- src/cli/src/main.rs | 10 +- src/cli/src/repl.rs | 182 +++++++++++++++++------------------- src/cli/tests/repl_tests.rs | 69 -------------- 5 files changed, 186 insertions(+), 206 deletions(-) delete mode 100644 src/cli/tests/repl_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 79489cd1a..fca21de96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" @@ -176,6 +176,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.7.14", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi 0.3.9", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "csv" version = "1.1.4" @@ -441,6 +466,7 @@ dependencies = [ name = "koto_cli" version = "0.8.1" dependencies = [ + "crossterm", "indexmap", "jemallocator", "koto", @@ -451,7 +477,6 @@ dependencies = [ "koto_yaml", "pico-args", "pulldown-cmark", - "termion", ] [[package]] @@ -588,9 +613,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.80" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce" [[package]] name = "linked-hash-map" @@ -650,12 +675,25 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.1", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow 0.3.7", + "ntapi", + "winapi 0.3.9", +] + [[package]] name = "mio-extras" version = "2.0.6" @@ -664,7 +702,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" dependencies = [ "lazycell", "log", - "mio", + "mio 0.6.22", "slab", ] @@ -680,6 +718,15 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "net2" version = "0.2.35" @@ -703,12 +750,21 @@ dependencies = [ "fsevent-sys", "inotify", "libc", - "mio", + "mio 0.6.22", "mio-extras", "walkdir", "winapi 0.3.9", ] +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -728,12 +784,6 @@ dependencies = [ "libc", ] -[[package]] -name = "numtoa" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" - [[package]] name = "oorandom" version = "11.1.2" @@ -890,15 +940,6 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" -[[package]] -name = "redox_termios" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -dependencies = [ - "redox_syscall", -] - [[package]] name = "regex" version = "1.4.2" @@ -1034,6 +1075,36 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "signal-hook" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +dependencies = [ + "libc", + "mio 0.7.14", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.2" @@ -1071,18 +1142,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "termion" -version = "1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" -dependencies = [ - "libc", - "numtoa", - "redox_syscall", - "redox_termios", -] - [[package]] name = "textwrap" version = "0.11.0" diff --git a/src/cli/Cargo.toml b/src/cli/Cargo.toml index dd363fd08..4d27ef2c1 100644 --- a/src/cli/Cargo.toml +++ b/src/cli/Cargo.toml @@ -25,8 +25,8 @@ koto_tempfile = { path = "../../libs/tempfile", version = "^0.8.0"} koto_toml = { path = "../../libs/toml", version = "^0.8.0"} koto_yaml = { path = "../../libs/yaml", version = "^0.8.0"} +crossterm = "0.22.1" # A crossplatform terminal library for manipulating terminals. indexmap = "1.4.0" -termion = "1.5.5" [dependencies.pulldown-cmark] # Markdown parsing diff --git a/src/cli/src/main.rs b/src/cli/src/main.rs index 85a3edd45..d2a20fc1d 100644 --- a/src/cli/src/main.rs +++ b/src/cli/src/main.rs @@ -2,6 +2,7 @@ mod help; mod repl; use { + crossterm::tty::IsTty, koto::{bytecode::Chunk, Koto, KotoSettings}, repl::{Repl, ReplSettings}, std::{ @@ -137,8 +138,7 @@ fn run() -> Result<(), ()> { Some(script), ) } - } else if termion::is_tty(&stdin) || std::env::var_os("KOTO_FORCE_REPL_MODE").is_some() { - // Forcing REPL mode is useful for testing the behaviour of the REPL + } else if stdin.is_tty() { (None, None) } else { let mut script = String::new(); @@ -186,6 +186,8 @@ fn run() -> Result<(), ()> { return Err(()); } } + + Ok(()) } else { let mut repl = Repl::with_settings( ReplSettings { @@ -194,8 +196,6 @@ fn run() -> Result<(), ()> { }, koto_settings, ); - repl.run(); + repl.run().map_err(|_| ()) } - - Ok(()) } diff --git a/src/cli/src/repl.rs b/src/cli/src/repl.rs index fb7abcb6a..8dc785569 100644 --- a/src/cli/src/repl.rs +++ b/src/cli/src/repl.rs @@ -1,14 +1,18 @@ use { crate::help::Help, + crossterm::{ + cursor, + event::{read, Event, KeyCode, KeyEvent, KeyModifiers}, + execute, queue, style, + terminal::{self, ClearType}, + tty::IsTty, + Result, + }, koto::{bytecode::Chunk, Koto, KotoSettings}, std::{ fmt, io::{self, Stdout, Write}, }, - termion::{ - clear, color, cursor, cursor::DetectCursorPos, event::Key, input::TermRead, - raw::IntoRawMode, raw::RawTerminal, style, - }, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -56,62 +60,54 @@ impl Repl { } } - pub fn run(&mut self) { - let stdin = io::stdin(); + pub fn run(&mut self) -> Result<()> { let mut stdout = io::stdout(); - let mut tty = if termion::is_tty(&stdout) { - match termion::get_tty() { - Ok(tty) => Some(tty.into_raw_mode().expect("Failed to activate raw mode")), - Err(_) => None, - } - } else { - None - }; write!(stdout, "Welcome to Koto v{}\r\n{}", VERSION, PROMPT).unwrap(); stdout.flush().unwrap(); - for c in stdin.keys() { - self.on_keypress(c.unwrap(), &mut stdout, &mut tty); + loop { + if stdout.is_tty() { + terminal::enable_raw_mode()?; + } - if let Some(ref mut tty) = tty { - let (_, cursor_y) = stdout.cursor_pos().unwrap(); + if let Event::Key(key_event) = read()? { + self.on_keypress(key_event, &mut stdout)?; - let prompt = if self.continued_lines.is_empty() { - PROMPT - } else { - CONTINUED - }; + if stdout.is_tty() { + let (_, cursor_y) = cursor::position()?; - write!( - tty, - "{move_cursor}{clear}{prompt}{input}", - move_cursor = cursor::Goto(1, cursor_y), - clear = clear::CurrentLine, - prompt = prompt, - input = self.input - ) - .unwrap(); + let prompt = if self.continued_lines.is_empty() { + PROMPT + } else { + CONTINUED + }; - if let Some(position) = self.cursor { - if position < self.input.len() { - let x_offset = (self.input.len() - position) as u16; - let (cursor_x, cursor_y) = stdout.cursor_pos().unwrap(); - write!(tty, "{}", cursor::Goto(cursor_x - x_offset, cursor_y),).unwrap(); + queue!( + stdout, + cursor::MoveTo(0, cursor_y), + terminal::Clear(ClearType::CurrentLine), + style::Print(prompt), + style::Print(&self.input), + )?; + + if let Some(position) = self.cursor { + if position < self.input.len() { + let x_offset = (self.input.len() - position) as u16; + let (cursor_x, cursor_y) = cursor::position()?; + queue!(stdout, cursor::MoveTo(cursor_x - x_offset, cursor_y))?; + } } + + stdout.flush().unwrap(); } } - - stdout.flush().unwrap(); } } - fn on_keypress(&mut self, key: Key, stdout: &mut Stdout, tty: &mut Option>) - where - T: Write, - { - match key { - Key::Up => { + fn on_keypress(&mut self, event: KeyEvent, stdout: &mut Stdout) -> Result<()> { + match event.code { + KeyCode::Up => { if !self.input_history.is_empty() { let new_position = match self.history_position { Some(position) => { @@ -128,7 +124,7 @@ impl Repl { self.history_position = Some(new_position); } } - Key::Down => { + KeyCode::Down => { self.history_position = match self.history_position { Some(position) => { if position < self.input_history.len() - 1 { @@ -146,7 +142,7 @@ impl Repl { } self.cursor = None; } - Key::Left => match self.cursor { + KeyCode::Left => match self.cursor { Some(position) => { if position > 0 { self.cursor = Some(position - 1); @@ -158,7 +154,7 @@ impl Repl { } } }, - Key::Right => { + KeyCode::Right => { if let Some(position) = self.cursor { if position < self.input.len() - 1 { self.cursor = Some(position + 1); @@ -167,7 +163,7 @@ impl Repl { } } } - Key::Backspace => { + KeyCode::Backspace => { let cursor = self.cursor; match cursor { Some(position) => { @@ -184,26 +180,14 @@ impl Repl { } } } - Key::Char(c) => match c { - '\n' => self.on_enter(stdout, tty), - _ => { - let cursor = self.cursor; - match cursor { - Some(position) => { - self.input.insert(position, c); - self.cursor = Some(position + 1); - } - None => self.input.push(c), - } - } - }, - Key::Ctrl(c) => match c { + KeyCode::Enter => self.on_enter(stdout)?, + KeyCode::Char(c) if event.modifiers.contains(KeyModifiers::CONTROL) => match c { 'c' => { if self.input.is_empty() { write!(stdout, "^C\r\n").unwrap(); stdout.flush().unwrap(); - if let Some(tty) = tty { - tty.suspend_raw_mode().unwrap(); + if stdout.is_tty() { + terminal::disable_raw_mode()?; } std::process::exit(0) } else { @@ -214,27 +198,36 @@ impl Repl { 'd' if self.input.is_empty() => { write!(stdout, "^D\r\n").unwrap(); stdout.flush().unwrap(); - if let Some(tty) = tty { - tty.suspend_raw_mode().unwrap(); + if stdout.is_tty() { + terminal::disable_raw_mode()?; } std::process::exit(0) } _ => {} }, + KeyCode::Char(c) => { + let cursor = self.cursor; + match cursor { + Some(position) => { + self.input.insert(position, c); + self.cursor = Some(position + 1); + } + None => self.input.push(c), + } + } _ => {} } - } - fn on_enter(&mut self, stdout: &mut Stdout, tty: &mut Option>) - where - T: Write, - { - write!(stdout, "\r\n").unwrap(); + Ok(()) + } - if let Some(tty) = tty { - tty.suspend_raw_mode().unwrap(); + fn on_enter(&mut self, stdout: &mut Stdout) -> Result<()> { + if stdout.is_tty() { + terminal::disable_raw_mode()?; } + println!(""); + let mut indent_next_line = false; let input_is_whitespace = self.input.chars().all(char::is_whitespace); @@ -266,7 +259,7 @@ impl Repl { if let Some(help) = self.run_help(&input) { writeln!(stdout, "{}\n", help).unwrap() } else { - self.print_error(stdout, tty, &error) + self.print_error(stdout, &error)?; } } } @@ -279,7 +272,7 @@ impl Repl { } else if let Some(help) = self.run_help(&input) { writeln!(stdout, "{}\n", help).unwrap() } else { - self.print_error(stdout, tty, &e.to_string()); + self.print_error(stdout, &e.to_string())?; self.continued_lines.clear(); } } @@ -297,10 +290,6 @@ impl Repl { } } - if let Some(tty) = tty { - tty.activate_raw_mode().unwrap(); - } - if !input_is_whitespace && (self.input_history.is_empty() || self.input_history.last().unwrap() != &self.input) { @@ -327,6 +316,8 @@ impl Repl { }; self.input = " ".repeat(indent); + + Ok(()) } fn run_help(&mut self, input: &str) -> Option { @@ -348,26 +339,25 @@ impl Repl { help.get_help(search) } - fn print_error(&self, stdout: &mut Stdout, tty: &mut Option>, error: &E) + fn print_error(&self, stdout: &mut Stdout, error: &E) -> Result<()> where - T: Write, E: fmt::Display, { - if let Some(tty) = tty { - write!( - tty, - "{red}error{reset}: {bold}", - red = color::Fg(color::Red), - bold = style::Bold, - reset = style::Reset, + if stdout.is_tty() { + use style::*; + + execute!( + stdout, + SetForegroundColor(Color::DarkRed), + Print("error"), + ResetColor, + Print(": "), + SetAttribute(Attribute::Bold), + Print(format!("{:#}\n\n", error)), + SetAttribute(Attribute::Reset), ) - .unwrap(); - tty.suspend_raw_mode().unwrap(); - println!("{:#}\n", error); - tty.activate_raw_mode().unwrap(); - write!(tty, "{}", style::Reset).unwrap(); } else { - writeln!(stdout, "{:#}", error).unwrap(); + writeln!(stdout, "{:#}", error) } } } diff --git a/src/cli/tests/repl_tests.rs b/src/cli/tests/repl_tests.rs deleted file mode 100644 index 398d61cc1..000000000 --- a/src/cli/tests/repl_tests.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{ - env, - io::Write, - process::{Command, Stdio}, -}; - -fn run_koto_repl_test(inputs_and_expected_outputs: &[(&str, Option<&str>)]) { - let mut process = Command::new(env!("CARGO_BIN_EXE_koto")) - .env("KOTO_FORCE_REPL_MODE", "") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to execute child"); - - let stdin = process.stdin.as_mut().expect("failed to get stdin"); - - for (input, _) in inputs_and_expected_outputs.iter() { - stdin - .write_all(input.as_bytes()) - .expect("Failed to write to stdin"); - stdin.write_all(b"\n").expect("Failed to write to stdin"); - } - - let output = process.wait_with_output().expect("Failed to get output"); - assert!(output.status.success()); - - let stdout = String::from_utf8(output.stdout).expect("Failed to get output"); - let mut output_lines = stdout.lines().skip_while(|line| line != &"ยป "); - - for (_, expected) in inputs_and_expected_outputs.iter() { - output_lines.next(); // Skip empty line - if let Some(expected) = expected { - assert_eq!(output_lines.next().expect("Missing output"), *expected); - output_lines.next(); // Skip empty line - } - } -} - -mod repl_tests { - use super::*; - - #[test] - fn basic_arithmetic() { - run_koto_repl_test(&[("a = 2", Some("2")), ("a + a", Some("4"))]); - } - - #[test] - fn for_loop() { - run_koto_repl_test(&[ - ("for x in 1..=5", None), - (" x", None), - ("", Some("5")), - ("x * x", Some("25")), - ]); - } - - #[test] - fn tuple_assignment() { - run_koto_repl_test(&[("x = 1, 2, 3", Some("(1, 2, 3)")), ("x", Some("(1, 2, 3)"))]); - } - - #[test] - fn import_assert() { - run_koto_repl_test(&[ - ("import test.assert", Some("||")), - ("assert true", Some("()")), - ]); - } -}