Update crossterm to version 0.26.1 (#560)

* update crossterm to 0.26.1

* add event_listener_kitty_proto example

* add comment

* remove trait
This commit is contained in:
WindSoilder 2023-04-14 01:24:17 +08:00 committed by GitHub
parent 89cb811838
commit 27f4417191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 271 additions and 79 deletions

4
Cargo.lock generated
View File

@ -187,9 +187,9 @@ dependencies = [
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.24.0" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9f7409c70a38a56216480fba371ee460207dd8926ccf5b4160591759559170" checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",

View File

@ -15,7 +15,7 @@ doctest = true
[dependencies] [dependencies]
chrono = "0.4.19" chrono = "0.4.19"
clipboard = { version = "0.5.0", optional = true } 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" itertools = "0.10.3"
nu-ansi-term = "0.47.0" nu-ansi-term = "0.47.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@ -12,7 +12,7 @@ use {
}, },
}; };
use crossterm::cursor::CursorShape; use crossterm::cursor::SetCursorStyle;
use reedline::CursorConfig; use reedline::CursorConfig;
#[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))] #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
use reedline::FileBackedHistory; use reedline::FileBackedHistory;
@ -61,8 +61,8 @@ fn main() -> Result<()> {
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2)); let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2));
let cursor_config = CursorConfig { let cursor_config = CursorConfig {
vi_insert: Some(CursorShape::Line), vi_insert: Some(SetCursorStyle::BlinkingBar),
vi_normal: Some(CursorShape::Block), vi_normal: Some(SetCursorStyle::SteadyBlock),
emacs: None, emacs: None,
}; };

View File

@ -38,11 +38,17 @@ fn print_events_helper() -> Result<()> {
// It's guaranteed that read() wont block if `poll` returns `Ok(true)` // It's guaranteed that read() wont block if `poll` returns `Ok(true)`
let event = crossterm::event::read()?; 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 { match code {
KeyCode::Char(c) => { KeyCode::Char(c) => {
println!( println!(
"Char: {} code: {:#08x}; Modifier {:?}; Flags {:#08b}\r", "Char: {} code: {:#08x}; Modifier {:?}; Flags {:#08b}; Kind {kind:?}; state {state:?}\r",
c, c,
u32::from(c), u32::from(c),
modifiers, modifiers,
@ -51,7 +57,7 @@ fn print_events_helper() -> Result<()> {
} }
_ => { _ => {
println!( println!(
"Keycode: {code:?}; Modifier {modifiers:?}; Flags {modifiers:#08b}\r" "Keycode: {code:?}; Modifier {modifiers:?}; Flags {modifiers:#08b}; Kind {kind:?}; state {state:?}\r"
); );
} }
} }

View File

@ -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(())
}

View File

@ -1,5 +1,7 @@
use crate::{enums::ReedlineEvent, PromptEditMode}; use crate::{
pub use crossterm::event::Event; enums::{ReedlineEvent, ReedlineRawEvent},
PromptEditMode,
};
/// Define the style of parsing for the edit events /// Define the style of parsing for the edit events
/// Available default options: /// Available default options:
@ -7,7 +9,7 @@ pub use crossterm::event::Event;
/// - Vi /// - Vi
pub trait EditMode: Send { pub trait EditMode: Send {
/// Translate the given user input event into what the `LineEditor` understands /// 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 /// What to display in the prompt indicator
fn edit_mode(&self) -> PromptEditMode; fn edit_mode(&self) -> PromptEditMode;

View File

@ -1,13 +1,13 @@
use crossterm::cursor::CursorShape; use crossterm::cursor::SetCursorStyle;
/// Maps cursor shapes to each edit mode (emacs, vi normal & vi insert). /// 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. /// If any of the fields is `None`, the cursor won't get changed by Reedline for that mode.
#[derive(Default)] #[derive(Default)]
pub struct CursorConfig { pub struct CursorConfig {
/// The cursor to be used when in vi insert mode /// The cursor to be used when in vi insert mode
pub vi_insert: Option<CursorShape>, pub vi_insert: Option<SetCursorStyle>,
/// The cursor to be used when in vi normal mode /// The cursor to be used when in vi normal mode
pub vi_normal: Option<CursorShape>, pub vi_normal: Option<SetCursorStyle>,
/// The cursor to be used when in emacs mode /// The cursor to be used when in emacs mode
pub emacs: Option<CursorShape>, pub emacs: Option<SetCursorStyle>,
} }

View File

@ -6,7 +6,7 @@ use crate::{
}, },
EditMode, EditMode,
}, },
enums::{EditCommand, ReedlineEvent}, enums::{EditCommand, ReedlineEvent, ReedlineRawEvent},
PromptEditMode, PromptEditMode,
}; };
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
@ -108,9 +108,11 @@ impl Default for Emacs {
} }
impl EditMode for Emacs { impl EditMode for Emacs {
fn parse_event(&mut self, event: Event) -> ReedlineEvent { fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent {
match event { match event.into() {
Event::Key(KeyEvent { code, modifiers }) => match (modifiers, code) { Event::Key(KeyEvent {
code, modifiers, ..
}) => match (modifiers, code) {
(modifier, KeyCode::Char(c)) => { (modifier, KeyCode::Char(c)) => {
// Note. The modifier can also be a combination of modifiers, for // Note. The modifier can also be a combination of modifiers, for
// example: // example:
@ -152,6 +154,9 @@ impl EditMode for Emacs {
Event::Mouse(_) => ReedlineEvent::Mouse, Event::Mouse(_) => ReedlineEvent::Mouse,
Event::Resize(width, height) => ReedlineEvent::Resize(width, height), 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] #[test]
fn ctrl_l_leads_to_clear_screen_event() { fn ctrl_l_leads_to_clear_screen_event() {
let mut emacs = Emacs::default(); let mut emacs = Emacs::default();
let ctrl_l = Event::Key(KeyEvent { let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::CONTROL, KeyCode::Char('l'),
code: KeyCode::Char('l'), KeyModifiers::CONTROL,
}); )))
.unwrap();
let result = emacs.parse_event(ctrl_l); let result = emacs.parse_event(ctrl_l);
assert_eq!(result, ReedlineEvent::ClearScreen); assert_eq!(result, ReedlineEvent::ClearScreen);
@ -194,10 +200,11 @@ mod test {
); );
let mut emacs = Emacs::new(keybindings); let mut emacs = Emacs::new(keybindings);
let ctrl_l = Event::Key(KeyEvent { let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::CONTROL, KeyCode::Char('l'),
code: KeyCode::Char('l'), KeyModifiers::CONTROL,
}); )))
.unwrap();
let result = emacs.parse_event(ctrl_l); let result = emacs.parse_event(ctrl_l);
assert_eq!(result, ReedlineEvent::HistoryHintComplete); assert_eq!(result, ReedlineEvent::HistoryHintComplete);
@ -206,10 +213,11 @@ mod test {
#[test] #[test]
fn inserting_character_works() { fn inserting_character_works() {
let mut emacs = Emacs::default(); let mut emacs = Emacs::default();
let l = Event::Key(KeyEvent { let l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::NONE, KeyCode::Char('l'),
code: KeyCode::Char('l'), KeyModifiers::NONE,
}); )))
.unwrap();
let result = emacs.parse_event(l); let result = emacs.parse_event(l);
assert_eq!( assert_eq!(
@ -222,10 +230,11 @@ mod test {
fn inserting_capital_character_works() { fn inserting_capital_character_works() {
let mut emacs = Emacs::default(); let mut emacs = Emacs::default();
let uppercase_l = Event::Key(KeyEvent { let uppercase_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::SHIFT, KeyCode::Char('l'),
code: KeyCode::Char('l'), KeyModifiers::SHIFT,
}); )))
.unwrap();
let result = emacs.parse_event(uppercase_l); let result = emacs.parse_event(uppercase_l);
assert_eq!( assert_eq!(
@ -239,10 +248,11 @@ mod test {
let keybindings = Keybindings::default(); let keybindings = Keybindings::default();
let mut emacs = Emacs::new(keybindings); let mut emacs = Emacs::new(keybindings);
let ctrl_l = Event::Key(KeyEvent { let ctrl_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::CONTROL, KeyCode::Char('l'),
code: KeyCode::Char('l'), KeyModifiers::CONTROL,
}); )))
.unwrap();
let result = emacs.parse_event(ctrl_l); let result = emacs.parse_event(ctrl_l);
assert_eq!(result, ReedlineEvent::None); assert_eq!(result, ReedlineEvent::None);
@ -252,10 +262,11 @@ mod test {
fn inserting_capital_character_for_non_ascii_remains_as_is() { fn inserting_capital_character_for_non_ascii_remains_as_is() {
let mut emacs = Emacs::default(); let mut emacs = Emacs::default();
let uppercase_l = Event::Key(KeyEvent { let uppercase_l = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::SHIFT, KeyCode::Char('😀'),
code: KeyCode::Char('😀'), KeyModifiers::SHIFT,
}); )))
.unwrap();
let result = emacs.parse_event(uppercase_l); let result = emacs.parse_event(uppercase_l);
assert_eq!( assert_eq!(

View File

@ -4,7 +4,7 @@ mod emacs;
mod keybindings; mod keybindings;
mod vi; mod vi;
pub use base::{EditMode, Event}; pub use base::EditMode;
pub use cursors::CursorConfig; pub use cursors::CursorConfig;
pub use emacs::{default_emacs_keybindings, Emacs}; pub use emacs::{default_emacs_keybindings, Emacs};
pub use keybindings::Keybindings; pub use keybindings::Keybindings;

View File

@ -11,7 +11,7 @@ use self::motion::ViCharSearch;
use super::EditMode; use super::EditMode;
use crate::{ use crate::{
edit_mode::{keybindings::Keybindings, vi::parser::parse}, edit_mode::{keybindings::Keybindings, vi::parser::parse},
enums::{EditCommand, ReedlineEvent}, enums::{EditCommand, ReedlineEvent, ReedlineRawEvent},
PromptEditMode, PromptViMode, PromptEditMode, PromptViMode,
}; };
@ -57,9 +57,11 @@ impl Vi {
} }
impl EditMode for Vi { impl EditMode for Vi {
fn parse_event(&mut self, event: Event) -> ReedlineEvent { fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent {
match event { match event.into() {
Event::Key(KeyEvent { code, modifiers }) => match (self.mode, modifiers, code) { Event::Key(KeyEvent {
code, modifiers, ..
}) => match (self.mode, modifiers, code) {
(ViMode::Normal, modifier, KeyCode::Char(c)) => { (ViMode::Normal, modifier, KeyCode::Char(c)) => {
let c = c.to_ascii_lowercase(); let c = c.to_ascii_lowercase();
@ -149,6 +151,9 @@ impl EditMode for Vi {
Event::Mouse(_) => ReedlineEvent::Mouse, Event::Mouse(_) => ReedlineEvent::Mouse,
Event::Resize(width, height) => ReedlineEvent::Resize(width, height), 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] #[test]
fn esc_leads_to_normal_mode_test() { fn esc_leads_to_normal_mode_test() {
let mut vi = Vi::default(); let mut vi = Vi::default();
let esc = Event::Key(KeyEvent { let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::NONE, KeyCode::Esc,
code: KeyCode::Esc, KeyModifiers::NONE,
}); )))
.unwrap();
let result = vi.parse_event(esc); let result = vi.parse_event(esc);
assert_eq!( assert_eq!(
@ -197,10 +203,11 @@ mod test {
..Default::default() ..Default::default()
}; };
let esc = Event::Key(KeyEvent { let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::NONE, KeyCode::Char('e'),
code: KeyCode::Char('e'), KeyModifiers::NONE,
}); )))
.unwrap();
let result = vi.parse_event(esc); let result = vi.parse_event(esc);
assert_eq!(result, ReedlineEvent::ClearScreen); assert_eq!(result, ReedlineEvent::ClearScreen);
@ -222,10 +229,11 @@ mod test {
..Default::default() ..Default::default()
}; };
let esc = Event::Key(KeyEvent { let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::SHIFT, KeyCode::Char('$'),
code: KeyCode::Char('$'), KeyModifiers::SHIFT,
}); )))
.unwrap();
let result = vi.parse_event(esc); let result = vi.parse_event(esc);
assert_eq!(result, ReedlineEvent::CtrlD); assert_eq!(result, ReedlineEvent::CtrlD);
@ -241,10 +249,11 @@ mod test {
..Default::default() ..Default::default()
}; };
let esc = Event::Key(KeyEvent { let esc = ReedlineRawEvent::convert_from(Event::Key(KeyEvent::new(
modifiers: KeyModifiers::NONE, KeyCode::Char('q'),
code: KeyCode::Char('q'), KeyModifiers::NONE,
}); )))
.unwrap();
let result = vi.parse_event(esc); let result = vi.parse_event(esc);
assert_eq!(result, ReedlineEvent::None); assert_eq!(result, ReedlineEvent::None);

View File

@ -1,4 +1,4 @@
use crate::CursorConfig; use crate::{enums::ReedlineRawEvent, CursorConfig};
#[cfg(feature = "bashisms")] #[cfg(feature = "bashisms")]
use crate::{ use crate::{
history::SearchFilter, history::SearchFilter,
@ -493,7 +493,7 @@ impl Reedline {
self.repaint(prompt)?; self.repaint(prompt)?;
let mut crossterm_events: Vec<Event> = vec![]; let mut crossterm_events: Vec<ReedlineRawEvent> = vec![];
let mut reedline_events: Vec<ReedlineEvent> = vec![]; let mut reedline_events: Vec<ReedlineEvent> = vec![];
loop { loop {
@ -528,18 +528,29 @@ impl Reedline {
enter @ Event::Key(KeyEvent { enter @ Event::Key(KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
..
}) => { }) => {
crossterm_events.push(enter); let enter = ReedlineRawEvent::convert_from(enter);
// Break early to check if the input is complete and match enter {
// can be send to the hosting application. If Some(enter) => {
// multiple complete entries are submitted, events crossterm_events.push(enter);
// are still in the crossterm queue for us to // Break early to check if the input is complete and
// process. // can be send to the hosting application. If
paste_enter_state = crossterm_events.len() > EVENTS_THRESHOLD; // multiple complete entries are submitted, events
break; // are still in the crossterm queue for us to
// process.
paste_enter_state = crossterm_events.len() > EVENTS_THRESHOLD;
break;
}
None => continue,
}
} }
x => { x => {
crossterm_events.push(x); let raw_event = ReedlineRawEvent::convert_from(x);
match raw_event {
Some(evt) => crossterm_events.push(evt),
None => continue,
}
} }
} }
} }

View File

@ -1,3 +1,4 @@
use crossterm::event::{Event, KeyEvent, KeyEventKind};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use strum_macros::EnumIter; use strum_macros::EnumIter;
@ -560,3 +561,42 @@ pub(crate) enum EventStatus {
Inapplicable, Inapplicable,
Exits(Signal), 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<Self> {
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
}
}

View File

@ -230,7 +230,7 @@ pub use core_editor::Editor;
pub use core_editor::LineBuffer; pub use core_editor::LineBuffer;
mod enums; mod enums;
pub use enums::{EditCommand, ReedlineEvent, Signal, UndoBehavior}; pub use enums::{EditCommand, ReedlineEvent, ReedlineRawEvent, Signal, UndoBehavior};
mod painting; mod painting;
pub use painting::{Painter, StyledText}; pub use painting::{Painter, StyledText};
@ -259,7 +259,7 @@ pub use prompt::{
mod edit_mode; mod edit_mode;
pub use edit_mode::{ pub use edit_mode::{
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
CursorConfig, EditMode, Emacs, Event, Keybindings, Vi, CursorConfig, EditMode, Emacs, Keybindings, Vi,
}; };
mod highlighter; mod highlighter;

View File

@ -186,7 +186,7 @@ impl Painter {
_ => None, _ => None,
}; };
if let Some(shape) = shape { if let Some(shape) = shape {
self.stdout.queue(cursor::SetCursorShape(shape))?; self.stdout.queue(shape)?;
} }
} }
self.stdout.queue(cursor::Show)?; self.stdout.queue(cursor::Show)?;

View File

@ -57,6 +57,15 @@ impl Display for ReedLineCrossTermKeyCode {
KeyCode::Char(_) => write!(f, "Char_<letter>"), KeyCode::Char(_) => write!(f, "Char_<letter>"),
KeyCode::Null => write!(f, "Null"), KeyCode::Null => write!(f, "Null"),
KeyCode::Esc => write!(f, "Esc"), 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<media>"),
KeyCode::Modifier(_) => write!(f, "Modifier<modifier>"),
}, },
} }
} }