diff options
Diffstat (limited to 'src/pane')
| -rw-r--r-- | src/pane/terminal.rs | 298 |
1 files changed, 273 insertions, 25 deletions
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 8841bad0..75b4a8a5 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -11,6 +11,7 @@ use tracing::{debug, error}; use unicode_width::UnicodeWidthStr; use crate::layout::PaneId; +use crate::protocol::CellData; use super::{ input::{ @@ -50,6 +51,18 @@ pub struct TerminalCursorState { pub shape: u8, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct TerminalDirtyPatch { + pub rows: Vec<(u16, Vec<CellData>)>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum TerminalDirtyPatchOutcome { + Clean, + Patch(TerminalDirtyPatch), + Fallback, +} + fn decscusr_cursor_shape(style: crate::ghostty::CursorVisualStyle, blinking: bool) -> u8 { match (style, blinking) { (crate::ghostty::CursorVisualStyle::Block, true) @@ -200,6 +213,14 @@ impl PaneTerminal { self.ghostty.render(frame, area, show_cursor); } + pub fn collect_dirty_patch( + &self, + area_width: u16, + area_height: u16, + ) -> TerminalDirtyPatchOutcome { + self.ghostty.collect_dirty_patch(area_width, area_height) + } + pub fn visible_hyperlinks(&self, area: Rect) -> Vec<((u16, u16), String, String)> { self.ghostty.visible_hyperlinks(area) } @@ -371,6 +392,7 @@ impl GhosttyPaneTerminal { bytes: &[u8], _response_writer: &mpsc::Sender<Bytes>, ) -> ProcessBytesResult { + crate::render_prof::counter("pty.bytes", bytes.len() as u64); let Ok(mut core) = self.core.lock() else { error!(pane = pane_id.raw(), "ghostty core lock poisoned in reader"); return ProcessBytesResult { @@ -424,7 +446,9 @@ impl GhosttyPaneTerminal { core.default_color_event_tracker .observe(filtered_bytes.as_ref()); respond_to_default_color_events(&mut core, &mut terminal_responses); + let write_started = crate::render_prof::timer(); core.terminal.write(filtered_bytes.as_ref()); + crate::render_prof::duration_since("pty.ghostty_write", write_started); terminal_responses.extend(self.drain_pending_pty_responses()); let has_kitty_graphics_sequence = crate::kitty_graphics::is_enabled() @@ -439,10 +463,21 @@ impl GhosttyPaneTerminal { .terminal .mode_get(crate::ghostty::MODE_SYNCHRONIZED_OUTPUT) .unwrap_or(false); + let request_render = !synchronized_output && !has_kitty_graphics_sequence; + let render_delay = (!synchronized_output && has_kitty_graphics_sequence) + .then_some(KITTY_GRAPHICS_REDRAW_SETTLE); + if request_render { + crate::render_prof::event("pty.request_render"); + } + if render_delay.is_some() { + crate::render_prof::event("pty.request_render_delayed"); + } + if synchronized_output { + crate::render_prof::event("pty.synchronized_output_suppressed"); + } ProcessBytesResult { - request_render: !synchronized_output && !has_kitty_graphics_sequence, - render_delay: (!synchronized_output && has_kitty_graphics_sequence) - .then_some(KITTY_GRAPHICS_REDRAW_SETTLE), + request_render, + render_delay, clipboard_writes, terminal_responses, } @@ -948,9 +983,10 @@ impl GhosttyPaneTerminal { }; let mut x = 0u16; while x < area.width && cells.next() { - let wide = cells.wide().unwrap_or(crate::ghostty::CellWide::Narrow); + let basic = cells.basic_data().unwrap_or_default(); let style = ghostty_cell_style( &cells, + &basic, default_fg, default_bg, resolved_fg, @@ -958,7 +994,7 @@ impl GhosttyPaneTerminal { ); let symbol = match ghostty_buffer_symbol_into( &cells, - wide, + basic.wide, hide_kitty_placeholders, &mut grapheme_bytes, &mut symbol_scratch, @@ -966,7 +1002,7 @@ impl GhosttyPaneTerminal { Ok(symbol) => symbol, Err(_) => { symbol_scratch.clear(); - symbol_scratch.push_str(ghostty_blank_symbol_for_width(wide)); + symbol_scratch.push_str(ghostty_blank_symbol_for_width(basic.wide)); symbol_scratch.as_str() } }; @@ -992,6 +1028,8 @@ impl GhosttyPaneTerminal { } } + ghostty_clear_render_dirty(render_state, area.height); + if show_cursor && render_state.cursor_visible().ok() == Some(true) { if let Ok(Some(cursor)) = render_state.cursor_viewport() { if cursor.x < area.width && cursor.y < area.height { @@ -1000,10 +1038,193 @@ impl GhosttyPaneTerminal { } } } + + pub fn collect_dirty_patch( + &self, + area_width: u16, + area_height: u16, + ) -> TerminalDirtyPatchOutcome { + self.core + .lock() + .ok() + .map(|mut core| ghostty_collect_dirty_patch(&mut core, area_width, area_height)) + .unwrap_or(TerminalDirtyPatchOutcome::Fallback) + } } type VisibleHyperlinks = Vec<((u16, u16), String, String)>; +fn ghostty_clear_render_dirty(render_state: &mut crate::ghostty::RenderState, area_height: u16) { + let Ok(mut row_iterator) = crate::ghostty::RowIterator::new() else { + return; + }; + let Ok(mut rows) = render_state.populate_row_iterator(&mut row_iterator) else { + return; + }; + let mut y = 0u16; + while y < area_height && rows.next() { + let _ = rows.clear_dirty(); + y += 1; + } + let _ = render_state.set_dirty(crate::ghostty::Dirty::Clean); +} + +fn ghostty_collect_dirty_patch( + core: &mut GhosttyPaneCore, + area_width: u16, + area_height: u16, +) -> TerminalDirtyPatchOutcome { + let prof_started = crate::render_prof::timer(); + macro_rules! finish { + ($outcome:expr) => {{ + let outcome = $outcome; + if let Some(started) = prof_started { + crate::render_prof::duration("dirty_collect.total", started.elapsed()); + match &outcome { + TerminalDirtyPatchOutcome::Clean => { + crate::render_prof::event("dirty_collect.clean"); + } + TerminalDirtyPatchOutcome::Fallback => { + crate::render_prof::event("dirty_collect.fallback"); + } + TerminalDirtyPatchOutcome::Patch(patch) => { + crate::render_prof::event("dirty_collect.patch"); + crate::render_prof::counter("dirty_collect.rows", patch.rows.len() as u64); + let cells = patch.rows.iter().map(|(_, cells)| cells.len() as u64).sum(); + crate::render_prof::counter("dirty_collect.cells", cells); + } + } + } + return outcome; + }}; + } + macro_rules! fallback { + ($reason:literal) => {{ + crate::render_prof::event(concat!("dirty_fallback.", $reason)); + finish!(TerminalDirtyPatchOutcome::Fallback); + }}; + } + + let host_theme = core.host_terminal_theme; + let initial_default_foreground = core.initial_default_foreground; + let initial_default_background = core.initial_default_background; + let GhosttyPaneCore { + terminal, + render_state, + .. + } = core; + if render_state.update(terminal).is_err() { + fallback!("render_state_update_error"); + } + match render_state.dirty() { + Ok(crate::ghostty::Dirty::Clean) => finish!(TerminalDirtyPatchOutcome::Clean), + Ok(crate::ghostty::Dirty::Partial) => {} + Ok(crate::ghostty::Dirty::Full) => fallback!("dirty_full"), + Err(_) => fallback!("dirty_read_error"), + } + + let colors = render_state.colors().ok(); + let default_bg = colors + .and_then(|c| ghostty_default_bg(c.background, host_theme, initial_default_background)); + let default_fg = colors + .and_then(|c| ghostty_default_fg(c.foreground, host_theme, initial_default_foreground)); + let resolved_fg = colors.map(|c| ghostty_color(c.foreground)); + let resolved_bg = colors.map(|c| ghostty_color(c.background)); + let hide_kitty_placeholders = crate::kitty_graphics::is_enabled(); + + let Ok(mut row_iterator) = crate::ghostty::RowIterator::new() else { + fallback!("row_iterator_new_error"); + }; + let Ok(mut row_cells) = crate::ghostty::RowCells::new() else { + fallback!("row_cells_new_error"); + }; + let Ok(mut rows) = render_state.populate_row_iterator(&mut row_iterator) else { + fallback!("populate_rows_error"); + }; + let mut grapheme_bytes = Vec::new(); + let mut symbol_scratch = String::new(); + let mut patch_rows = Vec::new(); + let mut y = 0u16; + while y < area_height && rows.next() { + let Ok(dirty) = rows.dirty() else { + fallback!("row_dirty_read_error"); + }; + if dirty { + match rows.selection() { + Ok(None) => {} + Ok(Some(_)) => fallback!("row_selection_present"), + Err(_) => fallback!("row_selection_error"), + } + let Ok(mut cells) = rows.populate_cells(&mut row_cells) else { + fallback!("populate_cells_error"); + }; + let mut patch_cells = Vec::with_capacity(usize::from(area_width)); + let mut x = 0u16; + while x < area_width && cells.next() { + let Ok(basic) = cells.basic_data() else { + fallback!("basic_data_error"); + }; + if basic.has_hyperlink { + fallback!("hyperlink_present"); + } + let style = ghostty_cell_style( + &cells, + &basic, + default_fg, + default_bg, + resolved_fg, + resolved_bg, + ); + let symbol = match ghostty_buffer_symbol_into( + &cells, + basic.wide, + hide_kitty_placeholders, + &mut grapheme_bytes, + &mut symbol_scratch, + ) { + Ok(symbol) => symbol.to_owned(), + Err(_) => ghostty_blank_symbol_for_width(basic.wide).to_owned(), + }; + patch_cells.push(cell_data_from_style(symbol, style)); + x += 1; + } + while x < area_width { + patch_cells.push(blank_cell_data(default_fg, default_bg)); + x += 1; + } + patch_rows.push((y, patch_cells)); + } + y += 1; + } + + let dirty_ys: std::collections::HashSet<u16> = patch_rows.iter().map(|(row, _)| *row).collect(); + if !dirty_ys.is_empty() { + let Ok(mut clear_row_iterator) = crate::ghostty::RowIterator::new() else { + fallback!("clear_row_iterator_new_error"); + }; + let Ok(mut clear_rows) = render_state.populate_row_iterator(&mut clear_row_iterator) else { + fallback!("clear_populate_rows_error"); + }; + let mut clear_y = 0u16; + while clear_y < area_height && clear_rows.next() { + if dirty_ys.contains(&clear_y) && clear_rows.clear_dirty().is_err() { + fallback!("clear_dirty_error"); + } + clear_y += 1; + } + } + if render_state + .set_dirty(crate::ghostty::Dirty::Clean) + .is_err() + { + fallback!("set_clean_error"); + } + + finish!(TerminalDirtyPatchOutcome::Patch(TerminalDirtyPatch { + rows: patch_rows + })); +} + fn ghostty_visible_hyperlinks( core: &mut GhosttyPaneCore, area: Rect, @@ -1283,15 +1504,49 @@ fn ghostty_reset_cell( } } +fn blank_cell_data(default_fg: Option<Color>, default_bg: Option<Color>) -> CellData { + cell_data_from_style( + " ".to_string(), + ghostty_default_style(default_fg, default_bg), + ) +} + +fn cell_data_from_style(symbol: String, style: Style) -> CellData { + CellData { + symbol: if symbol.is_empty() { + " ".to_string() + } else { + symbol + }, + fg: crate::protocol::color_to_u32(style.fg.unwrap_or(Color::Reset)), + bg: crate::protocol::color_to_u32(style.bg.unwrap_or(Color::Reset)), + modifier: crate::protocol::modifier_to_u16(style.add_modifier), + skip: false, + hyperlink: None, + } +} + +fn ghostty_default_style(default_fg: Option<Color>, default_bg: Option<Color>) -> Style { + let mut style = Style::default(); + if let Some(fg) = default_fg { + style = style.fg(fg); + } + if let Some(bg) = default_bg { + style = style.bg(bg); + } + style +} + fn ghostty_cell_style( cells: &crate::ghostty::RowCellIter<'_>, + basic: &crate::ghostty::CellBasicData, default_fg: Option<Color>, default_bg: Option<Color>, resolved_fg: Option<Color>, resolved_bg: Option<Color>, ) -> Style { - let style_data = cells.style().unwrap_or_default(); - let mut fg = style_data + let mut fg = basic + .style .fg_color .map(ghostty_cell_color) .or_else(|| cells.fg_color().ok().flatten().map(ghostty_color)) @@ -1300,14 +1555,14 @@ fn ghostty_cell_style( .content_bg_color() .ok() .flatten() - .or(style_data.bg_color) + .or(basic.style.bg_color) .map(ghostty_cell_color) .or_else(|| cells.bg_color().ok().flatten().map(ghostty_color)) .or(default_bg); - if style_data.invisible { + if basic.style.invisible { fg = bg.or(default_bg); } - if style_data.inverse { + if basic.style.inverse { // When the background is transparent (None), resolve it to the // actual terminal background color before swapping. Otherwise // the swapped fg becomes None (Color::Reset) which the host @@ -1322,31 +1577,24 @@ fn ghostty_cell_style( std::mem::swap(&mut fg, &mut bg); } - let mut style = Style::default(); - if let Some(fg) = fg { - style = style.fg(fg); - } - if let Some(bg) = bg { - style = style.bg(bg); - } - + let style = ghostty_default_style(fg, bg); let mut modifiers = Modifier::empty(); - if style_data.bold { + if basic.style.bold { modifiers |= Modifier::BOLD; } - if style_data.italic { + if basic.style.italic { modifiers |= Modifier::ITALIC; } - if style_data.faint { + if basic.style.faint { modifiers |= Modifier::DIM; } - if style_data.blink { + if basic.style.blink { modifiers |= Modifier::SLOW_BLINK; } - if style_data.underlined { + if basic.style.underlined { modifiers |= Modifier::UNDERLINED; } - if style_data.strikethrough { + if basic.style.strikethrough { modifiers |= Modifier::CROSSED_OUT; } style.add_modifier(modifiers) |
