diff --git a/CHANGELOG.md b/CHANGELOG.md index f6675e0..d3b6e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Next +- Add REPL to the `cpc` CLI - Add support for numbers with thousands separators `_`, ` ` and ` ` (thin space) - Format numbers with thousands separator diff --git a/Cargo.lock b/Cargo.lock index b032940..6eb2e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "cmake" version = "0.1.58" @@ -167,6 +176,7 @@ dependencies = [ "js-sys", "regex", "reqwest", + "rustyline", "serde", "serde-wasm-bindgen", "serde_json", @@ -203,12 +213,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "fastnum" version = "0.7.5" @@ -360,6 +382,15 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.2" @@ -711,6 +742,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -874,6 +926,16 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radix_trie" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.9.4" @@ -1093,6 +1155,27 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustyline" +version = "18.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.61.2", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1515,6 +1598,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -1539,6 +1628,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 3c02d03..92ca2f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ unicode-segmentation = "1.13" web-time = "1.1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +rustyline = "18.0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] reqwest = { version = "0.13", features = ["blocking", "json"] } diff --git a/README.md b/README.md index b857a28..d0182b9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,13 @@ To install it manually, grab the appropriate binary from the [GitHub Releases pa cpc '2h/3 to min' ``` +## CLI REPL Usage +``` +cpc +> sin(1) +≈ 0.84147098480789650665250232163029899962 +``` + ## Examples ``` 3 + 4 * 2 diff --git a/src/main.rs b/src/main.rs index 1510bb0..e341e14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use cpc::eval; use std::env; -use std::io::{self, BufRead, Write}; +use std::path::PathBuf; use std::process::exit; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -22,46 +22,135 @@ fn get_args() -> env::Args { args } -/// Interactive REPL, used when no expression argument is passed -fn repl(verbose: bool) { - let stdin = io::stdin(); - let mut stdout = io::stdout(); +fn history_path() -> Option { + #[cfg(target_os = "windows")] + { + env::var("APPDATA") + .ok() + .map(|p| PathBuf::from(p).join("cpc").join("history")) + } + #[cfg(not(target_os = "windows"))] + { + env::var("HOME").ok().map(|p| { + PathBuf::from(p) + .join(".local") + .join("share") + .join("cpc") + .join("history") + }) + } +} - loop { - print!("> "); - if stdout.flush().is_err() { - break; - } +// rustyline helper for live next-line results - let mut line = String::new(); - match stdin.lock().read_line(&mut line) { - Ok(0) => break, // EOF (e.g. Ctrl-D) - Ok(_) => {} - Err(_) => break, - } +struct CpcHint(String); + +impl rustyline::hint::Hint for CpcHint { + fn display(&self) -> &str { + self.0.as_str() + } + fn completion(&self) -> Option<&str> { + None + } +} + +struct CpcHelper; + +impl rustyline::hint::Hinter for CpcHelper { + type Hint = CpcHint; + fn hint(&self, line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { let expression = line.trim(); if expression.is_empty() { - continue; + return None; + } + match eval(expression, true, false) { + Ok(answer) => Some(CpcHint(format!("\n{answer}"))), + Err(_) => None, + } + } +} + +impl rustyline::highlight::Highlighter for CpcHelper {} + +impl rustyline::validate::Validator for CpcHelper {} + +impl rustyline::completion::Completer for CpcHelper { + type Candidate = rustyline::completion::Pair; + + fn complete( + &self, + _line: &str, + _pos: usize, + _ctx: &rustyline::Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + Ok((0, Vec::new())) + } +} + +impl rustyline::Helper for CpcHelper {} + +// ── REPL ─────────────────────────────────────────────────────────────────── + +fn repl(verbose: bool) { + use rustyline::{Editor, config::Builder, history::FileHistory}; + + let config = Builder::new() + .history_ignore_space(true) + .auto_add_history(true) + .build(); + + let mut rl = Editor::::with_config(config) + .expect("Failed to create rustyline editor"); + + rl.set_helper(Some(CpcHelper)); + + let hist_path = history_path(); + if let Some(ref path) = hist_path { + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); } + let _ = rl.load_history(path); + } + + loop { + match rl.readline("> ") { + Ok(line) => { + if let Some(ref path) = hist_path { + let _ = rl.save_history(path); + } - match eval(expression, true, verbose) { - Ok(answer) => { - if !verbose { - println!("{answer}"); + let expression = line.trim(); + if expression.is_empty() { + continue; + } + + match eval(expression, true, verbose) { + Ok(answer) => { + if !verbose { + println!("{answer}"); + } + } + Err(e) => { + eprintln!("{e}"); + } } } - Err(e) => { - eprintln!("{e}"); + Err(rustyline::error::ReadlineError::Interrupted) => { + break; + } + Err(rustyline::error::ReadlineError::Eof) => { + println!(); + break; + } + Err(err) => { + eprintln!("Error: {err}"); + break; } - } - if stdout.flush().is_err() { - break; } } } -/// CLI interface fn main() { // parse these first so they work if there are unexpected args for arg in get_args() {