diff --git a/Cargo.lock b/Cargo.lock index 83ad0c9..050725c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.24.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9f7409c70a38a56216480fba371ee460207dd8926ccf5b4160591759559170" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags", "crossterm_winapi", diff --git a/Cargo.toml b/Cargo.toml index 34215e7..74531f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ doctest = true [dependencies] chrono = "0.4.19" clipboard = { version = "0.5.0", optional = true } -crossterm = { version = "0.24.0", features = ["serde"] } +crossterm = { version = "0.26.1", features = ["serde"] } itertools = "0.10.3" nu-ansi-term = "0.47.0" serde = { version = "1.0", features = ["derive"] } diff --git a/examples/demo.rs b/examples/demo.rs index a17b48f..b5d6066 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -12,7 +12,7 @@ use { }, }; -use crossterm::cursor::CursorShape; +use crossterm::cursor::SetCursorStyle; use reedline::CursorConfig; #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))] use reedline::FileBackedHistory; @@ -61,8 +61,8 @@ fn main() -> Result<()> { let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2)); let cursor_config = CursorConfig { - vi_insert: Some(CursorShape::Line), - vi_normal: Some(CursorShape::Block), + vi_insert: Some(SetCursorStyle::BlinkingBar), + vi_normal: Some(SetCursorStyle::SteadyBlock), emacs: None, }; diff --git a/examples/event_listener.rs b/examples/event_listener.rs index 770cf82..30c1ae1 100644 --- a/examples/event_listener.rs +++ b/examples/event_listener.rs @@ -38,11 +38,17 @@ fn print_events_helper() -> Result<()> { // It's guaranteed that read() wont block if `poll` returns `Ok(true)` let event = crossterm::event::read()?; - if let Event::Key(KeyEvent { code, modifiers }) = event { + if let Event::Key(KeyEvent { + code, + modifiers, + kind, + state, + }) = event + { match code { KeyCode::Char(c) => { println!( - "Char: {} code: {:#08x}; Modifier {:?}; Flags {:#08b}\r", + "Char: {} code: {:#08x}; Modifier {:?}; Flags {:#08b}; Kind {kind:?}; state {state:?}\r", c, u32::from(c), modifiers, @@ -51,7 +57,7 @@ fn print_events_helper() -> Result<()> { } _ => { println!( - "Keycode: {code:?}; Modifier {modifiers:?}; Flags {modifiers:#08b}\r" + "Keycode: {code:?}; Modifier {modifiers:?}; Flags {modifiers:#08b}; Kind {kind:?}; state {state:?}\r" ); } } diff --git a/examples/event_listener_kitty_proto.rs b/examples/event_listener_kitty_proto.rs new file mode 100644 index 0000000..8c0e35e --- /dev/null +++ b/examples/event_listener_kitty_proto.rs @@ -0,0 +1,104 @@ +use crossterm::event::{ + KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; +use crossterm::execute; +use { + crossterm::{ + event::{poll, Event, KeyCode, KeyEvent}, + terminal, Result, + }, + std::{ + io::{stdout, Write}, + time::Duration, + }, +}; + +fn main() -> Result<()> { + println!("Ready to print events (Abort with ESC):"); + print_events()?; + println!(); + Ok(()) +} + +/// **For debugging purposes only:** Track the terminal events observed by [`Reedline`] and print them. +pub fn print_events() -> Result<()> { + stdout().flush()?; + terminal::enable_raw_mode()?; + // enable kitty protocol + // + // Note that, currently, only the following support this protocol: + // * [kitty terminal](https://sw.kovidgoyal.net/kitty/) + // * [foot terminal](https://codeberg.org/dnkl/foot/issues/319) + // * [WezTerm terminal](https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html) + // * [notcurses library](https://github.com/dankamongmen/notcurses/issues/2131) + // * [neovim text editor](https://github.com/neovim/neovim/pull/18181) + // * [kakoune text editor](https://github.com/mawww/kakoune/issues/4103) + // * [dte text editor](https://gitlab.com/craigbarnes/dte/-/issues/138) + // + // Refer to https://sw.kovidgoyal.net/kitty/keyboard-protocol/ if you're curious. + execute!( + stdout(), + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::REPORT_EVENT_TYPES + | KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + ) + ) + .unwrap(); + let result = print_events_helper(); + execute!(stdout(), PopKeyboardEnhancementFlags).unwrap(); + terminal::disable_raw_mode()?; + + result +} + +// this fn is totally ripped off from crossterm's examples +// it's really a diagnostic routine to see if crossterm is +// even seeing the events. if you press a key and no events +// are printed, it's a good chance your terminal is eating +// those events. +fn print_events_helper() -> Result<()> { + loop { + // Wait up to 5s for another event + if poll(Duration::from_millis(5_000))? { + // It's guaranteed that read() wont block if `poll` returns `Ok(true)` + let event = crossterm::event::read()?; + + if let Event::Key(KeyEvent { + code, + modifiers, + kind, + state, + }) = event + { + match code { + KeyCode::Char(c) => { + println!( + "Char: {} code: {:#08x}; Modifier {:?}; Flags {:#08b}; Kind {kind:?}; state {state:?}\r", + c, + u32::from(c), + modifiers, + modifiers + ); + } + _ => { + println!( + "Keycode: {code:?}; Modifier {modifiers:?}; Flags {modifiers:#08b}; Kind {kind:?}; state {state:?}\r" + ); + } + } + } else { + println!("Event::{event:?}\r"); + } + + // hit the esc key to git out + if event == Event::Key(KeyCode::Esc.into()) { + break; + } + } else { + // Timeout expired, no event for 5s + println!("Waiting for you to type...\r"); + } + } + + Ok(()) +} diff --git a/src/edit_mode/base.rs b/src/edit_mode/base.rs index 932aee8..455e2c8 100644 --- a/src/edit_mode/base.rs +++ b/src/edit_mode/base.rs @@ -1,5 +1,7 @@ -use crate::{enums::ReedlineEvent, PromptEditMode}; -pub use crossterm::event::Event; +use crate::{ + enums::{ReedlineEvent, ReedlineRawEvent}, + PromptEditMode, +}; /// Define the style of parsing for the edit events /// Available default options: @@ -7,7 +9,7 @@ pub use crossterm::event::Event; /// - Vi pub trait EditMode: Send { /// Translate the given user input event into what the `LineEditor` understands - fn parse_event(&mut self, event: Event) -> ReedlineEvent; + fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent; /// What to display in the prompt indicator fn edit_mode(&self) -> PromptEditMode; diff --git a/src/edit_mode/cursors.rs b/src/edit_mode/cursors.rs index b975d5c..67a1765 100644 --- a/src/edit_mode/cursors.rs +++ b/src/edit_mode/cursors.rs @@ -1,13 +1,13 @@ -use crossterm::cursor::CursorShape; +use crossterm::cursor::SetCursorStyle; /// Maps cursor shapes to each edit mode (emacs, vi normal & vi insert). /// If any of the fields is `None`, the cursor won't get changed by Reedline for that mode. #[derive(Default)] pub struct CursorConfig { /// The cursor to be used when in vi insert mode - pub vi_insert: Option, + pub vi_insert: Option, /// The cursor to be used when in vi normal mode - pub vi_normal: Option, + pub vi_normal: Option, /// The cursor to be used when in emacs mode - pub emacs: Option, + pub emacs: Option, } diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index b8cbf92..1172634 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -6,7 +6,7 @@ use crate::{ }, EditMode, }, - enums::{EditCommand, ReedlineEvent}, + enums::{EditCommand, ReedlineEvent, ReedlineRawEvent}, PromptEditMode, }; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; @@ -108,9 +108,11 @@ impl Default for Emacs { } impl EditMode for Emacs { - fn parse_event(&mut self, event: Event) -> ReedlineEvent { - match event { - Event::Key(KeyEvent { code, modifiers }) => match (modifiers, code) { + fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { + match event.into() { + Event::Key(KeyEvent { + code, modifiers, .. + }) => match (modifiers, code) { (modifier, KeyCode::Char(c)) => { // Note. The modifier can also be a combination of modifiers, for // example: @@ -152,6 +154,9 @@ impl EditMode for Emacs { Event::Mouse(_) => ReedlineEvent::Mouse, Event::Resize(width, height) => ReedlineEvent::Resize(width, height), + Event::FocusGained => ReedlineEvent::None, + Event::FocusLost => ReedlineEvent::None, + Event::Paste(_) => ReedlineEvent::None, } } @@ -175,10 +180,11 @@ mod test { #[test] fn ctrl_l_leads_to_clear_screen_event() { let mut emacs = Emacs::default(); - let ctrl_l = Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('l'), - }); + let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('l'), + KeyModifiers::CONTROL, + ))) + .unwrap(); let result = emacs.parse_event(ctrl_l); assert_eq!(result, ReedlineEvent::ClearScreen); @@ -194,10 +200,11 @@ mod test { ); let mut emacs = Emacs::new(keybindings); - let ctrl_l = Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('l'), - }); + let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('l'), + KeyModifiers::CONTROL, + ))) + .unwrap(); let result = emacs.parse_event(ctrl_l); assert_eq!(result, ReedlineEvent::HistoryHintComplete); @@ -206,10 +213,11 @@ mod test { #[test] fn inserting_character_works() { let mut emacs = Emacs::default(); - let l = Event::Key(KeyEvent { - modifiers: KeyModifiers::NONE, - code: KeyCode::Char('l'), - }); + let l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('l'), + KeyModifiers::NONE, + ))) + .unwrap(); let result = emacs.parse_event(l); assert_eq!( @@ -222,10 +230,11 @@ mod test { fn inserting_capital_character_works() { let mut emacs = Emacs::default(); - let uppercase_l = Event::Key(KeyEvent { - modifiers: KeyModifiers::SHIFT, - code: KeyCode::Char('l'), - }); + let uppercase_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('l'), + KeyModifiers::SHIFT, + ))) + .unwrap(); let result = emacs.parse_event(uppercase_l); assert_eq!( @@ -239,10 +248,11 @@ mod test { let keybindings = Keybindings::default(); let mut emacs = Emacs::new(keybindings); - let ctrl_l = Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('l'), - }); + let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('l'), + KeyModifiers::CONTROL, + ))) + .unwrap(); let result = emacs.parse_event(ctrl_l); assert_eq!(result, ReedlineEvent::None); @@ -252,10 +262,11 @@ mod test { fn inserting_capital_character_for_non_ascii_remains_as_is() { let mut emacs = Emacs::default(); - let uppercase_l = Event::Key(KeyEvent { - modifiers: KeyModifiers::SHIFT, - code: KeyCode::Char('😀'), - }); + let uppercase_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('😀'), + KeyModifiers::SHIFT, + ))) + .unwrap(); let result = emacs.parse_event(uppercase_l); assert_eq!( diff --git a/src/edit_mode/mod.rs b/src/edit_mode/mod.rs index 0ddee6f..38e1456 100644 --- a/src/edit_mode/mod.rs +++ b/src/edit_mode/mod.rs @@ -4,7 +4,7 @@ mod emacs; mod keybindings; mod vi; -pub use base::{EditMode, Event}; +pub use base::EditMode; pub use cursors::CursorConfig; pub use emacs::{default_emacs_keybindings, Emacs}; pub use keybindings::Keybindings; diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 88beff7..f8c8a7d 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -11,7 +11,7 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ edit_mode::{keybindings::Keybindings, vi::parser::parse}, - enums::{EditCommand, ReedlineEvent}, + enums::{EditCommand, ReedlineEvent, ReedlineRawEvent}, PromptEditMode, PromptViMode, }; @@ -57,9 +57,11 @@ impl Vi { } impl EditMode for Vi { - fn parse_event(&mut self, event: Event) -> ReedlineEvent { - match event { - Event::Key(KeyEvent { code, modifiers }) => match (self.mode, modifiers, code) { + fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { + match event.into() { + Event::Key(KeyEvent { + code, modifiers, .. + }) => match (self.mode, modifiers, code) { (ViMode::Normal, modifier, KeyCode::Char(c)) => { let c = c.to_ascii_lowercase(); @@ -149,6 +151,9 @@ impl EditMode for Vi { Event::Mouse(_) => ReedlineEvent::Mouse, Event::Resize(width, height) => ReedlineEvent::Resize(width, height), + Event::FocusGained => ReedlineEvent::None, + Event::FocusLost => ReedlineEvent::None, + Event::Paste(_) => ReedlineEvent::None, } } @@ -168,10 +173,11 @@ mod test { #[test] fn esc_leads_to_normal_mode_test() { let mut vi = Vi::default(); - let esc = Event::Key(KeyEvent { - modifiers: KeyModifiers::NONE, - code: KeyCode::Esc, - }); + let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Esc, + KeyModifiers::NONE, + ))) + .unwrap(); let result = vi.parse_event(esc); assert_eq!( @@ -197,10 +203,11 @@ mod test { ..Default::default() }; - let esc = Event::Key(KeyEvent { - modifiers: KeyModifiers::NONE, - code: KeyCode::Char('e'), - }); + let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('e'), + KeyModifiers::NONE, + ))) + .unwrap(); let result = vi.parse_event(esc); assert_eq!(result, ReedlineEvent::ClearScreen); @@ -222,10 +229,11 @@ mod test { ..Default::default() }; - let esc = Event::Key(KeyEvent { - modifiers: KeyModifiers::SHIFT, - code: KeyCode::Char('$'), - }); + let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('$'), + KeyModifiers::SHIFT, + ))) + .unwrap(); let result = vi.parse_event(esc); assert_eq!(result, ReedlineEvent::CtrlD); @@ -241,10 +249,11 @@ mod test { ..Default::default() }; - let esc = Event::Key(KeyEvent { - modifiers: KeyModifiers::NONE, - code: KeyCode::Char('q'), - }); + let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new( + KeyCode::Char('q'), + KeyModifiers::NONE, + ))) + .unwrap(); let result = vi.parse_event(esc); assert_eq!(result, ReedlineEvent::None); diff --git a/src/engine.rs b/src/engine.rs index caf3841..cc13b8e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,4 +1,4 @@ -use crate::CursorConfig; +use crate::{enums::ReedlineRawEvent, CursorConfig}; #[cfg(feature = "bashisms")] use crate::{ history::SearchFilter, @@ -493,7 +493,7 @@ impl Reedline { self.repaint(prompt)?; - let mut crossterm_events: Vec = vec![]; + let mut crossterm_events: Vec = vec![]; let mut reedline_events: Vec = vec![]; loop { @@ -528,18 +528,29 @@ impl Reedline { enter @ Event::Key(KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, + .. }) => { - crossterm_events.push(enter); - // Break early to check if the input is complete and - // can be send to the hosting application. If - // multiple complete entries are submitted, events - // are still in the crossterm queue for us to - // process. - paste_enter_state = crossterm_events.len() > EVENTS_THRESHOLD; - break; + let enter = ReedlineRawEvent::convert_from(enter); + match enter { + Some(enter) => { + crossterm_events.push(enter); + // Break early to check if the input is complete and + // can be send to the hosting application. If + // multiple complete entries are submitted, events + // are still in the crossterm queue for us to + // process. + paste_enter_state = crossterm_events.len() > EVENTS_THRESHOLD; + break; + } + None => continue, + } } x => { - crossterm_events.push(x); + let raw_event = ReedlineRawEvent::convert_from(x); + match raw_event { + Some(evt) => crossterm_events.push(evt), + None => continue, + } } } } diff --git a/src/enums.rs b/src/enums.rs index ec6375d..009d6ed 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,3 +1,4 @@ +use crossterm::event::{Event, KeyEvent, KeyEventKind}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use strum_macros::EnumIter; @@ -560,3 +561,42 @@ pub(crate) enum EventStatus { Inapplicable, Exits(Signal), } + +/// A simple wrapper for [crossterm::event::Event] +/// +/// Which will make sure that the given event doesn't contain [KeyEventKind::Release] +/// and convert from [KeyEventKind::Repeat] to [KeyEventKind::Press] +pub struct ReedlineRawEvent { + inner: Event, +} + +impl ReedlineRawEvent { + /// It will return None if `evt` is released Key. + pub fn convert_from(evt: Event) -> Option { + match evt { + Event::Key(KeyEvent { + kind: KeyEventKind::Release, + .. + }) => None, + Event::Key(KeyEvent { + code, + modifiers, + kind: KeyEventKind::Repeat, + state, + }) => Some(Self { + inner: Event::Key(KeyEvent { + code, + modifiers, + kind: KeyEventKind::Press, + state, + }), + }), + other => Some(Self { inner: other }), + } + } + + /// Consume and get crossterm event object. + pub fn into(self) -> Event { + self.inner + } +} diff --git a/src/lib.rs b/src/lib.rs index 4d34d0f..c1c3b87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,7 +230,7 @@ pub use core_editor::Editor; pub use core_editor::LineBuffer; mod enums; -pub use enums::{EditCommand, ReedlineEvent, Signal, UndoBehavior}; +pub use enums::{EditCommand, ReedlineEvent, ReedlineRawEvent, Signal, UndoBehavior}; mod painting; pub use painting::{Painter, StyledText}; @@ -259,7 +259,7 @@ pub use prompt::{ mod edit_mode; pub use edit_mode::{ default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, - CursorConfig, EditMode, Emacs, Event, Keybindings, Vi, + CursorConfig, EditMode, Emacs, Keybindings, Vi, }; mod highlighter; diff --git a/src/painting/painter.rs b/src/painting/painter.rs index 0458bdc..617b598 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -186,7 +186,7 @@ impl Painter { _ => None, }; if let Some(shape) = shape { - self.stdout.queue(cursor::SetCursorShape(shape))?; + self.stdout.queue(shape)?; } } self.stdout.queue(cursor::Show)?; diff --git a/src/utils/query.rs b/src/utils/query.rs index 90e01d1..2b762e9 100644 --- a/src/utils/query.rs +++ b/src/utils/query.rs @@ -57,6 +57,15 @@ impl Display for ReedLineCrossTermKeyCode { KeyCode::Char(_) => write!(f, "Char_"), KeyCode::Null => write!(f, "Null"), KeyCode::Esc => write!(f, "Esc"), + KeyCode::CapsLock => write!(f, "CapsLock"), + KeyCode::ScrollLock => write!(f, "ScrollLock"), + KeyCode::NumLock => write!(f, "NumLock"), + KeyCode::PrintScreen => write!(f, "PrintScreen"), + KeyCode::Pause => write!(f, "Pause"), + KeyCode::Menu => write!(f, "Menu"), + KeyCode::KeypadBegin => write!(f, "KeypadBegin"), + KeyCode::Media(_) => write!(f, "Media"), + KeyCode::Modifier(_) => write!(f, "Modifier"), }, } }