From c7fc7655894dcba06b7262a9f3456e79483c5d15 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 17 Apr 2024 18:25:10 +0200 Subject: [PATCH] 22 allow skip input validation (#27) added: - Shift + Enter as force - Shift + Back as clear all * reworked input to use macros * additional keys and fixes for windows * reworked input example * fixed test failure * some more changes to the new key combinations --- examples/input.rs | 6 +- results.xml | 0 src/menu/input.rs | 211 ++++++++++++++++++++++++++++------------------ src/menu/mod.rs | 67 ++++++++++----- 4 files changed, 181 insertions(+), 103 deletions(-) create mode 100644 results.xml diff --git a/examples/input.rs b/examples/input.rs index d2485b3..e0ca530 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -2,9 +2,11 @@ use regex::Regex; use zenity::menu::input::{valid_path, valid_regex}; fn main() { + println!("Input Preview:"); + println!( "\n\nReturn: {}", - valid_regex(Regex::new(r"^\d{3}$").unwrap()) + valid_regex(Regex::new(r"^\d{3}$").unwrap(), Some("369"), false) ); - println!("\n\nPath: {:?}", valid_path()); + println!("\n\nPath: {:?}", valid_path(None, true)); } diff --git a/results.xml b/results.xml new file mode 100644 index 0000000..e69de29 diff --git a/src/menu/input.rs b/src/menu/input.rs index 82a0f20..21e9415 100644 --- a/src/menu/input.rs +++ b/src/menu/input.rs @@ -1,6 +1,12 @@ //! Input Validation Widgets //! -//! This module provides functions for validating user input with various criteria, such as regex patterns or file paths +//! **Note:** This module is a work in progress, +//! and breaking changes could be made soon without increasing the major version +//! for different reasons, such as improvements or bug fixes. +//! +//! +//! This module provides functions for validating user input with various criteria, +//! such as regex patterns or file paths //! The functions prompt the user for input, validate it, and return the validated input //! //! # Examples @@ -35,12 +41,12 @@ //! This module is a work in progress, and contributions are welcome //! +use std::io; use std::io::stdout; use std::path::{Path, PathBuf}; use crossterm::{ - cursor::MoveTo, - execute, + cursor, execute, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, }; use regex::Regex; @@ -48,15 +54,72 @@ use regex::Regex; use crate::menu::handle_key_input; use crate::style::{Color, Print, SetForegroundColor}; +macro_rules! input_loop { + ($buffer:expr, $validate:expr, $default:expr, $allow_force:expr) => { + let mut force: bool = false; + + // clear console enter release in windows + let mut invalid_buffer = String::new(); + handle_key_input(&mut invalid_buffer, &mut force); + + loop { + render_input_prompt(&$buffer, &$validate, $default); + + if handle_key_input(&mut $buffer, &mut force) + && $default.is_some() + && $buffer.is_empty() + { + $buffer = $default.unwrap().to_string(); + break; + } + + if force && $allow_force { + println!("Force!!"); + break; + } + + if handle_key_input(&mut $buffer, &mut force) && $buffer.is_empty() && $validate { + break; + } + } + }; +} + +macro_rules! raw_mode_wrapper { + ($content:expr) => { + enable_raw_mode().expect("Failed to enable raw-mode"); + + $content; + + disable_raw_mode().expect("Failed to disable raw-mode"); + execute!( + stdout(), + cursor::MoveTo(0, 0), + Clear(ClearType::FromCursorDown), + cursor::DisableBlinking + ) + .unwrap(); + }; +} + /// Validates and returns a string that matches the specified regex pattern. /// /// This function prompts the user to enter input and validates the input against the provided /// regex pattern. It continues to prompt the user until the entered input matches the regex pattern. /// The function returns the validated input as a string. /// +/// If `default` is provided and the user enters an empty string, the default value will be used. +/// +/// Note: The `allow_force` option is currently not fully supported due to console issues. See +/// [this issue](https://github.com/crossterm-rs/crossterm/issues/685) for more details. However, +/// users can force input by pressing Shift+Enter. Pressing Shift+Enter will clear the full input. +/// +/// /// # Arguments /// /// * `regex` - A `Regex` object representing the regex pattern to match against the user input. +/// * `default` - An optional default value to be used if the user enters an empty string. +/// * `allow_force` - A boolean indicating whether to allow the user to force input (not fully supported). /// /// # Returns /// @@ -72,37 +135,19 @@ use crate::style::{Color, Print, SetForegroundColor}; /// let regex = Regex::new(r"^\d{3}$").unwrap(); /// /// // Prompt the user to enter input matching the regex pattern -/// let input = valid_regex(regex); +/// let input = valid_regex(regex, Some("default_value"), false); /// /// println!("Valid input: {}", input); /// ``` -pub fn valid_regex(regex: Regex) -> String { - enable_raw_mode().expect("Failed to enable raw-mode"); - +pub fn valid_regex(regex: Regex, default: Option<&str>, allow_force: bool) -> String { let mut buffer = String::new(); - loop { - if handle_key_input(&mut buffer) && validate_input(&buffer, ®ex) { - break; - } - - execute!( - stdout(), - MoveTo(0, 0), - Clear(ClearType::CurrentLine), - Print("Enter input: "), - if !regex.is_match(&buffer) { - SetForegroundColor(Color::DarkRed) - } else { - SetForegroundColor(Color::Green) - }, - Print(&buffer), - SetForegroundColor(Color::Reset) - ) - .unwrap(); - } - - disable_raw_mode().expect("Failed to disable raw-mode"); + raw_mode_wrapper!(input_loop!( + buffer, + validate_input(&buffer, ®ex), + default, + allow_force + )); buffer } @@ -113,28 +158,38 @@ pub fn valid_regex(regex: Regex) -> String { /// it returns a `PathBuf` containing the path. Otherwise, it continues prompting the user until a valid /// path is entered. /// +/// If `default` is provided and the user enters an empty string, the default value will be used. +/// +/// Note: The `allow_force` option is currently not fully supported due to console issues. See +/// [this issue](https://github.com/crossterm-rs/crossterm/issues/685) for more details. However, +/// users can force input by pressing Shift+Enter. Pressing Shift+Enter will clear the full input. +/// +/// # Arguments +/// +/// * `default` - An optional default value to be used if the user enters an empty string. +/// * `allow_force` - A boolean indicating whether to allow the user to force input (not fully supported). +/// +/// # Returns +/// +/// A `Box` representing the validated path entered by the user. +/// /// # Examples /// /// ```rust,ignore /// use zenity::menu::input::valid_path; /// -/// let path = valid_path(); +/// let path = valid_path(Some("/home/user"), true); /// println!("Entered path: {:?}", path); /// ``` -pub fn valid_path() -> Box { - enable_raw_mode().expect("Failed to enable raw-mode"); - +pub fn valid_path(default: Option<&str>, allow_force: bool) -> Box { let mut buffer = String::new(); - loop { - if handle_key_input(&mut buffer) && validate_path(&buffer) { - break; - } - - render_input_prompt(&buffer, &validate_path(&buffer)); - } - - disable_raw_mode().expect("Failed to disable raw-mode"); + raw_mode_wrapper!(input_loop!( + buffer, + validate_path(&buffer), + default, + allow_force + )); let path = PathBuf::from(buffer); @@ -150,56 +205,52 @@ fn validate_input(buffer: &str, regex: &Regex) -> bool { if regex.is_match(buffer) { true } else { - execute!(stdout(), MoveTo(0, 0), Clear(ClearType::CurrentLine)).unwrap(); + execute!( + io::stdout(), + cursor::MoveTo(0, 0), + Clear(ClearType::CurrentLine) + ) + .unwrap(); false } } -fn render_input_prompt(buffer: &str, is_valid: &bool) { +fn render_input_prompt(buffer: &str, is_valid: &bool, default: Option<&str>) { execute!( - stdout(), - MoveTo(0, 0), + io::stdout(), + cursor::MoveTo(0, 6), Clear(ClearType::CurrentLine), - Print("Enter path: "), - if !is_valid { - SetForegroundColor(Color::DarkRed) - } else { - SetForegroundColor(Color::Green) - }, - Print(buffer), - SetForegroundColor(Color::Reset) ) .unwrap(); + if !buffer.is_empty() || default.is_none() { + execute!( + io::stdout(), + Print("Enter path: "), + if !is_valid { + SetForegroundColor(Color::DarkRed) + } else { + SetForegroundColor(Color::Green) + }, + Print(buffer), + ) + .unwrap(); + } else { + execute!( + io::stdout(), + Print("Enter path: "), + SetForegroundColor(Color::Grey), + Print(default.unwrap()), + Print(" (Default)"), + ) + .unwrap(); + } + execute!(io::stdout(), SetForegroundColor(Color::Reset),).unwrap(); } #[cfg(test)] mod tests { - use std::thread; - use std::time::Duration; - use super::*; - // TODO!: better testing for the valid_regex and valid_path functions - #[test] - fn test_valid_regex() { - let _handle = thread::spawn(|| { - valid_regex(Regex::new(r"^\d{3}$").unwrap()); - }); - - thread::sleep(Duration::from_secs(5)); - - // If the test reaches this point without crashing, consider it a success - } - - #[test] - fn test_valid_path() { - let _handle = thread::spawn(|| { - valid_path(); - }); - - thread::sleep(Duration::from_secs(5)); - } - #[test] fn test_validate_path_existing_file() { // Create a temporary file for testing @@ -219,12 +270,12 @@ mod tests { let file_path = "nonexistent_file.txt"; // Validate the path of the nonexistent file - assert!(validate_path(file_path)); + assert!(!validate_path(file_path)); } #[test] fn test_render_input_prompt() { // Call the render_input_prompt function with a mock Stdout - render_input_prompt("123", &true); + render_input_prompt("123", &true, None); } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 82a2d4e..a2a9fb0 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -1,30 +1,39 @@ //! implementation for menus //! (work in progress checkout: [issue#20](https://github.com/Arteiii/zenity/issues/20)) -use crossterm::event::{Event, KeyCode, KeyEvent}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; pub mod input; #[cfg(unix)] -pub(crate) fn handle_key_input(buffer: &mut String) -> bool { - handle_key_input_unix(buffer, crossterm::event::read().unwrap()) +pub(crate) fn handle_key_input(buffer: &mut String, force: &mut bool) -> bool { + handle_key_input_unix(buffer, crossterm::event::read().unwrap(), force) } #[cfg(windows)] -pub(crate) fn handle_key_input(buffer: &mut String) -> bool { - handle_key_input_windows(buffer, crossterm::event::read().unwrap()) +pub(crate) fn handle_key_input(buffer: &mut String, force: &mut bool) -> bool { + handle_key_input_windows(buffer, crossterm::event::read().unwrap(), force) } #[cfg(unix)] -fn handle_key_input_unix(buffer: &mut String, event: Event) -> bool { +fn handle_key_input_unix(buffer: &mut String, event: Event, force: &mut bool) -> bool { if let Event::Key(key_event) = event { - let KeyEvent { code, .. } = key_event; + let KeyEvent { + code, modifiers, .. + } = key_event; match code { KeyCode::Enter => { + if modifiers.contains(KeyModifiers::SHIFT) { + *force = true; + } return true; } KeyCode::Backspace => { - buffer.pop(); + if modifiers.contains(KeyModifiers::SHIFT) { + buffer.clear(); + } else { + buffer.pop(); + } } KeyCode::Char(c) => { buffer.push(c); @@ -37,21 +46,37 @@ fn handle_key_input_unix(buffer: &mut String, event: Event) -> bool { } #[cfg(windows)] -fn handle_key_input_windows(buffer: &mut String, event: Event) -> bool { +fn handle_key_input_windows(buffer: &mut String, event: Event, force: &mut bool) -> bool { static mut SKIP_NEXT: bool = false; + // true to fix execute keypress-release to be used as keypress static mut SKIP_NEXT_BACK: bool = false; + static mut SKIP_NEXT_ENTER: bool = false; if let Event::Key(key_event) = event { - let KeyEvent { code, .. } = key_event; + let KeyEvent { + code, modifiers, .. + } = key_event; // TODO!: fix unsafe usage!!! match code { - KeyCode::Enter => { - return true; - } + KeyCode::Enter => unsafe { + if !SKIP_NEXT_ENTER { + SKIP_NEXT_ENTER = true; + if modifiers.contains(KeyModifiers::SHIFT) { + *force = true; + } + return true; + } else { + SKIP_NEXT_ENTER = false + } + }, KeyCode::Backspace => unsafe { if !SKIP_NEXT_BACK { - buffer.pop(); + if modifiers.contains(KeyModifiers::SHIFT) { + buffer.clear(); + } else { + buffer.pop(); + } SKIP_NEXT_BACK = true } else { SKIP_NEXT_BACK = false @@ -74,7 +99,7 @@ fn handle_key_input_windows(buffer: &mut String, event: Event) -> bool { #[cfg(test)] mod tests { - use crossterm::event::{KeyEventKind, KeyEventState, KeyModifiers}; + use crossterm::event::{KeyEventKind, KeyEventState}; use super::*; @@ -88,7 +113,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - assert!(handle_key_input_unix(&mut buffer, event)); + assert!(handle_key_input_unix(&mut buffer, event, &mut false)); } #[cfg(unix)] @@ -101,7 +126,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - handle_key_input_unix(&mut buffer, event); + handle_key_input_unix(&mut buffer, event, &mut false); assert_eq!(buffer, "tes"); } @@ -115,7 +140,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - handle_key_input_unix(&mut buffer, event); + handle_key_input_unix(&mut buffer, event, &mut false); assert_eq!(buffer, "a"); } @@ -129,7 +154,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - assert!(handle_key_input_windows(&mut buffer, event)); + assert!(handle_key_input_windows(&mut buffer, event, &mut false)); } #[cfg(windows)] @@ -142,7 +167,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - handle_key_input_windows(&mut buffer, event); + handle_key_input_windows(&mut buffer, event, &mut false); assert_eq!(buffer, "tes"); } @@ -156,7 +181,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - handle_key_input_windows(&mut buffer, event); + handle_key_input_windows(&mut buffer, event, &mut false); assert_eq!(buffer, "a"); } }