aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-08 00:44:49 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-08 00:44:49 +0300
commitd6285aaa59e0f7908c2ece81314aff2b98847e31 (patch)
treeec1306441ebb87592ff120e2bd34c55028f1fd33 /src/server
parent35e9838a44470c418ae51204bab54115d15d87bd (diff)
fix: avoid render-time plain url scans
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs28
-rw-r--r--src/server/render_stream.rs135
2 files changed, 8 insertions, 155 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 57561947..e6d2b833 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -163,13 +163,6 @@ fn dirty_patch_intersects_hyperlinks(
false
}
-fn dirty_patch_contains_plain_url_candidate(patch: &crate::pane::TerminalDirtyPatch) -> bool {
- patch.rows.iter().any(|(_, row_cells)| {
- let row: String = row_cells.iter().map(|cell| cell.symbol.as_str()).collect();
- !crate::app::actions::visible_url_spans(&row).is_empty()
- })
-}
-
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
@@ -3203,9 +3196,6 @@ impl HeadlessServer {
if dirty_patch_intersects_hyperlinks(&frame, info.inner_rect, &patch) {
retained_fallback!("hyperlink_intersection");
}
- if dirty_patch_contains_plain_url_candidate(&patch) {
- retained_fallback!("plain_url_candidate");
- }
if !apply_terminal_dirty_patch(&mut frame, info.inner_rect, patch) {
retained_fallback!("patch_apply_failed");
}
@@ -3382,7 +3372,6 @@ impl HeadlessServer {
let hyperlinks = crate::server::render_stream::visible_hyperlinks(
&self.app.state,
&self.app.terminal_runtimes,
- Some(&buffer),
);
crate::render_prof::duration_since(
"full_render.visible_hyperlinks",
@@ -7524,7 +7513,7 @@ next_tab = ""
}
#[tokio::test]
- async fn retained_pty_update_declines_when_dirty_row_creates_plain_url() {
+ async fn retained_pty_update_allows_dirty_row_that_creates_plain_url() {
let (mut server, client_rx, pane_id) = retained_test_server(b"plain");
server.render_and_stream();
let _ = client_rx
@@ -7538,20 +7527,15 @@ next_tab = ""
.expect("runtime");
runtime.test_process_pty_bytes(b"\rhttps://example.com/new");
- assert!(!server.render_retained_pty_update_and_stream());
- assert!(client_rx.recv_timeout(Duration::from_millis(50)).is_err());
-
- server.render_and_stream();
- let full = read_server_frame(
+ assert!(server.render_retained_pty_update_and_stream());
+ let patched = read_server_frame(
client_rx
.recv_timeout(Duration::from_millis(100))
- .expect("full frame after plain URL"),
+ .expect("retained frame after plain URL"),
);
assert!(
- full.hyperlinks
- .iter()
- .any(|uri| uri == "https://example.com/new"),
- "full render should synthesize plain URL hyperlink metadata"
+ patched.hyperlinks.is_empty(),
+ "retained render should not synthesize plain URL hyperlink metadata"
);
}
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index 4368bde6..d21dea56 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -1,7 +1,5 @@
//! Virtual rendering helpers for headless client frame streaming.
-use std::collections::HashSet;
-
use ratatui::backend::{Backend, ClearType, TestBackend, WindowSize};
use ratatui::layout::{Position, Rect, Size};
@@ -9,8 +7,7 @@ use crate::app::state::AppState;
use crate::app::Mode;
use crate::protocol::render_ansi::{BlitEncoder, EncodedBlit};
use crate::protocol::{CursorState, FrameData, RenderEncoding, ServerMessage, TerminalFrame};
-use crate::selection::Selection;
-use crate::terminal::{TerminalRuntime, TerminalRuntimeRegistry};
+use crate::terminal::TerminalRuntimeRegistry;
/// Per-client render baseline for the negotiated render encoding.
pub(crate) enum ClientRenderState {
@@ -363,7 +360,6 @@ pub(crate) fn render_terminal_virtual(
pub(crate) fn visible_hyperlinks(
app_state: &AppState,
terminal_runtimes: &TerminalRuntimeRegistry,
- final_buffer: Option<&ratatui::buffer::Buffer>,
) -> Vec<((u16, u16), String, String)> {
let Some(ws_idx) = app_state.active else {
return Vec::new();
@@ -373,143 +369,16 @@ pub(crate) fn visible_hyperlinks(
}
let mut links = Vec::new();
- let mut occupied = HashSet::new();
for info in &app_state.view.pane_infos {
if let Some(runtime) =
app_state.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)
{
- for link in runtime.visible_hyperlinks(info.inner_rect) {
- occupied.insert(link.0);
- links.push(link);
- }
- let metrics = app_state.pane_scroll_metrics(terminal_runtimes, info.id);
- for link in plain_url_hyperlinks_for_pane(
- runtime,
- info.id,
- info.inner_rect,
- metrics,
- final_buffer,
- ) {
- if occupied.insert(link.0) {
- links.push(link);
- }
- }
+ links.extend(runtime.visible_hyperlinks(info.inner_rect));
}
}
links
}
-fn plain_url_hyperlinks_for_pane(
- runtime: &TerminalRuntime,
- pane_id: crate::layout::PaneId,
- area: Rect,
- metrics: Option<crate::pane::ScrollMetrics>,
- final_buffer: Option<&ratatui::buffer::Buffer>,
-) -> Vec<((u16, u16), String, String)> {
- if area.width == 0 || area.height == 0 {
- return Vec::new();
- }
-
- let selection = Selection::line_range(
- pane_id,
- Selection::absolute_row_for_viewport(0, metrics),
- Selection::absolute_row_for_viewport(area.height.saturating_sub(1), metrics),
- area.width.saturating_sub(1),
- );
- let Some(text) = runtime.extract_selection(&selection) else {
- return Vec::new();
- };
- let mut links = Vec::new();
- let cells = crate::app::actions::visible_text_cells(&text, area.width);
- let mut line_start = 0;
- for (byte_idx, ch) in text.char_indices() {
- if ch == '\n' {
- push_plain_url_line_links(
- &mut links,
- &text[line_start..byte_idx],
- line_start,
- &cells,
- area,
- );
- line_start = byte_idx + ch.len_utf8();
- }
- }
- push_plain_url_line_links(&mut links, &text[line_start..], line_start, &cells, area);
- if links.is_empty() {
- return links;
- }
- let pane_buffer = final_buffer.and_then(|_| render_runtime_buffer(runtime, area));
- links
- .into_iter()
- .filter(|((x, y), _, _)| {
- pane_cell_is_visible(
- x.saturating_sub(area.x),
- y.saturating_sub(area.y),
- *x,
- *y,
- pane_buffer.as_ref(),
- final_buffer,
- )
- })
- .collect()
-}
-
-fn render_runtime_buffer(runtime: &TerminalRuntime, area: Rect) -> Option<ratatui::buffer::Buffer> {
- let backend = TestBackend::new(area.width, area.height);
- let mut terminal = ratatui::Terminal::new(backend).ok()?;
- terminal
- .draw(|frame| {
- runtime.render(frame, Rect::new(0, 0, area.width, area.height), false);
- })
- .ok()?;
- Some(terminal.backend().buffer().clone())
-}
-
-fn push_plain_url_line_links(
- links: &mut Vec<((u16, u16), String, String)>,
- line: &str,
- line_start: usize,
- cells: &[crate::app::actions::VisibleTextCell],
- area: Rect,
-) {
- for (start_col, end_col, url) in crate::app::actions::visible_url_spans(line) {
- for cell in cells.iter().filter(|cell| {
- cell.byte_index >= line_start
- && cell.byte_index < line_start + line.len()
- && cell.logical_col >= start_col
- && cell.logical_col <= end_col
- && cell.screen_row < area.height
- }) {
- let x = area.x.saturating_add(cell.screen_col);
- let y = area.y.saturating_add(cell.screen_row);
- links.push(((x, y), cell.ch.to_string(), url.to_owned()));
- }
- }
-}
-
-fn pane_cell_is_visible(
- pane_x: u16,
- pane_y: u16,
- frame_x: u16,
- frame_y: u16,
- pane_buffer: Option<&ratatui::buffer::Buffer>,
- final_buffer: Option<&ratatui::buffer::Buffer>,
-) -> bool {
- let (Some(pane_buffer), Some(final_buffer)) = (pane_buffer, final_buffer) else {
- return true;
- };
- let Some(pane_cell) = pane_buffer.cell((pane_x, pane_y)) else {
- return false;
- };
- let Some(final_cell) = final_buffer.cell((frame_x, frame_y)) else {
- return false;
- };
- pane_cell.symbol() == final_cell.symbol()
- && pane_cell.fg == final_cell.fg
- && pane_cell.bg == final_cell.bg
- && pane_cell.modifier == final_cell.modifier
-}
-
pub(crate) fn focused_terminal_cursor(
app_state: &AppState,
terminal_runtimes: &TerminalRuntimeRegistry,