diff --git a/Cargo.lock b/Cargo.lock index a35b5db5..c25013bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "alacritty-test" +version = "0.1.0" +source = "git+https://github.com/YizhePKU/alacritty-test.git?rev=6c4df50#6c4df50be8b94182928895df2b6fb73ca0d6cf73" +dependencies = [ + "alacritty_terminal", + "polling", + "unicode-width-16", +] + +[[package]] +name = "alacritty_terminal" +version = "0.24.1-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef001fb819ec89184604b7f073e1cb5e5df57666e7fcfc7f8e229116f866a89e" +dependencies = [ + "base64", + "bitflags 2.5.0", + "home", + "libc", + "log", + "miow", + "parking_lot", + "piper", + "polling", + "regex-automata", + "rustix-openpty", + "serde", + "signal-hook", + "unicode-width-16", + "vte 0.13.0", + "windows-sys 0.52.0", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -61,12 +95,24 @@ dependencies = [ "x11rb", ] +[[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.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -134,6 +180,15 @@ dependencies = [ "error-code", ] +[[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 = "core-foundation-sys" version = "0.8.6" @@ -222,6 +277,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + [[package]] name = "derive-new" version = "0.6.0" @@ -323,6 +384,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "gethostname" version = "0.4.3" @@ -370,6 +437,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "home" version = "0.5.9" @@ -512,13 +585,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "log", "wasi", "windows-sys 0.52.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 = "nix" version = "0.28.0" @@ -637,12 +719,44 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -691,8 +805,9 @@ dependencies = [ [[package]] name = "reedline" -version = "0.35.0" +version = "0.36.0" dependencies = [ + "alacritty-test", "arboard", "chrono", "crossbeam", @@ -808,11 +923,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.14" @@ -916,7 +1043,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" dependencies = [ - "vte", + "vte 0.11.1", ] [[package]] @@ -981,6 +1108,22 @@ dependencies = [ "syn", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + [[package]] name = "tree_magic_mini" version = "3.1.4" @@ -1013,6 +1156,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-width-16" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eba15036aa0f5bf8ed6cd12a624ddb61fd50b0779b1c05d89b663bcaed7b5c2" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1041,6 +1190,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 12544dcf..b747c837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ unicode-segmentation = "1.9.0" unicode-width = "0.1.9" [dev-dependencies] +alacritty-test = { git = "https://github.com/YizhePKU/alacritty-test.git", rev = "6c4df50" } gethostname = "0.4.0" pretty_assertions = "1.4.0" rstest = { version = "0.18.0", default-features = false } diff --git a/src/bin/testbin.rs b/src/bin/testbin.rs new file mode 100644 index 00000000..49374f44 --- /dev/null +++ b/src/bin/testbin.rs @@ -0,0 +1,25 @@ +//! Test binary for UX testing. + +use reedline::{DefaultPrompt, DefaultPromptSegment, Reedline, Signal}; +use std::io; + +fn main() -> io::Result<()> { + let mut line_editor = Reedline::create(); + let prompt = DefaultPrompt::new( + DefaultPromptSegment::Basic("Reedline".to_string()), + DefaultPromptSegment::Empty, + ); + + loop { + let sig = line_editor.read_line(&prompt)?; + match sig { + Signal::Success(buffer) => { + println!("We processed: {buffer}"); + } + Signal::CtrlD | Signal::CtrlC => { + println!("\nAborted!"); + break Ok(()); + } + } + } +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 00000000..0cb682cd --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,198 @@ +use alacritty_test::{extract_text, pty_spawn, PtyExt, Terminal}; +use std::time::Duration; + +/// Return the absolute path to the test binary. +fn testbin() -> String { + if let Ok(nextest) = std::env::var("NEXTEST") { + if nextest == "1" { + return std::env::var("NEXTEST_BIN_EXE_testbin").unwrap(); + } + } + + #[cfg(not(windows))] + let path = "target/debug/testbin"; + #[cfg(windows)] + let path = "target/debug/testbin.exe"; + + std::fs::canonicalize(path) + .unwrap() + .to_string_lossy() + .to_string() +} + +/// Test if Reedline prints the prompt at startup. +#[test] +fn prints_prompt() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..11], "Reedline〉"); + + Ok(()) +} + +/// Test if Reedline echos back input when the user presses Enter. +#[test] +fn echos_input() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"Hello World!\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + + assert_eq!(&text[0][..23], "Reedline〉Hello World!"); + assert_eq!(&text[1][0..26], "We processed: Hello World!"); + + Ok(()) +} + +/// Test if Reedline handles backspace correctly. +#[test] +fn backspace() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"Hello World")?; + pty.write_all(b"\x7f\x7f\x7f\x7f\x7f")?; + pty.write_all(b"Bread!\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..23], "Reedline〉Hello Bread!"); + assert_eq!(&text[1][0..26], "We processed: Hello Bread!"); + + Ok(()) +} + +/// Test if Reedline supports history via up/down arrow. +#[test] +fn history() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"Hello World!\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + pty.write_all(b"Goodbye!\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + // arrow up + pty.write_all(b"\x1b[A")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[4][..19], "Reedline〉Goodbye!"); + + // press Enter to execute it + pty.write_all(b"\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[5][..22], "We processed: Goodbye!"); + + // arrow up twice + pty.write_all(b"\x1b[A\x1b[A")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[6][..23], "Reedline〉Hello World!"); + + // arrow down twice + pty.write_all(b"\x1b[B\x1b[B")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[6][..23], "Reedline〉 "); + + // type "Hell" then arrow up + pty.write_all(b"Hell\x1b[A")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[6][..23], "Reedline〉Hello World!"); + + // TODO: not sure how reverse search works in Reedline + + Ok(()) +} + +/// Test if Reedline supports ctrl-b/ctrl-f/ctrl-left/ctrl-right style movement. +#[test] +fn word_movement() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"foo bar baz")?; + + // Ctrl-left twice, Ctrl-right once, Ctrl-b twice, Ctrl-f once. + pty.write_all(b"\x1b[1;5D\x1b[1;5D")?; + pty.write_all(b"\x1b[1;5C")?; + pty.write_all(b"\x02\x02")?; + pty.write_all(b"\x06")?; + + // Insert some more text, then press enter. + pty.write_all(b"za\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..24], "Reedline〉foo bazar baz"); + assert_eq!(&text[1][..27], "We processed: foo bazar baz"); + + Ok(()) +} + +/// Test if Ctrl-l clears the screen while keeping current entry. +#[test] +fn clear_screen() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"Hello World!\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"Hello again!\x0c\r")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..23], "Reedline〉Hello again!"); + + Ok(()) +} + +/// Test if Reedline supports common Emacs keybindings. +#[test] +fn emacs_keybinds() -> std::io::Result<()> { + let mut pty = pty_spawn(&testbin(), vec![], None)?; + let mut terminal = Terminal::new(); + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + + pty.write_all(b"Hello World!")?; + + // undo with Ctrl-z + pty.write_all(b"\x1a")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..23], "Reedline〉Hello "); + + // redo with Ctrl-g + pty.write_all(b"\x07")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..23], "Reedline〉Hello World!"); + + // delete "World" with alt+left, alt+backspace + pty.write_all(b"\x1b[1;3D\x1b\x7f")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..23], "Reedline〉Hello ! "); + + // make "Hello" ALL CAPS with alt+b, alt+u + pty.write_all(b"\x1bb\x1bu")?; + terminal.read_from_pty(&mut pty, Some(Duration::from_millis(50)))?; + let text = extract_text(terminal.inner()); + assert_eq!(&text[0][..23], "Reedline〉HELLO ! "); + + Ok(()) +}