diff --git a/Cargo.lock b/Cargo.lock index 2d339ec7da..1396dc6d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "alacritty_terminal" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5ad9e2d4c1f7e17fccec9493eeb4e9c1f00e1167519d3940272b708ed8a069" +dependencies = [ + "base64 0.22.1", + "bitflags 2.5.0", + "home", + "libc", + "log", + "miow", + "parking_lot", + "piper", + "polling", + "regex-automata", + "rustix-openpty", + "serde", + "signal-hook", + "unicode-width", + "vte 0.13.0", + "windows-sys 0.48.0", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -297,6 +321,12 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae037714f313c1353189ead58ef9eec30a8e8dc101b2622d461418fd59e28a9" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.2.0" @@ -888,6 +918,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.8" @@ -1129,6 +1168,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + [[package]] name = "deranged" version = "0.3.11" @@ -2632,6 +2677,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "miow" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "mockito" version = "1.4.0" @@ -2772,6 +2826,7 @@ dependencies = [ name = "nu" version = "0.94.1" dependencies = [ + "alacritty_terminal", "assert_cmd", "crossterm", "ctrlc", @@ -3304,6 +3359,7 @@ dependencies = [ name = "nu-test-support" version = "0.94.1" dependencies = [ + "alacritty_terminal", "nu-glob", "nu-path", "nu-utils", @@ -3981,6 +4037,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -4417,6 +4484,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "pori" version = "0.0.0" @@ -5123,11 +5205,23 @@ checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", + "itoa", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustix-openpty" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12" +dependencies = [ + "errno", + "libc", + "rustix", +] + [[package]] name = "rustversion" version = "1.0.15" @@ -6543,6 +6637,20 @@ dependencies = [ "vte_generate_state_changes", ] +[[package]] +name = "vte" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "log", + "serde", + "utf8parse", + "vte_generate_state_changes", +] + [[package]] name = "vte_generate_state_changes" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index a8576baa95..df1ace819c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ ] [workspace.dependencies] +alacritty_terminal = "0.24" alphanumeric-sort = "1.5" ansi-str = "0.8" anyhow = "1.0.82" @@ -221,6 +222,7 @@ nix = { workspace = true, default-features = false, features = [ nu-test-support = { path = "./crates/nu-test-support", version = "0.94.1" } nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.94.1" } nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.1" } +alacritty_terminal = { workspace = true } assert_cmd = "2.0" dirs-next = { workspace = true } tango-bench = "0.5" @@ -305,4 +307,4 @@ bench = false # Run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` [[bench]] name = "benchmarks" -harness = false \ No newline at end of file +harness = false diff --git a/crates/nu-test-support/Cargo.toml b/crates/nu-test-support/Cargo.toml index e2525d8c78..90bc9faae4 100644 --- a/crates/nu-test-support/Cargo.toml +++ b/crates/nu-test-support/Cargo.toml @@ -18,4 +18,5 @@ nu-utils = { path = "../nu-utils", version = "0.94.1" } num-format = { workspace = true } which = { workspace = true } -tempfile = { workspace = true } \ No newline at end of file +tempfile = { workspace = true } +alacritty_terminal = { workspace = true } diff --git a/crates/nu-test-support/src/lib.rs b/crates/nu-test-support/src/lib.rs index 5319830920..07135e5ba8 100644 --- a/crates/nu-test-support/src/lib.rs +++ b/crates/nu-test-support/src/lib.rs @@ -3,6 +3,7 @@ pub mod fs; pub mod locale_override; pub mod macros; pub mod playground; +pub mod terminal; use std::process::ExitStatus; // Needs to be reexported for `nu!` macro diff --git a/crates/nu-test-support/src/terminal.rs b/crates/nu-test-support/src/terminal.rs new file mode 100644 index 0000000000..852611e2f4 --- /dev/null +++ b/crates/nu-test-support/src/terminal.rs @@ -0,0 +1,126 @@ +//! Helper functions for tests that requires a terminal emulator. + +use alacritty_terminal::{ + event::{Event, EventListener, WindowSize}, + grid::Indexed, + term::{test::TermSize, Config}, + tty::{self, EventedReadWrite, Options, Pty, Shell}, + vte::ansi::{Processor, StdSyncHandler}, + Term, +}; +use std::{ + collections::HashMap, + io::{ErrorKind, Read, Write}, + path::PathBuf, + sync::mpsc, + time::Duration, +}; + +pub struct EventProxy(mpsc::Sender); + +impl EventListener for EventProxy { + fn send_event(&self, event: Event) { + let _ = self.0.send(event); + } +} + +/// Creates a 24x80 terminal with default configurations. Returns the terminal +/// and a `mpsc::Receiver` that receives terminal events. +pub fn default_terminal() -> (Term, mpsc::Receiver) { + let config = Config::default(); + let size = TermSize { + screen_lines: 24, + columns: 80, + }; + let (tx, rx) = mpsc::channel(); + (Term::new(config, &size, EventProxy(tx)), rx) +} + +/// Creates a PTY and connect the slave end to a Nushell process. If `pwd` is +/// None, the Nushell process will inherit PWD from the current process. +pub fn pty_with_nushell(args: Vec, pwd: Option) -> Pty { + let executable = crate::fs::executable_path().to_string_lossy().to_string(); + let options = Options { + shell: Some(Shell::new(executable, args)), + working_directory: pwd, + hold: false, + env: HashMap::new(), + }; + let window_size = WindowSize { + num_lines: 24, + num_cols: 80, + cell_width: 0, + cell_height: 0, + }; + tty::new(&options, window_size, 0).unwrap() +} + +/// Reads from `pty` until no more data is available. Will periodically call +/// `event_handler` to handle terminal events. +pub fn read_to_end( + terminal: &mut Term, + pty: &mut Pty, + events: &mut mpsc::Receiver, + mut event_handler: impl FnMut(&mut Term, &mut Pty, Event), +) { + let mut parser: Processor = Processor::new(); + loop { + // Read from the PTY. + let mut buf = [0; 512]; + match pty.reader().read(&mut buf) { + Ok(n) => { + if n == 0 { + return; + } else { + // Update the terminal state. + for byte in &buf[..n] { + parser.advance(terminal, *byte); + } + + // Handle terminal events. + while let Ok(event) = events.try_recv() { + event_handler(terminal, pty, event); + } + + // Poll again after 100ms. The delay is necessary so that + // the child process can respond to any new data we might + // have sent in the event handler. + std::thread::sleep(Duration::from_millis(100)); + } + } + Err(err) => { + if let ErrorKind::Interrupted = err.kind() { + // retry + } else { + return; + } + } + } + } +} + +/// An event handler that only responds to `Event::PtyWrite`. This is the +/// minimum amount of event handling you need to get Nushell working. +pub fn pty_write_handler(_terminal: &mut Term, pty: &mut Pty, event: Event) { + if let Event::PtyWrite(text) = event { + pty.writer().write_all(text.as_bytes()).unwrap(); + } +} + +/// Extracts the current cursor position. +pub fn extract_cursor(terminal: &Term) -> (usize, usize) { + let cursor = terminal.grid().cursor.point; + (cursor.line.0 as usize, cursor.column.0 as usize) +} + +/// Extracts all visible text, ignoring text styles. +pub fn extract_text(terminal: &Term) -> Vec { + let mut text: Vec = vec![]; + for Indexed { point, cell } in terminal.grid().display_iter() { + if point.column == 0 { + text.push(String::new()); + } + text.last_mut().unwrap().push(cell.c); + } + text +}