fix: always open pane context menu on right-click

The Down(Right) handler forwarded the click to the inner pane runtime
when mouse reporting was active, then early-returned before creating the
context menu state. For panes running a TUI that enables mouse reporting
(e.g. Claude Code), right-clicking focused the pane but never opened the
herdr context menu, even though right-click on plain shells or Codex
still worked.

Stop forwarding Down(Right) to the pane runtime altogether. Right-click
on a pane now always focuses it and opens the herdr context menu,
matching how terminal emulators like Ghostty, Alacritty, Kitty and iTerm2
treat right-click. This means the previous context menu actions
(close pane, split, fullscreen, etc.) now also reach the intended pane
via focus_pane before the menu is displayed.

Remove Up(Right) and Drag(Right) from the middle/right forward branch so
the inner TUI never sees an Up or Drag without a matching Down. Middle
button forwarding is preserved because it is still used for paste in
many terminal protocols.

Tighten the existing right-click regression test with assertions that
Mode::ContextMenu is entered and context_menu is populated. Without
these extra checks the earlier version of the test only verified focus
and failed to catch this exact bug.
This commit is contained in:
Othavio Quiliao 2026-04-09 16:06:27 -03:00
parent 5e711fb7d3
commit 42fa2648ee
1 changed files with 74 additions and 7 deletions

View File

@ -2336,10 +2336,7 @@ impl AppState {
}
}
MouseEventKind::Up(MouseButton::Middle)
| MouseEventKind::Up(MouseButton::Right)
| MouseEventKind::Drag(MouseButton::Middle)
| MouseEventKind::Drag(MouseButton::Right)
MouseEventKind::Up(MouseButton::Middle) | MouseEventKind::Drag(MouseButton::Middle)
if !in_sidebar =>
{
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
@ -2440,9 +2437,7 @@ impl AppState {
MouseEventKind::Down(MouseButton::Right) if !in_sidebar => {
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
if self.forward_pane_mouse_button(&info, mouse) {
return None;
}
self.focus_pane(info.id);
self.context_menu = Some(ContextMenuState {
kind: ContextMenuKind::Pane,
x: mouse.column,
@ -4639,4 +4634,76 @@ mod tests {
assert_eq!(app.state.mode, Mode::Terminal);
}
#[tokio::test]
async fn clicking_unfocused_pane_with_mouse_reporting_focuses_it_via_right_button() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let first_pane = ws.tabs[0].root_pane;
let second_pane = ws.test_split(ratatui::layout::Direction::Vertical);
let terminal_area = Rect::new(26, 2, 80, 18);
let pane_infos = ws.tabs[0].layout.panes(terminal_area);
let first_info = pane_infos
.iter()
.find(|p| p.id == first_pane)
.expect("first pane info")
.clone();
let second_info = pane_infos
.iter()
.find(|p| p.id == second_pane)
.expect("second pane info")
.clone();
ws.tabs[0].runtimes.insert(
first_pane,
crate::pane::PaneRuntime::test_with_screen_bytes(
first_info.inner_rect.width.max(1),
first_info.inner_rect.height.max(1),
b"",
),
);
ws.tabs[0].runtimes.insert(
second_pane,
crate::pane::PaneRuntime::test_with_screen_bytes(
second_info.inner_rect.width.max(1),
second_info.inner_rect.height.max(1),
b"\x1b[?1002h",
),
);
ws.tabs[0].layout.focus_pane(first_pane);
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;
assert_eq!(
app.state.workspaces[0].tabs[0].layout.focused(),
first_pane,
"first pane should be focused before click"
);
app.handle_mouse(mouse(
MouseEventKind::Down(MouseButton::Right),
second_info.inner_rect.x + 2,
second_info.inner_rect.y + 2,
));
assert_eq!(
app.state.workspaces[0].tabs[0].layout.focused(),
second_pane,
"right-clicking a pane with mouse reporting should move focus to it"
);
assert_eq!(
app.state.mode,
Mode::ContextMenu,
"right-click should enter ContextMenu mode"
);
assert!(
app.state.context_menu.is_some(),
"right-click should populate context_menu state"
);
}
}