diff --git a/Cargo.lock b/Cargo.lock index 787e1df..c7e4220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -519,6 +530,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "env_logger" version = "0.10.0" @@ -568,12 +585,33 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "fastrand" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "flate2" version = "1.0.26" @@ -745,6 +783,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "http" version = "0.2.9" @@ -1012,6 +1059,7 @@ dependencies = [ "rbx_xml", "regex", "reqwest", + "rustyline", "serde", "serde_json", "serde_yaml", @@ -1111,6 +1159,27 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "static_assertions", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1314,6 +1383,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -1674,6 +1753,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustyline" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +dependencies = [ + "bitflags 2.3.3", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1939,6 +2041,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strsim" version = "0.10.0" @@ -2289,6 +2397,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" diff --git a/Cargo.toml b/Cargo.toml index 03e06cb..0bc1d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ regex = { optional = true, version = "1.7", default-features = false, features = "std", "unicode-perl", ] } +rustyline = "12.0.0" ### ROBLOX diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0a97c6f..a40e70c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -10,6 +10,7 @@ use tokio::{ }; pub(crate) mod gen; +pub(crate) mod repl; pub(crate) mod setup; pub(crate) mod utils; @@ -146,12 +147,14 @@ impl Cli { if generate_file_requested { return Ok(ExitCode::SUCCESS); } + // HACK: We know that we didn't get any arguments here but since // script_path is optional clap will not error on its own, to fix - // we will duplicate the cli command and make arguments required, - // which will then fail and print out the normal help message - let cmd = Cli::command(); - cmd.arg_required_else_help(true).get_matches(); + // we will duplicate the CLI command and fetch the version of + // lune to display + let exit_code_status = repl::show_interface(Cli::command()).await; + + return exit_code_status; } // Figure out if we should read from stdin or from a file, // reading from stdin is marked by passing a single "-" diff --git a/src/cli/repl.rs b/src/cli/repl.rs new file mode 100644 index 0000000..3991592 --- /dev/null +++ b/src/cli/repl.rs @@ -0,0 +1,101 @@ +use std::{path::PathBuf, process::ExitCode}; + +use anyhow::{Error, Result}; +use clap::Command; +use directories::UserDirs; +use lune::Lune; +use rustyline::{error::ReadlineError, DefaultEditor}; + +#[derive(PartialEq)] +enum PromptState { + Regular, + Continuation, +} + +// Isn't dependency injection plain awesome?! +pub async fn show_interface(cmd: Command) -> Result { + let lune_version = cmd.get_version(); + + // The version is mandatory and will always exist + println!("Lune v{}", lune_version.unwrap()); + + let lune_instance = Lune::new(); + + let mut repl = DefaultEditor::new()?; + let history_file_path: &PathBuf = &UserDirs::new() + .ok_or(Error::msg("cannot find user home directory"))? + .home_dir() + .join(".lune_history"); + + if !history_file_path.exists() { + std::fs::write(&history_file_path, String::new())?; + } + + repl.load_history(history_file_path)?; + + let mut interrupt_counter = 0u32; + let mut prompt_state: PromptState = PromptState::Regular; + let mut source_code = String::new(); + + loop { + let prompt = match prompt_state { + PromptState::Regular => "> ", + PromptState::Continuation => ">> ", + }; + + match repl.readline(prompt) { + Ok(code) => { + if prompt_state == PromptState::Continuation { + source_code.push_str(&code); + } else if prompt_state == PromptState::Regular { + source_code = code.clone(); + } + + repl.add_history_entry(code.as_str())?; + + // If source code eval was requested, we reset the counter + interrupt_counter = 0; + } + + Err(ReadlineError::Interrupted) => { + // HACK: We actually want the user to do ^C twice to exit, + // but the user would need to ^C one more time even after + // the check passes, so we check for 1 instead of 2 + if interrupt_counter != 1 { + println!("Interrupt: ^C again to exit"); + + // Increment the counter + interrupt_counter += 1; + } else { + repl.save_history(history_file_path)?; + break; + } + } + Err(ReadlineError::Eof) => { + repl.save_history(history_file_path)?; + break; + } + Err(err) => { + eprintln!("REPL ERROR: {err}"); + return Ok(ExitCode::FAILURE); + } + }; + + let eval_result = lune_instance.run("REPL", source_code.clone()).await; + + match eval_result { + Ok(_) => prompt_state = PromptState::Regular, + + Err(err) => { + if err.is_incomplete_input() { + prompt_state = PromptState::Continuation; + source_code.push('\n') + } else { + eprintln!("{err}"); + } + } + }; + } + + Ok(ExitCode::SUCCESS) +}