diff --git a/Cargo.lock b/Cargo.lock index c56d975..03215f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,10 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "anes" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.2.1" @@ -29,6 +34,14 @@ dependencies = [ "crossterm 0.12.1 (git+https://github.com/crossterm-rs/crossterm)", ] +[[package]] +name = "crossterm-interactive-test" +version = "0.0.1" +dependencies = [ + "anes 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "crossterm 0.12.1 (git+https://github.com/crossterm-rs/crossterm)", +] + [[package]] name = "crossterm_winapi" version = "0.3.0" @@ -222,6 +235,7 @@ dependencies = [ ] [metadata] +"checksum anes 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e5e04b263cb24d859f06563beb27213c7bbfff2c61513763578eba297f4314e4" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum crossterm 0.12.1 (git+https://github.com/crossterm-rs/crossterm)" = "" diff --git a/Cargo.toml b/Cargo.toml index 4cdcb16..d713964 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "examples", "first-depth-search", - "snake" + "snake", + "interactive-test" ] diff --git a/README.md b/README.md index d3d6469..73cae13 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ $ cargo run --bin alternate_screen ## License -This project is licensed under the MIT License - see the [LICENSE.md](./LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details. [s2]: https://img.shields.io/badge/license-MIT-blue.svg -[l2]: ./LICENSE +[l2]: LICENSE [s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord [l5]: https://discord.gg/K4nyTDB diff --git a/examples/src/bin/command_bar.rs b/examples/src/bin/command_bar.rs index df85488..fecc822 100644 --- a/examples/src/bin/command_bar.rs +++ b/examples/src/bin/command_bar.rs @@ -5,7 +5,7 @@ use std::{thread, time}; use crossterm::cursor::{Hide, Show}; use crossterm::{ cursor::MoveTo, - input::{input, InputEvent, KeyEvent}, + event::{input, InputEvent, KeyEvent}, screen::RawScreen, terminal::{self, Clear, ClearType}, ExecutableCommand, Output, Result, diff --git a/examples/src/bin/foo.rs b/examples/src/bin/foo.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/src/bin/foo.rs @@ -0,0 +1 @@ + diff --git a/examples/src/bin/input.rs b/examples/src/bin/input.rs index b99b2c4..bc91c60 100644 --- a/examples/src/bin/input.rs +++ b/examples/src/bin/input.rs @@ -1,4 +1,4 @@ -use crossterm::input::input; +use crossterm::event::input; fn read_char() { let input = input(); diff --git a/examples/src/bin/key_events.rs b/examples/src/bin/key_events.rs index 4ba366d..4674f7b 100644 --- a/examples/src/bin/key_events.rs +++ b/examples/src/bin/key_events.rs @@ -3,7 +3,7 @@ use std::{thread, time::Duration}; use crossterm::{ - input::{input, InputEvent, KeyEvent, MouseButton, MouseEvent}, + event::{input, InputEvent, KeyEvent, MouseButton, MouseEvent}, screen::RawScreen, Result, }; diff --git a/examples/src/bin/stderr.rs b/examples/src/bin/stderr.rs index ba37835..8e35071 100644 --- a/examples/src/bin/stderr.rs +++ b/examples/src/bin/stderr.rs @@ -10,7 +10,7 @@ use std::io::{stderr, Write}; use crossterm::{ cursor::{Hide, MoveTo, Show}, - input::input, + event::input, queue, screen::{EnterAlternateScreen, LeaveAlternateScreen, RawScreen}, Output, Result, diff --git a/interactive-test/Cargo.toml b/interactive-test/Cargo.toml new file mode 100644 index 0000000..e03e5a9 --- /dev/null +++ b/interactive-test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crossterm-interactive-test" +version = "0.0.1" +authors = ["T. Post", "Robert Vojta "] +edition = "2018" +description = "Interactive test for crossterm." +license = "MIT" +exclude = ["target", "Cargo.lock"] +readme = "README.md" +publish = false + +[dependencies] +crossterm = { git = "https://github.com/crossterm-rs/crossterm" } +anes = "0.1.4" # needed till `MoveCursorToNextLine`, `MoveCursorToColumn`, `MoveCursorToPreviousLine` are implemented for crossterm. \ No newline at end of file diff --git a/interactive-test/src/macros.rs b/interactive-test/src/macros.rs new file mode 100644 index 0000000..02ce9e2 --- /dev/null +++ b/interactive-test/src/macros.rs @@ -0,0 +1,29 @@ +macro_rules! run_tests { + ( + $dst:expr, + $( + $testfn:ident + ),* + $(,)? + ) => { + use crossterm::{queue, style, terminal, cursor}; + $( + queue!( + $dst, + style::ResetColor, + terminal::Clear(terminal::ClearType::All), + cursor::MoveTo(1, 1), + cursor::Show, + cursor::EnableBlinking + )?; + + $testfn($dst)?; + + match $crate::read_char() { + Ok('q') => return Ok(()), + Err(e) => return Err(e), + _ => { }, + }; + )* + } +} diff --git a/interactive-test/src/main.rs b/interactive-test/src/main.rs new file mode 100644 index 0000000..4da4d37 --- /dev/null +++ b/interactive-test/src/main.rs @@ -0,0 +1,139 @@ +#![allow(clippy::cognitive_complexity)] + +use std::io::{self, Write}; + +use crate::event::KeyEvent; +use crossterm::event::KeyCode; +pub use crossterm::{ + cursor, + event::{self, Event}, + execute, queue, screen, style, + terminal::{self, ClearType}, + Command, Result, +}; + +#[macro_use] +mod macros; +mod test; + +struct MoveCursorToNextLine(u16); + +impl Command for MoveCursorToNextLine { + type AnsiType = String; + + fn ansi_code(&self) -> Self::AnsiType { + format!("{}", anes::MoveCursorToNextLine(self.0)) + } + + fn execute_winapi(&self) -> Result<()> { + unimplemented!() + } +} + +struct MoveCursorToPreviousLine(u16); + +impl Command for MoveCursorToPreviousLine { + type AnsiType = String; + + fn ansi_code(&self) -> Self::AnsiType { + format!("{}", anes::MoveCursorToPreviousLine(self.0)) + } + + fn execute_winapi(&self) -> Result<()> { + unimplemented!() + } +} + +struct MoveCursorToColumn(u16); + +impl Command for MoveCursorToColumn { + type AnsiType = String; + + fn ansi_code(&self) -> Self::AnsiType { + format!("{}", anes::MoveCursorToColumn(self.0)) + } + + fn execute_winapi(&self) -> Result<()> { + unimplemented!() + } +} + +const MENU: &str = r#"Crossterm interactive test + +Controls: + + - 'q' - quit interactive test (or return to this menu) + - any other key - continue with next step + +Available tests: + +1. cursor +2. color (foreground, background) +3. attributes (bold, italic, ...) +4. input + +Select test to run ('1', '2', ...) or hit 'q' to quit. +"#; + +fn run(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!(w, screen::EnterAlternateScreen)?; + + let _raw = screen::RawScreen::into_raw_mode()?; + + loop { + queue!( + w, + style::ResetColor, + terminal::Clear(ClearType::All), + cursor::Hide, + cursor::MoveTo(1, 1) + )?; + + for line in MENU.split('\n') { + queue!(w, style::Print(line), MoveCursorToNextLine(1))?; + } + + w.flush()?; + + match read_char()? { + '1' => test::cursor::run(w)?, + '2' => test::color::run(w)?, + '3' => test::attribute::run(w)?, + '4' => test::event::run(w)?, + 'q' => break, + _ => {} + }; + } + + execute!( + w, + style::ResetColor, + cursor::Show, + screen::LeaveAlternateScreen + )?; + Ok(()) +} + +pub fn read_char() -> Result { + loop { + if let Ok(Event::Key(KeyEvent { + code: KeyCode::Char(c), + .. + })) = event::read() + { + return Ok(c); + } + } +} + +pub fn buffer_size() -> Result<(u16, u16)> { + terminal::size() +} + +fn main() -> Result<()> { + let mut stderr = io::stdout(); + run(&mut stderr) +} diff --git a/interactive-test/src/test.rs b/interactive-test/src/test.rs new file mode 100644 index 0000000..89f1140 --- /dev/null +++ b/interactive-test/src/test.rs @@ -0,0 +1,4 @@ +pub mod attribute; +pub mod color; +pub mod cursor; +pub mod event; diff --git a/interactive-test/src/test/attribute.rs b/interactive-test/src/test/attribute.rs new file mode 100644 index 0000000..5cc8e49 --- /dev/null +++ b/interactive-test/src/test/attribute.rs @@ -0,0 +1,52 @@ +#![allow(clippy::cognitive_complexity)] + +use crate::{MoveCursorToNextLine, Result}; +use crossterm::{queue, style}; +use std::io::Write; + +const ATTRIBUTES: [(style::Attribute, style::Attribute); 6] = [ + (style::Attribute::Bold, style::Attribute::NoBold), + (style::Attribute::Italic, style::Attribute::NoItalic), + (style::Attribute::Underlined, style::Attribute::NoUnderline), + (style::Attribute::Reverse, style::Attribute::NoReverse), + ( + style::Attribute::CrossedOut, + style::Attribute::NotCrossedOut, + ), + (style::Attribute::SlowBlink, style::Attribute::NoBlink), +]; + +fn test_set_display_attributes(w: &mut W) -> Result<()> +where + W: Write, +{ + queue!( + w, + style::Print("Display attributes"), + MoveCursorToNextLine(2) + )?; + + for (on, off) in &ATTRIBUTES { + queue!( + w, + style::SetAttribute(*on), + style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)), + style::SetAttribute(*off), + style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)), + style::ResetColor, + MoveCursorToNextLine(1) + )?; + } + + w.flush()?; + + Ok(()) +} + +pub fn run(w: &mut W) -> Result<()> +where + W: Write, +{ + run_tests!(w, test_set_display_attributes,); + Ok(()) +} diff --git a/interactive-test/src/test/color.rs b/interactive-test/src/test/color.rs new file mode 100644 index 0000000..4052222 --- /dev/null +++ b/interactive-test/src/test/color.rs @@ -0,0 +1,199 @@ +#![allow(clippy::cognitive_complexity)] + +use crate::{MoveCursorToNextLine, Result}; +use crossterm::{cursor, queue, style, style::Color}; +use std::io::Write; + +const COLORS: [Color; 21] = [ + Color::Black, + Color::DarkGrey, + Color::Grey, + Color::White, + Color::DarkRed, + Color::Red, + Color::DarkGreen, + Color::Green, + Color::DarkYellow, + Color::Yellow, + Color::DarkBlue, + Color::Blue, + Color::DarkMagenta, + Color::Magenta, + Color::DarkCyan, + Color::Cyan, + Color::AnsiValue(0), + Color::AnsiValue(15), + Color::Rgb { r: 255, g: 0, b: 0 }, + Color::Rgb { r: 0, g: 255, b: 0 }, + Color::Rgb { r: 0, g: 0, b: 255 }, +]; + +fn test_set_foreground_color(w: &mut W) -> Result<()> +where + W: Write, +{ + queue!( + w, + style::Print("Foreground colors on the black & white background"), + MoveCursorToNextLine(2) + )?; + + for color in &COLORS { + queue!( + w, + style::SetForegroundColor(*color), + style::SetBackgroundColor(Color::Black), + style::Print(format!( + "{:>width$} ", + format!("{:?} ████████████", color), + width = 40 + )), + style::SetBackgroundColor(Color::White), + style::Print(format!( + "{:>width$}", + format!("{:?} ████████████", color), + width = 40 + )), + MoveCursorToNextLine(1) + )?; + } + + w.flush()?; + + Ok(()) +} + +fn test_set_background_color(w: &mut W) -> Result<()> +where + W: Write, +{ + queue!( + w, + style::Print("Background colors with black & white foreground"), + MoveCursorToNextLine(2) + )?; + + for color in &COLORS { + queue!( + w, + style::SetBackgroundColor(*color), + style::SetForegroundColor(Color::Black), + style::Print(format!( + "{:>width$} ", + format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color), + width = 40 + )), + style::SetForegroundColor(Color::White), + style::Print(format!( + "{:>width$}", + format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color), + width = 40 + )), + MoveCursorToNextLine(1) + )?; + } + + w.flush()?; + + Ok(()) +} + +fn test_color_values_matrix_16x16(w: &mut W, title: &str, color: F) -> Result<()> +where + W: Write, + F: Fn(u16, u16) -> Color, +{ + queue!(w, style::Print(title))?; + + for idx in 0..=15 { + queue!( + w, + cursor::MoveTo(1, idx + 4), + style::Print(format!("{:>width$}", idx, width = 2)) + )?; + queue!( + w, + cursor::MoveTo(idx * 3 + 3, 3), + style::Print(format!("{:>width$}", idx, width = 3)) + )?; + } + + for row in 0..=15u16 { + queue!(w, cursor::MoveTo(4, row + 4))?; + for col in 0..=15u16 { + queue!( + w, + style::SetForegroundColor(color(col, row)), + style::Print("███") + )?; + } + queue!( + w, + style::SetForegroundColor(Color::White), + style::Print(format!("{:>width$} ..= ", row * 16, width = 3)), + style::Print(format!("{:>width$}", row * 16 + 15, width = 3)) + )?; + } + + w.flush()?; + + Ok(()) +} + +fn test_color_ansi_values(w: &mut W) -> Result<()> +where + W: Write, +{ + test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| { + Color::AnsiValue((row * 16 + col) as u8) + }) +} + +fn test_rgb_red_values(w: &mut W) -> Result<()> +where + W: Write, +{ + test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb { + r: (row * 16 + col) as u8, + g: 0 as u8, + b: 0, + }) +} + +fn test_rgb_green_values(w: &mut W) -> Result<()> +where + W: Write, +{ + test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb { + r: 0, + g: (row * 16 + col) as u8, + b: 0, + }) +} + +fn test_rgb_blue_values(w: &mut W) -> Result<()> +where + W: Write, +{ + test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb { + r: 0, + g: 0, + b: (row * 16 + col) as u8, + }) +} + +pub fn run(w: &mut W) -> Result<()> +where + W: Write, +{ + run_tests!( + w, + test_set_foreground_color, + test_set_background_color, + test_color_ansi_values, + test_rgb_red_values, + test_rgb_green_values, + test_rgb_blue_values, + ); + Ok(()) +} diff --git a/interactive-test/src/test/cursor.rs b/interactive-test/src/test/cursor.rs new file mode 100644 index 0000000..7c44c6c --- /dev/null +++ b/interactive-test/src/test/cursor.rs @@ -0,0 +1,210 @@ +#![allow(clippy::cognitive_complexity)] + +use std::io::Write; + +use crate::{MoveCursorToColumn, MoveCursorToNextLine, MoveCursorToPreviousLine, Result}; +use crossterm::cursor::MoveTo; +use crossterm::{cursor, execute, queue, style, style::Colorize, Command}; +use std::thread; +use std::time::Duration; + +fn test_move_cursor_up(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2)) +} + +fn test_move_cursor_down(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2)) +} + +fn test_move_cursor_left(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2)) +} + +fn test_move_cursor_right(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2)) +} + +fn test_move_cursor_to_previous_line(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "MoveCursorToPreviousLine (2)", |_, _| { + MoveCursorToPreviousLine(2) + }) +} + +fn test_move_cursor_to_next_line(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "MoveCursorToNextLine (2)", |_, _| { + MoveCursorToNextLine(2) + }) +} + +fn test_move_cursor_to_column(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box(w, "MoveCursorToColumn (2)", |center_x, _| { + MoveCursorToColumn(center_x + 2) + }) +} + +fn test_hide_cursor(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!(w, style::Print("HideCursor"), cursor::Hide) +} + +fn test_show_cursor(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!(w, style::Print("ShowCursor"), cursor::Show) +} + +fn test_enable_cursor_blinking(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!( + w, + style::Print("EnableCursorBlinking"), + cursor::EnableBlinking + ) +} + +fn test_disable_cursor_blinking(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!( + w, + style::Print("DisableCursorBlinking"), + cursor::DisableBlinking + ) +} + +fn test_move_cursor_to(w: &mut W) -> Result<()> +where + W: Write, +{ + draw_cursor_box( + w, + "MoveTo (x: 1, y: 1) removed from center", + |center_x, center_y| MoveTo(center_x + 1, center_y + 1), + ) +} + +fn test_save_restore_cursor_position(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!(w, + cursor::MoveTo(0, 0), + style::Print("Save position, print character else were, after three seconds restore to old position."), + MoveCursorToNextLine(2), + style::Print("Save ->[ ]<- Position"), + cursor::MoveTo(8, 2), + cursor::SavePosition, + cursor::MoveTo(10,10), + style::Print("Move To ->[√]<- Position") + )?; + + thread::sleep(Duration::from_secs(3)); + + execute!(w, cursor::RestorePosition, style::Print("√")) +} + +/// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command. +fn draw_cursor_box(w: &mut W, description: &str, cursor_command: F) -> Result<()> +where + W: Write, + F: Fn(u16, u16) -> T, + T: Command, +{ + execute!( + w, + cursor::Hide, + cursor::MoveTo(0, 0), + style::SetForegroundColor(style::Color::Red), + style::Print(format!( + "Red box is the center. After the action: '{}' another box is drawn.", + description + )) + )?; + + let start_y = 2; + let width = 21; + let height = 11 + start_y; + let center_x = width / 2; + let center_y = (height + start_y) / 2; + + for row in start_y..=10 + start_y { + for column in 0..=width { + if (row == start_y || row == height - 1) || (column == 0 || column == width) { + queue!( + w, + cursor::MoveTo(column, row), + style::PrintStyledContent("▓".red()) + )?; + } else { + queue!( + w, + cursor::MoveTo(column, row), + style::PrintStyledContent("_".red().on_white()) + )?; + } + } + } + + queue!( + w, + cursor::MoveTo(center_x, center_y), + style::PrintStyledContent("▀".red().on_white()) + )?; + queue!( + w, + cursor_command(center_x, center_y), + style::PrintStyledContent("√".magenta().on_white()) + )?; + w.flush()?; + Ok(()) +} + +pub fn run(w: &mut W) -> Result<()> +where + W: Write, +{ + run_tests!( + w, + test_hide_cursor, + test_show_cursor, + test_enable_cursor_blinking, + test_disable_cursor_blinking, + test_move_cursor_left, + test_move_cursor_right, + test_move_cursor_up, + test_move_cursor_down, + test_move_cursor_to, + test_move_cursor_to_next_line, + test_move_cursor_to_previous_line, + test_move_cursor_to_column, + test_save_restore_cursor_position + ); + Ok(()) +} diff --git a/interactive-test/src/test/event.rs b/interactive-test/src/test/event.rs new file mode 100644 index 0000000..9cf7b71 --- /dev/null +++ b/interactive-test/src/test/event.rs @@ -0,0 +1,40 @@ +#![allow(clippy::cognitive_complexity)] + +use crossterm::{ + cursor::position, + event::{read, EnableMouseCapture, Event, KeyCode}, + execute, Result, +}; +use std::io::Write; + +fn test_event(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!(w, EnableMouseCapture)?; + + loop { + // Blocking read + let event = read()?; + + println!("Event::{:?}\r", event); + + if event == Event::Key(KeyCode::Char('c').into()) { + println!("Cursor position: {:?}\r", position()); + } + + if event == Event::Key(KeyCode::Char('q').into()) { + break; + } + } + + Ok(()) +} + +pub fn run(w: &mut W) -> Result<()> +where + W: Write, +{ + run_tests!(w, test_event); + Ok(()) +}