From 606fdcd76e8f0c801e60cd051f927c19f99ad8e0 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 18 Apr 2024 15:31:27 +0200 Subject: [PATCH] 22 allow skip input validation 1 (#29) * reworked key input to work correctly on windows * fixed performance issues * added title option * added title to example * removed unused code added a test * cargo fmt --- examples/input.rs | 12 +++- examples/selection.rs | 34 ----------- src/menu/input.rs | 64 ++++++++++--------- src/menu/mod.rs | 139 ++++++++---------------------------------- src/menu/selection.rs | 49 --------------- src/progress/mod.rs | 4 +- src/terminal.rs | 1 + 7 files changed, 71 insertions(+), 232 deletions(-) delete mode 100644 examples/selection.rs delete mode 100644 src/menu/selection.rs diff --git a/examples/input.rs b/examples/input.rs index e0ca530..9427a48 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -6,7 +6,15 @@ fn main() { println!( "\n\nReturn: {}", - valid_regex(Regex::new(r"^\d{3}$").unwrap(), Some("369"), false) + valid_regex( + "Enter string:", + Regex::new(r"^\d{3}$").unwrap(), + Some("369"), + false + ) + ); + println!( + "\n\nPath: {:?}", + valid_path("Enter A Valid Path:", None, true) ); - println!("\n\nPath: {:?}", valid_path(None, true)); } diff --git a/examples/selection.rs b/examples/selection.rs deleted file mode 100644 index 226e8e0..0000000 --- a/examples/selection.rs +++ /dev/null @@ -1,34 +0,0 @@ -// rmeoved for now will work on it soon - -// use zenity::menu::selection::{MenuOption, SelectionMenu}; -// use zenity::style::{Color, StyledString}; -// -// fn main() { -// let hello_world = MenuOption { -// text: StyledString::new("Hello World"), -// notes: StyledString::new("bli bla blub"), -// }; -// -// let method2 = MenuOption { -// text: StyledString::new("method2 says Hi"), -// notes: StyledString::new("No!"), -// }; -// -// let menu = SelectionMenu { -// title: StyledString::simple("Selection Menu", Some(Color::Red), None, None), -// options: [hello_world.clone(), method2.clone()].to_vec(), -// }; -// -// let selected_option = menu.single(); -// -// if selected_option == hello_world { -// println!("User selected Hello World"); -// } else if selected_option == method2 { -// println!("Method2 says Hi"); -// } else { -// // handle other cases (shouldn't be any...) -// } -// -// -// } -fn main() {} diff --git a/src/menu/input.rs b/src/menu/input.rs index 21e9415..084ca81 100644 --- a/src/menu/input.rs +++ b/src/menu/input.rs @@ -42,7 +42,6 @@ //! use std::io; -use std::io::stdout; use std::path::{Path, PathBuf}; use crossterm::{ @@ -55,30 +54,22 @@ 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) => { + ($title:expr, $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); + render_input_prompt($title, &$buffer, &$validate, $default); - if handle_key_input(&mut $buffer, &mut force) - && $default.is_some() - && $buffer.is_empty() - { - $buffer = $default.unwrap().to_string(); - break; + if handle_key_input(&mut $buffer, &mut force) { + if !$buffer.is_empty() && $validate { + break; + } else if $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; } } @@ -93,7 +84,7 @@ macro_rules! raw_mode_wrapper { disable_raw_mode().expect("Failed to disable raw-mode"); execute!( - stdout(), + io::stdout(), cursor::MoveTo(0, 0), Clear(ClearType::FromCursorDown), cursor::DisableBlinking @@ -139,10 +130,11 @@ macro_rules! raw_mode_wrapper { /// /// println!("Valid input: {}", input); /// ``` -pub fn valid_regex(regex: Regex, default: Option<&str>, allow_force: bool) -> String { +pub fn valid_regex(title: &str, regex: Regex, default: Option<&str>, allow_force: bool) -> String { let mut buffer = String::new(); raw_mode_wrapper!(input_loop!( + title, buffer, validate_input(&buffer, ®ex), default, @@ -181,10 +173,11 @@ pub fn valid_regex(regex: Regex, default: Option<&str>, allow_force: bool) -> St /// let path = valid_path(Some("/home/user"), true); /// println!("Entered path: {:?}", path); /// ``` -pub fn valid_path(default: Option<&str>, allow_force: bool) -> Box { +pub fn valid_path(title: &str, default: Option<&str>, allow_force: bool) -> Box { let mut buffer = String::new(); raw_mode_wrapper!(input_loop!( + title, buffer, validate_path(&buffer), default, @@ -196,18 +189,20 @@ pub fn valid_path(default: Option<&str>, allow_force: bool) -> Box { Box::new(path) } +#[inline] fn validate_path(path: &str) -> bool { // useless function but might change something here later... Path::new(path).exists() } +#[inline] fn validate_input(buffer: &str, regex: &Regex) -> bool { if regex.is_match(buffer) { true } else { execute!( io::stdout(), - cursor::MoveTo(0, 0), + cursor::MoveTo(0, 5), Clear(ClearType::CurrentLine) ) .unwrap(); @@ -215,17 +210,19 @@ fn validate_input(buffer: &str, regex: &Regex) -> bool { } } -fn render_input_prompt(buffer: &str, is_valid: &bool, default: Option<&str>) { +fn render_input_prompt(title: &str, buffer: &str, is_valid: &bool, default: Option<&str>) { execute!( io::stdout(), - cursor::MoveTo(0, 6), + cursor::MoveTo(0, 4), Clear(ClearType::CurrentLine), ) .unwrap(); if !buffer.is_empty() || default.is_none() { execute!( io::stdout(), - Print("Enter path: "), + Print(title), + cursor::MoveToNextLine(1), + Clear(ClearType::CurrentLine), if !is_valid { SetForegroundColor(Color::DarkRed) } else { @@ -233,16 +230,18 @@ fn render_input_prompt(buffer: &str, is_valid: &bool, default: Option<&str>) { }, Print(buffer), ) - .unwrap(); + .expect("execute print buffer failed"); } else { execute!( io::stdout(), - Print("Enter path: "), + Print(title), + cursor::MoveToNextLine(1), + Clear(ClearType::CurrentLine), SetForegroundColor(Color::Grey), Print(default.unwrap()), Print(" (Default)"), ) - .unwrap(); + .expect("execute print default failed"); } execute!(io::stdout(), SetForegroundColor(Color::Reset),).unwrap(); } @@ -276,6 +275,13 @@ mod tests { #[test] fn test_render_input_prompt() { // Call the render_input_prompt function with a mock Stdout - render_input_prompt("123", &true, None); + render_input_prompt("Title", "123", &true, Some("Default stuff")); + } + + #[test] + fn test_validate_input() { + // Call the render_input_prompt function with a mock Stdout + assert!(validate_input("123", &Regex::new(r"^\d{3}$").unwrap())); + assert!(!validate_input("abc", &Regex::new(r"^\d{3}$").unwrap())); } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index a2a9fb0..5aa92b6 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -1,32 +1,35 @@ //! implementation for menus //! (work in progress checkout: [issue#20](https://github.com/Arteiii/zenity/issues/20)) -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; pub mod input; -#[cfg(unix)] pub(crate) fn handle_key_input(buffer: &mut String, force: &mut bool) -> bool { - handle_key_input_unix(buffer, crossterm::event::read().unwrap(), force) + _handle_key_input(buffer, crossterm::event::read().unwrap(), force) } -#[cfg(windows)] -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, force: &mut bool) -> bool { +#[inline] +fn _handle_key_input(buffer: &mut String, event: Event, force: &mut bool) -> bool { if let Event::Key(key_event) = event { let KeyEvent { - code, modifiers, .. + code, + modifiers, + kind, + .. } = key_event; - match code { + if kind != KeyEventKind::Press { + return false; + } + + return match code { KeyCode::Enter => { if modifiers.contains(KeyModifiers::SHIFT) { *force = true; + } else { + return true; } - return true; + false } KeyCode::Backspace => { if modifiers.contains(KeyModifiers::SHIFT) { @@ -34,64 +37,14 @@ fn handle_key_input_unix(buffer: &mut String, event: Event, force: &mut bool) -> } else { buffer.pop(); } + false } KeyCode::Char(c) => { buffer.push(c); + false } - _ => {} - } - } - - false -} - -#[cfg(windows)] -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, modifiers, .. - } = key_event; - - // TODO!: fix unsafe usage!!! - match code { - 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 { - if modifiers.contains(KeyModifiers::SHIFT) { - buffer.clear(); - } else { - buffer.pop(); - } - SKIP_NEXT_BACK = true - } else { - SKIP_NEXT_BACK = false - } - }, - KeyCode::Char(c) => unsafe { - if !SKIP_NEXT { - buffer.push(c); - SKIP_NEXT = true - } else { - SKIP_NEXT = false - } - }, - _ => {} - } + _ => false, + }; } false @@ -99,52 +52,10 @@ fn handle_key_input_windows(buffer: &mut String, event: Event, force: &mut bool) #[cfg(test)] mod tests { - use crossterm::event::{KeyEventKind, KeyEventState}; + use crossterm::event::KeyEventState; use super::*; - #[cfg(unix)] - #[test] - fn test_handle_key_input_unix_enter() { - let mut buffer = String::new(); - let event = Event::Key(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::empty(), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - }); - assert!(handle_key_input_unix(&mut buffer, event, &mut false)); - } - - #[cfg(unix)] - #[test] - fn test_handle_key_input_unix_backspace() { - let mut buffer = String::from("test"); - let event = Event::Key(KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::empty(), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - }); - handle_key_input_unix(&mut buffer, event, &mut false); - assert_eq!(buffer, "tes"); - } - - #[cfg(unix)] - #[test] - fn test_handle_key_input_unix_char() { - let mut buffer = String::new(); - let event = Event::Key(KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::empty(), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - }); - handle_key_input_unix(&mut buffer, event, &mut false); - assert_eq!(buffer, "a"); - } - - #[cfg(windows)] #[test] fn test_handle_key_input_windows_enter() { let mut buffer = String::new(); @@ -154,10 +65,9 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - assert!(handle_key_input_windows(&mut buffer, event, &mut false)); + assert!(_handle_key_input(&mut buffer, event, &mut false)); } - #[cfg(windows)] #[test] fn test_handle_key_input_windows_backspace() { let mut buffer = String::from("test"); @@ -167,11 +77,10 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - handle_key_input_windows(&mut buffer, event, &mut false); + _handle_key_input(&mut buffer, event, &mut false); assert_eq!(buffer, "tes"); } - #[cfg(windows)] #[test] fn test_handle_key_input_windows_char() { let mut buffer = String::new(); @@ -181,7 +90,7 @@ mod tests { kind: KeyEventKind::Press, state: KeyEventState::empty(), }); - handle_key_input_windows(&mut buffer, event, &mut false); + _handle_key_input(&mut buffer, event, &mut false); assert_eq!(buffer, "a"); } } diff --git a/src/menu/selection.rs b/src/menu/selection.rs deleted file mode 100644 index 23b00ce..0000000 --- a/src/menu/selection.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! selection menus work in progress - -use crate::style::StyledString; -use crate::terminal::{console_cursor, console_render}; - -/// Represents an option in a selection menu -#[derive(Clone, Debug, PartialEq)] -pub struct MenuOption { - /// Text to display for this option - pub text: StyledString, - /// Additional notes or information about this option - pub notes: StyledString, -} - -/// Represents a selection menu -#[derive(Clone)] -pub struct SelectionMenu { - /// the title for the - pub title: StyledString, - - /// List of options along with the correct return id - pub options: Vec, -} - -impl Default for SelectionMenu { - /// Creates a new default selection menu - fn default() -> Self { - todo!() - } -} - -impl SelectionMenu { - /// render the single selection - pub fn single(self) -> MenuOption{ - let title = self.title.clone(); - - console_cursor::save_hide_cursor(); - - console_render::render_styled_line(1, &[title]); - - console_cursor::next_line(4); - - for (index, option) in self.options.iter().enumerate().clone() { - console_render::render_styled_line((index + 2).try_into().unwrap(), &[option.text.clone()]); - } - - self.options[0].clone() - } -} diff --git a/src/progress/mod.rs b/src/progress/mod.rs index 783c9a7..dc6e8ae 100644 --- a/src/progress/mod.rs +++ b/src/progress/mod.rs @@ -53,11 +53,9 @@ pub mod frames; /// /// thread::sleep(Duration::from_millis(rand::thread_rng().gen_range(1..=70))); /// } +/// /// ``` -#[derive(Clone)] pub struct ProgressBar { - // TODO: instead of random ids go after creation and increment by one - // this would allow to render them line for line based on this and order them correctly bar: Arc>>, stop: Arc>, } diff --git a/src/terminal.rs b/src/terminal.rs index 553f605..30bc87d 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -127,6 +127,7 @@ mod tests { let content = vec![ StyledString::simple("Hello, ", Some(Color::Red), None, None), StyledString::simple(" world", Some(Color::Green), None, None), + StyledString::default(), ]; console_render::render_styled(4, &content);