parent
0065a0ef34
commit
b33e5f973f
|
|
@ -6,6 +6,7 @@
|
|||
- Relicensed Herdr from AGPL-3.0-or-later to Apache-2.0.
|
||||
|
||||
### Fixed
|
||||
- `ui.copy_on_select = false` now retains drag and double-click word selections without copying; `Ctrl+C`, or `Cmd+C` when the host terminal forwards it, copies and clears the selection.
|
||||
- Pane and agent read responses now report `truncated: true` when older terminal rows were omitted. (#1717)
|
||||
- Pane applications that query OSC 4 palette colors now inherit the host terminal palette. (#1752)
|
||||
- Ctrl-clicking a pane URL no longer forwards an unmatched mouse release to alternate-screen applications, preventing duplicate browser tabs. (#1761)
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@
|
|||
"key": "ui.copy_on_select",
|
||||
"type": "boolean",
|
||||
"default": "true",
|
||||
"description": "Copy text selected with the mouse."
|
||||
"description": "Automatically copy text selected by mouse drag or double-click. When disabled, Ctrl+C or a host-forwarded Cmd+C copies and clears the retained selection."
|
||||
},
|
||||
{
|
||||
"key": "ui.host_cursor",
|
||||
|
|
|
|||
|
|
@ -2063,7 +2063,7 @@ impl AppState {
|
|||
self.selection_autoscroll = None;
|
||||
}
|
||||
|
||||
pub(crate) fn copy_word_at_pane_cell(
|
||||
pub(crate) fn select_word_at_pane_cell(
|
||||
&mut self,
|
||||
terminal_runtimes: &crate::terminal::TerminalRuntimeRegistry,
|
||||
pane_id: crate::layout::PaneId,
|
||||
|
|
@ -2113,23 +2113,30 @@ impl AppState {
|
|||
return false;
|
||||
};
|
||||
|
||||
// Copy the token and keep its selection visible as short-lived feedback.
|
||||
let mut selection = Selection::range(pane_id, viewport_row, start_col, end_col, metrics);
|
||||
if !selection.finish() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(text) = rt
|
||||
.extract_selection(&selection)
|
||||
.filter(|text| !text.is_empty())
|
||||
else {
|
||||
self.clear_selection();
|
||||
return false;
|
||||
let text = if self.copy_on_select {
|
||||
let Some(text) = rt
|
||||
.extract_selection(&selection)
|
||||
.filter(|text| !text.is_empty())
|
||||
else {
|
||||
self.clear_selection();
|
||||
return false;
|
||||
};
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.request_clipboard_write = Some(text.into_bytes());
|
||||
|
||||
self.selection = Some(selection);
|
||||
self.selection_autoscroll = None;
|
||||
info!("copied double-clicked token to clipboard");
|
||||
if let Some(text) = text {
|
||||
self.request_clipboard_write = Some(text.into_bytes());
|
||||
info!("copied double-clicked token to clipboard");
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -2184,7 +2191,7 @@ impl AppState {
|
|||
Some(sel) => sel,
|
||||
None => return,
|
||||
};
|
||||
if !sel.finish() {
|
||||
if !sel.is_finalized() && !sel.finish() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,269 @@
|
|||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
use crate::{
|
||||
app::{App, InputSourceId},
|
||||
input::TerminalKey,
|
||||
};
|
||||
|
||||
fn is_retained_selection_copy_key(key: TerminalKey) -> bool {
|
||||
matches!(key.code, KeyCode::Char('c' | 'C'))
|
||||
&& matches!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn dispatch_pending_clipboard_write(&mut self) -> bool {
|
||||
let Some(content) = self.state.request_clipboard_write.take() else {
|
||||
return false;
|
||||
};
|
||||
if self
|
||||
.event_tx
|
||||
.try_send(crate::events::AppEvent::ClipboardWrite { content })
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("failed to queue clipboard write event");
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn try_copy_retained_selection(
|
||||
&mut self,
|
||||
source_id: InputSourceId,
|
||||
key: TerminalKey,
|
||||
) -> bool {
|
||||
if self.state.copy_on_select
|
||||
|| !is_retained_selection_copy_key(key)
|
||||
|| !self
|
||||
.state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(crate::selection::Selection::is_finalized)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
self.state.copy_selection(&self.terminal_runtimes);
|
||||
if !self.dispatch_pending_clipboard_write() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.suppressed_repeat_keys.insert((source_id, key.code));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bytes::Bytes;
|
||||
use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
use super::super::{app_for_mouse_test, mouse};
|
||||
use super::*;
|
||||
use crate::{app::Mode, events::AppEvent, workspace::Workspace};
|
||||
|
||||
fn app_with_screen_bytes_and_input(
|
||||
bytes: &[u8],
|
||||
) -> (
|
||||
App,
|
||||
crate::layout::PaneInfo,
|
||||
tokio::sync::mpsc::Receiver<Bytes>,
|
||||
) {
|
||||
let mut app = app_for_mouse_test();
|
||||
let mut ws = Workspace::test_new("test");
|
||||
let pane_id = ws.tabs[0].root_pane;
|
||||
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
|
||||
let info = pane_infos[0].clone();
|
||||
let (runtime, input_rx) =
|
||||
crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
|
||||
info.inner_rect.width,
|
||||
info.inner_rect.height,
|
||||
0,
|
||||
bytes,
|
||||
4,
|
||||
);
|
||||
ws.insert_test_runtime(pane_id, runtime);
|
||||
|
||||
app.state.workspaces = vec![ws];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
app.state.view.pane_infos = pane_infos;
|
||||
(app, info, input_rx)
|
||||
}
|
||||
|
||||
fn drag_select_range(
|
||||
app: &mut App,
|
||||
info: &crate::layout::PaneInfo,
|
||||
start_col: u16,
|
||||
end_col: u16,
|
||||
) {
|
||||
let row = info.inner_rect.y;
|
||||
let start_col = info.inner_rect.x + start_col;
|
||||
let end_col = info.inner_rect.x + end_col;
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
start_col,
|
||||
row,
|
||||
));
|
||||
app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), end_col, row));
|
||||
app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), end_col, row));
|
||||
}
|
||||
|
||||
fn clipboard_write_content(app: &mut App) -> Vec<u8> {
|
||||
match app.event_rx.try_recv().expect("clipboard write event") {
|
||||
AppEvent::ClipboardWrite { content } => content,
|
||||
event => panic!("unexpected event: {event:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_visible_selection(app: &App) {
|
||||
assert!(app
|
||||
.state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(crate::selection::Selection::is_visible));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_on_select_disabled_ctrl_c_copies_and_clears_retained_selection() {
|
||||
let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
drag_select_range(&mut app, &info, 0, 4);
|
||||
assert_visible_selection(&app);
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
|
||||
let ctrl_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
let source_id = 41;
|
||||
app.route_client_events_from(
|
||||
source_id,
|
||||
vec![crate::raw_input::RawInputEvent::Key(ctrl_c)],
|
||||
false,
|
||||
);
|
||||
|
||||
let content = clipboard_write_content(&mut app);
|
||||
assert_eq!(content, b"alpha");
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(input_rx.try_recv().is_err());
|
||||
|
||||
app.handle_internal_event(AppEvent::ClipboardWrite { content });
|
||||
assert_eq!(
|
||||
app.state
|
||||
.copy_feedback
|
||||
.as_ref()
|
||||
.map(|feedback| feedback.message.as_str()),
|
||||
Some("copied to clipboard")
|
||||
);
|
||||
|
||||
app.route_client_events_from(
|
||||
source_id,
|
||||
vec![crate::raw_input::RawInputEvent::Key(
|
||||
ctrl_c.with_kind(KeyEventKind::Repeat),
|
||||
)],
|
||||
false,
|
||||
);
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
assert!(input_rx.try_recv().is_err());
|
||||
|
||||
app.route_client_events_from(
|
||||
source_id,
|
||||
vec![crate::raw_input::RawInputEvent::Key(
|
||||
ctrl_c.with_kind(KeyEventKind::Release),
|
||||
)],
|
||||
false,
|
||||
);
|
||||
app.route_client_events_from(
|
||||
source_id,
|
||||
vec![crate::raw_input::RawInputEvent::Key(ctrl_c)],
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(),
|
||||
b"\x03"
|
||||
);
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_on_select_disabled_cmd_c_copies_retained_selection() {
|
||||
let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
drag_select_range(&mut app, &info, 0, 4);
|
||||
|
||||
app.handle_terminal_key_headless(TerminalKey::new(KeyCode::Char('c'), KeyModifiers::SUPER));
|
||||
|
||||
assert_eq!(clipboard_write_content(&mut app), b"alpha");
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(input_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retained_selection_copy_shortcut_is_disabled_with_copy_on_select() {
|
||||
let (mut app, _info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta");
|
||||
let pane_id = app.state.workspaces[0].tabs[0].root_pane;
|
||||
let mut selection = crate::selection::Selection::range(
|
||||
pane_id,
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
app.state
|
||||
.pane_scroll_metrics(&app.terminal_runtimes, pane_id),
|
||||
);
|
||||
assert!(selection.finish());
|
||||
app.state.selection = Some(selection);
|
||||
app.state.copy_on_select = true;
|
||||
|
||||
app.handle_terminal_key_headless(TerminalKey::new(
|
||||
KeyCode::Char('c'),
|
||||
KeyModifiers::CONTROL,
|
||||
));
|
||||
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
assert_eq!(
|
||||
input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(),
|
||||
b"\x03"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retained_selection_copy_shortcut_forwards_when_selection_text_is_empty() {
|
||||
let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"");
|
||||
app.state.copy_on_select = false;
|
||||
drag_select_range(&mut app, &info, 0, 4);
|
||||
assert_visible_selection(&app);
|
||||
|
||||
app.handle_terminal_key_headless(TerminalKey::new(
|
||||
KeyCode::Char('c'),
|
||||
KeyModifiers::CONTROL,
|
||||
));
|
||||
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
assert_eq!(
|
||||
input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(),
|
||||
b"\x03"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retained_selection_copy_shortcut_requires_exact_modifiers() {
|
||||
let (mut app, info, mut input_rx) = app_with_screen_bytes_and_input(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
drag_select_range(&mut app, &info, 0, 4);
|
||||
|
||||
app.handle_terminal_key_headless(TerminalKey::new(
|
||||
KeyCode::Char('C'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
||||
));
|
||||
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
assert_eq!(
|
||||
input_rx
|
||||
.try_recv()
|
||||
.expect("forwarded Ctrl-Shift-C")
|
||||
.as_ref(),
|
||||
b"\x03"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,15 +22,7 @@ impl App {
|
|||
}
|
||||
self.state
|
||||
.handle_copy_mode_key(&self.terminal_runtimes, key);
|
||||
if let Some(content) = self.state.request_clipboard_write.take() {
|
||||
if self
|
||||
.event_tx
|
||||
.try_send(crate::events::AppEvent::ClipboardWrite { content })
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("failed to queue clipboard write event");
|
||||
}
|
||||
}
|
||||
self.dispatch_pending_clipboard_write();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ fn modified_url_click_modifier_matches_terminal_mouse_reporting() {
|
|||
assert_eq!(modified_url_click_modifier(), KeyModifiers::CONTROL);
|
||||
}
|
||||
|
||||
mod clipboard;
|
||||
mod copy_mode;
|
||||
mod modal;
|
||||
mod mouse;
|
||||
|
|
@ -401,15 +402,7 @@ impl App {
|
|||
self.save_agent_panel_sort(self.state.agent_panel_sort);
|
||||
}
|
||||
|
||||
if let Some(content) = self.state.request_clipboard_write.take() {
|
||||
if self
|
||||
.event_tx
|
||||
.try_send(crate::events::AppEvent::ClipboardWrite { content })
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("failed to queue clipboard write event");
|
||||
}
|
||||
}
|
||||
self.dispatch_pending_clipboard_write();
|
||||
|
||||
// Sync autoscroll deadline with state (mouse handler may have
|
||||
// set or cleared selection_autoscroll during handle_mouse).
|
||||
|
|
@ -569,14 +562,12 @@ impl App {
|
|||
};
|
||||
|
||||
// Require the second click to land near the first click in the same pane
|
||||
// and within the double-click window so adjacent interactions do not copy.
|
||||
// and within the double-click window so adjacent interactions do not select a word.
|
||||
if !self.take_pane_double_click(click) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Preserve a short highlight after copying so the user gets visible
|
||||
// confirmation without leaving a persistent selection behind.
|
||||
self.copy_double_clicked_word(click)
|
||||
self.select_double_clicked_word(click)
|
||||
}
|
||||
|
||||
fn pane_click_candidate(&mut self, mouse: MouseEvent) -> Option<PaneClickState> {
|
||||
|
|
@ -620,18 +611,20 @@ impl App {
|
|||
true
|
||||
}
|
||||
|
||||
fn copy_double_clicked_word(&mut self, click: PaneClickState) -> bool {
|
||||
let copied = self.state.copy_word_at_pane_cell(
|
||||
fn select_double_clicked_word(&mut self, click: PaneClickState) -> bool {
|
||||
let selected = self.state.select_word_at_pane_cell(
|
||||
&self.terminal_runtimes,
|
||||
click.pane_id,
|
||||
click.viewport_row,
|
||||
click.col,
|
||||
);
|
||||
if copied {
|
||||
self.selection_highlight_clear_deadline =
|
||||
Some(std::time::Instant::now() + super::PANE_COPY_HIGHLIGHT_DURATION);
|
||||
if selected {
|
||||
self.selection_highlight_clear_deadline = self
|
||||
.state
|
||||
.copy_on_select
|
||||
.then(|| std::time::Instant::now() + super::PANE_COPY_HIGHLIGHT_DURATION);
|
||||
}
|
||||
copied
|
||||
selected
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -796,7 +796,7 @@ impl AppState {
|
|||
|
||||
MouseEventKind::Up(MouseButton::Left) => {
|
||||
// Mouse-up either finishes a drag selection or releases after a
|
||||
// double-click copy; the latter is already finalized.
|
||||
// double-click word selection; the latter is already finalized.
|
||||
if let Some(selection) = self.selection.as_ref() {
|
||||
let was_click = selection.was_just_click();
|
||||
let was_finalized = selection.is_finalized();
|
||||
|
|
@ -808,7 +808,7 @@ impl AppState {
|
|||
if was_click {
|
||||
self.selection = None;
|
||||
} else if was_finalized {
|
||||
// Double-click copy already finalized this selection.
|
||||
// Double-click already finalized this word selection.
|
||||
} else if self.copy_on_select {
|
||||
self.copy_selection(terminal_runtimes);
|
||||
} else if let Some(selection) = self.selection.as_mut() {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crossterm::event::KeyCode;
|
|||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
app::{App, Mode, TerminalInputTarget},
|
||||
app::{App, InputSourceId, Mode, TerminalInputTarget},
|
||||
input::TerminalKey,
|
||||
};
|
||||
|
||||
|
|
@ -28,9 +28,18 @@ fn is_modifier_only_key(code: &KeyCode) -> bool {
|
|||
}
|
||||
|
||||
impl App {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn handle_terminal_key_headless(
|
||||
&mut self,
|
||||
key: TerminalKey,
|
||||
) -> Option<TerminalInputTarget> {
|
||||
self.handle_terminal_key_headless_from(crate::app::LOCAL_INPUT_SOURCE, key)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_terminal_key_headless_from(
|
||||
&mut self,
|
||||
source_id: InputSourceId,
|
||||
key: TerminalKey,
|
||||
) -> Option<TerminalInputTarget> {
|
||||
match self.prepare_popup_key_forward(key) {
|
||||
PreparedPopupInput::NotOpen => {}
|
||||
|
|
@ -44,20 +53,27 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
let input = self.prepare_terminal_key_forward(key)?;
|
||||
let input = self.prepare_terminal_key_forward(source_id, key)?;
|
||||
let sent = self
|
||||
.lookup_runtime_sender(input.ws_idx, input.pane_id)
|
||||
.is_some_and(|runtime| runtime.try_send_bytes(input.bytes).is_ok());
|
||||
sent.then_some(input.target)
|
||||
}
|
||||
|
||||
fn prepare_terminal_key_forward(&mut self, key: TerminalKey) -> Option<PreparedPaneInput> {
|
||||
fn prepare_terminal_key_forward(
|
||||
&mut self,
|
||||
source_id: InputSourceId,
|
||||
key: TerminalKey,
|
||||
) -> Option<PreparedPaneInput> {
|
||||
let key_event = key.as_key_event();
|
||||
if self.try_copy_retained_selection(source_id, key) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.state.clear_selection();
|
||||
self.selection_autoscroll_deadline = None;
|
||||
self.state.update_dismissed = true;
|
||||
|
||||
let key_event = key.as_key_event();
|
||||
|
||||
if let Some(action) = super::terminal_direct_non_indexed_navigation_action(&self.state, key)
|
||||
{
|
||||
debug!(
|
||||
|
|
@ -365,7 +381,7 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
let input = self.prepare_terminal_key_forward(key)?;
|
||||
let input = self.prepare_terminal_key_forward(crate::app::LOCAL_INPUT_SOURCE, key)?;
|
||||
let sent = if let Some(runtime) = self.lookup_runtime_sender(input.ws_idx, input.pane_id) {
|
||||
runtime.send_bytes(input.bytes).await.is_ok()
|
||||
} else {
|
||||
|
|
@ -714,7 +730,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_on_select_disabled_keeps_explicit_double_click_copy() {
|
||||
async fn copy_on_select_disabled_retains_double_clicked_word_until_shortcut() {
|
||||
let (mut app, info) = app_with_screen_bytes(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
let col = info.inner_rect.x + 2;
|
||||
|
|
@ -722,16 +738,22 @@ mod tests {
|
|||
|
||||
double_click(&mut app, col, row);
|
||||
|
||||
assert_eq!(clipboard_write_content(&mut app), b"alpha");
|
||||
assert_visible_selection(&app);
|
||||
assert!(app.selection_highlight_clear_deadline.is_some());
|
||||
assert!(app.selection_highlight_clear_deadline.is_none());
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
|
||||
app.handle_terminal_key_headless(TerminalKey::new(
|
||||
KeyCode::Char('c'),
|
||||
KeyModifiers::CONTROL,
|
||||
));
|
||||
|
||||
assert_eq!(clipboard_write_content(&mut app), b"alpha");
|
||||
assert!(app.state.selection.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_drag_cancels_stale_double_click_highlight_deadline() {
|
||||
let (mut app, info) = app_with_screen_bytes(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
let row = info.inner_rect.y;
|
||||
let word_col = info.inner_rect.x + 2;
|
||||
|
||||
|
|
@ -740,6 +762,7 @@ mod tests {
|
|||
let stale_deadline = app
|
||||
.selection_highlight_clear_deadline
|
||||
.expect("double-click highlight deadline");
|
||||
app.state.copy_on_select = false;
|
||||
|
||||
let start_col = info.inner_rect.x + 6;
|
||||
let end_col = info.inner_rect.x + 9;
|
||||
|
|
|
|||
|
|
@ -1621,7 +1621,9 @@ impl App {
|
|||
if self.state.popup_pane.is_some() || self.state.mode == Mode::Terminal
|
||||
{
|
||||
self.suppressed_repeat_keys.remove(&pressed_key_id);
|
||||
if let Some(target) = self.handle_terminal_key_headless(key) {
|
||||
if let Some(target) =
|
||||
self.handle_terminal_key_headless_from(source_id, key)
|
||||
{
|
||||
if !key.is_text_commit {
|
||||
self.pressed_terminal_keys.insert(
|
||||
pressed_key_id,
|
||||
|
|
@ -1650,7 +1652,7 @@ impl App {
|
|||
|| self.state.mode == Mode::Terminal)
|
||||
&& !self.suppressed_repeat_keys.contains(&pressed_key_id)
|
||||
{
|
||||
let _ = self.handle_terminal_key_headless(key);
|
||||
let _ = self.handle_terminal_key_headless_from(source_id, key);
|
||||
}
|
||||
}
|
||||
crossterm::event::KeyEventKind::Release => {
|
||||
|
|
|
|||
|
|
@ -269,8 +269,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
|
|||
# Pane apps like lazygit and btop can still receive mouse when they request it.
|
||||
# mouse_capture = true
|
||||
|
||||
# Automatically copy text selected by mouse drag.
|
||||
# Set false to keep drag selection visible without copying; double-click still copies a word.
|
||||
# Automatically copy text selected with the mouse.
|
||||
# Set false to retain drag or double-click word selection until Ctrl+C,
|
||||
# or Cmd+C when the host forwards it, copies and clears it.
|
||||
# copy_on_select = true
|
||||
|
||||
# Host cursor policy: "auto", "native", or "drawn".
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//! MouseUp → Selection finalized; optionally copied by the caller
|
||||
//! Next click / key → A retained selection is cleared
|
||||
//!
|
||||
//! Double-click copy also briefly highlights the selected word.
|
||||
//! Double-click selects a word; the caller decides whether to copy it immediately.
|
||||
//!
|
||||
//! Rows are stored in screen-buffer coordinates instead of viewport-relative
|
||||
//! coordinates. That keeps selection stable while the pane scrolls.
|
||||
|
|
|
|||
Loading…
Reference in New Issue