From 317faa62ee8d42cff20b26ca8b18cb687a49d9f7 Mon Sep 17 00:00:00 2001 From: Sicheng Chen Date: Wed, 1 Jul 2026 18:59:16 +0800 Subject: [PATCH] fix(terminal): ignore Windows key release events --- src/element_program.rs | 11 ++++++----- src/event.rs | 44 +++++++++++++++++++++++++++++++++++++++++- src/program.rs | 11 ++++++----- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/element_program.rs b/src/element_program.rs index 575a59b..62594bd 100644 --- a/src/element_program.rs +++ b/src/element_program.rs @@ -148,12 +148,13 @@ impl ElementProgram { event = event_stream.next() => { match event { Some(Ok(ct_event)) => { - let ev: Event = ct_event.into(); - let msg: M::Msg = ev.into(); - if let Some(cmd) = model.update(msg) { - Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone()); + if let Some(ev) = Event::from_crossterm(ct_event) { + let msg: M::Msg = ev.into(); + if let Some(cmd) = model.update(msg) { + Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone()); + } + dirty = true; } - dirty = true; } Some(Err(_)) => break, None => break, diff --git a/src/event.rs b/src/event.rs index 913604a..a7275db 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,6 +1,6 @@ //! Terminal event types (keyboard, mouse, resize, focus). -use crossterm::event::{KeyCode, KeyModifiers, MouseEventKind as CtMouseEventKind}; +use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseEventKind as CtMouseEventKind}; /// A terminal event. #[derive(Debug, Clone, PartialEq, Eq)] @@ -120,6 +120,15 @@ impl From for Event { } } +impl Event { + pub fn from_crossterm(event: crossterm::event::Event) -> Option { + match event { + crossterm::event::Event::Key(key) if key.kind == KeyEventKind::Release => None, + event => Some(event.into()), + } + } +} + fn convert_mouse_button(button: crossterm::event::MouseButton) -> MouseButton { match button { crossterm::event::MouseButton::Left => MouseButton::Left, @@ -145,6 +154,39 @@ fn convert_mouse_kind(kind: CtMouseEventKind) -> MouseEventKind { mod tests { use super::*; + #[test] + fn crossterm_key_release_is_ignored() { + let event = crossterm::event::Event::Key(crossterm::event::KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::NONE, + crossterm::event::KeyEventKind::Release, + )); + + assert_eq!(Event::from_crossterm(event), None); + } + + #[test] + fn crossterm_key_press_and_repeat_are_kept() { + for kind in [ + crossterm::event::KeyEventKind::Press, + crossterm::event::KeyEventKind::Repeat, + ] { + let event = crossterm::event::Event::Key(crossterm::event::KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::NONE, + kind, + )); + + assert_eq!( + Event::from_crossterm(event), + Some(Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::NONE, + })) + ); + } + } + #[test] fn is_char() { let e = KeyEvent { diff --git a/src/program.rs b/src/program.rs index e2b28ab..a871eb7 100644 --- a/src/program.rs +++ b/src/program.rs @@ -114,15 +114,16 @@ impl Program { event = event_stream.next() => { match event { Some(Ok(ct_event)) => { - immediate = true; // A resize shifts every row — force a full clear+redraw. if matches!(ct_event, crossterm::event::Event::Resize(_, _)) { renderer.invalidate(); } - let ev: Event = ct_event.into(); - let msg: M::Msg = ev.into(); - if let Some(cmd) = model.update(msg) { - Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone()); + if let Some(ev) = Event::from_crossterm(ct_event) { + immediate = true; + let msg: M::Msg = ev.into(); + if let Some(cmd) = model.update(msg) { + Self::dispatch_cmd(cmd, msg_tx.clone(), quit_flag.clone()); + } } } Some(Err(_)) => break,