aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorKataoka Katsuki <49934462+kataokatsuki@users.noreply.github.com>2026-08-04 23:46:43 +0900
committerGitHub <noreply@github.com>2026-08-04 17:46:43 +0300
commit1997b88b3fa45f838d44e69dcebde8acf33899fc (patch)
treeff406e43cb3eec45f57e38ccf77a36127df35b53 /src/app
parentcc9fa47540d8525f24fe46a10f759ca40090a6e0 (diff)
fix(input): keep pending url clicks across host focus loss (#2291)
Opening a URL raises the browser, which takes focus away from the host terminal before the mouse release arrives. release_input_source(_headless) cleared pending_url_click_sources on that focus loss, so the release was forwarded to the pane and the agent opened the same URL again. Clear the set in clear_input_source instead, whose only production caller is remove_client. refs #2290 Co-authored-by: kataokatsuki <kataokatsuki@users.noreply.github.com>
Diffstat (limited to 'src/app')
-rw-r--r--src/app/input/terminal.rs59
-rw-r--r--src/app/mod.rs5
2 files changed, 62 insertions, 2 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs
index 1ba2789c..2442353e 100644
--- a/src/app/input/terminal.rs
+++ b/src/app/input/terminal.rs
@@ -350,7 +350,7 @@ impl App {
}
pub(crate) fn release_input_source_headless(&mut self, source_id: crate::app::InputSourceId) {
- self.pending_url_click_sources.remove(&source_id);
+ // Pending URL clicks survive this call; see clear_input_source.
for pressed in self.take_pressed_keys_for_source(source_id) {
let release = pressed
.key
@@ -360,7 +360,7 @@ impl App {
}
pub(crate) async fn release_input_source(&mut self, source_id: crate::app::InputSourceId) {
- self.pending_url_click_sources.remove(&source_id);
+ // Pending URL clicks survive this call; see clear_input_source.
for pressed in self.take_pressed_keys_for_source(source_id) {
let release = pressed
.key
@@ -926,6 +926,61 @@ mod tests {
#[cfg(unix)]
#[tokio::test]
+ async fn outer_focus_loss_does_not_forward_pending_url_click_release_to_pane() {
+ let line = "see https://github.com/herdrdev/herdr/issues/1761";
+ let col = line.find("github").expect("url host") as u16;
+ let (mut app, info) = app_with_screen_bytes(b"");
+ let pane_id = app.state.workspaces[0].tabs[0].root_pane;
+ let screen = format!("\x1b[?1049h\x1b[?1000h\x1b[?1006h{line}");
+ let (runtime, mut input_rx) =
+ crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ info.inner_rect.width,
+ info.inner_rect.height,
+ 0,
+ screen.as_bytes(),
+ 4,
+ );
+ app.state.insert_test_runtime(pane_id, runtime);
+ install_test_link_handler(&mut app);
+ let url_x = info.inner_rect.x + col;
+
+ app.handle_mouse_from_input_source(
+ 41,
+ modified_mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ url_x,
+ info.inner_rect.y,
+ KeyModifiers::CONTROL,
+ ),
+ );
+ assert_eq!(app.state.plugin_command_logs.len(), 1);
+
+ // Opening the URL raises the browser, so the host terminal loses focus
+ // while the button is still down.
+ app.route_client_events_from(
+ 41,
+ vec![crate::raw_input::RawInputEvent::OuterFocusLost],
+ false,
+ );
+
+ app.handle_mouse_from_input_source(
+ 41,
+ modified_mouse(
+ MouseEventKind::Up(MouseButton::Left),
+ url_x,
+ info.inner_rect.y,
+ KeyModifiers::empty(),
+ ),
+ );
+
+ assert!(
+ input_rx.try_recv().is_err(),
+ "focus loss must not clear a pending URL click, so its release must stay out of the pane"
+ );
+ }
+
+ #[cfg(unix)]
+ #[tokio::test]
async fn ctrl_click_url_invokes_plugin_link_handler_but_super_click_does_not() {
let line = "see https://github.com/herdrdev/herdr/issues/398";
let col = line.find("github").expect("url host") as u16;
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 82ac1a60..c27c249e 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1778,6 +1778,11 @@ impl App {
}
pub(crate) fn clear_input_source(&mut self, source_id: InputSourceId) {
+ // Call this only when the input source is gone for good. A pending URL
+ // click has to outlive a plain focus change, because opening the URL
+ // raises the browser and costs the host terminal its focus before the
+ // mouse release arrives.
+ self.pending_url_click_sources.remove(&source_id);
self.release_input_source_headless(source_id);
}