From 8e58d996f8ea905885a297351e9e0efa5f8858fb Mon Sep 17 00:00:00 2001 From: ayush5harma <47772616+ayush5harma@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:19:15 +0530 Subject: [PATCH] fix(input): attach clipboard images on Ctrl+V press outside Windows - read the clipboard on the Ctrl+V event the terminal actually reports: the release on Windows, the press elsewhere, where the app never requests release reporting and a release never arrives - give that decision one owner, is_clipboard_paste_trigger, used by both the event filter and the paste handler - stop dispatching a Ctrl+V release outside Windows so a terminal that still reports one cannot attach the image twice - cover the press trigger, the raw 0x16 encoding and the ignored release --- src/app/events/mod.rs | 2 +- src/app/events/tests/mod.rs | 15 ++++++++++++++ src/app/keys.rs | 40 +++++++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/app/events/mod.rs b/src/app/events/mod.rs index 96a0a1f6..7c7b3c09 100644 --- a/src/app/events/mod.rs +++ b/src/app/events/mod.rs @@ -156,7 +156,7 @@ fn should_dispatch_key_event(key: crossterm::event::KeyEvent) -> bool { && matches!(key.code, crossterm::event::KeyCode::Char(ch) if !ch.is_control() && unicode_width::UnicodeWidthChar::width(ch) == Some(0)); key.kind == KeyEventKind::Press - || (key.kind == KeyEventKind::Release && super::keys::is_clipboard_paste_shortcut(key)) + || super::keys::is_clipboard_paste_trigger(key) || zero_width_text_release } diff --git a/src/app/events/tests/mod.rs b/src/app/events/tests/mod.rs index 121261be..4936fed9 100644 --- a/src/app/events/tests/mod.rs +++ b/src/app/events/tests/mod.rs @@ -3583,6 +3583,7 @@ fn settings_view_ignores_paste_events() { } #[test] +#[cfg(windows)] fn clipboard_paste_shortcut_dispatches_on_release() { let key = crossterm::event::KeyEvent { code: KeyCode::Char('v'), @@ -3626,6 +3627,20 @@ fn conpty_release_only_accent_survives_input_and_prompt_dispatch() { assert!(app.input.is_empty()); } +#[test] +#[cfg(not(windows))] +fn clipboard_paste_shortcut_release_is_ignored_outside_windows() { + // The press already attached the image; dispatching a release a terminal + // might still send would attach it a second time. + let key = crossterm::event::KeyEvent { + code: KeyCode::Char('v'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Release, + state: crossterm::event::KeyEventState::NONE, + }; + assert!(!should_dispatch_key_event(key)); +} + #[test] fn non_paste_shortcut_release_is_ignored() { let key = crossterm::event::KeyEvent { diff --git a/src/app/keys.rs b/src/app/keys.rs index e0f4ca28..05b1bb87 100644 --- a/src/app/keys.rs +++ b/src/app/keys.rs @@ -603,10 +603,7 @@ fn handle_mode_cycle(app: &mut App) -> bool { } fn handle_clipboard_paste_key(app: &mut App, key: KeyEvent) -> bool { - if !is_clipboard_paste_shortcut(key) { - return false; - } - if key.kind != KeyEventKind::Release { + if !is_clipboard_paste_trigger(key) { return false; } @@ -663,8 +660,15 @@ fn handle_clipboard_paste_key(app: &mut App, key: KeyEvent) -> bool { } } -pub(super) fn is_clipboard_paste_shortcut(key: KeyEvent) -> bool { - is_ctrl_char_shortcut(key, 'v') +/// The Ctrl+V event that reads an image from the clipboard. Windows consoles +/// deliver the shortcut on release. Other terminals report presses only: the +/// app never requests release reporting (`REPORT_EVENT_TYPES`, see +/// `terminal_runtime/modes.rs`), so waiting for a release there never attaches. +const CLIPBOARD_PASTE_TRIGGER_KIND: KeyEventKind = + if cfg!(windows) { KeyEventKind::Release } else { KeyEventKind::Press }; + +pub(super) fn is_clipboard_paste_trigger(key: KeyEvent) -> bool { + key.kind == CLIPBOARD_PASTE_TRIGGER_KIND && is_ctrl_char_shortcut(key, 'v') } pub(super) fn reclaim_input_from_inline_prompt_if_needed(app: &mut App) { @@ -979,6 +983,30 @@ mod tests { assert!(is_ctrl_char_shortcut(key, 'v')); } + #[test] + #[cfg(not(windows))] + fn clipboard_paste_reads_the_clipboard_on_ctrl_v_press() { + // Outside Windows the terminal reports Ctrl+V as a press only; a + // release never arrives, so a release-only trigger never attaches. + for code in [KeyCode::Char('v'), KeyCode::Char('\u{16}')] { + let modifiers = + if code == KeyCode::Char('v') { KeyModifiers::CONTROL } else { KeyModifiers::NONE }; + let press = KeyEvent::new_with_kind(code, modifiers, KeyEventKind::Press); + let release = KeyEvent::new_with_kind(code, modifiers, KeyEventKind::Release); + assert!(is_clipboard_paste_trigger(press), "{code:?} press"); + assert!(!is_clipboard_paste_trigger(release), "{code:?} release"); + } + } + + #[test] + #[cfg(windows)] + fn clipboard_paste_reads_the_clipboard_on_ctrl_v_release() { + let ctrl_v = + |kind| KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, kind); + assert!(is_clipboard_paste_trigger(ctrl_v(KeyEventKind::Release))); + assert!(!is_clipboard_paste_trigger(ctrl_v(KeyEventKind::Press))); + } + #[test] fn ctrl_shortcut_rejects_raw_control_character_with_alt() { let key = KeyEvent::new(KeyCode::Char('\u{16}'), KeyModifiers::ALT);