aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/terminal.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-18 00:11:20 +0300
committerGitHub <noreply@github.com>2026-08-18 00:11:20 +0300
commit76211757ecce73cba981bdf6704954b25a100a8c (patch)
tree8686202ba5d6bb32d13e9d911b3aa4236bb30a6c /src/app/input/terminal.rs
parent19022d03c902de586009b1d156bc20b01ce48135 (diff)
fix: reap ctrl-click url openers (#2906)
* fix: reap ctrl-click url openers refs #2903 * fix: scope linux opener test import refs #2903 * test: isolate url opener regression refs #2903 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src/app/input/terminal.rs')
-rw-r--r--src/app/input/terminal.rs61
1 files changed, 61 insertions, 0 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs
index 12d09e6a..e5b40a74 100644
--- a/src/app/input/terminal.rs
+++ b/src/app/input/terminal.rs
@@ -411,6 +411,8 @@ mod tests {
use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
use ratatui::layout::Rect;
+ #[cfg(target_os = "linux")]
+ use super::super::wait_for_detached_process_reap;
use super::super::{app_for_mouse_test, mouse, numbered_lines_bytes};
#[cfg(unix)]
use super::super::{unique_temp_path, wait_for_file};
@@ -885,6 +887,65 @@ mod tests {
assert!(app.selection_highlight_clear_deadline.is_none());
}
+ #[cfg(target_os = "linux")]
+ #[tokio::test]
+ async fn ctrl_click_url_reaps_failed_opener() {
+ let opener_dir = unique_temp_path("url-opener");
+ let record_path = opener_dir.join("record");
+ let opener_path = opener_dir.join("xdg-open");
+ std::fs::create_dir_all(&opener_dir).expect("fake opener directory");
+ std::fs::write(
+ &opener_path,
+ "#!/bin/sh\nprintf '%s\\n%s\\n' \"$$\" \"$1\" > \"$2\"\nexit 3\n",
+ )
+ .expect("fake opener script");
+
+ let url = "https://example.com/akbash-2903";
+ let line = format!("see {url}");
+ let (mut app, info) = app_with_screen_bytes(line.as_bytes());
+ let col = info.inner_rect.x + line.find("example").expect("url host") as u16;
+ let handled = app.handle_modified_url_click_with(
+ 41,
+ modified_mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ col,
+ info.inner_rect.y,
+ KeyModifiers::CONTROL,
+ ),
+ |clicked_url| {
+ std::process::Command::new("/bin/sh")
+ .arg(&opener_path)
+ .arg(clicked_url)
+ .arg(&record_path)
+ .stdin(std::process::Stdio::null())
+ .stdout(std::process::Stdio::null())
+ .stderr(std::process::Stdio::null())
+ .spawn()
+ .map(Some)
+ },
+ );
+ assert!(handled);
+
+ let record = wait_for_file(&record_path);
+ let mut lines = record.lines();
+ let pid = lines
+ .next()
+ .expect("opener pid")
+ .parse::<u32>()
+ .expect("numeric opener pid");
+ assert_eq!(lines.next(), Some(url));
+
+ let reaped = wait_for_detached_process_reap(&mut app, pid).await;
+ if !reaped {
+ unsafe {
+ libc::waitpid(pid as libc::pid_t, std::ptr::null_mut(), 0);
+ }
+ }
+
+ let _ = std::fs::remove_dir_all(&opener_dir);
+ assert!(reaped, "failed URL opener child {pid} was not reaped");
+ }
+
#[cfg(unix)]
#[tokio::test]
async fn ctrl_click_url_does_not_forward_release_to_mouse_reporting_pane() {