From ccf753de245bfeafc466131e3d1dcf6e774231d0 Mon Sep 17 00:00:00 2001 From: Freddy Date: Thu, 8 Oct 2026 19:11:25 +0200 Subject: [PATCH 1/2] fix(input): attach clipboard images on Ctrl+V press outside Windows The clipboard image handler only ran on the Ctrl+V key release. Outside Windows a release never arrives: the app never requests REPORT_EVENT_TYPES, so macOS and Linux terminals report presses only and Ctrl+V never attached an image. One predicate, is_clipboard_paste_trigger, now owns which event kind triggers the clipboard read: the release on Windows (where Windows Terminal consumes the press for its own paste binding) and the press everywhere else. Both the event filter and the key handler use it. Fixes #431. Refs #385. Co-Authored-By: Claude Opus 5.5 --- src/app/events/mod.rs | 2 +- src/app/events/tests/mod.rs | 21 +++++++++++----- src/app/keys.rs | 50 ++++++++++++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 13 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..b1227abb 100644 --- a/src/app/events/tests/mod.rs +++ b/src/app/events/tests/mod.rs @@ -3583,16 +3583,25 @@ 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'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Release, - state: crossterm::event::KeyEventState::NONE, - }; + // Windows Terminal consumes the Ctrl+V press, so the release is the only + // event that can attach a clipboard image. + let key = + KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, KeyEventKind::Release); assert!(should_dispatch_key_event(key)); } +#[test] +#[cfg(not(windows))] +fn clipboard_paste_shortcut_release_is_ignored_outside_windows() { + // The press already attached the image; dispatching a release that a + // terminal still reports would attach it a second time. + let key = + KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, KeyEventKind::Release); + assert!(!should_dispatch_key_event(key)); +} + #[test] #[cfg(windows)] fn conpty_release_only_accent_survives_input_and_prompt_dispatch() { diff --git a/src/app/keys.rs b/src/app/keys.rs index e0f4ca28..3927d5df 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,17 @@ 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 key event kind on which Ctrl+V reads an image from the clipboard. +/// +/// Windows consoles report releases, and Windows Terminal consumes the Ctrl+V +/// press for its own paste binding, so only the release reliably reaches the +/// app. Other platforms report presses only, because the app never requests +/// `REPORT_EVENT_TYPES` (see `terminal_runtime/modes.rs`). +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 +985,38 @@ mod tests { assert!(is_ctrl_char_shortcut(key, 'v')); } + // Ghostty and kitty send Ctrl+V as `ESC [118;5u`; terminals without the + // kitty protocol (tmux, for example) send the raw 0x16 byte. + fn ctrl_v_encodings(kind: KeyEventKind) -> [KeyEvent; 2] { + [ + KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, kind), + KeyEvent::new_with_kind(KeyCode::Char('\u{16}'), KeyModifiers::NONE, kind), + ] + } + + #[test] + #[cfg(not(windows))] + fn clipboard_paste_triggers_on_ctrl_v_press_outside_windows() { + // macOS and Linux terminals never report the release here (#431, #385). + for key in ctrl_v_encodings(KeyEventKind::Press) { + assert!(is_clipboard_paste_trigger(key), "{key:?}"); + } + for key in ctrl_v_encodings(KeyEventKind::Release) { + assert!(!is_clipboard_paste_trigger(key), "{key:?}"); + } + } + + #[test] + #[cfg(windows)] + fn clipboard_paste_triggers_on_ctrl_v_release_on_windows() { + for key in ctrl_v_encodings(KeyEventKind::Release) { + assert!(is_clipboard_paste_trigger(key), "{key:?}"); + } + for key in ctrl_v_encodings(KeyEventKind::Press) { + assert!(!is_clipboard_paste_trigger(key), "{key:?}"); + } + } + #[test] fn ctrl_shortcut_rejects_raw_control_character_with_alt() { let key = KeyEvent::new(KeyCode::Char('\u{16}'), KeyModifiers::ALT); From 4750ea68144feaa0edc96599464f241ba7777d78 Mon Sep 17 00:00:00 2001 From: Freddy Saez Date: Thu, 8 Oct 2026 20:22:04 +0200 Subject: [PATCH 2/2] refactor(input): drop comments from Ctrl+V paste trigger Co-Authored-By: Claude Opus 5.5 --- src/app/events/tests/mod.rs | 4 ---- src/app/keys.rs | 9 --------- 2 files changed, 13 deletions(-) diff --git a/src/app/events/tests/mod.rs b/src/app/events/tests/mod.rs index b1227abb..8d4d513d 100644 --- a/src/app/events/tests/mod.rs +++ b/src/app/events/tests/mod.rs @@ -3585,8 +3585,6 @@ fn settings_view_ignores_paste_events() { #[test] #[cfg(windows)] fn clipboard_paste_shortcut_dispatches_on_release() { - // Windows Terminal consumes the Ctrl+V press, so the release is the only - // event that can attach a clipboard image. let key = KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, KeyEventKind::Release); assert!(should_dispatch_key_event(key)); @@ -3595,8 +3593,6 @@ fn clipboard_paste_shortcut_dispatches_on_release() { #[test] #[cfg(not(windows))] fn clipboard_paste_shortcut_release_is_ignored_outside_windows() { - // The press already attached the image; dispatching a release that a - // terminal still reports would attach it a second time. let key = KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, KeyEventKind::Release); assert!(!should_dispatch_key_event(key)); diff --git a/src/app/keys.rs b/src/app/keys.rs index 3927d5df..586182c0 100644 --- a/src/app/keys.rs +++ b/src/app/keys.rs @@ -660,12 +660,6 @@ fn handle_clipboard_paste_key(app: &mut App, key: KeyEvent) -> bool { } } -/// The key event kind on which Ctrl+V reads an image from the clipboard. -/// -/// Windows consoles report releases, and Windows Terminal consumes the Ctrl+V -/// press for its own paste binding, so only the release reliably reaches the -/// app. Other platforms report presses only, because the app never requests -/// `REPORT_EVENT_TYPES` (see `terminal_runtime/modes.rs`). const CLIPBOARD_PASTE_TRIGGER_KIND: KeyEventKind = if cfg!(windows) { KeyEventKind::Release } else { KeyEventKind::Press }; @@ -985,8 +979,6 @@ mod tests { assert!(is_ctrl_char_shortcut(key, 'v')); } - // Ghostty and kitty send Ctrl+V as `ESC [118;5u`; terminals without the - // kitty protocol (tmux, for example) send the raw 0x16 byte. fn ctrl_v_encodings(kind: KeyEventKind) -> [KeyEvent; 2] { [ KeyEvent::new_with_kind(KeyCode::Char('v'), KeyModifiers::CONTROL, kind), @@ -997,7 +989,6 @@ mod tests { #[test] #[cfg(not(windows))] fn clipboard_paste_triggers_on_ctrl_v_press_outside_windows() { - // macOS and Linux terminals never report the release here (#431, #385). for key in ctrl_v_encodings(KeyEventKind::Press) { assert!(is_clipboard_paste_trigger(key), "{key:?}"); }