Extend copy-on-select to composer text (#53753)

## What changed

Apply `tui.copy_on_select` to composer mouse selections on release, including X11 PRIMARY publication when enabled. Keep editable selections highlighted so typing can replace the selected text, even when `tui.copy_on_select_clear_selection` is enabled.

Share clipboard destination handling across the transcript, footer, and composer. Apply automatic deselection to footer selections after confirmed delivery, and release composer and footer PRIMARY ownership on focus loss or resume. Update configuration descriptions to clarify the behavior across all three surfaces.

## Testing

Add composer tests covering copy on release, retained selection highlights, replacement by typing, PRIMARY ownership lifetime, and clipboard-only status reporting for pending copies.

GitOrigin-RevId: 46999a32170149193fca561741000e97dfc458cf
This commit is contained in:
Felipe Coury
2026-10-13 19:45:14 +00:00
committed by copyberry
parent e762c05bd6
commit 3517671c60
23 changed files with 362 additions and 73 deletions
+5 -4
View File
@@ -786,7 +786,7 @@ pub enum CopyOnSelect {
/// Use the terminal-specific default.
#[default]
Auto,
/// Copy every nonempty transcript or footer mouse selection on release.
/// Copy every nonempty transcript, footer, or composer mouse selection on release.
Always,
/// Require an explicit copy action.
Never,
@@ -891,15 +891,16 @@ pub struct Tui {
#[schemars(schema_with = "crate::tui_mouse_scroll::schema")]
pub mouse_scroll_speed: Option<f64>,
/// Copy selected transcript or footer text when the mouse button is released.
/// Copy selected transcript, footer, or composer text when the mouse button is released.
/// Defaults to `auto`: enabled except in direct terminals known to forward their native
/// copy shortcut (Ghostty 1.2+, Kitty on macOS, Windows Terminal, and VS Code on Windows).
/// Unknown terminals, Ghostty without a recognized version, and tmux/Zellij default to copying.
#[serde(default)]
pub copy_on_select: CopyOnSelect,
/// Deselect transcript text after Copy on select confirms clipboard delivery.
/// Defaults to `false` and has no effect when Copy on select is off.
/// Deselect read-only transcript or footer text after Copy on select confirms delivery.
/// Editable composer selections remain visible. Defaults to `false` and has no effect when
/// Copy on select is off.
#[serde(default)]
pub copy_on_select_clear_selection: bool,
+3 -3
View File
@@ -1430,7 +1430,7 @@
"type": "string"
},
{
"description": "Copy every nonempty transcript or footer mouse selection on release.",
"description": "Copy every nonempty transcript, footer, or composer mouse selection on release.",
"enum": [
"always"
],
@@ -4683,11 +4683,11 @@
}
],
"default": "auto",
"description": "Copy selected transcript or footer text when the mouse button is released. Defaults to `auto`: enabled except in direct terminals known to forward their native copy shortcut (Ghostty 1.2+, Kitty on macOS, Windows Terminal, and VS Code on Windows). Unknown terminals, Ghostty without a recognized version, and tmux/Zellij default to copying."
"description": "Copy selected transcript, footer, or composer text when the mouse button is released. Defaults to `auto`: enabled except in direct terminals known to forward their native copy shortcut (Ghostty 1.2+, Kitty on macOS, Windows Terminal, and VS Code on Windows). Unknown terminals, Ghostty without a recognized version, and tmux/Zellij default to copying."
},
"copy_on_select_clear_selection": {
"default": false,
"description": "Deselect transcript text after Copy on select confirms clipboard delivery. Defaults to `false` and has no effect when Copy on select is off.",
"description": "Deselect read-only transcript or footer text after Copy on select confirms delivery. Editable composer selections remain visible. Defaults to `false` and has no effect when Copy on select is off.",
"type": "boolean"
},
"disable_paste_burst": {
+2 -2
View File
@@ -828,10 +828,10 @@ pub struct Config {
/// Mouse wheel speed multiplier for transcript scrolling; defaults to one row per event.
pub tui_mouse_scroll_speed: Option<f64>,
/// Override the terminal-specific default for copying transcript mouse selections.
/// Override the terminal default for copying transcript, footer, and composer selections.
pub tui_copy_on_select: codex_config::types::CopyOnSelect,
/// Whether to deselect transcript text after a confirmed automatic copy.
/// Whether to deselect read-only transcript or footer text after confirmed automatic copy.
pub tui_copy_on_select_clear_selection: bool,
/// Right-click text paste fallback for the fullscreen TUI.
+6
View File
@@ -78,6 +78,12 @@ impl App {
}
if !current || matches!(event, TuiEvent::FocusLost | TuiEvent::Resume) {
self.transcript_view.cancel_primary();
self.chat_widget.cancel_composer_primary();
self.chat_widget
.rendered_selection
.borrow_mut()
.view
.cancel_primary();
}
}
}
+2
View File
@@ -32,6 +32,8 @@ impl App {
selection.view.copy_on_select = self
.local_settings
.copy_on_select(&codex_terminal_detection::terminal_info());
selection.view.copy_on_select_clear_selection =
self.local_settings.tui.copy_on_select_clear_selection;
selection.view.primary_selection = self.right_click_paste_environment.primary;
selection.handle_event(event, tui.terminal.previous_buffer())
};
+38
View File
@@ -39,6 +39,43 @@ impl App {
false
}
/// Publish a completed composer selection with the transcript/footer copy policy.
fn handle_composer_selection_release(
&mut self,
tui: &mut tui::Tui,
event: crossterm::event::MouseEvent,
) -> bool {
let selection_copy = crate::tui::SelectionCopy::on_select(
self.local_settings
.copy_on_select(&codex_terminal_detection::terminal_info()),
self.right_click_paste_environment.primary,
// Editable selections remain visible so the next input can replace them.
/*clear_selection*/
false,
);
if !selection_copy.copies_clipboard() && !selection_copy.publishes_primary() {
return false;
}
let Some((char_count, result)) = self.chat_widget.copy_composer_selection_on_release(
event,
selection_copy,
|text, selection_copy| {
tui.copy_selection(
text,
crate::clipboard_copy::CopyFormat::PlainText,
selection_copy,
text,
)
},
) else {
return false;
};
if let Some(result) = result {
self.transcript_view.show_copy_feedback(&result, char_count);
}
true
}
fn sync_owned_transcript(&mut self, width: u16) -> bool {
let chat_widget = &self.chat_widget;
let transcript_width = chat_widget.history_wrap_width(width);
@@ -434,6 +471,7 @@ impl App {
return Ok(true);
}
if composer_ready && self.chat_widget.handle_composer_mouse(*mouse) {
self.handle_composer_selection_release(tui, *mouse);
if !matches!(
mouse.kind,
crossterm::event::MouseEventKind::ScrollUp
@@ -249,7 +249,7 @@
//! Submission flushes expired characters and buffers before classifying Enter, independent of
//! UI flush ticks. `disable_paste_burst` bypasses detection; setting it flushes and clears in-flight state.
//! Mouse edits flush pending typing; selection and copy behavior lives in [`mouse`]. Confirmed
//! copies clear the selection while preserving the draft and cursor. Footer text uses a separate
//! explicit copies clear the selection while preserving the draft and cursor. Footer text uses a separate
//! read-only selection owner over its final rendered cells.
//!
//! See `codex-rs/tui/src/bottom_pane/paste_burst.rs` for the detailed state machine.
@@ -1,11 +1,14 @@
//! Mouse gestures flush pending typing before layout and hit testing. Left dragging selects
//! editable text using the textarea's last rendered viewport and hides completion suggestions.
//! Double/triple clicks select words/logical lines using the transcript's shared gesture rules.
//! Copy preserves the draft and cursor; confirmed copies clear selection for every gesture.
//! Selection release follows the transcript/footer copy policy while keeping editable text selected.
//! Explicit copy preserves the draft and cursor, and clears after confirmed delivery.
//! Wheel browsing uses the current viewport without changing popup or selection ownership.
use super::*;
use crate::clipboard_copy::CopyStatus;
use crate::clipboard_copy::worker::CopyResult;
use crate::tui::SelectionCopy;
use crate::tui::TuiEvent;
use crossterm::event::MouseButton;
use crossterm::event::MouseEvent;
@@ -87,6 +90,26 @@ impl ChatComposer {
Some((char_count, result))
}
pub(crate) fn copy_selection_on_release(
&mut self,
event: MouseEvent,
selection_copy: SelectionCopy,
copy: impl FnOnce(&str, SelectionCopy) -> (CopyResult, Option<std::sync::Arc<()>>),
) -> Option<(usize, Option<CopyResult>)> {
if event.kind != MouseEventKind::Up(MouseButton::Left) {
return None;
}
let range = self.draft.textarea.mouse_selection_range()?;
let text = &self.draft.textarea.text()[range];
let char_count = text.chars().count();
let (result, primary_owner) = copy(text, selection_copy);
self.draft.textarea.retain_primary(primary_owner);
if selection_copy.copies_clipboard() {
return Some((char_count, Some(result)));
}
Some((char_count, None))
}
pub(crate) fn clear_mouse_selection(&mut self) {
self.draft.textarea.set_cursor(self.draft.textarea.cursor());
}
@@ -95,6 +118,10 @@ impl ChatComposer {
self.draft.textarea.end_mouse_drag();
}
pub(crate) fn cancel_primary(&mut self) {
self.draft.textarea.cancel_primary();
}
pub(crate) fn prepare_mouse(&mut self, event: MouseEvent) -> bool {
if !self.draft.input_enabled
|| self.blocks_direct_input
@@ -214,3 +214,100 @@ fn right_click_copy_uses_the_refreshed_editor_bounds() {
paste_target
);
}
#[test]
fn selection_release_copies_and_keeps_the_editable_selection() {
let (mut composer, _rx) = new_test_composer();
composer.insert_str("hello world");
let area = Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 40, /*height*/ 4,
);
composer.render(area, &mut Buffer::empty(area));
let (x, y) = composer.cursor_pos(area).unwrap();
let release = MouseEvent {
kind: Up(Left),
column: x - 6,
row: y,
modifiers: KeyModifiers::NONE,
};
for (kind, column) in [
(Down(Left), x - 11),
(MouseEventKind::Drag(Left), x - 6),
(Up(Left), x - 6),
] {
mouse(&mut composer, area, kind, column, y);
}
let selection_copy = crate::tui::SelectionCopy::on_select(
/*clipboard*/ true, /*primary*/ true, /*clear_selection*/ false,
);
let owner = std::sync::Arc::new(());
let retained = std::sync::Arc::downgrade(&owner);
assert_eq!(
composer.copy_selection_on_release(release, selection_copy, |text, policy| {
assert_eq!((text, policy), ("hello", selection_copy));
(Ok(CopyStatus::Confirmed), Some(owner))
}),
Some((5, Some(Ok(CopyStatus::Confirmed))))
);
let mut buffer = Buffer::empty(area);
composer.render(area, &mut buffer);
let highlight = (x - 11..x)
.map(|column| {
buffer[(column, y)]
.modifier
.contains(ratatui::style::Modifier::REVERSED)
})
.collect::<Vec<_>>();
assert_eq!(
highlight,
vec![
true, true, true, true, true, false, false, false, false, false, false
]
);
assert!(retained.upgrade().is_some());
composer.insert_str("X");
assert_eq!(composer.current_text(), "X world");
assert!(retained.upgrade().is_none());
}
#[test]
fn selection_release_reports_only_clipboard_result() {
for (clipboard, expected) in [(true, Some(Ok(CopyStatus::Pending(7)))), (false, None)] {
let (mut composer, _rx) = new_test_composer();
composer.insert_str("hello world");
let area = Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 40, /*height*/ 4,
);
composer.render(area, &mut Buffer::empty(area));
let (x, y) = composer.cursor_pos(area).unwrap();
let release = MouseEvent {
kind: Up(Left),
column: x - 6,
row: y,
modifiers: KeyModifiers::NONE,
};
for (kind, column) in [
(Down(Left), x - 11),
(MouseEventKind::Drag(Left), x - 6),
(Up(Left), x - 6),
] {
mouse(&mut composer, area, kind, column, y);
}
let selection = composer.draft.textarea.mouse_selection_range();
let policy = crate::tui::SelectionCopy::on_select(
clipboard, /*primary*/ true, /*clear_selection*/ false,
);
assert_eq!(
composer.copy_selection_on_release(release, policy, |text, actual| {
assert_eq!((text, actual), ("hello", policy));
(Ok(CopyStatus::Pending(7)), Some(std::sync::Arc::new(())))
}),
Some((5, expected))
);
composer.finish_copy(&(7, Ok(CopyStatus::Confirmed)), /*current*/ true);
assert_eq!(composer.draft.textarea.mouse_selection_range(), selection);
}
}
+23
View File
@@ -1891,6 +1891,10 @@ impl BottomPane {
self.composer.end_mouse_drag();
}
pub(crate) fn cancel_composer_primary(&mut self) {
self.composer.cancel_primary();
}
pub(crate) fn finish_composer_copy(
&mut self,
completion: &(u64, crate::clipboard_copy::worker::CopyResult),
@@ -1911,6 +1915,25 @@ impl BottomPane {
self.composer.copy_selection(event, copy)
}
pub(crate) fn copy_composer_selection_on_release(
&mut self,
event: crossterm::event::MouseEvent,
selection_copy: crate::tui::SelectionCopy,
copy: impl FnOnce(
&str,
crate::tui::SelectionCopy,
) -> (
crate::clipboard_copy::worker::CopyResult,
Option<std::sync::Arc<()>>,
),
) -> Option<(usize, Option<crate::clipboard_copy::worker::CopyResult>)> {
if self.has_active_view() || self.questions.as_ref().is_some_and(|q| q.expanded) {
return None;
}
self.composer
.copy_selection_on_release(event, selection_copy, copy)
}
pub(crate) fn clipboard_paste_target(
&self,
shortcut: Option<KeyCode>,
@@ -6,10 +6,12 @@ use crossterm::event::MouseButton;
use crossterm::event::MouseEvent;
use crossterm::event::MouseEventKind;
use ratatui::layout::Position;
use std::sync::Arc;
#[derive(Debug)]
pub(super) struct MouseSelection {
pending_copy: Option<(u64, Range<usize>)>,
primary_owner: Option<Arc<()>>,
origin: Range<usize>,
unit: SelectionUnit,
dragging: bool,
@@ -25,6 +27,16 @@ impl TextArea {
}
}
pub(in crate::bottom_pane) fn retain_primary(&mut self, owner: Option<Arc<()>>) {
if let Some(selection) = &mut self.mouse_selection {
selection.primary_owner = owner;
}
}
pub(in crate::bottom_pane) fn cancel_primary(&mut self) {
self.retain_primary(/*owner*/ None);
}
pub(in crate::bottom_pane) fn finish_copy(
&mut self,
completion: &(u64, crate::clipboard_copy::worker::CopyResult),
@@ -136,6 +148,7 @@ impl TextArea {
crate::text_selection::click_count(&mut self.last_click, event.column, event.row);
MouseSelection {
pending_copy: None,
primary_owner: None,
origin: pos..pos,
// Padding remains an insertion target on repeated clicks.
unit: SelectionUnit::from_clicks(if pos == line.end - 1 { 1 } else { clicks }),
@@ -23,6 +23,10 @@ impl ChatWidget {
self.bottom_pane.end_composer_drag();
}
pub(crate) fn cancel_composer_primary(&mut self) {
self.bottom_pane.cancel_composer_primary();
}
pub(crate) fn copy_composer_selection(
&mut self,
event: &crate::tui::TuiEvent,
@@ -31,6 +35,16 @@ impl ChatWidget {
self.bottom_pane.copy_composer_selection(event, copy)
}
pub(crate) fn copy_composer_selection_on_release(
&mut self,
event: crossterm::event::MouseEvent,
selection_copy: crate::tui::SelectionCopy,
copy: impl FnOnce(&str, crate::tui::SelectionCopy) -> (CopyResult, Option<Arc<()>>),
) -> Option<(usize, Option<CopyResult>)> {
self.bottom_pane
.copy_composer_selection_on_release(event, selection_copy, copy)
}
pub(crate) fn handle_composer_mouse(&mut self, event: crossterm::event::MouseEvent) -> bool {
self.bottom_pane.handle_composer_mouse(event)
}
+2 -2
View File
@@ -410,14 +410,14 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::TextSelection,
label: "Copy on select",
description: "Copy transcript selections on release, with Auto following terminal defaults",
description: "Copy transcript, footer, and composer selections on release; Auto follows terminal defaults",
keywords: "clipboard mouse selection automatic drag",
value: ConfigEditor::CopyOnSelect.value(tui, status),
},
ConfigSetting {
section: ConfigSection::TextSelection,
label: "Deselect after copy",
description: "Remove the selection highlight after Copy on select succeeds",
description: "Remove read-only transcript and footer highlights after automatic copy succeeds",
keywords: "clipboard deselect clear highlight automatic mouse selection",
value: ConfigSettingValue::DependentToggle {
setting: ConfigToggle::CopyOnSelectClearSelection,
+1 -1
View File
@@ -172,7 +172,7 @@ pub(crate) fn copy_on_select(
SelectionViewParams {
title: Some("Copy on select".to_string()),
subtitle: Some(
"Copy transcript selections on release, with Auto following terminal defaults"
"Copy transcript, footer, and composer selections on release; Auto follows terminal defaults"
.to_string(),
),
footer_hint: Some(picker_hint_line_for_keymap(keymap)),
@@ -8,8 +8,8 @@ expression: "render(&panel, 140)"
Appearance Notifications Input Sessions Text selection
› Copy on select Auto · On › Copy transcript selections on release, with Auto following terminal defaults
Deselect after copy [ ] Off Remove the selection highlight after Copy on select succeeds
› Copy on select Auto · On › Copy transcript, footer, and composer selections on release; Auto follows terminal defaults
Deselect after copy [ ] Off Remove read-only transcript and footer highlights after automatic copy succeeds
Right-click paste Auto · Off › Paste when nothing is selected, with Auto following platform defaults and On also enabling mac…
@@ -4,7 +4,7 @@ expression: "render(&picker, 100)"
---
Copy on select
Copy transcript selections on release, with Auto following terminal defaults
Copy transcript, footer, and composer selections on release; Auto follows terminal defaults
› 1. Auto (default) (current)
@@ -8,8 +8,8 @@ expression: "render(&panel, 140)"
Appearance Notifications Input Sessions Text selection
Copy on select Auto · On › Copy transcript selections on release, with Auto following terminal defaults
Deselect after copy [ ] Off Remove the selection highlight after Copy on select succeeds
Copy on select Auto · On › Copy transcript, footer, and composer selections on release; Auto follows terminal defaults
Deselect after copy [ ] Off Remove read-only transcript and footer highlights after automatic copy succeeds
› Right-click paste On · Unavailable › Unavailable in inline mode; use your terminal's paste command
+1 -20
View File
@@ -30,6 +30,7 @@ use crate::history_cell::HistoryCell;
use crate::history_cell::HistoryRenderMode;
use crate::pager_overlay::TranscriptHistoryState;
use crate::terminal_hyperlinks::HyperlinkLine;
use crate::tui::SelectionCopy;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
@@ -49,26 +50,6 @@ pub(crate) use input::JumpTarget;
pub(crate) use input::ViewAction;
pub(crate) use layout::ActivityTranscriptLines;
/// Copy origin plus the deselection policy applied after confirmed delivery.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SelectionCopy {
Explicit,
OnSelect { clear_selection: bool },
}
impl SelectionCopy {
fn clear_selection(self) -> bool {
match self {
Self::Explicit => true,
Self::OnSelect { clear_selection } => clear_selection,
}
}
fn publishes_primary(self) -> bool {
matches!(self, Self::OnSelect { .. })
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum EntryKey {
Cell(usize),
@@ -55,8 +55,16 @@ fn copy_on_select_waits_for_release_and_confirmed_delivery() {
assert!(!view.tick_selection(&cells));
if let Some(copied) = copied {
let selection_copy = selection_copy.expect("copy on select policy");
assert_eq!(selection_copy, SelectionCopy::OnSelect { clear_selection });
assert!(selection_copy.publishes_primary());
assert_eq!(
selection_copy,
SelectionCopy::on_select(
/*clipboard*/ true,
/*primary*/ false,
clear_selection,
)
);
assert!(selection_copy.copies_clipboard());
assert!(!selection_copy.publishes_primary());
let buffer = render(&mut view, &cells, /*width*/ 20, /*height*/ 1);
let highlight = (0..20)
.map(|column| {
+9 -5
View File
@@ -93,15 +93,19 @@ impl TranscriptView {
/// Automatic copies may deselect but still publish PRIMARY, so retain their source.
pub(crate) fn selection_copy(&self, action: &ViewAction) -> SelectionCopy {
match action {
ViewAction::CopyOnSelect(_) => SelectionCopy::OnSelect {
clear_selection: self.copy_on_select_clear_selection,
},
ViewAction::CopyOnSelect(_) => SelectionCopy::on_select(
/*clipboard*/ true,
self.primary_selection,
self.copy_on_select_clear_selection,
),
ViewAction::PrimarySelection(_) => SelectionCopy::on_select(
/*clipboard*/ false, /*primary*/ true, /*clear_selection*/ false,
),
ViewAction::Changed
| ViewAction::ExitSelectMode
| ViewAction::PrimarySelection(_)
| ViewAction::Copy(_)
| ViewAction::CopyAndFollow(_)
| ViewAction::OpenLink(_) => SelectionCopy::Explicit,
| ViewAction::OpenLink(_) => SelectionCopy::EXPLICIT,
}
}
+20 -25
View File
@@ -7,7 +7,6 @@ use unicode_segmentation::UnicodeSegmentation;
use super::*;
use crate::clipboard_copy::CopyFormat;
use crate::clipboard_copy::worker::CopyDestination;
use crate::clipboard_copy::worker::CopyResult;
use crate::text_selection::SelectionUnit;
use crate::tui::Tui;
@@ -326,14 +325,16 @@ impl TranscriptView {
/// A PRIMARY write is silent: it neither claims CLIPBOARD was copied nor ends selection.
pub(crate) fn publish_primary(&mut self, tui: &mut Tui, text: &str) {
let (_, owner) = tui.clipboard.select(
text.into(),
let (_, owner) = tui.copy_selection(
text,
CopyFormat::PlainText,
CopyDestination::Primary,
tui.frame_requester(),
SelectionCopy::on_select(
/*clipboard*/ false, /*primary*/ true, /*clear_selection*/ false,
),
text,
);
if let Some(selection) = &mut self.selection {
selection.primary_owner = Some(owner);
selection.primary_owner = owner;
}
}
@@ -442,18 +443,20 @@ impl TranscriptView {
let text = match action {
ViewAction::Copy(text)
| ViewAction::CopyOnSelect(text)
| ViewAction::PrimarySelection(text)
| ViewAction::CopyAndFollow(text) => text.as_str(),
ViewAction::PrimarySelection(text) => {
self.publish_primary(tui, text);
return None;
}
ViewAction::Changed | ViewAction::ExitSelectMode | ViewAction::OpenLink(_) => {
return None;
}
};
let selection_copy = self.selection_copy(action);
if !selection_copy.copies_clipboard() {
if selection_copy.publishes_primary() {
self.publish_primary(tui, text);
}
return None;
}
let clear_selection = selection_copy.clear_selection();
let publish_primary = selection_copy.publishes_primary() && self.primary_selection;
let mut primary_owner = None;
let result =
self.copy_selected_text_with(cells, text, clear_selection, |rich, rich_format| {
@@ -464,21 +467,13 @@ impl TranscriptView {
(text, CopyFormat::MarkdownSelection(source))
}
};
if publish_primary {
let (result, owner) = tui.clipboard.select(
copy_text.into(),
format,
CopyDestination::ClipboardAndPrimary(text.into()),
tui.frame_requester(),
);
primary_owner = Some(owner);
result
} else {
tui.clipboard
.copy(copy_text.into(), format, tui.frame_requester())
}
let (result, owner) = tui.copy_selection(copy_text, format, selection_copy, text);
primary_owner = owner;
result
});
if publish_primary && let Some(selection) = &mut self.selection {
if selection_copy.publishes_primary()
&& let Some(selection) = &mut self.selection
{
selection.primary_owner = primary_owner;
}
if matches!(action, ViewAction::CopyAndFollow(_))
+2
View File
@@ -94,6 +94,8 @@ mod windows_console;
#[cfg(any(windows, test))]
mod windows_key_sequence;
pub(crate) use selection_clipboard::SelectionCopy;
/// Target frame interval for UI redraw scheduling.
pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME_INTERVAL;
+80 -2
View File
@@ -3,14 +3,92 @@
use super::Tui;
use crate::clipboard_copy::CopyFormat;
use crate::clipboard_copy::CopyStatus;
use crate::clipboard_copy::worker::CopyDestination;
use crate::clipboard_copy::worker::CopyResult;
use std::sync::Arc;
/// Clipboard destinations and completion behavior shared by selectable surfaces.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SelectionCopy {
clipboard: bool,
primary: bool,
clear_selection: bool,
}
impl SelectionCopy {
pub(crate) const EXPLICIT: Self = Self {
clipboard: true,
primary: false,
clear_selection: true,
};
pub(crate) fn on_select(clipboard: bool, primary: bool, clear_selection: bool) -> Self {
Self {
clipboard,
primary,
clear_selection,
}
}
pub(crate) fn copies_clipboard(self) -> bool {
self.clipboard
}
pub(crate) fn publishes_primary(self) -> bool {
self.primary
}
pub(crate) fn clear_selection(self) -> bool {
self.clear_selection
}
}
impl Tui {
/// Copy to CLIPBOARD and/or publish X11 PRIMARY under one selection policy.
pub(crate) fn copy_selection(
&mut self,
text: &str,
format: CopyFormat,
selection_copy: SelectionCopy,
primary_text: &str,
) -> (CopyResult, Option<Arc<()>>) {
let text: Arc<str> = text.into();
match (
selection_copy.copies_clipboard(),
selection_copy.publishes_primary(),
) {
(true, true) => {
let (result, owner) = self.clipboard.select(
Arc::clone(&text),
format,
CopyDestination::ClipboardAndPrimary(primary_text.into()),
self.frame_requester(),
);
(result, Some(owner))
}
(true, false) => (
self.clipboard.copy(text, format, self.frame_requester()),
None,
),
(false, true) => {
let (result, owner) = self.clipboard.select(
text,
format,
CopyDestination::Primary,
self.frame_requester(),
);
(result, Some(owner))
}
(false, false) => (Err("selection copy has no destination".into()), None),
}
}
pub(crate) fn copy_transcript_selection(
&mut self,
text: &str,
format: CopyFormat,
) -> Result<CopyStatus, String> {
self.clipboard
.copy(text.into(), format, self.frame_requester())
self.copy_selection(text, format, SelectionCopy::EXPLICIT, text)
.0
}
}