From 70bf4caa40328bef6aafd5f50985ba6ee7fcf783 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Sun, 24 Dec 2023 15:30:17 -0800 Subject: [PATCH 1/6] Fix with_starting_cursor not being respected on selectable prompts (#197) --- inquire/src/prompts/multiselect/mod.rs | 10 ++-- inquire/src/prompts/multiselect/prompt.rs | 9 +++- inquire/src/prompts/multiselect/test.rs | 65 +++++++++++++++++++++++ inquire/src/prompts/select/mod.rs | 10 ++-- inquire/src/prompts/select/prompt.rs | 9 +++- inquire/src/prompts/select/test.rs | 64 ++++++++++++++++++++++ 6 files changed, 159 insertions(+), 8 deletions(-) diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 48a9b57e..0c97c25e 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -302,6 +302,9 @@ where } /// Sets the starting cursor index. + /// + /// This index might be overriden if the `reset_cursor` option is set to true (default) + /// and starting_filter_input is set to something other than None. pub fn with_starting_cursor(mut self, starting_cursor: usize) -> Self { self.starting_cursor = starting_cursor; self @@ -313,9 +316,10 @@ where self } - /// Sets the reset_cursor behaviour. - /// Will reset cursor to first option on filter input change. - /// Defaults to true. + /// Sets the reset_cursor behaviour. Defaults to true. + /// + /// When there's an input change that results in a different list of options being displayed, + /// whether by filtering or re-ordering, the cursor will be reset to highlight the first option. pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self { self.reset_cursor = reset_cursor; self diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index 22ed075d..02dd4c5d 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -199,7 +199,14 @@ where let mut options = self.score_options(); options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); - self.scored_options = options.into_iter().map(|(idx, _)| idx).collect(); + let new_scored_options = options.iter().map(|(idx, _)| *idx).collect::>(); + + if self.scored_options == new_scored_options { + return; + } + + self.scored_options = new_scored_options; + if self.config.reset_cursor { let _ = self.update_cursor_position(0); } else if self.scored_options.len() <= self.cursor_index { diff --git a/inquire/src/prompts/multiselect/test.rs b/inquire/src/prompts/multiselect/test.rs index a4b4aa62..2419f82f 100644 --- a/inquire/src/prompts/multiselect/test.rs +++ b/inquire/src/prompts/multiselect/test.rs @@ -129,3 +129,68 @@ fn list_option_indexes_are_relative_to_input_vec() { assert_eq!(vec![ListOption::new(1, 2), ListOption::new(2, 3)], ans); } + +#[test] +// Anti-regression test: https://github.com/mikaelmello/inquire/issues/195 +fn starting_cursor_is_respected() { + let read: Vec = [KeyCode::Char(' '), KeyCode::Enter] + .iter() + .map(|c| KeyEvent::from(*c)) + .collect(); + + let mut read = read.iter(); + + let options = vec![1, 2, 3]; + + let mut write: Vec = Vec::new(); + let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + + let ans = MultiSelect::new("Question", options) + .with_starting_cursor(2) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(vec![ListOption::new(2, 3)], ans); +} + +#[test] +fn naive_assert_fuzzy_match_as_default_scorer() { + let read: Vec = [ + KeyCode::Char('w'), + KeyCode::Char('r'), + KeyCode::Char('r'), + KeyCode::Char('y'), + KeyCode::Char(' '), + KeyCode::Enter, + ] + .iter() + .map(|c| KeyEvent::from(*c)) + .collect(); + + let mut read = read.iter(); + + let options = vec![ + "Banana", + "Apple", + "Strawberry", + "Grapes", + "Lemon", + "Tangerine", + "Watermelon", + "Orange", + "Pear", + "Avocado", + "Pineapple", + ]; + + let mut write: Vec = Vec::new(); + let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + + let ans = MultiSelect::new("Question", options) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(vec![ListOption::new(2, "Strawberry")], ans); +} diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index 9bfff8a8..9f3b9912 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -244,6 +244,9 @@ where } /// Sets the starting cursor index. + /// + /// This index might be overriden if the `reset_cursor` option is set to true (default) + /// and starting_filter_input is set to something other than None. pub fn with_starting_cursor(mut self, starting_cursor: usize) -> Self { self.starting_cursor = starting_cursor; self @@ -255,9 +258,10 @@ where self } - /// Sets the reset_cursor behaviour. - /// Will reset cursor to first option on filter input change. - /// Defaults to true. + /// Sets the reset_cursor behaviour. Defaults to true. + /// + /// When there's an input change that results in a different list of options being displayed, + /// whether by filtering or re-ordering, the cursor will be reset to highlight the first option. pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self { self.reset_cursor = reset_cursor; self diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index 16a07b9b..d79314c8 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -138,7 +138,14 @@ where let mut options = self.score_options(); options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); - self.scored_options = options.into_iter().map(|(idx, _)| idx).collect(); + let new_scored_options = options.iter().map(|(idx, _)| *idx).collect::>(); + + if self.scored_options == new_scored_options { + return; + } + + self.scored_options = new_scored_options; + if self.config.reset_cursor { let _ = self.update_cursor_position(0); } else if self.scored_options.len() <= self.cursor_index { diff --git a/inquire/src/prompts/select/test.rs b/inquire/src/prompts/select/test.rs index da0da91b..ff9f2d51 100644 --- a/inquire/src/prompts/select/test.rs +++ b/inquire/src/prompts/select/test.rs @@ -93,3 +93,67 @@ fn down_arrow_on_empty_list_does_not_panic() { assert_eq!(ListOption::new(0, 1), ans); } + +#[test] +// Anti-regression test: https://github.com/mikaelmello/inquire/issues/195 +fn starting_cursor_is_respected() { + let read: Vec = [KeyCode::Enter] + .iter() + .map(|c| KeyEvent::from(*c)) + .collect(); + + let mut read = read.iter(); + + let options = vec![1, 2, 3]; + + let mut write: Vec = Vec::new(); + let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + + let ans = Select::new("Question", options) + .with_starting_cursor(2) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(ListOption::new(2, 3), ans); +} + +#[test] +fn naive_assert_fuzzy_match_as_default_scorer() { + let read: Vec = [ + KeyCode::Char('w'), + KeyCode::Char('r'), + KeyCode::Char('r'), + KeyCode::Char('y'), + KeyCode::Enter, + ] + .iter() + .map(|c| KeyEvent::from(*c)) + .collect(); + + let mut read = read.iter(); + + let options = vec![ + "Banana", + "Apple", + "Strawberry", + "Grapes", + "Lemon", + "Tangerine", + "Watermelon", + "Orange", + "Pear", + "Avocado", + "Pineapple", + ]; + + let mut write: Vec = Vec::new(); + let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + + let ans = Select::new("Question", options) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(ListOption::new(2, "Strawberry"), ans); +} From 79f02db04ae4f7e4e5068b149a0aa55c7a13334d Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Tue, 26 Dec 2023 17:31:06 -0800 Subject: [PATCH 2/6] Use tty instead of stdin on termion (#199) * Use tty instead of stdin on termion * Rename Tty enum variant to TTY * Update CHANGELOG * Allow upper-case acronym --- CHANGELOG.md | 11 ++++++----- inquire/src/error.rs | 5 ++++- inquire/src/terminal/crossterm.rs | 9 +++------ inquire/src/terminal/termion.rs | 32 +++++++++++++------------------ 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f860f3d..0a1a7144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,19 +14,20 @@ - Expand workflow clippy task to lint all-features in workspace. - Add docs badge to readme. - **Breaking** The Select and Multiselect Filter now scores input and is now expected to return an `Option`, making it possible to order/rank the list of options. [#176](https://github.com/mikaelmello/inquire/pull/176) - `None`: Will not be displayed in the list of options. - `Some(score)`: score determines the order of options, higher score, higher on the list of options. + `None`: Will not be displayed in the list of options. + `Some(score)`: score determines the order of options, higher score, higher on the list of options. - Implement fuzzy search as default on Select and MultiSelect prompts. [#176](https://github.com/mikaelmello/inquire/pull/176) - Add new option on Select/MultiSelect prompts allowing to reset selection to the first item on filter-input changes. [#176](https://github.com/mikaelmello/inquire/pull/176) - Emacs-like keybindings added where applicable: - - Ctrl-p/Ctrl-n for up/down - - Ctrl-b/Ctrl-f for left/right - - Ctrl-j/Ctrl-g for enter/cancel +- Ctrl-p/Ctrl-n for up/down +- Ctrl-b/Ctrl-f for left/right +- Ctrl-j/Ctrl-g for enter/cancel - Added 'with_starting_filter_input' to both Select and MultiSelect, which allows for setting an initial value to the filter section of the prompt. ### Fixes - Fixed typos in the code's comments. +- Fixed issue where inquire, using termion, would crash when receiving piped inputs. ### Dependency changes (some breaking) diff --git a/inquire/src/error.rs b/inquire/src/error.rs index 2da9b0a9..62206d99 100644 --- a/inquire/src/error.rs +++ b/inquire/src/error.rs @@ -65,7 +65,10 @@ impl From for InquireError { impl From for InquireError { fn from(err: io::Error) -> Self { - InquireError::IO(err) + match err.raw_os_error() { + Some(25 | 6) => InquireError::NotTTY, + _ => InquireError::IO(err), + } } } diff --git a/inquire/src/terminal/crossterm.rs b/inquire/src/terminal/crossterm.rs index d7676ac2..d0adcdf9 100644 --- a/inquire/src/terminal/crossterm.rs +++ b/inquire/src/terminal/crossterm.rs @@ -5,12 +5,12 @@ use crossterm::{ event::{self, KeyCode, KeyEvent, KeyModifiers}, queue, style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, - terminal::{self, enable_raw_mode, ClearType}, + terminal::{self, ClearType}, Command, }; use crate::{ - error::{InquireError, InquireResult}, + error::InquireResult, ui::{Attributes, Key, Styled}, }; @@ -34,10 +34,7 @@ pub struct CrosstermTerminal<'a> { impl<'a> CrosstermTerminal<'a> { pub fn new() -> InquireResult { - enable_raw_mode().map_err(|e| match e.raw_os_error() { - Some(25 | 6) => InquireError::NotTTY, - _ => InquireError::from(e), - })?; + crossterm::terminal::enable_raw_mode()?; Ok(Self { io: IO::Std { w: stderr() }, diff --git a/inquire/src/terminal/termion.rs b/inquire/src/terminal/termion.rs index f6a59564..b8434501 100644 --- a/inquire/src/terminal/termion.rs +++ b/inquire/src/terminal/termion.rs @@ -1,5 +1,8 @@ use core::fmt; -use std::io::{stderr, stdin, Result, Stderr, Stdin, Write}; +use std::{ + fs::File, + io::{Result, Write}, +}; use termion::{ color::{self, Color}, @@ -11,18 +14,15 @@ use termion::{ }; use crate::{ - error::{InquireError, InquireResult}, + error::InquireResult, ui::{Attributes, Styled}, }; use super::{Terminal, INITIAL_IN_MEMORY_CAPACITY}; +#[allow(clippy::upper_case_acronyms)] enum IO<'a> { - #[allow(unused)] - Std { - r: Keys, - w: RawTerminal, - }, + TTY(RawTerminal, Keys), #[allow(unused)] Custom { r: &'a mut dyn Iterator, @@ -38,18 +38,12 @@ pub struct TermionTerminal<'a> { impl<'a> TermionTerminal<'a> { #[allow(unused)] pub fn new() -> InquireResult { - let raw_mode = stderr() - .into_raw_mode() - .map_err(|e| match e.raw_os_error() { - Some(25 | 6) => InquireError::NotTTY, - _ => e.into(), - }); + let tty = termion::get_tty()?; + let raw_terminal = tty.into_raw_mode()?; + let keys = raw_terminal.try_clone()?.keys(); Ok(Self { - io: IO::Std { - r: stdin().keys(), - w: raw_mode?, - }, + io: IO::TTY(raw_terminal, keys), in_memory_content: String::with_capacity(INITIAL_IN_MEMORY_CAPACITY), }) } @@ -73,7 +67,7 @@ impl<'a> TermionTerminal<'a> { fn get_writer(&mut self) -> &mut dyn Write { match &mut self.io { - IO::Std { r: _, w } => w, + IO::TTY(w, _) => w, IO::Custom { r: _, w } => w, } } @@ -126,7 +120,7 @@ impl<'a> Terminal for TermionTerminal<'a> { fn read_key(&mut self) -> Result { loop { match &mut self.io { - IO::Std { r, w: _ } => { + IO::TTY(_, r) => { if let Some(key) = r.next() { return key.map(|k| k.into()); } From 2948f2a65c6739445be10386efe5ff1e9b9f3983 Mon Sep 17 00:00:00 2001 From: darkecho731 <92435436+darkecho731@users.noreply.github.com> Date: Wed, 27 Dec 2023 06:23:06 +0100 Subject: [PATCH 3/6] Add `with_answered_prompt_prefix` to RenderConfig (#191) * add `with_answered_prompt_prefix` to RenderConfig * update changelog --- CHANGELOG.md | 1 + inquire/src/ui/render_config.rs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1a7144..151fcd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Ctrl-b/Ctrl-f for left/right - Ctrl-j/Ctrl-g for enter/cancel - Added 'with_starting_filter_input' to both Select and MultiSelect, which allows for setting an initial value to the filter section of the prompt. +- Added 'with_answered_prompt_prefix' to RenderConfig to allow customization of answered prompt prefix ### Fixes diff --git a/inquire/src/ui/render_config.rs b/inquire/src/ui/render_config.rs index 30d72042..668bc83e 100644 --- a/inquire/src/ui/render_config.rs +++ b/inquire/src/ui/render_config.rs @@ -217,6 +217,12 @@ impl<'a> RenderConfig<'a> { self } + /// Sets the answered prompt prefix and its style sheet. + pub fn with_answered_prompt_prefix(mut self, answered_prompt_prefix: Styled<&'a str>) -> Self { + self.answered_prompt_prefix = answered_prompt_prefix; + self + } + /// Sets style for text inputs. pub fn with_text_input(mut self, text_input: StyleSheet) -> Self { self.text_input = text_input; From 81f33c5a469b9b891b584a737e009b100e6e86ae Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Tue, 26 Dec 2023 21:25:06 -0800 Subject: [PATCH 4/6] Add 'without_filtering' to selectable prompts (#203) * Add 'without_filtering' to selectable prompts * Add changelog entry --- CHANGELOG.md | 1 + inquire/src/prompts/multiselect/mod.rs | 20 +++++ inquire/src/prompts/multiselect/prompt.rs | 93 +++++++++++++---------- inquire/src/prompts/multiselect/test.rs | 42 ++++++++++ inquire/src/prompts/prompt.rs | 16 +++- inquire/src/prompts/select/mod.rs | 20 +++++ inquire/src/prompts/select/prompt.rs | 66 ++++++++-------- inquire/src/prompts/select/test.rs | 41 ++++++++++ inquire/src/ui/backend.rs | 20 +++-- 9 files changed, 240 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 151fcd09..9b6253a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Ctrl-b/Ctrl-f for left/right - Ctrl-j/Ctrl-g for enter/cancel - Added 'with_starting_filter_input' to both Select and MultiSelect, which allows for setting an initial value to the filter section of the prompt. +- Added 'without_filtering' to both Select and MultiSelect, useful when you want to simplify the UX if the filter does not add any value, such as when the list is already short. - Added 'with_answered_prompt_prefix' to RenderConfig to allow customization of answered prompt prefix ### Fixes diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 0c97c25e..a2f52241 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -91,6 +91,11 @@ pub struct MultiSelect<'a, T> { /// Defaults to true. pub reset_cursor: bool, + /// Whether to allow the option list to be filtered by user input or not. + /// + /// Defaults to true. + pub filter_input_enabled: bool, + /// Function called with the current user input to score the provided /// options. /// The list of options is sorted in descending order (highest score first) @@ -201,6 +206,10 @@ where /// Defaults to true. pub const DEFAULT_RESET_CURSOR: bool = true; + /// Default filter input enabled behaviour. + /// Defaults to true. + pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true; + /// Default behavior of keeping or cleaning the current filter value. pub const DEFAULT_KEEP_FILTER: bool = true; @@ -220,6 +229,7 @@ where starting_cursor: Self::DEFAULT_STARTING_CURSOR, starting_filter_input: None, reset_cursor: Self::DEFAULT_RESET_CURSOR, + filter_input_enabled: Self::DEFAULT_FILTER_INPUT_ENABLED, keep_filter: Self::DEFAULT_KEEP_FILTER, scorer: Self::DEFAULT_SCORER, formatter: Self::DEFAULT_FORMATTER, @@ -325,6 +335,16 @@ where self } + /// Disables the filter input, which means the user will not be able to filter the options + /// by typing. + /// + /// This is useful when you want to simplify the UX if the filter does not add any value, + /// such as when the list is already short. + pub fn without_filtering(mut self) -> Self { + self.filter_input_enabled = false; + self + } + /// Sets the provided color theme to this prompt. /// /// Note: The default render config considers if the NO_COLOR environment variable diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index 02dd4c5d..1ef6cd82 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -23,7 +23,7 @@ pub struct MultiSelectPrompt<'a, T> { help_message: Option<&'a str>, cursor_index: usize, checked: BTreeSet, - input: Input, + input: Option, scored_options: Vec, scorer: Scorer<'a, T>, formatter: MultiOptionFormatter<'a, T>, @@ -66,6 +66,13 @@ where }) .unwrap_or_default(); + let input = match mso.filter_input_enabled { + true => Some(Input::new_with( + mso.starting_filter_input.unwrap_or_default(), + )), + false => None, + }; + Ok(Self { message: mso.message, config: (&mso).into(), @@ -74,10 +81,7 @@ where scored_options, help_message: mso.help_message, cursor_index: mso.starting_cursor, - input: mso - .starting_filter_input - .map(Input::new_with) - .unwrap_or_else(Input::new), + input, scorer: mso.scorer, formatter: mso.formatter, validator: mso.validator, @@ -86,22 +90,6 @@ where }) } - fn score_options(&self) -> Vec<(usize, i64)> { - self.options - .iter() - .enumerate() - .filter_map(|(i, opt)| { - (self.scorer)( - self.input.content(), - opt, - self.string_options.get(i).unwrap(), - i, - ) - .map(|score| (i, score)) - }) - .collect::>() - } - fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult { let new_position = if wrap { let after_wrap = qty.saturating_sub(self.cursor_index); @@ -152,11 +140,23 @@ where self.checked.insert(*idx); } + ActionResult::NeedsRedraw + } + + fn clear_input_if_needed(&mut self, action: MultiSelectPromptAction) -> ActionResult { if !self.config.keep_filter { - self.input.clear(); + return ActionResult::Clean; } - ActionResult::NeedsRedraw + match action { + MultiSelectPromptAction::ToggleCurrentOption + | MultiSelectPromptAction::SelectAll + | MultiSelectPromptAction::ClearSelections => { + self.input.as_mut().map(Input::clear); + ActionResult::NeedsRedraw + } + _ => ActionResult::Clean, + } } fn validate_current_answer(&self) -> InquireResult { @@ -196,7 +196,21 @@ where } fn run_scorer(&mut self) { - let mut options = self.score_options(); + let content = match &self.input { + Some(input) => input.content(), + None => return, + }; + + let mut options = self + .options + .iter() + .enumerate() + .filter_map(|(i, opt)| { + (self.scorer)(content, opt, self.string_options.get(i).unwrap(), i) + .map(|score| (i, score)) + }) + .collect::>(); + options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); let new_scored_options = options.iter().map(|(idx, _)| *idx).collect::>(); @@ -270,33 +284,28 @@ where for idx in &self.scored_options { self.checked.insert(*idx); } - - if !self.config.keep_filter { - self.input.clear(); - } - ActionResult::NeedsRedraw } MultiSelectPromptAction::ClearSelections => { self.checked.clear(); - - if !self.config.keep_filter { - self.input.clear(); - } - ActionResult::NeedsRedraw } - MultiSelectPromptAction::FilterInput(input_action) => { - let result = self.input.handle(input_action); + MultiSelectPromptAction::FilterInput(input_action) => match self.input.as_mut() { + Some(input) => { + let result = input.handle(input_action); - if let InputActionResult::ContentChanged = result { - self.run_scorer(); - } + if let InputActionResult::ContentChanged = result { + self.run_scorer(); + } - result.into() - } + result.into() + } + None => ActionResult::Clean, + }, }; + let result = self.clear_input_if_needed(action).merge(result); + Ok(result) } @@ -307,7 +316,7 @@ where backend.render_error_message(err)?; } - backend.render_multiselect_prompt(prompt, &self.input)?; + backend.render_multiselect_prompt(prompt, self.input.as_ref())?; let choices = self .scored_options diff --git a/inquire/src/prompts/multiselect/test.rs b/inquire/src/prompts/multiselect/test.rs index 2419f82f..6a60800e 100644 --- a/inquire/src/prompts/multiselect/test.rs +++ b/inquire/src/prompts/multiselect/test.rs @@ -194,3 +194,45 @@ fn naive_assert_fuzzy_match_as_default_scorer() { assert_eq!(vec![ListOption::new(2, "Strawberry")], ans); } + +#[test] +fn chars_do_not_affect_prompt_without_filtering() { + let read: Vec = [ + KeyCode::Char('w'), + KeyCode::Char('r'), + KeyCode::Char('r'), + KeyCode::Char('y'), + KeyCode::Char(' '), + KeyCode::Enter, + ] + .iter() + .map(|c| KeyEvent::from(*c)) + .collect(); + + let mut read = read.iter(); + + let options = vec![ + "Banana", + "Apple", + "Strawberry", + "Grapes", + "Lemon", + "Tangerine", + "Watermelon", + "Orange", + "Pear", + "Avocado", + "Pineapple", + ]; + + let mut write: Vec = Vec::new(); + let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + + let ans = MultiSelect::new("Question", options) + .without_filtering() + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(vec![ListOption::new(0, "Banana")], ans); +} diff --git a/inquire/src/prompts/prompt.rs b/inquire/src/prompts/prompt.rs index f95bd4c6..65e80980 100644 --- a/inquire/src/prompts/prompt.rs +++ b/inquire/src/prompts/prompt.rs @@ -16,6 +16,20 @@ pub enum ActionResult { Clean, } +impl ActionResult { + pub fn merge(self, other: Self) -> Self { + match (self, other) { + (Self::NeedsRedraw, _) | (_, Self::NeedsRedraw) => Self::NeedsRedraw, + (Self::Clean, Self::Clean) => Self::Clean, + } + } + + /// Returns whether the action requires a redraw. + pub fn needs_redraw(&self) -> bool { + matches!(self, Self::NeedsRedraw) + } +} + impl From for ActionResult { fn from(value: InputActionResult) -> Self { if value.needs_redraw() { @@ -106,7 +120,7 @@ where let mut last_handle = ActionResult::NeedsRedraw; let final_answer = loop { - if let ActionResult::NeedsRedraw = last_handle { + if last_handle.needs_redraw() { backend.frame_setup()?; self.render(backend)?; backend.frame_finish()?; diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index 9f3b9912..64dc70d6 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -97,6 +97,11 @@ pub struct Select<'a, T> { /// Defaults to true. pub reset_cursor: bool, + /// Whether to allow the option list to be filtered by user input or not. + /// + /// Defaults to true. + pub filter_input_enabled: bool, + /// Function called with the current user input to score the provided /// options. pub scorer: Scorer<'a, T>, @@ -186,6 +191,10 @@ where /// Defaults to true. pub const DEFAULT_RESET_CURSOR: bool = true; + /// Default filter input enabled behaviour. + /// Defaults to true. + pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true; + /// Default help message. pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = Some("↑↓ to move, enter to select, type to filter"); @@ -200,6 +209,7 @@ where vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, reset_cursor: Self::DEFAULT_RESET_CURSOR, + filter_input_enabled: Self::DEFAULT_FILTER_INPUT_ENABLED, scorer: Self::DEFAULT_SCORER, formatter: Self::DEFAULT_FORMATTER, render_config: get_configuration(), @@ -267,6 +277,16 @@ where self } + /// Disables the filter input, which means the user will not be able to filter the options + /// by typing. + /// + /// This is useful when you want to simplify the UX if the filter does not add any value, + /// such as when the list is already short. + pub fn without_filtering(mut self) -> Self { + self.filter_input_enabled = false; + self + } + /// Sets the provided color theme to this prompt. /// /// Note: The default render config considers if the NO_COLOR environment variable diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index d79314c8..e7c065d5 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -22,7 +22,7 @@ pub struct SelectPrompt<'a, T> { scored_options: Vec, help_message: Option<&'a str>, cursor_index: usize, - input: Input, + input: Option, scorer: Scorer<'a, T>, formatter: OptionFormatter<'a, T>, } @@ -49,6 +49,13 @@ where let string_options = so.options.iter().map(T::to_string).collect(); let scored_options = (0..so.options.len()).collect(); + let input = match so.filter_input_enabled { + true => Some(Input::new_with( + so.starting_filter_input.unwrap_or_default(), + )), + false => None, + }; + Ok(Self { message: so.message, config: (&so).into(), @@ -57,31 +64,12 @@ where scored_options, help_message: so.help_message, cursor_index: so.starting_cursor, - input: so - .starting_filter_input - .map(Input::new_with) - .unwrap_or_else(Input::new), + input, scorer: so.scorer, formatter: so.formatter, }) } - fn score_options(&self) -> Vec<(usize, i64)> { - self.options - .iter() - .enumerate() - .filter_map(|(i, opt)| { - (self.scorer)( - self.input.content(), - opt, - self.string_options.get(i).unwrap(), - i, - ) - .map(|score| (i, score)) - }) - .collect::>() - } - fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult { let new_position = if wrap { let after_wrap = qty.saturating_sub(self.cursor_index); @@ -135,7 +123,21 @@ where } fn run_scorer(&mut self) { - let mut options = self.score_options(); + let content = match &self.input { + Some(input) => input.content(), + None => return, + }; + + let mut options = self + .options + .iter() + .enumerate() + .filter_map(|(i, opt)| { + (self.scorer)(content, opt, self.string_options.get(i).unwrap(), i) + .map(|score| (i, score)) + }) + .collect::>(); + options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); let new_scored_options = options.iter().map(|(idx, _)| *idx).collect::>(); @@ -197,15 +199,19 @@ where SelectPromptAction::PageDown => self.move_cursor_down(self.config.page_size, false), SelectPromptAction::MoveToStart => self.move_cursor_up(usize::MAX, false), SelectPromptAction::MoveToEnd => self.move_cursor_down(usize::MAX, false), - SelectPromptAction::FilterInput(input_action) => { - let result = self.input.handle(input_action); - if let InputActionResult::ContentChanged = result { - self.run_scorer(); - } + SelectPromptAction::FilterInput(input_action) => match self.input.as_mut() { + Some(input) => { + let result = input.handle(input_action); - result.into() - } + if let InputActionResult::ContentChanged = result { + self.run_scorer(); + } + + result.into() + } + None => ActionResult::Clean, + }, }; Ok(result) @@ -214,7 +220,7 @@ where fn render(&self, backend: &mut Backend) -> InquireResult<()> { let prompt = &self.message; - backend.render_select_prompt(prompt, &self.input)?; + backend.render_select_prompt(prompt, self.input.as_ref())?; let choices = self .scored_options diff --git a/inquire/src/prompts/select/test.rs b/inquire/src/prompts/select/test.rs index ff9f2d51..c3d2b29e 100644 --- a/inquire/src/prompts/select/test.rs +++ b/inquire/src/prompts/select/test.rs @@ -157,3 +157,44 @@ fn naive_assert_fuzzy_match_as_default_scorer() { assert_eq!(ListOption::new(2, "Strawberry"), ans); } + +#[test] +fn chars_do_not_affect_prompt_without_filtering() { + let read: Vec = [ + KeyCode::Char('w'), + KeyCode::Char('r'), + KeyCode::Char('r'), + KeyCode::Char('y'), + KeyCode::Enter, + ] + .iter() + .map(|c| KeyEvent::from(*c)) + .collect(); + + let mut read = read.iter(); + + let options = vec![ + "Banana", + "Apple", + "Strawberry", + "Grapes", + "Lemon", + "Tangerine", + "Watermelon", + "Orange", + "Pear", + "Avocado", + "Pineapple", + ]; + + let mut write: Vec = Vec::new(); + let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + + let ans = Select::new("Question", options) + .without_filtering() + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(ListOption::new(0, "Banana"), ans); +} diff --git a/inquire/src/ui/backend.rs b/inquire/src/ui/backend.rs index 7b950899..6506ebf9 100644 --- a/inquire/src/ui/backend.rs +++ b/inquire/src/ui/backend.rs @@ -45,12 +45,12 @@ pub trait EditorBackend: CommonBackend { } pub trait SelectBackend: CommonBackend { - fn render_select_prompt(&mut self, prompt: &str, cur_input: &Input) -> Result<()>; + fn render_select_prompt(&mut self, prompt: &str, cur_input: Option<&Input>) -> Result<()>; fn render_options(&mut self, page: Page<'_, ListOption>) -> Result<()>; } pub trait MultiSelectBackend: CommonBackend { - fn render_multiselect_prompt(&mut self, prompt: &str, cur_input: &Input) -> Result<()>; + fn render_multiselect_prompt(&mut self, prompt: &str, cur_input: Option<&Input>) -> Result<()>; fn render_options( &mut self, page: Page<'_, ListOption>, @@ -502,8 +502,12 @@ impl<'a, T> SelectBackend for Backend<'a, T> where T: Terminal, { - fn render_select_prompt(&mut self, prompt: &str, cur_input: &Input) -> Result<()> { - self.print_prompt_with_input(prompt, None, cur_input) + fn render_select_prompt(&mut self, prompt: &str, cur_input: Option<&Input>) -> Result<()> { + if let Some(input) = cur_input { + self.print_prompt_with_input(prompt, None, input) + } else { + self.print_prompt(prompt) + } } fn render_options(&mut self, page: Page<'_, ListOption>) -> Result<()> { @@ -530,8 +534,12 @@ impl<'a, T> MultiSelectBackend for Backend<'a, T> where T: Terminal, { - fn render_multiselect_prompt(&mut self, prompt: &str, cur_input: &Input) -> Result<()> { - self.print_prompt_with_input(prompt, None, cur_input) + fn render_multiselect_prompt(&mut self, prompt: &str, cur_input: Option<&Input>) -> Result<()> { + if let Some(input) = cur_input { + self.print_prompt_with_input(prompt, None, input) + } else { + self.print_prompt(prompt) + } } fn render_options( From 1503f5766d268ec83edf61145acd1bb34dc996bc Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Tue, 26 Dec 2023 22:39:02 -0800 Subject: [PATCH 5/6] Simplify creation of fake backend for tests (#206) * Simplify creation of fake backend for tests * Remove test temporarily --- inquire/src/prompts/dateselect/test.rs | 26 +-- inquire/src/prompts/mod.rs | 2 + inquire/src/prompts/multiselect/test.rs | 138 ++++----------- inquire/src/prompts/password/test.rs | 172 +++++++++---------- inquire/src/prompts/select/test.rs | 126 ++++---------- inquire/src/prompts/test.rs | 18 ++ inquire/src/prompts/text/test.rs | 87 +++++----- inquire/src/terminal/crossterm.rs | 214 ++++++++++++++---------- 8 files changed, 345 insertions(+), 438 deletions(-) create mode 100644 inquire/src/prompts/test.rs diff --git a/inquire/src/prompts/dateselect/test.rs b/inquire/src/prompts/dateselect/test.rs index 4ec6ef59..76d1d15c 100644 --- a/inquire/src/prompts/dateselect/test.rs +++ b/inquire/src/prompts/dateselect/test.rs @@ -1,12 +1,11 @@ use crate::{ date_utils::get_current_date, - terminal::crossterm::CrosstermTerminal, - ui::{Backend, RenderConfig}, + test::fake_backend, + ui::{Key, KeyModifiers}, validator::Validation, DateSelect, }; use chrono::NaiveDate; -use crossterm::event::{KeyCode, KeyEvent}; fn default<'a>() -> DateSelect<'a> { DateSelect::new("Question?") @@ -20,12 +19,7 @@ macro_rules! date_test { ($name:ident,$input:expr,$output:expr,$prompt:expr) => { #[test] fn $name() { - let read: Vec = $input.into_iter().map(KeyEvent::from).collect(); - let mut read = read.iter(); - - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + let mut backend = fake_backend($input); let ans = $prompt.prompt_with_backend(&mut backend).unwrap(); @@ -34,11 +28,11 @@ macro_rules! date_test { }; } -date_test!(today_date, vec![KeyCode::Enter], get_current_date()); +date_test!(today_date, vec![Key::Enter], get_current_date()); date_test!( custom_default_date, - vec![KeyCode::Enter], + vec![Key::Enter], NaiveDate::from_ymd_opt(2021, 1, 9).unwrap(), DateSelect::new("Date").with_default(NaiveDate::from_ymd_opt(2021, 1, 9).unwrap()) ); @@ -47,11 +41,7 @@ date_test!( /// Tests that a closure that actually closes on a variable can be used /// as a DateSelect validator. fn closure_validator() { - let read: Vec = vec![KeyCode::Enter, KeyCode::Left, KeyCode::Enter] - .into_iter() - .map(KeyEvent::from) - .collect(); - let mut read = read.iter(); + let mut backend = fake_backend(vec![Key::Enter, Key::Left(KeyModifiers::NONE), Key::Enter]); let today_date = get_current_date(); @@ -63,10 +53,6 @@ fn closure_validator() { } }; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = DateSelect::new("Question") .with_validator(validator) .prompt_with_backend(&mut backend) diff --git a/inquire/src/prompts/mod.rs b/inquire/src/prompts/mod.rs index 9c6b600c..1475629a 100644 --- a/inquire/src/prompts/mod.rs +++ b/inquire/src/prompts/mod.rs @@ -10,6 +10,8 @@ mod one_liners; mod password; mod prompt; mod select; +#[cfg(test)] +pub(crate) mod test; mod text; pub use action::*; diff --git a/inquire/src/prompts/multiselect/test.rs b/inquire/src/prompts/multiselect/test.rs index 6a60800e..8c5bd098 100644 --- a/inquire/src/prompts/multiselect/test.rs +++ b/inquire/src/prompts/multiselect/test.rs @@ -1,31 +1,22 @@ use crate::{ formatter::MultiOptionFormatter, list_option::ListOption, - terminal::crossterm::CrosstermTerminal, - ui::{Backend, RenderConfig}, + test::fake_backend, + ui::{Key, KeyModifiers}, MultiSelect, }; -use crossterm::event::{KeyCode, KeyEvent}; #[test] /// Tests that a closure that actually closes on a variable can be used /// as a Select formatter. fn closure_formatter() { - let read: Vec = vec![KeyCode::Char(' '), KeyCode::Enter] - .into_iter() - .map(KeyEvent::from) - .collect(); - let mut read = read.iter(); + let mut backend = fake_backend(vec![Key::Char(' ', KeyModifiers::NONE), Key::Enter]); let formatted = String::from("Thanks!"); let formatter: MultiOptionFormatter<'_, i32> = &|_| formatted.clone(); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = MultiSelect::new("Question", options) .with_formatter(formatter) .prompt_with_backend(&mut backend) @@ -37,27 +28,18 @@ fn closure_formatter() { #[test] // Anti-regression test: https://github.com/mikaelmello/inquire/issues/30 fn down_arrow_on_empty_list_does_not_panic() { - let read: Vec = [ - KeyCode::Char('9'), - KeyCode::Down, - KeyCode::Backspace, - KeyCode::Char('3'), - KeyCode::Down, - KeyCode::Backspace, - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('9', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Backspace, + Key::Char('3', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Backspace, + Key::Enter, + ]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = MultiSelect::new("Question", options) .prompt_with_backend(&mut backend) .unwrap(); @@ -67,19 +49,9 @@ fn down_arrow_on_empty_list_does_not_panic() { #[test] fn selecting_all_by_default_behavior() { - let read: Vec = [KeyCode::Enter, KeyCode::Enter] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); - + let mut backend = fake_backend(vec![Key::Enter, Key::Enter]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let answer_with_all_selected_by_default = MultiSelect::new("Question", options.clone()) .with_all_selected_by_default() .prompt_with_backend(&mut backend) @@ -105,24 +77,16 @@ fn selecting_all_by_default_behavior() { #[test] // Anti-regression test: https://github.com/mikaelmello/inquire/issues/31 fn list_option_indexes_are_relative_to_input_vec() { - let read: Vec = vec![ - KeyCode::Down, - KeyCode::Char(' '), - KeyCode::Down, - KeyCode::Char(' '), - KeyCode::Enter, - ] - .into_iter() - .map(KeyEvent::from) - .collect(); - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Down(KeyModifiers::NONE), + Key::Char(' ', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Char(' ', KeyModifiers::NONE), + Key::Enter, + ]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = MultiSelect::new("Question", options) .prompt_with_backend(&mut backend) .unwrap(); @@ -133,19 +97,9 @@ fn list_option_indexes_are_relative_to_input_vec() { #[test] // Anti-regression test: https://github.com/mikaelmello/inquire/issues/195 fn starting_cursor_is_respected() { - let read: Vec = [KeyCode::Char(' '), KeyCode::Enter] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); - + let mut backend = fake_backend(vec![Key::Char(' ', KeyModifiers::NONE), Key::Enter]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = MultiSelect::new("Question", options) .with_starting_cursor(2) .prompt_with_backend(&mut backend) @@ -156,19 +110,14 @@ fn starting_cursor_is_respected() { #[test] fn naive_assert_fuzzy_match_as_default_scorer() { - let read: Vec = [ - KeyCode::Char('w'), - KeyCode::Char('r'), - KeyCode::Char('r'), - KeyCode::Char('y'), - KeyCode::Char(' '), - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('w', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('y', KeyModifiers::NONE), + Key::Char(' ', KeyModifiers::NONE), + Key::Enter, + ]); let options = vec![ "Banana", @@ -184,10 +133,6 @@ fn naive_assert_fuzzy_match_as_default_scorer() { "Pineapple", ]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = MultiSelect::new("Question", options) .prompt_with_backend(&mut backend) .unwrap(); @@ -197,19 +142,14 @@ fn naive_assert_fuzzy_match_as_default_scorer() { #[test] fn chars_do_not_affect_prompt_without_filtering() { - let read: Vec = [ - KeyCode::Char('w'), - KeyCode::Char('r'), - KeyCode::Char('r'), - KeyCode::Char('y'), - KeyCode::Char(' '), - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('w', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('y', KeyModifiers::NONE), + Key::Char(' ', KeyModifiers::NONE), + Key::Enter, + ]); let options = vec![ "Banana", @@ -225,10 +165,6 @@ fn chars_do_not_affect_prompt_without_filtering() { "Pineapple", ]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = MultiSelect::new("Question", options) .without_filtering() .prompt_with_backend(&mut backend) diff --git a/inquire/src/prompts/password/test.rs b/inquire/src/prompts/password/test.rs index 494d2abc..cfad798a 100644 --- a/inquire/src/prompts/password/test.rs +++ b/inquire/src/prompts/password/test.rs @@ -1,14 +1,13 @@ use super::Password; -use crate::{ - terminal::crossterm::CrosstermTerminal, - ui::{Backend, RenderConfig}, - validator::{ErrorMessage, Validation}, -}; -use crossterm::event::{KeyCode, KeyEvent}; +use crate::ui::{Key, KeyModifiers}; +use crate::validator::{ErrorMessage, Validation}; macro_rules! text_to_events { ($text:expr) => {{ - $text.chars().map(KeyCode::Char) + $text + .chars() + .map(|c| Key::Char(c, KeyModifiers::NONE)) + .collect() }}; } @@ -17,12 +16,7 @@ macro_rules! password_test { #[test] $(#[$meta])? fn $name() { - let read: Vec = $input.into_iter().map(KeyEvent::from).collect(); - let mut read = read.iter(); - - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + let mut backend = crate::prompts::test::fake_backend($input); let ans = $prompt.prompt_with_backend(&mut backend).unwrap(); @@ -33,14 +27,14 @@ macro_rules! password_test { password_test!( empty, - vec![KeyCode::Enter], + vec![Key::Enter], "", Password::new("").without_confirmation() ); password_test!( single_letter, - vec![KeyCode::Char('b'), KeyCode::Enter], + vec![Key::Char('b', KeyModifiers::NONE), Key::Enter], "b", Password::new("").without_confirmation() ); @@ -63,13 +57,13 @@ password_test!( input_and_correction, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("normal input").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("normal input")); + events.push(Key::Enter); events }, "normal input", @@ -80,19 +74,19 @@ password_test!( input_and_excessive_correction, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("normal input").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("normal input")); + events.push(Key::Enter); events }, "normal input", @@ -103,15 +97,15 @@ password_test!( input_correction_after_validation_when_masked, { let mut events = vec![]; - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("yes").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("yes")); + events.push(Key::Enter); events }, "12345yes", @@ -128,15 +122,15 @@ password_test!( input_correction_after_validation_when_full, { let mut events = vec![]; - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("yes").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("yes")); + events.push(Key::Enter); events }, "12345yes", @@ -153,10 +147,10 @@ password_test!( input_correction_after_validation_when_hidden, { let mut events = vec![]; - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("yesyes").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); + events.append(&mut text_to_events!("yesyes")); + events.push(Key::Enter); events }, "yesyes", @@ -173,10 +167,10 @@ password_test!( input_confirmation_same, { let mut events = vec![]; - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); events }, "1234567890", @@ -188,10 +182,10 @@ password_test!( input_confirmation_different, { let mut events = vec![]; - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("abcdefghij").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); + events.append(&mut text_to_events!("abcdefghij")); + events.push(Key::Enter); events }, "", @@ -203,16 +197,16 @@ password_test!( prompt_with_hidden_should_clear_on_mismatch, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("anor2").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); + events.append(&mut text_to_events!("anor2")); + events.push(Key::Enter); // The problem is that the 1st input values were not cleared // and the lack of a change in the 1st prompt can be confusing. - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); events }, "anor", @@ -224,16 +218,16 @@ password_test!( prompt_with_full_should_clear_1st_on_mismatch, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("anor2").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); + events.append(&mut text_to_events!("anor2")); + events.push(Key::Enter); // The problem is that the 1st input values were not cleared // and the lack of a change in the 1st prompt can be confusing. - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); events }, "anor", @@ -245,16 +239,16 @@ password_test!( prompt_with_masked_should_clear_1st_on_mismatch, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("anor2").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); + events.append(&mut text_to_events!("anor2")); + events.push(Key::Enter); // The problem is that the 1st input values were not cleared // and the lack of a change in the 1st prompt can be confusing. - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Enter); events }, "anor", diff --git a/inquire/src/prompts/select/test.rs b/inquire/src/prompts/select/test.rs index c3d2b29e..8b00e387 100644 --- a/inquire/src/prompts/select/test.rs +++ b/inquire/src/prompts/select/test.rs @@ -1,31 +1,20 @@ use crate::{ formatter::OptionFormatter, list_option::ListOption, - terminal::crossterm::CrosstermTerminal, - ui::{Backend, RenderConfig}, + test::fake_backend, + ui::{Key, KeyModifiers}, Select, }; -use crossterm::event::{KeyCode, KeyEvent}; #[test] /// Tests that a closure that actually closes on a variable can be used /// as a Select formatter. fn closure_formatter() { - let read: Vec = vec![KeyCode::Down, KeyCode::Enter] - .into_iter() - .map(KeyEvent::from) - .collect(); - let mut read = read.iter(); - - let formatted = String::from("Thanks!"); - let formatter: OptionFormatter<'_, i32> = &|_| formatted.clone(); + let mut backend = fake_backend(vec![Key::Down(KeyModifiers::NONE), Key::Enter]); + let formatter: OptionFormatter<'_, i32> = &|_| String::from("Thanks!"); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = Select::new("Question", options) .with_formatter(formatter) .prompt_with_backend(&mut backend) @@ -37,25 +26,16 @@ fn closure_formatter() { #[test] // Anti-regression test: https://github.com/mikaelmello/inquire/issues/29 fn enter_arrow_on_empty_list_does_not_panic() { - let read: Vec = [ - KeyCode::Char('9'), - KeyCode::Enter, - KeyCode::Backspace, - KeyCode::Char('3'), - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('9', KeyModifiers::NONE), + Key::Enter, + Key::Backspace, + Key::Char('3', KeyModifiers::NONE), + Key::Enter, + ]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = Select::new("Question", options) .prompt_with_backend(&mut backend) .unwrap(); @@ -66,27 +46,18 @@ fn enter_arrow_on_empty_list_does_not_panic() { #[test] // Anti-regression test: https://github.com/mikaelmello/inquire/issues/30 fn down_arrow_on_empty_list_does_not_panic() { - let read: Vec = [ - KeyCode::Char('9'), - KeyCode::Down, - KeyCode::Backspace, - KeyCode::Char('3'), - KeyCode::Down, - KeyCode::Backspace, - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('9', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Backspace, + Key::Char('3', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Backspace, + Key::Enter, + ]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = Select::new("Question", options) .prompt_with_backend(&mut backend) .unwrap(); @@ -97,19 +68,10 @@ fn down_arrow_on_empty_list_does_not_panic() { #[test] // Anti-regression test: https://github.com/mikaelmello/inquire/issues/195 fn starting_cursor_is_respected() { - let read: Vec = [KeyCode::Enter] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![Key::Enter]); let options = vec![1, 2, 3]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = Select::new("Question", options) .with_starting_cursor(2) .prompt_with_backend(&mut backend) @@ -120,18 +82,13 @@ fn starting_cursor_is_respected() { #[test] fn naive_assert_fuzzy_match_as_default_scorer() { - let read: Vec = [ - KeyCode::Char('w'), - KeyCode::Char('r'), - KeyCode::Char('r'), - KeyCode::Char('y'), - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('w', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('y', KeyModifiers::NONE), + Key::Enter, + ]); let options = vec![ "Banana", @@ -147,10 +104,6 @@ fn naive_assert_fuzzy_match_as_default_scorer() { "Pineapple", ]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = Select::new("Question", options) .prompt_with_backend(&mut backend) .unwrap(); @@ -160,18 +113,13 @@ fn naive_assert_fuzzy_match_as_default_scorer() { #[test] fn chars_do_not_affect_prompt_without_filtering() { - let read: Vec = [ - KeyCode::Char('w'), - KeyCode::Char('r'), - KeyCode::Char('r'), - KeyCode::Char('y'), - KeyCode::Enter, - ] - .iter() - .map(|c| KeyEvent::from(*c)) - .collect(); - - let mut read = read.iter(); + let mut backend = fake_backend(vec![ + Key::Char('w', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('r', KeyModifiers::NONE), + Key::Char('y', KeyModifiers::NONE), + Key::Enter, + ]); let options = vec![ "Banana", @@ -187,10 +135,6 @@ fn chars_do_not_affect_prompt_without_filtering() { "Pineapple", ]; - let mut write: Vec = Vec::new(); - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); - let ans = Select::new("Question", options) .without_filtering() .prompt_with_backend(&mut backend) diff --git a/inquire/src/prompts/test.rs b/inquire/src/prompts/test.rs new file mode 100644 index 00000000..2f9f629c --- /dev/null +++ b/inquire/src/prompts/test.rs @@ -0,0 +1,18 @@ +use std::convert::TryFrom; + +use crossterm::event::KeyEvent; + +use crate::{ + terminal::crossterm::CrosstermTerminal, + ui::{Backend, Key, RenderConfig}, +}; + +pub fn fake_backend(input: Vec) -> Backend<'static, CrosstermTerminal> { + let events: Vec = input + .into_iter() + .map(|k| KeyEvent::try_from(k).expect("Could not convert Key to KeyEvent")) + .collect(); + let terminal = CrosstermTerminal::new_with_io(events.into()); + + Backend::new(terminal, RenderConfig::default()).unwrap() +} diff --git a/inquire/src/prompts/text/test.rs b/inquire/src/prompts/text/test.rs index 94f0099e..35d43e58 100644 --- a/inquire/src/prompts/text/test.rs +++ b/inquire/src/prompts/text/test.rs @@ -1,10 +1,6 @@ use super::Text; -use crate::{ - terminal::crossterm::CrosstermTerminal, - ui::{Backend, RenderConfig}, - validator::{ErrorMessage, Validation}, -}; -use crossterm::event::{KeyCode, KeyEvent}; +use crate::ui::{Key, KeyModifiers}; +use crate::validator::{ErrorMessage, Validation}; fn default<'a>() -> Text<'a> { Text::new("Question?") @@ -12,7 +8,10 @@ fn default<'a>() -> Text<'a> { macro_rules! text_to_events { ($text:expr) => {{ - $text.chars().map(KeyCode::Char) + $text + .chars() + .map(|c| Key::Char(c, KeyModifiers::NONE)) + .collect::>() }}; } @@ -24,13 +23,7 @@ macro_rules! text_test { ($name:ident,$input:expr,$output:expr,$prompt:expr) => { #[test] fn $name() { - let read: Vec = $input.into_iter().map(KeyEvent::from).collect(); - let mut read = read.iter(); - - let mut write: Vec = Vec::new(); - - let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap(); + let mut backend = crate::prompts::test::fake_backend($input); let ans = $prompt.prompt_with_backend(&mut backend).unwrap(); @@ -39,9 +32,13 @@ macro_rules! text_test { }; } -text_test!(empty, vec![KeyCode::Enter], ""); +text_test!(empty, vec![Key::Enter], ""); -text_test!(single_letter, vec![KeyCode::Char('b'), KeyCode::Enter], "b"); +text_test!( + single_letter, + vec![Key::Char('b', KeyModifiers::NONE), Key::Enter], + "b" +); text_test!( letters_and_enter, @@ -59,13 +56,13 @@ text_test!( input_and_correction, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("normal input").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("normal input")); + events.push(Key::Enter); events }, "normal input" @@ -75,19 +72,19 @@ text_test!( input_and_excessive_correction, { let mut events = vec![]; - events.append(&mut text_to_events!("anor").collect()); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("normal input").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("anor")); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("normal input")); + events.push(Key::Enter); events }, "normal input" @@ -97,15 +94,15 @@ text_test!( input_correction_after_validation, { let mut events = vec![]; - events.append(&mut text_to_events!("1234567890").collect()); - events.push(KeyCode::Enter); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.push(KeyCode::Backspace); - events.append(&mut text_to_events!("yes").collect()); - events.push(KeyCode::Enter); + events.append(&mut text_to_events!("1234567890")); + events.push(Key::Enter); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.push(Key::Backspace); + events.append(&mut text_to_events!("yes")); + events.push(Key::Enter); events }, "12345yes", diff --git a/inquire/src/terminal/crossterm.rs b/inquire/src/terminal/crossterm.rs index d0adcdf9..555c7102 100644 --- a/inquire/src/terminal/crossterm.rs +++ b/inquire/src/terminal/crossterm.rs @@ -1,4 +1,7 @@ -use std::io::{stderr, Result, Stderr, Write}; +use std::{ + collections::VecDeque, + io::{stderr, Result, Stderr, Write}, +}; use crossterm::{ cursor, @@ -16,23 +19,23 @@ use crate::{ use super::{Terminal, INITIAL_IN_MEMORY_CAPACITY}; -enum IO<'a> { +enum IO { Std { w: Stderr, }, #[allow(unused)] - Custom { - r: &'a mut dyn Iterator, - w: &'a mut (dyn Write), + Test { + w: Vec, + r: VecDeque, }, } -pub struct CrosstermTerminal<'a> { - io: IO<'a>, +pub struct CrosstermTerminal { + io: IO, in_memory_content: String, } -impl<'a> CrosstermTerminal<'a> { +impl CrosstermTerminal { pub fn new() -> InquireResult { crossterm::terminal::enable_raw_mode()?; @@ -42,27 +45,10 @@ impl<'a> CrosstermTerminal<'a> { }) } - /// # Errors - /// - /// Will return `std::io::Error` if it fails to get terminal size - #[cfg(test)] - pub fn new_with_io( - writer: &'a mut W, - reader: &'a mut dyn Iterator, - ) -> Self { - Self { - io: IO::Custom { - r: reader, - w: writer, - }, - in_memory_content: String::with_capacity(INITIAL_IN_MEMORY_CAPACITY), - } - } - fn get_writer(&mut self) -> &mut dyn Write { match &mut self.io { IO::Std { w } => w, - IO::Custom { r: _, w } => w, + IO::Test { r: _, w } => w, } } @@ -102,7 +88,7 @@ impl<'a> CrosstermTerminal<'a> { } } -impl<'a> Terminal for CrosstermTerminal<'a> { +impl Terminal for CrosstermTerminal { fn cursor_up(&mut self, cnt: u16) -> Result<()> { self.write_command(cursor::MoveUp(cnt)) } @@ -123,9 +109,11 @@ impl<'a> Terminal for CrosstermTerminal<'a> { return Ok(key_event.into()); } } - IO::Custom { r, w: _ } => { - let key = r.next().expect("Custom stream of characters has ended"); - return Ok((*key).into()); + IO::Test { r, w: _ } => { + let key = r + .pop_front() + .expect("Custom stream of characters has ended"); + return Ok(key.into()); } } } @@ -194,12 +182,12 @@ impl<'a> Terminal for CrosstermTerminal<'a> { } } -impl<'a> Drop for CrosstermTerminal<'a> { +impl Drop for CrosstermTerminal { fn drop(&mut self) { let _unused = self.flush(); let _unused = match self.io { IO::Std { w: _ } => terminal::disable_raw_mode(), - IO::Custom { r: _, w: _ } => Ok(()), + IO::Test { r: _, w: _ } => Ok(()), }; } } @@ -322,116 +310,158 @@ impl From for Key { #[cfg(test)] mod test { + use std::collections::VecDeque; + use std::convert::TryFrom; + use std::convert::TryInto; + + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use crate::terminal::Terminal; + use crate::terminal::INITIAL_IN_MEMORY_CAPACITY; use crate::ui::Color; + use crate::ui::Key; use super::Attributes; use super::CrosstermTerminal; + use super::IO; - #[test] - fn writer() { - let mut write: Vec = Vec::new(); - let read = Vec::new(); - let mut read = read.iter(); + impl TryFrom for KeyModifiers { + type Error = (); - { - let mut terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); + fn try_from(value: crate::ui::KeyModifiers) -> Result { + Self::from_bits(value.bits()).ok_or(()) + } + } - terminal.write("testing ").unwrap(); - terminal.write("writing ").unwrap(); - terminal.flush().unwrap(); - terminal.write("wow").unwrap(); + impl TryFrom for KeyEvent { + type Error = (); + + fn try_from(value: Key) -> Result { + let key_event = match value { + Key::Escape => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), + Key::Enter => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), + Key::Backspace => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), + Key::Tab => KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), + Key::Delete(m) => KeyEvent::new(KeyCode::Delete, m.try_into()?), + Key::Home => KeyEvent::new(KeyCode::Home, KeyModifiers::NONE), + Key::End => KeyEvent::new(KeyCode::End, KeyModifiers::NONE), + Key::PageUp => KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE), + Key::PageDown => KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE), + Key::Up(m) => KeyEvent::new(KeyCode::Up, m.try_into()?), + Key::Down(m) => KeyEvent::new(KeyCode::Down, m.try_into()?), + Key::Left(m) => KeyEvent::new(KeyCode::Left, m.try_into()?), + Key::Right(m) => KeyEvent::new(KeyCode::Right, m.try_into()?), + Key::Char(c, m) => KeyEvent::new(KeyCode::Char(c), m.try_into()?), + #[allow(deprecated)] + Key::Any => KeyEvent::new(KeyCode::Null, KeyModifiers::NONE), + }; + + Ok(key_event) } + } + + impl CrosstermTerminal { + pub fn new_with_io(events: VecDeque) -> Self { + Self { + io: IO::Test { + r: events, + w: Vec::new(), + }, + in_memory_content: String::with_capacity(INITIAL_IN_MEMORY_CAPACITY), + } + } + + pub fn get_buffer_content(&mut self) -> Vec { + match &mut self.io { + IO::Std { w: _ } => panic!("Cannot get write buffer from standard output"), + IO::Test { r: _, w } => { + let mut buffer = Vec::new(); + std::mem::swap(&mut buffer, w); + buffer + } + } + } + } + + #[test] + fn writer() { + let mut terminal = CrosstermTerminal::new_with_io(VecDeque::new()); + + terminal.write("testing ").unwrap(); + terminal.write("writing ").unwrap(); + terminal.flush().unwrap(); + terminal.write("wow").unwrap(); #[cfg(unix)] - assert_eq!("testing writing wow", std::str::from_utf8(&write).unwrap()); + assert_eq!( + "testing writing wow", + std::str::from_utf8(&terminal.get_buffer_content()).unwrap() + ); } #[test] fn style_management() { - let mut write: Vec = Vec::new(); - let read = Vec::new(); - let mut read = read.iter(); + let mut terminal = CrosstermTerminal::new_with_io(VecDeque::new()); - { - let mut terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - - terminal.set_attributes(Attributes::BOLD).unwrap(); - terminal.set_attributes(Attributes::ITALIC).unwrap(); - terminal.set_attributes(Attributes::BOLD).unwrap(); - terminal.reset_attributes().unwrap(); - } + terminal.set_attributes(Attributes::BOLD).unwrap(); + terminal.set_attributes(Attributes::ITALIC).unwrap(); + terminal.set_attributes(Attributes::BOLD).unwrap(); + terminal.reset_attributes().unwrap(); #[cfg(unix)] assert_eq!( "\x1B[1m\x1B[3m\x1B[1m\x1B[0m", - std::str::from_utf8(&write).unwrap() + std::str::from_utf8(&terminal.get_buffer_content()).unwrap() ); } #[test] fn style_management_with_flags() { - let mut write: Vec = Vec::new(); - let read = Vec::new(); - let mut read = read.iter(); + let mut terminal = CrosstermTerminal::new_with_io(VecDeque::new()); - { - let mut terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - - terminal - .set_attributes(Attributes::BOLD | Attributes::ITALIC | Attributes::BOLD) - .unwrap(); - terminal.reset_attributes().unwrap(); - } + terminal + .set_attributes(Attributes::BOLD | Attributes::ITALIC | Attributes::BOLD) + .unwrap(); + terminal.reset_attributes().unwrap(); #[cfg(unix)] assert_eq!( "\x1B[1m\x1B[3m\x1B[0m", - std::str::from_utf8(&write).unwrap() + std::str::from_utf8(&terminal.get_buffer_content()).unwrap() ); } #[test] fn fg_color_management() { - let mut write: Vec = Vec::new(); - let read = Vec::new(); - let mut read = read.iter(); + let mut terminal = CrosstermTerminal::new_with_io(VecDeque::new()); - { - let mut terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - - terminal.set_fg_color(Color::LightRed).unwrap(); - terminal.reset_fg_color().unwrap(); - terminal.set_fg_color(Color::Black).unwrap(); - terminal.set_fg_color(Color::LightGreen).unwrap(); - } + terminal.set_fg_color(Color::LightRed).unwrap(); + terminal.reset_fg_color().unwrap(); + terminal.set_fg_color(Color::Black).unwrap(); + terminal.set_fg_color(Color::LightGreen).unwrap(); #[cfg(unix)] assert_eq!( "\x1B[38;5;9m\x1B[39m\x1B[38;5;0m\x1B[38;5;10m", - std::str::from_utf8(&write).unwrap() + std::str::from_utf8(&terminal.get_buffer_content()).unwrap() ); } #[test] fn bg_color_management() { - let mut write: Vec = Vec::new(); - let read = Vec::new(); - let mut read = read.iter(); + let mut terminal = CrosstermTerminal::new_with_io(VecDeque::new()); - { - let mut terminal = CrosstermTerminal::new_with_io(&mut write, &mut read); - - terminal.set_bg_color(Color::LightRed).unwrap(); - terminal.reset_bg_color().unwrap(); - terminal.set_bg_color(Color::Black).unwrap(); - terminal.set_bg_color(Color::LightGreen).unwrap(); - } + terminal.set_bg_color(Color::LightRed).unwrap(); + terminal.reset_bg_color().unwrap(); + terminal.set_bg_color(Color::Black).unwrap(); + terminal.set_bg_color(Color::LightGreen).unwrap(); #[cfg(unix)] assert_eq!( "\x1B[48;5;9m\x1B[49m\x1B[48;5;0m\x1B[48;5;10m", - std::str::from_utf8(&write).unwrap() + std::str::from_utf8(&terminal.get_buffer_content()).unwrap() ); } } From 2575ad18d1230feff7f7e916cb9e357ba52ac28c Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 27 Dec 2023 00:27:38 -0800 Subject: [PATCH 6/6] Several improvements on DateSelect (#205) * Add vim-like keybindings for monthly/yearly shifts in DateSelect * Revamp * Fix trailing whitespace --- CHANGELOG.md | 9 +- inquire/src/prompts/dateselect/action.rs | 75 +++--- inquire/src/prompts/dateselect/config.rs | 2 +- inquire/src/prompts/dateselect/mod.rs | 2 +- inquire/src/prompts/dateselect/prompt.rs | 39 ++-- inquire/src/prompts/dateselect/test.rs | 265 ++++++++++++++++++++++ inquire/src/prompts/multiselect/action.rs | 4 +- inquire/src/prompts/select/action.rs | 4 +- inquire/src/prompts/text/action.rs | 4 +- inquire/src/terminal/console.rs | 4 +- inquire/src/terminal/crossterm.rs | 19 +- inquire/src/terminal/termion.rs | 4 +- inquire/src/ui/key.rs | 7 +- 13 files changed, 364 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6253a6..a1ef8b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,13 @@ - Implement fuzzy search as default on Select and MultiSelect prompts. [#176](https://github.com/mikaelmello/inquire/pull/176) - Add new option on Select/MultiSelect prompts allowing to reset selection to the first item on filter-input changes. [#176](https://github.com/mikaelmello/inquire/pull/176) - Emacs-like keybindings added where applicable: -- Ctrl-p/Ctrl-n for up/down -- Ctrl-b/Ctrl-f for left/right -- Ctrl-j/Ctrl-g for enter/cancel + - Ctrl-p/Ctrl-n for up/down + - Ctrl-b/Ctrl-f for left/right + - Ctrl-j/Ctrl-g for enter/cancel - Added 'with_starting_filter_input' to both Select and MultiSelect, which allows for setting an initial value to the filter section of the prompt. - Added 'without_filtering' to both Select and MultiSelect, useful when you want to simplify the UX if the filter does not add any value, such as when the list is already short. -- Added 'with_answered_prompt_prefix' to RenderConfig to allow customization of answered prompt prefix +- Added 'with_answered_prompt_prefix' to RenderConfig to allow customization of answered prompt prefix. +- Revamped keybindings for DateSelect. ### Fixes diff --git a/inquire/src/prompts/dateselect/action.rs b/inquire/src/prompts/dateselect/action.rs index 451dc017..165f780e 100644 --- a/inquire/src/prompts/dateselect/action.rs +++ b/inquire/src/prompts/dateselect/action.rs @@ -30,38 +30,53 @@ pub enum DateSelectPromptAction { impl InnerAction for DateSelectPromptAction { type Config = DateSelectConfig; - fn from_key(key: Key, config: &DateSelectConfig) -> Option { - if config.vim_mode { - let action = match key { - Key::Char('k', KeyModifiers::NONE) => Some(Self::GoToPrevWeek), - Key::Char('j', KeyModifiers::NONE) => Some(Self::GoToNextWeek), - Key::Char('h', KeyModifiers::NONE) => Some(Self::GoToPrevDay), - Key::Char('l', KeyModifiers::NONE) => Some(Self::GoToNextDay), - _ => None, - }; + fn from_key(key: Key, _: &DateSelectConfig) -> Option { + let action = match key { + Key::Left(KeyModifiers::NONE) // standard + | Key::Char('b', KeyModifiers::CONTROL) // emacs + | Key::Char('h', KeyModifiers::NONE) // vim + => Self::GoToPrevDay, - if action.is_some() { - return action; - } - } + Key::Right(KeyModifiers::NONE) // standard + | Key::Char('f', KeyModifiers::CONTROL) // emacs + | Key::Char('l', KeyModifiers::NONE) // vim + => Self::GoToNextDay, + + Key::Up(KeyModifiers::NONE) // standard + | Key::Char('p', KeyModifiers::CONTROL) // emacs + | Key::Char('k', KeyModifiers::NONE) // vim + => Self::GoToPrevWeek, + + Key::Down(KeyModifiers::NONE) // standard + | Key::Char('n', KeyModifiers::CONTROL) // emacs + | Key::Char('j', KeyModifiers::NONE) // vim + | Key::Tab // not sure? keeping it for compatibility reasons now + => Self::GoToNextWeek, + + Key::PageUp(KeyModifiers::NONE) // standard + | Key::Char('[', KeyModifiers::NONE) // alternative when page up is not available + | Key::Left(_) // alternative 2, when the left above with no modifiers is not matched + | Key::Char('v' | 'V', KeyModifiers::ALT | KeyModifiers::META) // emacs + | Key::Char('b' | 'B', _) // vim, ideally ctrl-b should be used, but it's not available due to emacs + => Self::GoToPrevMonth, + + Key::PageDown(KeyModifiers::NONE) // standard + | Key::Char(']', KeyModifiers::NONE) // alternative when page down is not available + | Key::Right(_) // alternative 2, when the right above with no modifiers is not matched + | Key::Char('v' | 'V', KeyModifiers::CONTROL) // emacs + | Key::Char('f' | 'F', _) // vim, ideally ctrl-f should be used, but it's not available due to emacs + => Self::GoToNextMonth, + + Key::PageUp(_) // standard, when the above with no modifiers is not matched + | Key::Char('{' | '[', _) // alternative when page up is not available + | Key::Up(_) // alternative 2, when the up above with no modifiers is not matched + => Self::GoToPrevYear, + + Key::PageDown(_) // standard, when the above with no modifiers is not matched + | Key::Char('}' | ']', _) // alternative when page down is not available + | Key::Down(_) // alternative 2, when the down above with no modifiers is not matched + => Self::GoToNextYear, - let action = match key { - Key::Left(KeyModifiers::NONE) | Key::Char('b', KeyModifiers::CONTROL) => { - Self::GoToPrevDay - } - Key::Right(KeyModifiers::NONE) | Key::Char('f', KeyModifiers::CONTROL) => { - Self::GoToNextDay - } - Key::Up(KeyModifiers::NONE) | Key::Char('p', KeyModifiers::CONTROL) => { - Self::GoToPrevWeek - } - Key::Down(KeyModifiers::NONE) | Key::Char('n', KeyModifiers::CONTROL) | Key::Tab => { - Self::GoToNextWeek - } - Key::Left(KeyModifiers::CONTROL) => Self::GoToPrevMonth, - Key::Right(KeyModifiers::CONTROL) => Self::GoToNextMonth, - Key::Up(KeyModifiers::CONTROL) => Self::GoToPrevYear, - Key::Down(KeyModifiers::CONTROL) => Self::GoToNextYear, _ => return None, }; diff --git a/inquire/src/prompts/dateselect/config.rs b/inquire/src/prompts/dateselect/config.rs index f92867d0..61c26b54 100644 --- a/inquire/src/prompts/dateselect/config.rs +++ b/inquire/src/prompts/dateselect/config.rs @@ -5,7 +5,7 @@ use crate::DateSelect; /// Configuration settings used in the execution of a DateSelectPrompt. #[derive(Copy, Clone, Debug)] pub struct DateSelectConfig { - /// Whether to use vim-style keybindings. + /// Whether to support vim-style keybindings. pub vim_mode: bool, /// Min date allowed to be selected. diff --git a/inquire/src/prompts/dateselect/mod.rs b/inquire/src/prompts/dateselect/mod.rs index 6b890cd2..1a9e5002 100644 --- a/inquire/src/prompts/dateselect/mod.rs +++ b/inquire/src/prompts/dateselect/mod.rs @@ -119,7 +119,7 @@ impl<'a> DateSelect<'a> { /// Default help message. pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = - Some("arrows to move, with ctrl to move months and years, enter to select"); + Some("arrows to move, []{} move months and years, enter to select"); /// Default validators added to the [DateSelect] prompt, none. pub const DEFAULT_VALIDATORS: Vec> = vec![]; diff --git a/inquire/src/prompts/dateselect/prompt.rs b/inquire/src/prompts/dateselect/prompt.rs index 75a1eb77..cf968710 100644 --- a/inquire/src/prompts/dateselect/prompt.rs +++ b/inquire/src/prompts/dateselect/prompt.rs @@ -1,9 +1,9 @@ use std::{ - cmp::{max, min}, + cmp::{max, min, Ordering}, ops::Add, }; -use chrono::{Datelike, Duration, NaiveDate}; +use chrono::{Datelike, Duration, Months, NaiveDate}; use crate::{ date_utils::{get_current_date, get_month}, @@ -60,27 +60,22 @@ impl<'a> DateSelectPrompt<'a> { } fn shift_months(&mut self, qty: i32) -> ActionResult { - let date = self.current_date; - - let years = qty / 12; - let months = qty % 12; - - let new_year = date.year() + years; - let cur_month = date.month0() as i32; - let mut new_month = (cur_month + months) % 12; - if new_month < 0 { - new_month += 12; - } - - let new_date = date - .with_month0(new_month as u32) - .and_then(|d| d.with_year(new_year)); + let new_date = match qty.cmp(&0) { + Ordering::Greater | Ordering::Equal => { + let qty_as_months = Months::new(qty as u32); + self.current_date + .checked_add_months(qty_as_months) + .unwrap_or(NaiveDate::MAX) + } + Ordering::Less => { + let qty_as_months = Months::new((-qty) as u32); + self.current_date + .checked_sub_months(qty_as_months) + .unwrap_or(NaiveDate::MIN) + } + }; - if let Some(new_date) = new_date { - self.update_date(new_date) - } else { - ActionResult::Clean - } + self.update_date(new_date) } fn update_date(&mut self, new_date: NaiveDate) -> ActionResult { diff --git a/inquire/src/prompts/dateselect/test.rs b/inquire/src/prompts/dateselect/test.rs index 76d1d15c..1f0b7aaa 100644 --- a/inquire/src/prompts/dateselect/test.rs +++ b/inquire/src/prompts/dateselect/test.rs @@ -60,3 +60,268 @@ fn closure_validator() { assert_eq!(today_date.pred_opt().unwrap(), ans); } + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn daily_navigation_checks() { + let input = vec![ + Key::Left(KeyModifiers::NONE), + Key::Left(KeyModifiers::NONE), + Key::Left(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2023, 1, 20).unwrap(), ans); +} + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn weekly_navigation_checks() { + let input = vec![ + Key::Up(KeyModifiers::NONE), + Key::Up(KeyModifiers::NONE), + Key::Up(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2023, 2, 19).unwrap(), ans); +} + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn monthly_navigation_checks() { + let input = vec![ + Key::Char('[', KeyModifiers::NONE), + Key::Char(']', KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Char(']', KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2022, 11, 15).unwrap(), ans); +} + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn yearly_navigation_checks() { + let input = vec![ + Key::Char('}', KeyModifiers::NONE), + Key::Char('{', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Char('{', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(), ans); +} + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn naive_navigation_combination() { + let input = vec![ + // start: 2023-01-15 + Key::Up(KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Up(KeyModifiers::NONE), + Key::Left(KeyModifiers::NONE), + Key::Char(']', KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Left(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Left(KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Char('[', KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Char(']', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Char('{', KeyModifiers::NONE), + Key::Down(KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Char('{', KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Right(KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Up(KeyModifiers::NONE), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2024, 12, 24).unwrap(), ans); +} + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn emacs_naive_navigation_combination() { + let input = vec![ + // start: 2023-01-15 + Key::Char('p', KeyModifiers::CONTROL), + Key::Char('v', KeyModifiers::ALT), + Key::Char('p', KeyModifiers::CONTROL), + Key::Char('b', KeyModifiers::CONTROL), + Key::Char('v', KeyModifiers::CONTROL), + Key::Char('v', KeyModifiers::ALT), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('b', KeyModifiers::CONTROL), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('v', KeyModifiers::ALT), + Key::Char('b', KeyModifiers::CONTROL), + Key::Char('}', KeyModifiers::NONE), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('v', KeyModifiers::ALT), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('v', KeyModifiers::CONTROL), + Key::Char('}', KeyModifiers::NONE), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('{', KeyModifiers::NONE), + Key::Char('n', KeyModifiers::CONTROL), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('{', KeyModifiers::NONE), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('}', KeyModifiers::NONE), + Key::Char('f', KeyModifiers::CONTROL), + Key::Char('}', KeyModifiers::NONE), + Key::Char('p', KeyModifiers::CONTROL), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2024, 12, 24).unwrap(), ans); +} + +#[test] +/// Tests the behaviour of several keybindings in an admittedly naive way. +fn vim_naive_navigation_combination() { + let input = vec![ + // start: 2023-01-15 + Key::Char('k', KeyModifiers::NONE), + Key::Char('b', KeyModifiers::ALT), + Key::Char('k', KeyModifiers::NONE), + Key::Char('h', KeyModifiers::NONE), + Key::Char('f', KeyModifiers::ALT), + Key::Char('b', KeyModifiers::ALT), + Key::Char('j', KeyModifiers::NONE), + Key::Char('h', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('b', KeyModifiers::ALT), + Key::Char('h', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('b', KeyModifiers::ALT), + Key::Char('l', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('f', KeyModifiers::ALT), + Key::Char('}', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('{', KeyModifiers::NONE), + Key::Char('j', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('{', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Char('l', KeyModifiers::NONE), + Key::Char('}', KeyModifiers::NONE), + Key::Char('k', KeyModifiers::NONE), + Key::Enter, + ]; + let mut backend = fake_backend(input); + + let starting_date = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let ans = DateSelect::new("Question") + .with_starting_date(starting_date) + .prompt_with_backend(&mut backend) + .unwrap(); + + assert_eq!(NaiveDate::from_ymd_opt(2024, 12, 24).unwrap(), ans); +} diff --git a/inquire/src/prompts/multiselect/action.rs b/inquire/src/prompts/multiselect/action.rs index c02b76ff..e61a3fbf 100644 --- a/inquire/src/prompts/multiselect/action.rs +++ b/inquire/src/prompts/multiselect/action.rs @@ -48,11 +48,11 @@ impl InnerAction for MultiSelectPromptAction { let action = match key { Key::Up(KeyModifiers::NONE) | Key::Char('p', KeyModifiers::CONTROL) => Self::MoveUp, - Key::PageUp => Self::PageUp, + Key::PageUp(_) => Self::PageUp, Key::Home => Self::MoveToStart, Key::Down(KeyModifiers::NONE) | Key::Char('n', KeyModifiers::CONTROL) => Self::MoveDown, - Key::PageDown => Self::PageDown, + Key::PageDown(_) => Self::PageDown, Key::End => Self::MoveToEnd, Key::Char(' ', KeyModifiers::NONE) => Self::ToggleCurrentOption, diff --git a/inquire/src/prompts/select/action.rs b/inquire/src/prompts/select/action.rs index 9acc1022..c372c57c 100644 --- a/inquire/src/prompts/select/action.rs +++ b/inquire/src/prompts/select/action.rs @@ -42,11 +42,11 @@ impl InnerAction for SelectPromptAction { let action = match key { Key::Up(KeyModifiers::NONE) | Key::Char('p', KeyModifiers::CONTROL) => Self::MoveUp, - Key::PageUp => Self::PageUp, + Key::PageUp(_) => Self::PageUp, Key::Home => Self::MoveToStart, Key::Down(KeyModifiers::NONE) | Key::Char('n', KeyModifiers::CONTROL) => Self::MoveDown, - Key::PageDown => Self::PageDown, + Key::PageDown(_) => Self::PageDown, Key::End => Self::MoveToEnd, key => match InputAction::from_key(key, &()) { diff --git a/inquire/src/prompts/text/action.rs b/inquire/src/prompts/text/action.rs index ee18654a..b98f7b30 100644 --- a/inquire/src/prompts/text/action.rs +++ b/inquire/src/prompts/text/action.rs @@ -31,12 +31,12 @@ impl InnerAction for TextPromptAction { Key::Up(KeyModifiers::NONE) | Key::Char('p', KeyModifiers::CONTROL) => { Self::MoveToSuggestionAbove } - Key::PageUp => Self::MoveToSuggestionPageUp, + Key::PageUp(_) => Self::MoveToSuggestionPageUp, Key::Down(KeyModifiers::NONE) | Key::Char('n', KeyModifiers::CONTROL) => { Self::MoveToSuggestionBelow } - Key::PageDown => Self::MoveToSuggestionPageDown, + Key::PageDown(_) => Self::MoveToSuggestionPageDown, Key::Tab => Self::UseCurrentSuggestion, diff --git a/inquire/src/terminal/console.rs b/inquire/src/terminal/console.rs index d355d3a8..3ba56ce6 100644 --- a/inquire/src/terminal/console.rs +++ b/inquire/src/terminal/console.rs @@ -154,8 +154,8 @@ impl From for crate::ui::Key { Key::Del => Self::Delete(KeyModifiers::empty()), Key::Home => Self::Home, Key::End => Self::End, - Key::PageUp => Self::PageUp, - Key::PageDown => Self::PageDown, + Key::PageUp => Self::PageUp(KeyModifiers::empty()), + Key::PageDown => Self::PageDown(KeyModifiers::empty()), Key::ArrowUp => Self::Up(KeyModifiers::empty()), Key::ArrowDown => Self::Down(KeyModifiers::empty()), Key::ArrowLeft => Self::Left(KeyModifiers::empty()), diff --git a/inquire/src/terminal/crossterm.rs b/inquire/src/terminal/crossterm.rs index 555c7102..f49b8c7a 100644 --- a/inquire/src/terminal/crossterm.rs +++ b/inquire/src/terminal/crossterm.rs @@ -234,6 +234,15 @@ impl From for crate::ui::KeyModifiers { if m.contains(KeyModifiers::SHIFT) { modifiers |= crate::ui::KeyModifiers::SHIFT; } + if m.contains(KeyModifiers::SUPER) { + modifiers |= crate::ui::KeyModifiers::SUPER; + } + if m.contains(KeyModifiers::HYPER) { + modifiers |= crate::ui::KeyModifiers::HYPER; + } + if m.contains(KeyModifiers::META) { + modifiers |= crate::ui::KeyModifiers::META; + } modifiers } @@ -271,12 +280,14 @@ impl From for Key { } => Self::End, KeyEvent { code: KeyCode::PageUp, + modifiers: m, .. - } => Self::PageUp, + } => Self::PageUp(m.into()), KeyEvent { code: KeyCode::PageDown, + modifiers: m, .. - } => Self::PageDown, + } => Self::PageDown(m.into()), KeyEvent { code: KeyCode::Up, modifiers: m, @@ -347,8 +358,8 @@ mod test { Key::Delete(m) => KeyEvent::new(KeyCode::Delete, m.try_into()?), Key::Home => KeyEvent::new(KeyCode::Home, KeyModifiers::NONE), Key::End => KeyEvent::new(KeyCode::End, KeyModifiers::NONE), - Key::PageUp => KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE), - Key::PageDown => KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE), + Key::PageUp(m) => KeyEvent::new(KeyCode::PageUp, m.try_into()?), + Key::PageDown(m) => KeyEvent::new(KeyCode::PageDown, m.try_into()?), Key::Up(m) => KeyEvent::new(KeyCode::Up, m.try_into()?), Key::Down(m) => KeyEvent::new(KeyCode::Down, m.try_into()?), Key::Left(m) => KeyEvent::new(KeyCode::Left, m.try_into()?), diff --git a/inquire/src/terminal/termion.rs b/inquire/src/terminal/termion.rs index b8434501..9ad222ae 100644 --- a/inquire/src/terminal/termion.rs +++ b/inquire/src/terminal/termion.rs @@ -250,8 +250,8 @@ impl From for crate::ui::Key { Key::Delete => Self::Delete(KeyModifiers::empty()), Key::Home => Self::Home, Key::End => Self::End, - Key::PageUp => Self::PageUp, - Key::PageDown => Self::PageDown, + Key::PageUp => Self::PageUp(KeyModifiers::empty()), + Key::PageDown => Self::PageDown(KeyModifiers::empty()), Key::Up => Self::Up(KeyModifiers::empty()), Key::Down => Self::Down(KeyModifiers::empty()), Key::Left => Self::Left(KeyModifiers::empty()), diff --git a/inquire/src/ui/key.rs b/inquire/src/ui/key.rs index bf606247..fad99dde 100644 --- a/inquire/src/ui/key.rs +++ b/inquire/src/ui/key.rs @@ -8,6 +8,9 @@ bitflags! { const SHIFT = 0b0000_0001; const CONTROL = 0b0000_0010; const ALT = 0b0000_0100; + const SUPER = 0b0000_1000; + const HYPER = 0b0001_0000; + const META = 0b0010_0000; const NONE = 0b0000_0000; } } @@ -21,8 +24,8 @@ pub enum Key { Delete(KeyModifiers), Home, End, - PageUp, - PageDown, + PageUp(KeyModifiers), + PageDown(KeyModifiers), Up(KeyModifiers), Down(KeyModifiers), Left(KeyModifiers),