aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/mouse.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-08 16:26:47 +0300
committerGitHub <noreply@github.com>2026-08-08 16:26:47 +0300
commit10974c822d607f03e20e9741ec027910f0c1f93a (patch)
tree85453f5ba5bcfcc56c37cabdbbced8a7e701cbe3 /src/app/input/mouse.rs
parent1d6d1fdc6ba8a4fd0385a2071f157504871201a2 (diff)
feat(input): add per-pane right-click routing (#2504)
Diffstat (limited to 'src/app/input/mouse.rs')
-rw-r--r--src/app/input/mouse.rs121
1 files changed, 113 insertions, 8 deletions
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index d43dcfc4..2555d95d 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -1099,13 +1099,16 @@ impl AppState {
.and_then(|ws| ws.focused_pane_id());
let source_pane_id =
previous_focused_pane_id.filter(|pane_id| *pane_id != info.id);
- let has_manual_label = self
+ let pane_state = self
.workspaces
.get(ws_idx)
- .and_then(|ws| ws.pane_state(info.id))
+ .and_then(|ws| ws.pane_state(info.id));
+ let has_manual_label = pane_state
.and_then(|pane| self.terminals.get(&pane.attached_terminal_id))
.and_then(|terminal| terminal.manual_label.as_ref())
.is_some();
+ let right_click_passthrough =
+ pane_state.is_some_and(|pane| pane.right_click_passthrough);
self.context_menu = Some(ContextMenuState {
kind: ContextMenuKind::Pane {
ws_idx,
@@ -1113,6 +1116,7 @@ impl AppState {
pane_id: info.id,
source_pane_id,
has_manual_label,
+ right_click_passthrough,
},
x: mouse.column,
y: mouse.row,
@@ -1565,14 +1569,22 @@ impl AppState {
return false;
}
- let Some(modifiers) = self.right_click_passthrough_modifiers else {
+ let Some(info) = self.pane_at(mouse.column, mouse.row).cloned() else {
return false;
};
- if mouse.modifiers != modifiers {
- return false;
- }
-
- let Some(info) = self.pane_at(mouse.column, mouse.row).cloned() else {
+ let configured_modifiers = self
+ .right_click_passthrough_modifiers
+ .filter(|modifiers| mouse.modifiers == *modifiers);
+ let pane_passthrough = mouse.modifiers.is_empty()
+ && self.active.is_some_and(|ws_idx| {
+ self.workspaces
+ .get(ws_idx)
+ .and_then(|workspace| workspace.pane_state(info.id))
+ .is_some_and(|pane| pane.right_click_passthrough)
+ });
+ let Some(modifiers) = configured_modifiers
+ .or_else(|| pane_passthrough.then(crossterm::event::KeyModifiers::empty))
+ else {
return false;
};
@@ -2042,6 +2054,98 @@ mod tests {
}
#[tokio::test]
+ async fn pane_right_click_passthrough_is_isolated() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let passthrough_pane = ws.tabs[0].root_pane;
+ let default_pane = ws.test_split(Direction::Horizontal);
+ ws.pane_state_mut(passthrough_pane)
+ .unwrap()
+ .right_click_passthrough = true;
+ app.state.workspaces = vec![ws];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
+
+ let passthrough_info = app.state.pane_info_by_id(passthrough_pane).unwrap().clone();
+ let default_info = app.state.pane_info_by_id(default_pane).unwrap().clone();
+ let (passthrough_runtime, mut passthrough_input) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ passthrough_info.inner_rect.width,
+ passthrough_info.inner_rect.height,
+ 0,
+ b"\x1b[?1002h\x1b[?1006h",
+ 4,
+ );
+ let (default_runtime, mut default_input) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ default_info.inner_rect.width,
+ default_info.inner_rect.height,
+ 0,
+ b"\x1b[?1002h\x1b[?1006h",
+ 4,
+ );
+ app.state
+ .insert_test_runtime(passthrough_pane, passthrough_runtime);
+ app.state.insert_test_runtime(default_pane, default_runtime);
+
+ let col = passthrough_info.inner_rect.x + 2;
+ let row = passthrough_info.inner_rect.y + 3;
+ app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Right), col, row));
+
+ assert_eq!(app.state.mode, Mode::Terminal);
+ assert!(app.state.context_menu.is_none());
+ assert_eq!(
+ passthrough_input.try_recv().unwrap(),
+ Bytes::from_static(b"\x1b[<2;3;4M")
+ );
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Right),
+ default_info.inner_rect.x + 2,
+ default_info.inner_rect.y + 3,
+ ));
+
+ assert!(default_input.try_recv().is_err());
+ assert!(matches!(
+ app.state.context_menu.as_ref().map(|menu| &menu.kind),
+ Some(ContextMenuKind::Pane { pane_id, .. }) if *pane_id == default_pane
+ ));
+ }
+
+ #[tokio::test]
+ async fn pane_right_click_passthrough_falls_back_when_mouse_reporting_is_off() {
+ let mut app = app_for_mouse_test();
+ let mut ws = Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ ws.pane_state_mut(pane_id).unwrap().right_click_passthrough = true;
+ app.state.workspaces = vec![ws];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
+ let info = app.state.pane_info_by_id(pane_id).unwrap().clone();
+ app.state.insert_test_runtime(
+ pane_id,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
+ info.inner_rect.width,
+ info.inner_rect.height,
+ b"",
+ ),
+ );
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Right),
+ info.inner_rect.x + 2,
+ info.inner_rect.y + 3,
+ ));
+
+ assert_eq!(app.state.mode, Mode::ContextMenu);
+ assert!(app.state.context_menu.is_some());
+ }
+
+ #[tokio::test]
async fn configured_right_click_passthrough_forwards_full_gesture_to_pane() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
@@ -2966,6 +3070,7 @@ mod tests {
pane_id,
source_pane_id: None,
has_manual_label: false,
+ right_click_passthrough: false,
},
x: 2,
y: 2,