aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-06 23:59:59 +0300
committerGitHub <noreply@github.com>2026-07-06 23:59:59 +0300
commit4f22e9a73ba4d9029468e078235c5f5ce629d179 (patch)
tree49b56a29305192c8270aa43e87bb5f1d86d00233 /src/app/input
parent56edcbcba83b8ff7f6f69ee4231f163e97df3346 (diff)
fix: preserve wrapped pane links (#1099)
refs #1098 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/terminal.rs144
1 files changed, 144 insertions, 0 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs
index 282fa263..c9abbbc2 100644
--- a/src/app/input/terminal.rs
+++ b/src/app/input/terminal.rs
@@ -561,6 +561,150 @@ mod tests {
}
#[tokio::test]
+ async fn pane_cell_url_resolver_finds_soft_wrapped_url() {
+ let (_app, info) = app_with_screen_bytes(b"");
+ let prefix = "https://example.com/";
+ let padding = "a".repeat(info.inner_rect.width as usize - prefix.len());
+ let url = format!("{prefix}{padding}tail");
+ let (app, _info) = app_with_screen_bytes(url.as_bytes());
+ let pane_id = app.state.workspaces[0].tabs[0].root_pane;
+
+ assert_eq!(
+ app.state
+ .url_at_pane_cell(&app.terminal_runtimes, pane_id, 1, 1)
+ .as_deref(),
+ Some(url.as_str())
+ );
+ }
+
+ #[tokio::test]
+ async fn pane_cell_url_resolver_does_not_shift_after_zero_width_mark() {
+ let url = "https://example.com/mark";
+ let screen = format!("e\u{301} {url}");
+ let (app, _info) = app_with_screen_bytes(screen.as_bytes());
+ let pane_id = app.state.workspaces[0].tabs[0].root_pane;
+
+ assert_eq!(
+ app.state
+ .url_at_pane_cell(&app.terminal_runtimes, pane_id, 0, 2)
+ .as_deref(),
+ Some(url)
+ );
+ }
+
+ #[tokio::test]
+ async fn pane_cell_url_resolver_handles_hard_newline_after_full_row() {
+ let (_app, info) = app_with_screen_bytes(b"");
+ let full_row = "x".repeat(info.inner_rect.width as usize);
+ let url = "https://example.com/next";
+ let screen = format!("{full_row}\n{url}");
+ let (app, _info) = app_with_screen_bytes(screen.as_bytes());
+ let pane_id = app.state.workspaces[0].tabs[0].root_pane;
+
+ assert_eq!(
+ app.state
+ .url_at_pane_cell(&app.terminal_runtimes, pane_id, 1, 1)
+ .as_deref(),
+ None
+ );
+ assert_eq!(
+ app.state
+ .url_at_pane_cell(&app.terminal_runtimes, pane_id, 2, 1)
+ .as_deref(),
+ Some(url)
+ );
+ }
+
+ #[tokio::test]
+ async fn render_stream_synthesizes_soft_wrapped_url_hyperlinks() {
+ let (_app, info) = app_with_screen_bytes(b"");
+ let prefix = "https://example.com/";
+ let padding = "b".repeat(info.inner_rect.width as usize - prefix.len());
+ let url = format!("{prefix}{padding}tail");
+ let (app, info) = app_with_screen_bytes(url.as_bytes());
+
+ let links = crate::server::render_stream::visible_hyperlinks(
+ &app.state,
+ &app.terminal_runtimes,
+ None,
+ );
+
+ assert!(links.iter().any(|((x, y), symbol, uri)| {
+ *x == info.inner_rect.x + 1
+ && *y == info.inner_rect.y + 1
+ && symbol == "a"
+ && uri == &url
+ }));
+ }
+
+ #[tokio::test]
+ async fn render_stream_does_not_shift_url_hyperlinks_after_zero_width_mark() {
+ let url = "https://example.com/mark";
+ let screen = format!("e\u{301} {url}");
+ let (app, info) = app_with_screen_bytes(screen.as_bytes());
+
+ let links = crate::server::render_stream::visible_hyperlinks(
+ &app.state,
+ &app.terminal_runtimes,
+ None,
+ );
+
+ assert!(links.iter().any(|((x, y), symbol, uri)| {
+ *x == info.inner_rect.x + 2 && *y == info.inner_rect.y && symbol == "h" && uri == url
+ }));
+ }
+
+ #[tokio::test]
+ async fn render_stream_handles_hard_newline_after_full_row() {
+ let (_app, info) = app_with_screen_bytes(b"");
+ let full_row = "x".repeat(info.inner_rect.width as usize);
+ let url = "https://example.com/next";
+ let screen = format!("{full_row}\n{url}");
+ let (app, info) = app_with_screen_bytes(screen.as_bytes());
+ let links = crate::server::render_stream::visible_hyperlinks(
+ &app.state,
+ &app.terminal_runtimes,
+ None,
+ );
+
+ assert!(links.iter().any(|((x, y), symbol, uri)| {
+ *x == info.inner_rect.x + 1
+ && *y == info.inner_rect.y + 2
+ && symbol == "t"
+ && uri == url
+ }));
+ }
+
+ #[tokio::test]
+ async fn render_stream_filters_plain_url_when_final_cell_differs() {
+ let line = "see https://example.com/hidden";
+ let (mut app, _info) = app_with_screen_bytes(line.as_bytes());
+ let (mut buffer, _cursor) =
+ crate::server::render_stream::render_virtual_with_runtime_registry(
+ &mut app.state,
+ &app.terminal_runtimes,
+ ratatui::layout::Rect::new(0, 0, 106, 20),
+ false,
+ crate::kitty_graphics::HostCellSize::default(),
+ );
+ let info = app.state.view.pane_infos[0].clone();
+ let target_x = info.inner_rect.x + line.find("https").expect("url start") as u16;
+ let target_y = info.inner_rect.y;
+ buffer[(target_x, target_y)]
+ .set_style(ratatui::style::Style::default().fg(ratatui::style::Color::Red));
+
+ let links = crate::server::render_stream::visible_hyperlinks(
+ &app.state,
+ &app.terminal_runtimes,
+ Some(&buffer),
+ );
+
+ assert!(!links
+ .iter()
+ .any(|((x, y), _, uri)| *x == target_x && *y == target_y && uri == &line[4..]));
+ }
+
+ #[tokio::test]
async fn pane_cell_url_resolver_prefers_osc8_hyperlink() {
let (app, _info) = app_with_screen_bytes(
b"\x1b]8;;https://example.com/hidden-target\x1b\\label\x1b]8;;\x1b\\",