diff --git a/Cargo.lock b/Cargo.lock index a63dbbc..e2de11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -273,12 +282,113 @@ dependencies = [ "once_cell", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.14.2" @@ -448,6 +558,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.69" @@ -486,6 +602,13 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "ratatui-macros" +version = "0.3.0" +dependencies = [ + "ratatui", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -495,12 +618,85 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -519,6 +715,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "sharded-slab" version = "0.1.7" @@ -558,6 +760,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.11.2" @@ -680,6 +891,8 @@ dependencies = [ "indoc", "itertools", "ratatui", + "ratatui-macros", + "rstest", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e9ec08e..0ff5654 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ edition = "2021" crossterm = "0.27.0" itertools = "0.12.1" ratatui = "0.26.2" +ratatui-macros = { version = "0.3.0", path = "../ratatui-macros" } +rstest = "0.19.0" [dev-dependencies] clap = { version = "4.5.4", features = ["derive"] } diff --git a/src/text_prompt.rs b/src/text_prompt.rs index e6d0f69..c499270 100644 --- a/src/text_prompt.rs +++ b/src/text_prompt.rs @@ -204,15 +204,12 @@ where #[cfg(test)] mod tests { use crate::Status; + use ratatui_macros::line; + use rstest::{fixture, rstest}; use super::*; use ratatui::{assert_buffer_eq, backend::TestBackend, widgets::Borders}; - // TODO make these configurable - const PENDING_STYLE: Style = Style::new().fg(Color::Cyan); - const COMPLETE_STYLE: Style = Style::new().fg(Color::Green); - const ABORTED_STYLE: Style = Style::new().fg(Color::Red); - #[test] fn new() { const PROMPT: TextPrompt<'_> = TextPrompt::new(Cow::Borrowed("Enter your name")); @@ -234,6 +231,7 @@ mod tests { let prompt = TextPrompt::from("Enter your name"); assert_eq!(prompt.message, "Enter your name"); } + #[test] fn render() { let prompt = TextPrompt::from("prompt"); @@ -242,11 +240,8 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["? prompt › "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line!["?".cyan(), " ", "prompt".bold(), " › ".cyan().dim(), " ",]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); assert_eq!(state.cursor(), (11, 0)); } @@ -258,11 +253,8 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["? 🔍 › "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 1, 1), Style::new().bold()); - expected.set_style(Rect::new(4, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line!["?".cyan(), " ", "🔍".bold(), " › ".cyan().dim(), " "]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); assert_eq!(state.cursor(), (7, 0)); } @@ -274,11 +266,14 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["✔ prompt › "]); - expected.set_style(Rect::new(0, 0, 1, 1), COMPLETE_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line![ + "✔".green(), + " ", + "prompt".bold(), + " › ".cyan().dim(), + " " + ]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); } #[test] @@ -289,11 +284,8 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["✘ prompt › "]); - expected.set_style(Rect::new(0, 0, 1, 1), ABORTED_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line!["✘".red(), " ", "prompt".bold(), " › ".cyan().dim(), " "]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); } #[test] @@ -304,11 +296,14 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["? prompt › value "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line![ + "?".cyan(), + " ", + "prompt".bold(), + " › ".cyan().dim(), + "value ".to_string() + ]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); } #[test] @@ -325,9 +320,9 @@ mod tests { "│? prompt › │", "└─────────────┘", ]); - expected.set_style(Rect::new(1, 1, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(3, 1, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(9, 1, 3, 1), Style::new().cyan().dim()); + expected.set_style(Rect::new(1, 1, 1, 1), Color::Cyan); + expected.set_style(Rect::new(3, 1, 6, 1), Modifier::BOLD); + expected.set_style(Rect::new(9, 1, 3, 1), (Color::Cyan, Modifier::DIM)); assert_buffer_eq!(buffer, expected); } @@ -339,11 +334,14 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["? prompt › ***** "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line![ + "?".cyan(), + " ", + "prompt".bold(), + " › ".cyan().dim(), + "***** ".to_string() + ]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); } #[test] @@ -354,110 +352,106 @@ mod tests { prompt.render(buffer.area, &mut buffer, &mut state); - let mut expected = Buffer::with_lines(vec!["? prompt › "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - assert_buffer_eq!(buffer, expected); + let line = line![ + "?".cyan(), + " ", + "prompt".bold(), + " › ".cyan().dim(), + " ".to_string() + ]; + assert_buffer_eq!(buffer, Buffer::with_lines([line])); } - #[test] - fn draw_no_wrap() -> Result<(), Box<dyn std::error::Error>> { - let prompt = TextPrompt::from("prompt"); - let mut state = TextState::new().with_value("hello"); - let backend = TestBackend::new(17, 2); - let mut terminal = Terminal::new(backend)?; + #[fixture] + fn terminal() -> Terminal<TestBackend> { + Terminal::new(TestBackend::new(17, 2)).unwrap() + } - let mut expected = Buffer::with_lines(vec!["? prompt › hello ", " "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); + type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; + #[rstest] + fn draw_not_focused<'a>(mut terminal: Terminal<impl Backend>) -> Result<()> { + let prompt = TextPrompt::from("prompt"); + let mut state = TextState::new().with_value("hello"); // The cursor is not changed when the prompt is not focused. - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); + let _ = terminal.draw(|frame| prompt.draw(frame, frame.size(), &mut state))?; assert_eq!(state.cursor(), (11, 0)); assert_eq!(terminal.backend_mut().get_cursor().unwrap(), (0, 0)); + Ok(()) + } + #[rstest] + fn draw_focused<'a>(mut terminal: Terminal<impl Backend>) -> Result<()> { + let prompt = TextPrompt::from("prompt"); + let mut state = TextState::new().with_value("hello"); // The cursor is changed when the prompt is focused. state.focus(); - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); + let _ = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; assert_eq!(state.cursor(), (11, 0)); assert_eq!(terminal.backend_mut().get_cursor().unwrap(), (11, 0)); + Ok(()) + } + #[rstest] + #[case::position_0(0, (11, 0))] // start of value + #[case::position_3(2, (13, 0))] // middle of value + #[case::position_4(4, (15, 0))] // last character of value + #[case::position_5(5, (16, 0))] // one character beyond the value + #[case::position_6(6, (0, 1))] // FIXME: should not go beyond the value + #[case::position_7(7, (1, 1))] // FIXME: should not go beyond the value + #[case::position_22(22, (16, 1))] // FIXME: should not go beyond the value + #[case::position_99(99, (16, 1))] // FIXME: should not go beyond the value + fn draw_unwrapped_position<'a>( + #[case] position: usize, + #[case] expected_cursor: (u16, u16), + mut terminal: Terminal<impl Backend>, + ) -> Result<()> { + let prompt = TextPrompt::from("prompt"); + let mut state = TextState::new().with_value("hello"); + // expected: "? prompt › hello " + // " " + // position: 012345 + // cursor: 01234567890123456 // The cursor is changed when the prompt is focused and the position is changed. - *state.position_mut() = 3; - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - assert_eq!(state.cursor(), (14, 0)); - assert_eq!(terminal.get_cursor()?, (14, 0)); - - // The cursor does not go beyond the end of the value. - *state.position_mut() = 100; - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - // FIXME (I think these both should be 16, 0 probably) - assert_eq!(state.cursor(), (16, 1)); - assert_eq!(terminal.get_cursor()?, (16, 1)); + state.focus(); + *state.position_mut() = position; + let _ = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; + assert_eq!(state.cursor(), expected_cursor); + assert_eq!(terminal.get_cursor()?, expected_cursor); Ok(()) } - #[test] - #[allow(clippy::cognitive_complexity)] - fn draw_wrapped() -> Result<(), Box<dyn std::error::Error>> { + #[rstest] + #[case::position_0(0, (11, 0))] // start of value + #[case::position_1(3, (14, 0))] // middle of value + #[case::position_5(5, (16, 0))] // end of line + #[case::position_6(6, (0, 1))] // first character of the second line + #[case::position_7(7, (1, 1))] // second character of the second line + #[case::position_11(10, (4, 1))] // last character of the value + #[case::position_12(12, (6, 1))] // one character beyond the value + #[case::position_13(13, (7, 1))] // FIXME: should not go beyond the value + #[case::position_22(22, (16, 1))] // FIXME: should not go beyond the value + #[case::position_99(99, (16, 1))] // FIXME: should not go beyond the value + fn draw_wrapped_position<'a>( + #[case] position: usize, + #[case] expected_cursor: (u16, u16), + mut terminal: Terminal<impl Backend>, + ) -> Result<()> { let prompt = TextPrompt::from("prompt"); let mut state = TextState::new().with_value("hello world"); - let backend = TestBackend::new(17, 2); - let mut terminal = Terminal::new(backend)?; - - let mut expected = Buffer::with_lines(vec!["? prompt › hello ", "world "]); - expected.set_style(Rect::new(0, 0, 1, 1), PENDING_STYLE); - expected.set_style(Rect::new(2, 0, 6, 1), Style::new().bold()); - expected.set_style(Rect::new(8, 0, 3, 1), Style::new().cyan().dim()); - - // The cursor is not changed when the prompt is not focused. - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - assert_eq!(state.cursor(), (11, 0)); - assert_eq!(terminal.get_cursor()?, (0, 0)); - - // The cursor is changed when the prompt is focused. - state.focus(); - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - assert_eq!(state.cursor(), (11, 0)); - assert_eq!(terminal.get_cursor()?, (11, 0)); - + // line 1: "? prompt › hello " + // position: 012345 + // cursor: 01234567890123456 + // line 2: "world " + // position: 678901 + // cursor: 01234567890123456 // The cursor is changed when the prompt is focused and the position is changed. - *state.position_mut() = 3; - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - assert_eq!(state.cursor(), (14, 0)); - assert_eq!(terminal.get_cursor()?, (14, 0)); - - // The cursor wraps to the first column of the next line - *state.position_mut() = 6; - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - assert_eq!(state.cursor(), (0, 1)); - assert_eq!(terminal.get_cursor()?, (0, 1)); - - // The cursor continues to cover the second line - *state.position_mut() = 7; - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - assert_eq!(state.cursor(), (1, 1)); - assert_eq!(terminal.get_cursor()?, (1, 1)); - - // The cursor does not go beyond the end of the value. - *state.position_mut() = 100; - let frame = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; - assert_buffer_eq!(*frame.buffer, expected); - // FIXME (I think these both should be (5, 1) probably) - assert_eq!(state.cursor(), (16, 1)); - assert_eq!(terminal.get_cursor()?, (16, 1)); + state.focus(); + *state.position_mut() = position; + let _ = terminal.draw(|frame| prompt.clone().draw(frame, frame.size(), &mut state))?; + assert_eq!(state.cursor(), expected_cursor); + assert_eq!(terminal.get_cursor()?, expected_cursor); Ok(()) }