Support Ctrl+Insert for copying TUI selections (#50215)

## Why

Windows Terminal forwards `Ctrl+Insert` when its Copy action has no native terminal selection. Recognize this shortcut so it can copy text selected within Codex.

## What changed

Add `Ctrl+Insert` to the shared copy-key detection used by transcript and composer selections. Use the same detection for transcript input ownership so the selection consumes the shortcut before other input handlers.

## Testing

Extend transcript tests to verify `Ctrl+Insert` input ownership and copying. Extend composer tests and snapshots to verify that failed or unconfirmed copies preserve the selection, while confirmed copies clear it without changing the draft or cursor.

GitOrigin-RevId: 5a33c72cff4c4395511779a7051fc92f8bd5e5d7
This commit is contained in:
Eric Traut
2026-10-02 07:21:07 +00:00
committed by copyberry
parent 4dd51f4a5f
commit d64360f628
4 changed files with 26 additions and 12 deletions
@@ -801,10 +801,11 @@ async fn owned_search_and_selection_consume_input_before_composer_and_backtrack(
&TuiEvent::Key(key) &TuiEvent::Key(key)
)?); )?);
} }
let copy = app.transcript_view.handle_key( let copy_key = KeyEvent::new(KeyCode::Insert, KeyModifiers::CONTROL);
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), assert!(app.transcript_view.owns_interaction_key(copy_key));
&app.transcript_cells, let copy = app
); .transcript_view
.handle_key(copy_key, &app.transcript_cells);
assert!(matches!(copy, Some(ViewAction::Copy(text)) if !text.is_empty())); assert!(matches!(copy, Some(ViewAction::Copy(text)) if !text.is_empty()));
assert!(app.handle_owned_transcript_event(&mut tui, &mut app_server, &escape)?); assert!(app.handle_owned_transcript_event(&mut tui, &mut app_server, &escape)?);
assert!(!app.backtrack.overlay_preview_active); assert!(!app.backtrack.overlay_preview_active);
@@ -1414,6 +1415,7 @@ async fn fullscreen_composer_mouse_copy_and_input_ownership() -> Result<()> {
let copy_events = [ let copy_events = [
TuiEvent::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SUPER)), TuiEvent::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SUPER)),
TuiEvent::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), TuiEvent::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
TuiEvent::Key(KeyEvent::new(KeyCode::Insert, KeyModifiers::CONTROL)),
mouse(Down(Right), x + 2, y), mouse(Down(Right), x + 2, y),
]; ];
let mut selection_frames = Vec::new(); let mut selection_frames = Vec::new();
@@ -1473,6 +1475,7 @@ async fn fullscreen_composer_mouse_copy_and_input_ownership() -> Result<()> {
); );
let gesture = match event { let gesture = match event {
TuiEvent::Mouse(_) => "right-click", TuiEvent::Mouse(_) => "right-click",
TuiEvent::Key(key) if key.code == KeyCode::Insert => "ctrl-insert",
TuiEvent::Key(key) if key.modifiers == KeyModifiers::SUPER => "cmd-c", TuiEvent::Key(key) if key.modifiers == KeyModifiers::SUPER => "cmd-c",
_ => "ctrl-c", _ => "ctrl-c",
}; };
@@ -26,6 +26,18 @@ ctrl-c Ok(Confirmed)
hello world hello world
··········· ···········
ctrl-insert Err("clipboard unavailable")
hello world
^^^^^······
ctrl-insert Ok(Unconfirmed)
hello world
^^^^^······
ctrl-insert Ok(Confirmed)
hello world
···········
right-click Err("clipboard unavailable") right-click Err("clipboard unavailable")
hello world hello world
^^^^^······ ^^^^^······
+6 -4
View File
@@ -67,10 +67,12 @@ pub(crate) fn click_count(
pub(crate) fn is_copy_key(key: KeyEvent) -> bool { pub(crate) fn is_copy_key(key: KeyEvent) -> bool {
// Kitty reports Cmd+C as Super+C, including over SSH. Crossterm may report // Kitty reports Cmd+C as Super+C, including over SSH. Crossterm may report
// Ctrl+Shift+C as uppercase C with only Control set. // Ctrl+Shift+C as uppercase C with only Control set. Windows Terminal forwards
// Ctrl+Insert when its Copy action has no native terminal selection.
let (code, modifiers) = crate::key_hint::normalize_key_parts(key.code, key.modifiers); let (code, modifiers) = crate::key_hint::normalize_key_parts(key.code, key.modifiers);
key.kind != KeyEventKind::Release key.kind != KeyEventKind::Release
&& code == KeyCode::Char('c') && ((code == KeyCode::Char('c')
&& (matches!(modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER) && (matches!(modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER)
|| modifiers == (KeyModifiers::CONTROL | KeyModifiers::SHIFT)) || modifiers == (KeyModifiers::CONTROL | KeyModifiers::SHIFT)))
|| (code == KeyCode::Insert && modifiers == KeyModifiers::CONTROL))
} }
+1 -4
View File
@@ -136,10 +136,7 @@ impl TranscriptView {
return true; return true;
} }
if self.selection.is_some() { if self.selection.is_some() {
return (code == KeyCode::Char('c') return crate::text_selection::is_copy_key(key)
&& matches!(modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER))
|| (code == KeyCode::Char('c')
&& modifiers == (KeyModifiers::CONTROL | KeyModifiers::SHIFT))
|| code == KeyCode::Esc || code == KeyCode::Esc
|| (code == KeyCode::Enter && modifiers == KeyModifiers::NONE) || (code == KeyCode::Enter && modifiers == KeyModifiers::NONE)
|| (matches!(modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT) || (matches!(modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT)