mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 16:01:07 +00:00
@@ -361,6 +361,15 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left))
|
||||
&& self
|
||||
.state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_none_or(crate::selection::Selection::is_in_progress)
|
||||
{
|
||||
self.selection_highlight_clear_deadline = None;
|
||||
}
|
||||
}
|
||||
if previous_settings_section != crate::app::state::SettingsSection::Integrations
|
||||
&& self.state.settings.section == crate::app::state::SettingsSection::Integrations
|
||||
@@ -482,11 +491,6 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_pane_double_click(&mut self, mouse: MouseEvent) -> bool {
|
||||
if !self.state.copy_on_select {
|
||||
self.last_pane_click = None;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A pane press stops being a double-click candidate once it becomes
|
||||
// a drag or completes as a real text selection.
|
||||
match mouse.kind {
|
||||
|
||||
+13
-11
@@ -640,14 +640,12 @@ impl AppState {
|
||||
mouse.row - info.inner_rect.y,
|
||||
mouse.column - info.inner_rect.x,
|
||||
);
|
||||
if self.copy_on_select {
|
||||
self.selection = Some(Selection::anchor(
|
||||
info.id,
|
||||
row,
|
||||
col,
|
||||
self.pane_scroll_metrics(terminal_runtimes, info.id),
|
||||
));
|
||||
}
|
||||
self.selection = Some(Selection::anchor(
|
||||
info.id,
|
||||
row,
|
||||
col,
|
||||
self.pane_scroll_metrics(terminal_runtimes, info.id),
|
||||
));
|
||||
if let Some(ws_idx) = self.active {
|
||||
return Some(MouseAction::FocusPane {
|
||||
ws_idx,
|
||||
@@ -814,10 +812,10 @@ impl AppState {
|
||||
|
||||
MouseEventKind::Up(MouseButton::Left) => {
|
||||
// Mouse-up either finishes a drag selection or releases after a
|
||||
// double-click copy; the latter is already copied.
|
||||
// double-click copy; the latter is already finalized.
|
||||
if let Some(selection) = self.selection.as_ref() {
|
||||
let was_click = selection.was_just_click();
|
||||
let was_already_copied = selection.is_done();
|
||||
let was_finalized = selection.is_finalized();
|
||||
|
||||
self.workspace_press = None;
|
||||
self.tab_press = None;
|
||||
@@ -825,8 +823,12 @@ impl AppState {
|
||||
self.selection_autoscroll = None;
|
||||
if was_click {
|
||||
self.selection = None;
|
||||
} else if !was_already_copied {
|
||||
} else if was_finalized {
|
||||
// Double-click copy already finalized this selection.
|
||||
} else if self.copy_on_select {
|
||||
self.copy_selection(terminal_runtimes);
|
||||
} else if let Some(selection) = self.selection.as_mut() {
|
||||
selection.finish();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_on_select_disabled_ignores_drag_and_double_click_selection() {
|
||||
async fn copy_on_select_disabled_keeps_drag_selection_without_copying() {
|
||||
let (mut app, info) = app_with_screen_bytes(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
let row = info.inner_rect.y;
|
||||
@@ -561,20 +561,111 @@ mod tests {
|
||||
row,
|
||||
));
|
||||
app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), end_col, row));
|
||||
assert_visible_selection(&app);
|
||||
assert!(!app
|
||||
.state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(crate::selection::Selection::is_finalized));
|
||||
|
||||
app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), end_col, row));
|
||||
|
||||
assert!(app.state.selection.is_none());
|
||||
assert_visible_selection(&app);
|
||||
assert_eq!(
|
||||
app.state
|
||||
.selection
|
||||
.as_ref()
|
||||
.map(crate::selection::Selection::ordered_cells),
|
||||
Some(((0, 0), (0, 4)))
|
||||
);
|
||||
assert!(app
|
||||
.state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(crate::selection::Selection::is_finalized));
|
||||
assert!(app.state.selection_autoscroll.is_none());
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
|
||||
double_click(&mut app, start_col, row);
|
||||
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(app.last_pane_click.is_none());
|
||||
assert!(app.selection_autoscroll_deadline.is_none());
|
||||
assert!(app.selection_highlight_clear_deadline.is_none());
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_on_select_disabled_keeps_explicit_double_click_copy() {
|
||||
let (mut app, info) = app_with_screen_bytes(b"alpha beta");
|
||||
app.state.copy_on_select = false;
|
||||
let col = info.inner_rect.x + 2;
|
||||
let row = info.inner_rect.y;
|
||||
|
||||
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.event_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
double_click(&mut app, word_col, row);
|
||||
assert_eq!(clipboard_write_content(&mut app), b"alpha");
|
||||
let stale_deadline = app
|
||||
.selection_highlight_clear_deadline
|
||||
.expect("double-click highlight deadline");
|
||||
|
||||
let start_col = info.inner_rect.x + 6;
|
||||
let end_col = info.inner_rect.x + 9;
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
start_col,
|
||||
row,
|
||||
));
|
||||
assert!(app.selection_highlight_clear_deadline.is_none());
|
||||
app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), end_col, row));
|
||||
app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), end_col, row));
|
||||
|
||||
assert_visible_selection(&app);
|
||||
assert!(!app
|
||||
.clear_due_selection_highlight(stale_deadline + std::time::Duration::from_millis(1)));
|
||||
assert_visible_selection(&app);
|
||||
assert!(app.event_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignored_left_down_keeps_double_click_highlight_deadline() {
|
||||
let (mut app, info) = app_with_screen_bytes(b"alpha beta");
|
||||
let col = info.inner_rect.x + 2;
|
||||
let row = info.inner_rect.y;
|
||||
|
||||
double_click(&mut app, col, row);
|
||||
assert_eq!(clipboard_write_content(&mut app), b"alpha");
|
||||
let deadline = app
|
||||
.selection_highlight_clear_deadline
|
||||
.expect("double-click highlight deadline");
|
||||
app.state.toast = Some(crate::app::state::ToastNotification {
|
||||
kind: crate::app::state::ToastKind::Finished,
|
||||
title: "finished".into(),
|
||||
context: "missing".into(),
|
||||
position: None,
|
||||
target: Some(crate::app::state::ToastTarget {
|
||||
workspace_id: "missing".into(),
|
||||
pane_id: info.id,
|
||||
}),
|
||||
});
|
||||
app.state.view.toast_hit_area = Rect::new(0, 0, 1, 1);
|
||||
|
||||
app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), 0, 0));
|
||||
|
||||
assert_visible_selection(&app);
|
||||
assert_eq!(app.selection_highlight_clear_deadline, Some(deadline));
|
||||
assert!(app.clear_due_selection_highlight(deadline + std::time::Duration::from_millis(1)));
|
||||
assert!(app.state.selection.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn double_click_uses_display_columns_for_wide_chars() {
|
||||
let (mut app, info) = app_with_screen_bytes("echo 你好-world done".as_bytes());
|
||||
|
||||
+8
-15
@@ -1392,16 +1392,6 @@ impl App {
|
||||
.clamp(self.state.sidebar_min_width, self.state.sidebar_max_width);
|
||||
self.state.mouse_capture = config.ui.mouse_capture;
|
||||
self.state.copy_on_select = config.ui.copy_on_select;
|
||||
if !self.state.copy_on_select {
|
||||
if self.state.mode == Mode::Copy {
|
||||
self.state.stop_selection_autoscroll_state();
|
||||
} else {
|
||||
self.state.clear_selection();
|
||||
}
|
||||
self.last_pane_click = None;
|
||||
self.selection_autoscroll_deadline = None;
|
||||
self.selection_highlight_clear_deadline = None;
|
||||
}
|
||||
if self.state.redraw_on_focus_gained != config.ui.redraw_on_focus_gained {
|
||||
self.state.request_client_config_reload = true;
|
||||
}
|
||||
@@ -2629,11 +2619,14 @@ mod tests {
|
||||
assert_eq!(app.state.agent_panel_sort, state::AgentPanelSort::Priority);
|
||||
assert!(!app.state.redraw_on_focus_gained);
|
||||
assert!(!app.state.copy_on_select);
|
||||
assert!(app.state.selection.is_none());
|
||||
assert!(app.state.selection_autoscroll.is_none());
|
||||
assert!(app.selection_autoscroll_deadline.is_none());
|
||||
assert!(app.selection_highlight_clear_deadline.is_none());
|
||||
assert!(app.last_pane_click.is_none());
|
||||
assert!(app.state.selection.is_some());
|
||||
assert!(app.state.selection_autoscroll.is_some());
|
||||
assert_eq!(app.selection_autoscroll_deadline, Some(selection_deadline));
|
||||
assert_eq!(
|
||||
app.selection_highlight_clear_deadline,
|
||||
Some(selection_deadline)
|
||||
);
|
||||
assert!(app.last_pane_click.is_some());
|
||||
|
||||
app.state.mode = Mode::Copy;
|
||||
app.state.selection = Some(crate::selection::Selection::range(
|
||||
|
||||
+2
-2
@@ -269,8 +269,8 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
|
||||
# Pane apps like lazygit and btop can still receive mouse when they request it.
|
||||
# mouse_capture = true
|
||||
|
||||
# Copy text selected by mouse drag or double-click.
|
||||
# Set false to disable mouse text selection and copying.
|
||||
# Automatically copy text selected by mouse drag.
|
||||
# Set false to keep drag selection visible without copying; double-click still copies a word.
|
||||
# copy_on_select = true
|
||||
|
||||
# Host cursor policy: "auto", "native", or "drawn".
|
||||
|
||||
+6
-6
@@ -4,8 +4,8 @@
|
||||
//!
|
||||
//! MouseDown in pane → Anchor recorded (no visual yet)
|
||||
//! MouseDrag → Selection becomes active, cells highlighted
|
||||
//! MouseUp → Text extracted, copied via OSC 52, highlight stays
|
||||
//! Next click / key → Selection cleared
|
||||
//! 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.
|
||||
//!
|
||||
@@ -25,8 +25,8 @@ enum Phase {
|
||||
Anchored,
|
||||
/// Mouse has moved from the anchor point. Cells are being highlighted.
|
||||
Dragging,
|
||||
/// Mouse released after dragging. Selection is visible and text
|
||||
/// has been copied to clipboard. Cleared on next interaction.
|
||||
/// Mouse released after dragging. Selection is visible and the gesture is
|
||||
/// complete. Clipboard policy is owned by the caller.
|
||||
Done,
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ impl Selection {
|
||||
self.phase == Phase::Dragging || self.phase == Phase::Done
|
||||
}
|
||||
|
||||
/// Whether this selection was already finalized and copied.
|
||||
pub fn is_done(&self) -> bool {
|
||||
/// Whether this selection was already finalized.
|
||||
pub fn is_finalized(&self) -> bool {
|
||||
self.phase == Phase::Done
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user