384 lines
11 KiB
Rust

use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
/// Backend-agnostic key input kind.
///
/// This type is marked as `#[non_exhaustive]` since more keys may be supported in the future.
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Default)]
pub enum Key {
/// Normal letter key input
Char(char),
/// F1, F2, F3, ... keys
// F(u8),
/// Backspace key
Backspace,
/// Enter or return key
Enter,
/// Left arrow key
Left,
/// Right arrow key
Right,
/// Up arrow key
Up,
/// Down arrow key
Down,
/// Tab key
Tab,
/// Delete key
Delete,
/// Home key
Home,
/// End key
End,
/// Escape key
Esc,
/// Copy key. This key is supported by termwiz only
Copy,
/// Cut key. This key is supported by termwiz only
Cut,
/// Paste key. This key is supported by termwiz only
Paste,
/// An invalid key input (this key is always ignored by [`TextArea`](crate::TextArea))
#[default]
Null,
}
/// Backend-agnostic key input type.
///
/// When `crossterm`, `termion`, `termwiz` features are enabled, converting respective key input types into this
/// `Input` type is defined.
/// ```no_run
/// use tui_textarea::{TextArea, Input, Key};
/// use crossterm::event::{Event, read};
///
/// let event = read().unwrap();
///
/// // `Input::from` can convert backend-native event into `Input`
/// let input = Input::from(event.clone());
/// // or `Into::into`
/// let input: Input = event.clone().into();
/// // Conversion from `KeyEvent` value is also available
/// if let Event::Key(key) = event {
/// let input = Input::from(key);
/// }
/// ```
///
/// Creating `Input` instance directly can cause backend-agnostic input as follows.
///
/// ```
/// use tui_textarea::{TextArea, Input, Key};
///
/// let mut textarea = TextArea::default();
///
/// // Input Ctrl+A
/// textarea.input(Input {
/// key: Key::Char('a'),
/// ctrl: true,
/// alt: false,
/// shift: false,
/// });
/// ```
#[derive(Debug, Clone, Default, PartialEq, Hash, Eq)]
pub struct Input {
/// Typed key.
pub key: Key,
/// Ctrl modifier key. `true` means Ctrl key was pressed.
pub ctrl: bool,
/// Alt modifier key. `true` means Alt key was pressed.
pub alt: bool,
/// Shift modifier key. `true` means Shift key was pressed.
pub shift: bool,
}
impl Input {
/// Returns Some(char) if the Input is a single char without any modifiers (except Shift for
/// case) and None otherwise.
pub fn maybe_char(&self) -> Option<char> {
match self {
Input {
key: Key::Char(c),
ctrl: false,
alt: false,
..
} => Some(*c),
_ => None,
}
}
/// Returns `true` if the Input represents a new line, except is caused by Enter key without
/// any modifiers. Useful for using Enter as a confirm key instead of a new line key.
/// - Ctrl+Enter key
/// - Alt+Enter key
/// - Shift+Enter key
/// - Char is \n or \r
#[inline]
pub fn is_newline_except_enter(&self) -> bool {
matches!(
self,
Input {
key: Key::Char('\n' | '\r'),
ctrl: false,
alt: false,
..
} | Input {
key: Key::Enter,
ctrl: true,
..
} | Input {
key: Key::Enter,
alt: true,
..
} | Input {
key: Key::Enter,
shift: true,
..
}
)
}
/// Returns `true` if the Input represents a new line (including Enter key).
/// - Enter key
///
/// + all conditions of `is_newline_except_enter`
#[inline]
pub fn is_newline(&self) -> bool {
self.is_newline_except_enter() || self.key == Key::Enter
}
/// Returns `true` if the Input is a single char without any modifiers (except Shift for upper
/// case).
#[inline]
pub fn is_char(&self) -> bool {
matches!(
self,
Input {
key: Key::Char(_),
ctrl: false,
alt: false,
..
}
)
}
/// Returns `true` if the Input is a Tab
#[inline]
pub fn is_tab(&self) -> bool {
self.key == Key::Tab && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is Backspace
#[inline]
pub fn is_backspace(&self) -> bool {
self.key == Key::Backspace && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is Delete
#[inline]
pub fn is_delete(&self) -> bool {
self.key == Key::Delete && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is key down arrow
#[inline]
pub fn is_down(&self) -> bool {
self.key == Key::Down && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is key up arrow
#[inline]
pub fn is_up(&self) -> bool {
self.key == Key::Up && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is key left arrow
#[inline]
pub fn is_left(&self) -> bool {
self.key == Key::Left && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is key right arrow
#[inline]
pub fn is_right(&self) -> bool {
self.key == Key::Right && !self.ctrl && !self.alt
}
/// Returns `true` if the Input is key Home
#[inline]
pub fn is_home(&self) -> bool {
self.key == Key::Home
}
/// Returns `true` if the Input is key End
#[inline]
pub fn is_end(&self) -> bool {
self.key == Key::End
}
/// Returns `true` if the Input is ctrl+left
#[inline]
pub fn is_ctrl_left(&self) -> bool {
self.key == Key::Left && self.ctrl && !self.alt
}
/// Returns `true` if the Input is ctrl+right
#[inline]
pub fn is_ctrl_right(&self) -> bool {
self.key == Key::Right && self.ctrl && !self.alt
}
/// Returns a string representing the kind of key input.
/// e.g ":delete", ":backspace", ":tab", ":enter", "char"
/// or empty string if the key is null.
/// uses the is_* methods to determine the kind of key input.
pub fn kind(&self) -> &str {
match self {
i if i.is_delete() => ":delete",
i if i.is_backspace() => ":backspace",
i if i.is_tab() => ":tab",
i if i.is_newline_except_enter() => ":non-enter-newline",
i if i.is_newline() => ":newline",
i if i.is_char() => ":char",
i if i.is_down() => ":down",
i if i.is_up() => ":up",
i if i.is_left() => ":left",
i if i.is_right() => ":right",
i if i.is_home() => ":home",
i if i.is_end() => ":end",
i if i.is_ctrl_left() => ":word-left",
i if i.is_ctrl_right() => ":word-right",
_ => "",
}
}
}
impl From<Event> for Input {
/// Convert [`crossterm::event::Event`] into [`Input`].
fn from(event: Event) -> Self {
match event {
Event::Key(key) => Self::from(key),
_ => Self::default(),
}
}
}
impl From<KeyCode> for Key {
/// Convert [`crossterm::event::KeyCode`] into [`Key`].
fn from(code: KeyCode) -> Self {
match code {
KeyCode::Char(c) => Key::Char(c),
KeyCode::Backspace => Key::Backspace,
KeyCode::Enter => Key::Enter,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::Tab => Key::Tab,
KeyCode::Delete => Key::Delete,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::Esc => Key::Esc,
_ => Key::Null,
}
}
}
impl From<KeyEvent> for Input {
/// Convert [`crossterm::event::KeyEvent`] into [`Input`].
fn from(key: KeyEvent) -> Self {
if key.kind == KeyEventKind::Release {
// On Windows or when `crossterm::event::PushKeyboardEnhancementFlags` is set,
// key release event can be reported. Ignore it. (#14)
return Self::default();
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let key = Key::from(key.code);
Self {
key,
ctrl,
alt,
shift,
}
}
}
#[cfg(test)]
mod tests {
use {super::*, crossterm::event::KeyEventState};
fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}
}
#[test]
fn key_to_input() {
for (from, to) in [
(
key_event(KeyCode::Char('a'), KeyModifiers::empty()),
input(Key::Char('a'), false, false, false),
),
(
key_event(KeyCode::Enter, KeyModifiers::empty()),
input(Key::Enter, false, false, false),
),
(
key_event(KeyCode::Left, KeyModifiers::CONTROL),
input(Key::Left, true, false, false),
),
(
key_event(KeyCode::Right, KeyModifiers::SHIFT),
input(Key::Right, false, false, true),
),
(
key_event(KeyCode::Home, KeyModifiers::ALT),
input(Key::Home, false, true, false),
),
(
key_event(KeyCode::NumLock, KeyModifiers::CONTROL),
input(Key::Null, true, false, false),
),
] {
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
}
}
#[test]
fn event_to_input() {
for (from, to) in [
(
Event::Key(key_event(KeyCode::Char('a'), KeyModifiers::empty())),
input(Key::Char('a'), false, false, false),
),
(Event::FocusGained, input(Key::Null, false, false, false)),
] {
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
}
}
// Regression for https://github.com/rhysd/tui-textarea/issues/14
#[test]
fn ignore_key_release_event() {
let mut from = key_event(KeyCode::Char('a'), KeyModifiers::empty());
from.kind = KeyEventKind::Release;
let to = input(Key::Null, false, false, false);
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
}
#[allow(dead_code)]
pub(crate) fn input(key: Key, ctrl: bool, alt: bool, shift: bool) -> Input {
Input {
key,
ctrl,
alt,
shift,
}
}
}