diff options
Diffstat (limited to 'src/server/render_stream.rs')
| -rw-r--r-- | src/server/render_stream.rs | 148 |
1 files changed, 137 insertions, 11 deletions
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs index 58f7a456..4368bde6 100644 --- a/src/server/render_stream.rs +++ b/src/server/render_stream.rs @@ -1,5 +1,7 @@ //! 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}; @@ -7,7 +9,8 @@ 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::terminal::TerminalRuntimeRegistry; +use crate::selection::Selection; +use crate::terminal::{TerminalRuntime, TerminalRuntimeRegistry}; /// Per-client render baseline for the negotiated render encoding. pub(crate) enum ClientRenderState { @@ -360,28 +363,151 @@ 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(); }; - let Some(tab) = app_state - .workspaces - .get(ws_idx) - .and_then(crate::workspace::Workspace::active_tab) - else { + if app_state.workspaces.get(ws_idx).is_none() { return Vec::new(); - }; + } let mut links = Vec::new(); + let mut occupied = HashSet::new(); for info in &app_state.view.pane_infos { - if let Some(runtime) = tab - .terminal_id(info.id) - .and_then(|terminal_id| terminal_runtimes.get(terminal_id)) + if let Some(runtime) = + app_state.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id) { - links.extend(runtime.visible_hyperlinks(info.inner_rect)); + 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 +} + +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( |
