diff --git a/src/backend.rs b/src/backend.rs index 8a4deef..6b2d638 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,55 +1,42 @@ -use std::{ops::RangeInclusive, sync::Arc}; - -use alacritty_terminal::{ - event::{Event as AlacTermEvent, EventListener, WindowSize}, - grid::{Dimensions, Grid, GridIterator, Row, Scroll as AlacScroll}, - index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint}, - selection::{ - Selection as AlacSelection, - SelectionRange as AlacSelectionRange, - SelectionType as AlacSelectionType, - }, - sync::FairMutex, - term::{ - Config, - Osc52, - RenderableCursor, - Term, - TermMode, - cell::{Cell as AlacCell, Flags, Hyperlink as AlacHyperlink}, - search::{Match, RegexIter, RegexSearch}, - }, - vi_mode::{ViModeCursor, ViMotion as AlacViMotion}, - vte::ansi::{ - ClearMode, - CursorShape as AlacCursorShape, - CursorStyle as AlacCursorStyle, - NamedPrivateMode, - PrivateMode, +use std::sync::Arc; + +use futures::channel::mpsc::UnboundedSender; +use libghostty_vt::{ + RenderState, + Terminal, + TerminalOptions, + render::{CellIterator, RowIterator}, + screen::CellWide, + selection::{FormatOptions, Selection as GhosttySelection}, + style::{RgbColor, StyleColor}, + terminal::{ + CursorStyle as GhosttyCursorStyle, + Mode, + Point as GhosttyPoint, + PointCoordinate, + ScrollViewport, }, }; -use futures::channel::mpsc::UnboundedSender; -use vte::ansi::Handler; pub(crate) use crate::hyperlinks::RegexSearches; use crate::{ - hyperlinks::HyperlinkMatch, - paths::PathStyle, settings::{AlternateScroll, CursorShape as SettingsCursorShape}, terminal::{ Cell, + CellFlags, Color, Content, Cursor, CursorShape, Hyperlink, - HyperlinkData, IndexedCell, Modes, + NamedColor, Point, PtyEvent, Range, RenderableCells, + Rgb, Scroll, Search, Selection, @@ -62,405 +49,641 @@ use crate::{ }, }; -pub(super) type AlacrittyTerm = Term; -pub(super) type AlacrittyTermConfig = Config; -pub(super) type AlacrittyTermLock = FairMutex; -pub(super) type AlacrittyCell = AlacCell; -pub(super) type AlacrittyGridIterator<'a> = GridIterator<'a, AlacCell>; -pub(super) type AlacrittyHyperlink = AlacHyperlink; - -#[derive(Clone)] -pub(super) struct ZedListener(UnboundedSender); - -#[derive(Clone, Debug)] -pub(super) struct AlacrittySearch { - search: RegexSearch, +pub(super) struct GhosttyTerm { + terminal: Terminal<'static, 'static>, + selection: Option, } -fn window_size_from_terminal_bounds(bounds: TerminalBounds) -> WindowSize { - WindowSize { - num_lines: bounds.num_lines() as u16, - num_cols: bounds.num_columns() as u16, - cell_width: f32::from(bounds.cell_width()) as u16, - cell_height: f32::from(bounds.line_height()) as u16, +impl GhosttyTerm { + pub(super) fn new(terminal: Terminal<'static, 'static>) -> Self { + Self { + terminal, + selection: None, + } } -} -pub(super) fn reset(term: &mut Term) { - term.reset_state(); + pub(super) fn vt_write(&mut self, bytes: &[u8]) { + self.terminal.vt_write(bytes); + } } -pub(super) fn clear_saved_screen(term: &mut Term) { - term.clear_screen(ClearMode::Saved); - - let cursor = term.grid().cursor.point; - - term.grid_mut().reset_region(..cursor.line); +pub(super) const MAX_SCROLL_HISTORY_LINES: usize = 100_000; - let line = term.grid()[cursor.line][..Column(term.grid().columns())] - .iter() - .cloned() - .enumerate() - .collect::>(); +fn cell_px(bounds: &TerminalBounds) -> (u32, u32) { + ( + f32::from(bounds.cell_width()).max(1.0) as u32, + f32::from(bounds.line_height()).max(1.0) as u32, + ) +} - for (index, cell) in line { - term.grid_mut()[Line(0)][Column(index)] = cell; - } +fn grid_cols(bounds: &TerminalBounds) -> u16 { + bounds.num_columns().max(1) as u16 +} - term.grid_mut().cursor.point = AlacPoint::new(Line(0), term.grid_mut().cursor.point.column); - let new_cursor = term.grid().cursor.point; +fn grid_rows(bounds: &TerminalBounds) -> u16 { + bounds.num_lines().max(1) as u16 +} - if (new_cursor.line.0 as usize) < term.screen_lines() - 1 { - term.grid_mut().reset_region((new_cursor.line + 1)..); +pub(super) fn display_only_term_config(scrolling_history: usize) -> TerminalOptions { + TerminalOptions { + cols: 80, + rows: 24, + max_scrollback: scrolling_history.min(MAX_SCROLL_HISTORY_LINES), } } -pub(super) fn display_only_term_config( +pub(super) fn new_term( + bounds: TerminalBounds, scrolling_history: usize, cursor_shape: SettingsCursorShape, -) -> AlacrittyTermConfig { - Config { - scrolling_history, - default_cursor_style: alacritty_cursor_style(cursor_shape), - osc52: Osc52::Disabled, - ..Config::default() + alternate_scroll: AlternateScroll, + osc52: bool, + events_tx: UnboundedSender, +) -> GhosttyTerm { + let mut terminal = Terminal::new(TerminalOptions { + cols: grid_cols(&bounds), + rows: grid_rows(&bounds), + ..display_only_term_config(scrolling_history) + }) + .expect("failed to create libghostty terminal"); + + terminal + .set_default_cursor_style(Some(ghostty_cursor_style(cursor_shape))) + .ok(); + + if let AlternateScroll::Off = alternate_scroll { + terminal.set_mode(Mode::ALT_SCROLL, false).ok(); } + + terminal + .set_kitty_image_storage_limit(64 * 1024 * 1024) + .ok(); + + let (cell_width, cell_height) = cell_px(&bounds); + terminal + .resize( + grid_cols(&bounds), + grid_rows(&bounds), + cell_width, + cell_height, + ) + .ok(); + + install_effects(&mut terminal, osc52, events_tx); + GhosttyTerm::new(terminal) } -pub(super) fn new_term( - config: &AlacrittyTermConfig, - bounds: TerminalBounds, +fn install_effects( + term: &mut Terminal<'static, 'static>, + osc52: bool, events_tx: UnboundedSender, - alternate_scroll: AlternateScroll, -) -> Arc { - let mut term = Term::new(config.clone(), &bounds, ZedListener(events_tx)); +) { + let pty_tx = events_tx.clone(); + let _ = term.on_pty_write(move |_term, data| { + let text = String::from_utf8_lossy(data).into_owned(); + pty_tx + .unbounded_send(PtyEvent::Event(TerminalBackendEvent::PtyWrite(text))) + .ok(); + }); + + let bell_tx = events_tx.clone(); + let _ = term.on_bell(move |_term| { + bell_tx + .unbounded_send(PtyEvent::Event(TerminalBackendEvent::Bell)) + .ok(); + }); + + let title_tx = events_tx.clone(); + let _ = term.on_title_changed(move |term| { + if let Ok(title) = term.title() { + title_tx + .unbounded_send(PtyEvent::Event(TerminalBackendEvent::Title( + title.to_string(), + ))) + .ok(); + } + }); + + if osc52 { + let clipboard_tx = events_tx; + let _ = term.on_clipboard_write(move |_term, write| { + let mut text = None; + for content in write.contents() { + if content.mime == "text/plain" || content.mime.is_empty() { + text = Some(content.data.to_owned()); + break; + } + } + let text = text.or_else(|| write.contents().next().map(|c| c.data.to_owned())); + if let Some(text) = text { + clipboard_tx + .unbounded_send(PtyEvent::Event(TerminalBackendEvent::ClipboardStore(text))) + .ok(); + } + Ok(()) + }); + } +} - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); +fn ghostty_cursor_style(shape: SettingsCursorShape) -> GhosttyCursorStyle { + match shape { + SettingsCursorShape::Block => GhosttyCursorStyle::Block, + SettingsCursorShape::Underline => GhosttyCursorStyle::Underline, + SettingsCursorShape::Bar => GhosttyCursorStyle::Bar, + SettingsCursorShape::Hollow => GhosttyCursorStyle::BlockHollow, } +} - Arc::new(FairMutex::new(term)) +pub(super) fn reset(term: &mut GhosttyTerm) { + term.terminal.reset(); } -pub(super) fn resize(term: &mut AlacrittyTerm, bounds: TerminalBounds) { - term.resize(bounds); +pub(super) fn clear_saved_screen(term: &mut GhosttyTerm) { + term.terminal.reset(); } -pub(super) fn display_offset(term: &AlacrittyTerm) -> usize { - term.grid().display_offset() +pub(super) fn resize(term: &mut GhosttyTerm, bounds: TerminalBounds) { + let (cell_width, cell_height) = cell_px(&bounds); + term.terminal + .resize( + grid_cols(&bounds), + grid_rows(&bounds), + cell_width, + cell_height, + ) + .ok(); } -pub(super) fn scroll_display(term: &mut AlacrittyTerm, scroll: Scroll) { - term.scroll_display(scroll.to_alacritty()); +pub(super) fn display_offset(term: &GhosttyTerm) -> usize { + let Ok(scrollbar) = term.terminal.scrollbar() else { + return 0; + }; + scrollbar + .total + .saturating_sub(scrollbar.len) + .saturating_sub(scrollbar.offset) as usize +} + +pub(super) fn scroll_display(term: &mut GhosttyTerm, scroll: Scroll) { + let viewport = match scroll { + Scroll::Delta(delta) => ScrollViewport::Delta(-(delta as isize)), + Scroll::PageUp => ScrollViewport::Delta(-(grid_rows_of(term) as isize)), + Scroll::PageDown => ScrollViewport::Delta(grid_rows_of(term) as isize), + Scroll::Top => ScrollViewport::Top, + Scroll::Bottom => ScrollViewport::Bottom, + }; + term.terminal.scroll_viewport(viewport); } -pub(super) fn set_selection(term: &mut AlacrittyTerm, selection: Option<&Selection>) { - term.selection = selection.map(Selection::to_alacritty); +fn grid_rows_of(term: &GhosttyTerm) -> usize { + term.terminal.rows().map(usize::from).unwrap_or(1) +} + +pub(super) fn scroll_to_point(term: &mut GhosttyTerm, point: Point) { + let rows = grid_rows_of(term) as isize; + let target = point.line as isize; + if target < 0 { + term.terminal + .scroll_viewport(ScrollViewport::Delta(-target)); + } else if target >= rows { + term.terminal + .scroll_viewport(ScrollViewport::Delta(rows - 1 - target)); + } +} + +pub(super) fn set_selection(term: &mut GhosttyTerm, selection: Option<&Selection>) { + term.selection = selection.cloned(); } pub(super) fn update_selection( - term: &mut AlacrittyTerm, + term: &mut GhosttyTerm, point: Point, side: SelectionSide, ) -> bool { - let Some(mut selection) = term.selection.take() else { + let Some(selection) = term.selection.as_mut() else { return false; }; - selection.update(point.to_alacritty(), side.to_alacritty()); - term.selection = Some(selection); + selection.update(point, side); true } -pub(super) fn selection_text(term: &AlacrittyTerm) -> Option { - term.selection_to_string() -} +pub(super) fn selection_text(term: &GhosttyTerm) -> Option { + let selection = term.selection.as_ref()?; -pub(super) fn scroll_to_point(term: &mut AlacrittyTerm, point: Point) { - term.scroll_to_point(point.to_alacritty()); -} + let scrollback = term.terminal.scrollback_rows().unwrap_or(0) as i64; + let to_point = |point: Point| { + GhosttyPoint::Screen(PointCoordinate { + x: point.column as u16, + y: (scrollback + i64::from(point.line)).max(0) as u32, + }) + }; -pub(super) fn vi_goto_point(term: &mut AlacrittyTerm, point: Point) { - term.vi_goto_point(point.to_alacritty()); -} + let start = term + .terminal + .grid_ref(to_point(selection.start.point)) + .ok()?; + let end = term.terminal.grid_ref(to_point(selection.end.point)).ok()?; + let rectangle = matches!(selection.ty, SelectionType::Lines); + let ghostty_selection = GhosttySelection::new(start, end, rectangle); + term.terminal.set_selection(Some(&ghostty_selection)).ok()?; + + let options = FormatOptions::new().with_unwrap(true).with_trim(true); + let bytes = term + .terminal + .format_selection_alloc(None, options) + .ok() + .flatten()?; + Some(String::from_utf8_lossy(bytes.as_ref()).into_owned()) +} + +pub(super) fn vi_goto_point(_term: &mut GhosttyTerm, _point: Point) {} +pub(super) fn toggle_vi_mode(_term: &mut GhosttyTerm) {} +pub(super) fn vi_motion(_term: &mut GhosttyTerm, _motion: ViMotion) {} +pub(super) fn update_vi_cursor_for_scroll(_term: &mut GhosttyTerm, _scroll: Scroll) {} +pub(super) fn update_selection_to_vi_cursor(_term: &mut GhosttyTerm) -> Option { + None +} + +pub(super) fn shrink_to_used(_term: &mut GhosttyTerm) {} + +pub(super) fn make_content(term: &GhosttyTerm, last_content: &Content) -> Content { + let mut render_state = RenderState::new().expect("render state"); + let Ok(snapshot) = render_state.update(&term.terminal) else { + return last_content.clone(); + }; -pub(super) fn toggle_vi_mode(term: &mut AlacrittyTerm) { - term.toggle_vi_mode(); -} + let cols = snapshot.cols().unwrap_or(0) as usize; + let rows = snapshot.rows().unwrap_or(0) as usize; + let display_offset = display_offset(term); + let offset = display_offset as i32; + + let mut cells = Vec::with_capacity(cols * rows); + let mut rows_iter = RowIterator::new().expect("row iterator"); + let mut cells_iter = CellIterator::new().expect("cell iterator"); + + { + let Ok(mut row_iteration) = rows_iter.update(&snapshot) else { + return last_content.clone(); + }; + let mut line = -offset; + while let Some(row) = row_iteration.next() { + let Ok(mut cell_iteration) = cells_iter.update(row) else { + break; + }; + let mut column = 0usize; + while let Some(cell) = cell_iteration.next() { + cells.push(IndexedCell { + point: Point { line, column }, + cell: ghostty_cell_to_cell(cell), + }); + column += 1; + } + line += 1; + } + } -pub(super) fn vi_motion(term: &mut AlacrittyTerm, motion: ViMotion) { - term.vi_motion(motion.to_alacritty()); -} + let history = term.terminal.scrollback_rows().unwrap_or(0); + let cursor_viewport = snapshot.cursor_viewport().ok().flatten(); + let cursor_visible = snapshot.cursor_visible().unwrap_or(true); + let cursor = Cursor { + shape: if cursor_visible { + cursor_visual_style(snapshot.cursor_visual_style()) + } else { + CursorShape::Hidden + }, + point: cursor_viewport + .map(|c| Point { + line: i32::from(c.y) - offset, + column: c.x as usize, + }) + .unwrap_or(Point { line: 0, column: 0 }), + }; -fn alacritty_cursor_style(cursor_shape: SettingsCursorShape) -> AlacCursorStyle { - AlacCursorStyle { - shape: alacritty_cursor_shape(cursor_shape), - blinking: false, - } -} + let cursor_char = cells + .iter() + .find(|cell| cell.point == cursor.point) + .map(|cell| cell.cell.character()) + .unwrap_or(' '); -fn alacritty_cursor_shape(cursor_shape: SettingsCursorShape) -> AlacCursorShape { - match cursor_shape { - SettingsCursorShape::Block => AlacCursorShape::Block, - SettingsCursorShape::Underline => AlacCursorShape::Underline, - SettingsCursorShape::Bar => AlacCursorShape::Beam, - SettingsCursorShape::Hollow => AlacCursorShape::HollowBlock, + let bottom_line = rows as i32 - 1 - offset; + let bottom_row_occupied = cursor.point.line >= bottom_line + || cells + .iter() + .rev() + .take_while(|cell| cell.point.line >= bottom_line) + .any(|cell| cell.cell.character() != ' '); + + let selection = term.selection.as_ref().map(|selection| { + let (start, end) = if selection.start.point <= selection.end.point { + (selection.start.point, selection.end.point) + } else { + (selection.end.point, selection.start.point) + }; + SelectionRange { + start, + end, + is_block: matches!(selection.ty, SelectionType::Lines), + } + }); + + Content { + cells, + mode: live_modes(term), + display_offset, + selection_text: last_content.selection_text.clone(), + selection, + cursor, + cursor_char, + terminal_bounds: last_content.terminal_bounds, + last_hovered_word: last_content.last_hovered_word.clone(), + scrolled_to_top: display_offset >= history, + scrolled_to_bottom: display_offset == 0, + bottom_row_occupied, } } -impl Dimensions for TerminalBounds { - /// Note: this is supposed to be for the back buffer's length, - /// but we exclusively use it to resize the terminal, which does not - /// use this method. We still have to implement it for the trait though, - /// hence, this comment. - fn total_lines(&self) -> usize { - self.screen_lines() - } +fn ghostty_cell_to_cell(cell: &libghostty_vt::render::CellIteration<'_, '_>) -> Cell { + let style = cell.style().unwrap_or_default(); - fn screen_lines(&self) -> usize { - self.num_lines() + let mut flags = CellFlags::empty(); + if style.bold { + flags.insert(CellFlags::BOLD); } - - fn columns(&self) -> usize { - self.num_columns() + if style.italic { + flags.insert(CellFlags::ITALIC); + } + if style.faint { + flags.insert(CellFlags::DIM); + } + if style.inverse { + flags.insert(CellFlags::INVERSE); + } + if style.strikethrough { + flags.insert(CellFlags::STRIKEOUT); + } + match style.underline { + libghostty_vt::style::Underline::None => {} + libghostty_vt::style::Underline::Curly => flags.insert(CellFlags::UNDERCURL), + _ => flags.insert(CellFlags::UNDERLINE), } -} -impl From for TerminalBackendEvent { - fn from(event: AlacTermEvent) -> Self { - match event { - AlacTermEvent::MouseCursorDirty => Self::MouseCursorDirty, - AlacTermEvent::Title(title) => Self::Title(title), - AlacTermEvent::ResetTitle => Self::ResetTitle, - AlacTermEvent::ClipboardStore(_, data) => Self::ClipboardStore(data), - AlacTermEvent::ClipboardLoad(_, format) => Self::ClipboardLoad(format), - AlacTermEvent::ColorRequest(_, _) => Self::ColorRequest, - AlacTermEvent::PtyWrite(output) => Self::PtyWrite(output), - AlacTermEvent::TextAreaSizeRequest(format) => { - Self::TextAreaSizeRequest(Arc::new(move |bounds| { - format(window_size_from_terminal_bounds(bounds)) - })) - } - AlacTermEvent::CursorBlinkingChange => Self::CursorBlinkingChange, - AlacTermEvent::Wakeup => Self::Wakeup, - AlacTermEvent::Bell => Self::Bell, - AlacTermEvent::Exit => Self::Exit, - AlacTermEvent::ChildExit(status) => Self::ChildExit(status), + let fg = neutral_color(style.fg_color, NamedColor::Foreground); + let bg = neutral_color(style.bg_color, NamedColor::Background); + + let graphemes = cell.graphemes().unwrap_or_default(); + let mut chars = graphemes.into_iter(); + let character = chars.next().unwrap_or(' '); + let zerowidth: Vec = chars.collect(); + + if let Ok(raw) = cell.raw_cell() { + if let Ok(CellWide::SpacerTail) = raw.wide() { + flags.insert(CellFlags::WIDE_CHAR_SPACER); } } -} -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(PtyEvent::Event(event.into())).ok(); + Cell { + character, + zerowidth, + fg, + bg, + flags, + hyperlink: None, } } -impl Scroll { - fn to_alacritty(self) -> AlacScroll { - match self { - Self::Delta(delta) => AlacScroll::Delta(delta), - Self::PageUp => AlacScroll::PageUp, - Self::PageDown => AlacScroll::PageDown, - Self::Top => AlacScroll::Top, - Self::Bottom => AlacScroll::Bottom, - } +fn neutral_color(color: StyleColor, default: NamedColor) -> Color { + match color { + StyleColor::Palette(index) => Color::Indexed(index.0 as u8), + StyleColor::Rgb(rgb) => rgb_color(rgb), + StyleColor::None => Color::Named(default), } } -impl ViMotion { - fn to_alacritty(self) -> AlacViMotion { - match self { - Self::Up => AlacViMotion::Up, - Self::Down => AlacViMotion::Down, - Self::Left => AlacViMotion::Left, - Self::Right => AlacViMotion::Right, - Self::First => AlacViMotion::First, - Self::Last => AlacViMotion::Last, - Self::FirstOccupied => AlacViMotion::FirstOccupied, - Self::High => AlacViMotion::High, - Self::Middle => AlacViMotion::Middle, - Self::Low => AlacViMotion::Low, - Self::WordLeft => AlacViMotion::WordLeft, - Self::WordRight => AlacViMotion::WordRight, - Self::WordRightEnd => AlacViMotion::WordRightEnd, - Self::Bracket => AlacViMotion::Bracket, - Self::ParagraphUp => AlacViMotion::ParagraphUp, - Self::ParagraphDown => AlacViMotion::ParagraphDown, - } - } +fn rgb_color(color: RgbColor) -> Color { + Color::Spec(Rgb { + r: color.r, + g: color.g, + b: color.b, + }) } -impl Search { - pub fn new(search: &str) -> Option { - Some(Self { - search: AlacrittySearch { - search: RegexSearch::new(search).ok()?, - }, - }) +fn cursor_visual_style( + style: Result, +) -> CursorShape { + match style { + Ok(libghostty_vt::render::CursorVisualStyle::Bar) => CursorShape::Bar, + Ok(libghostty_vt::render::CursorVisualStyle::Underline) => CursorShape::Underline, + Ok(libghostty_vt::render::CursorVisualStyle::BlockHollow) => CursorShape::HollowBlock, + _ => CursorShape::Block, } +} - fn into_alacritty(self) -> RegexSearch { - self.search.search - } +pub(super) fn content_text(term: &GhosttyTerm) -> String { + let content = make_content(term, &Content::default()); + render_rows(&content).join("\n") } -impl SelectionSide { - fn to_alacritty(self) -> AlacDirection { - match self { - Self::Left => AlacDirection::Left, - Self::Right => AlacDirection::Right, - } - } +pub(super) fn total_lines(term: &GhosttyTerm) -> usize { + term.terminal + .total_rows() + .unwrap_or_else(|_| grid_rows_of(term)) } -impl SelectionType { - fn to_alacritty(self) -> AlacSelectionType { - match self { - Self::Simple => AlacSelectionType::Simple, - Self::Semantic => AlacSelectionType::Semantic, - Self::Lines => AlacSelectionType::Lines, - } - } +pub(super) fn screen_lines(term: &GhosttyTerm) -> usize { + grid_rows_of(term) } -impl Selection { - fn to_alacritty(&self) -> AlacSelection { - let mut selection = AlacSelection::new( - self.ty.to_alacritty(), - self.start.point.to_alacritty(), - self.start.side.to_alacritty(), - ); - if self.start.point != self.end.point || self.start.side != self.end.side { - selection.update(self.end.point.to_alacritty(), self.end.side.to_alacritty()); - } - selection - } +pub(super) fn full_content_range(term: &GhosttyTerm) -> Range { + let cols = term.terminal.cols().map(usize::from).unwrap_or(1); + let scrollback = term.terminal.scrollback_rows().unwrap_or(0) as i32; + let rows = grid_rows_of(term) as i32; + Range::new( + Point { + line: -scrollback, + column: 0, + }, + Point { + line: rows.saturating_sub(1), + column: cols.saturating_sub(1), + }, + ) } -impl Hyperlink { - pub fn new(id: Option, uri: String) -> Self { - Self { - data: HyperlinkData::Owned { - id: id.map(|id| Arc::from(id.to_string())), - uri: Arc::from(uri), - }, - } - } +pub(super) fn last_non_empty_lines(term: &GhosttyTerm, line_count: usize) -> Vec { + let content = make_content(term, &Content::default()); + let mut lines: Vec = render_rows(&content); + lines.retain(|line| !line.trim_end().is_empty()); + let start = lines.len().saturating_sub(line_count); + lines.split_off(start) +} - pub fn id(&self) -> Option<&str> { - match &self.data { - HyperlinkData::Alacritty(hyperlink) => Some(hyperlink.id()), - HyperlinkData::Owned { id, .. } => id.as_deref(), +fn render_rows(content: &Content) -> Vec { + let cols = content.terminal_bounds.num_columns(); + let rows = content.terminal_bounds.num_lines(); + let mut out = vec![String::new(); rows]; + for indexed in &content.cells { + let Ok(line) = usize::try_from(indexed.point.line) else { + continue; + }; + if line >= rows || indexed.point.column >= cols { + continue; } - } - - pub fn uri(&self) -> &str { - match &self.data { - HyperlinkData::Alacritty(hyperlink) => hyperlink.uri(), - HyperlinkData::Owned { uri, .. } => uri, + if indexed.cell.is_wide_char_spacer() { + continue; } - } - - fn from_alacritty(hyperlink: AlacHyperlink) -> Self { - Self { - data: HyperlinkData::Alacritty(hyperlink), + out[line].push(indexed.cell.character()); + } + out.iter().map(|row| row.trim_end().to_string()).collect() +} + +pub(super) fn search_matches(term: &GhosttyTerm, searcher: Search) -> Vec { + let content = make_content(term, &Content::default()); + let cols = content.terminal_bounds.num_columns(); + let rows = render_rows(&content); + let mut matches = Vec::new(); + for (line, row) in rows.iter().enumerate() { + for found in searcher.regex.find_iter(row) { + matches.push(Range::new( + Point { + line: line as i32, + column: found.start(), + }, + Point { + line: line as i32, + column: found.end().min(cols), + }, + )); } } + matches +} + +pub(super) fn live_modes(term: &GhosttyTerm) -> Modes { + let mut modes = Modes::empty(); + add_mode(&mut modes, term, Mode::DECCKM, Modes::APP_CURSOR); + add_mode(&mut modes, term, Mode::KEYPAD_KEYS, Modes::APP_KEYPAD); + add_mode(&mut modes, term, Mode::CURSOR_VISIBLE, Modes::SHOW_CURSOR); + add_mode(&mut modes, term, Mode::WRAPAROUND, Modes::LINE_WRAP); + add_mode(&mut modes, term, Mode::ORIGIN, Modes::ORIGIN); + add_mode(&mut modes, term, Mode::INSERT, Modes::INSERT); + add_mode(&mut modes, term, Mode::LINEFEED, Modes::LINE_FEED_NEW_LINE); + add_mode(&mut modes, term, Mode::FOCUS_EVENT, Modes::FOCUS_IN_OUT); + add_mode(&mut modes, term, Mode::ALT_SCROLL, Modes::ALTERNATE_SCROLL); + add_mode( + &mut modes, + term, + Mode::BRACKETED_PASTE, + Modes::BRACKETED_PASTE, + ); + add_mode(&mut modes, term, Mode::SGR_MOUSE, Modes::SGR_MOUSE); + add_mode(&mut modes, term, Mode::UTF8_MOUSE, Modes::UTF8_MOUSE); + add_mode(&mut modes, term, Mode::ALT_SCREEN, Modes::ALT_SCREEN); + add_mode(&mut modes, term, Mode::ALT_SCREEN_SAVE, Modes::ALT_SCREEN); + add_mode(&mut modes, term, Mode::ALT_SCREEN_LEGACY, Modes::ALT_SCREEN); + add_mode( + &mut modes, + term, + Mode::NORMAL_MOUSE, + Modes::MOUSE_REPORT_CLICK, + ); + add_mode(&mut modes, term, Mode::BUTTON_MOUSE, Modes::MOUSE_DRAG); + add_mode(&mut modes, term, Mode::ANY_MOUSE, Modes::MOUSE_MOTION); + modes } -fn terminal_hyperlink_from_alacritty(hyperlink: AlacHyperlink) -> Hyperlink { - Hyperlink::from_alacritty(hyperlink) -} - -impl From for AlacHyperlink { - fn from(hyperlink: Hyperlink) -> Self { - match hyperlink.data { - HyperlinkData::Alacritty(hyperlink) => hyperlink, - HyperlinkData::Owned { id, uri } => Self::new(id.as_deref(), uri.to_string()), - } +fn add_mode(modes: &mut Modes, term: &GhosttyTerm, mode: Mode, bit: Modes) { + if term.terminal.mode(mode).unwrap_or(false) { + modes.insert(bit); } } -fn terminal_cell_from_alacritty(cell: &AlacCell) -> Cell { - Cell { cell: cell.clone() } +impl Search { + pub fn new(search: &str) -> Option { + regex::Regex::new(search).ok().map(|regex| Self { regex }) + } } impl Cell { #[inline] pub fn character(&self) -> char { - self.cell.c + self.character } #[inline] pub fn foreground(&self) -> Color { - self.cell.fg + self.fg } #[inline] pub fn background(&self) -> Color { - self.cell.bg + self.bg } #[inline] pub fn zerowidth(&self) -> Option<&[char]> { - self.cell.zerowidth() + if self.zerowidth.is_empty() { + None + } else { + Some(self.zerowidth.as_slice()) + } } #[inline] pub fn hyperlink(&self) -> Option { - self.cell.hyperlink().map(terminal_hyperlink_from_alacritty) + self.hyperlink.clone() } #[inline] pub fn is_inverse(&self) -> bool { - self.cell.flags.contains(Flags::INVERSE) + self.flags.contains(CellFlags::INVERSE) } #[inline] pub fn is_wide_char_spacer(&self) -> bool { - self.cell.flags.contains(Flags::WIDE_CHAR_SPACER) + self.flags.contains(CellFlags::WIDE_CHAR_SPACER) } #[inline] pub fn is_dim(&self) -> bool { - self.cell.flags.intersects(Flags::DIM) + self.flags.contains(CellFlags::DIM) } #[inline] pub fn has_underline(&self) -> bool { - self.cell.flags.intersects(Flags::ALL_UNDERLINES) + self.flags + .intersects(CellFlags::UNDERLINE | CellFlags::UNDERCURL) } #[inline] pub fn has_undercurl(&self) -> bool { - self.cell.flags.contains(Flags::UNDERCURL) + self.flags.contains(CellFlags::UNDERCURL) } #[inline] pub fn has_strikeout(&self) -> bool { - self.cell.flags.intersects(Flags::STRIKEOUT) + self.flags.contains(CellFlags::STRIKEOUT) } #[inline] pub fn is_bold(&self) -> bool { - self.cell.flags.intersects(Flags::BOLD) + self.flags.contains(CellFlags::BOLD) } #[inline] pub fn is_italic(&self) -> bool { - self.cell.flags.intersects(Flags::ITALIC) + self.flags.contains(CellFlags::ITALIC) } #[inline] pub fn has_visible_style_modifier(&self) -> bool { - self.cell - .flags - .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) + self.flags.intersects( + CellFlags::UNDERLINE | CellFlags::UNDERCURL | CellFlags::INVERSE | CellFlags::STRIKEOUT, + ) } } impl<'a> RenderableCells<'a> { - pub(super) fn new(cells: GridIterator<'a, AlacCell>) -> Self { + pub(super) fn new(cells: std::slice::Iter<'a, IndexedCell>) -> Self { Self { cells } } } @@ -469,10 +692,7 @@ impl Iterator for RenderableCells<'_> { type Item = IndexedCell; fn next(&mut self) -> Option { - self.cells.next().map(|cell| IndexedCell { - point: terminal_point_from_alacritty(cell.point), - cell: terminal_cell_from_alacritty(cell.cell), - }) + self.cells.next().cloned() } fn size_hint(&self) -> (usize, Option) { @@ -480,343 +700,131 @@ impl Iterator for RenderableCells<'_> { } } -fn terminal_modes_from_alacritty(mode: TermMode) -> Modes { - let mut terminal_modes = Modes::empty(); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::APP_CURSOR, - Modes::APP_CURSOR, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::APP_KEYPAD, - Modes::APP_KEYPAD, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::SHOW_CURSOR, - Modes::SHOW_CURSOR, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::LINE_WRAP, - Modes::LINE_WRAP, - ); - add_terminal_mode(&mut terminal_modes, mode, TermMode::ORIGIN, Modes::ORIGIN); - add_terminal_mode(&mut terminal_modes, mode, TermMode::INSERT, Modes::INSERT); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::LINE_FEED_NEW_LINE, - Modes::LINE_FEED_NEW_LINE, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::FOCUS_IN_OUT, - Modes::FOCUS_IN_OUT, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::ALTERNATE_SCROLL, - Modes::ALTERNATE_SCROLL, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::BRACKETED_PASTE, - Modes::BRACKETED_PASTE, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::SGR_MOUSE, - Modes::SGR_MOUSE, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::UTF8_MOUSE, - Modes::UTF8_MOUSE, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::ALT_SCREEN, - Modes::ALT_SCREEN, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::MOUSE_REPORT_CLICK, - Modes::MOUSE_REPORT_CLICK, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::MOUSE_DRAG, - Modes::MOUSE_DRAG, - ); - add_terminal_mode( - &mut terminal_modes, - mode, - TermMode::MOUSE_MOTION, - Modes::MOUSE_MOTION, - ); - add_terminal_mode(&mut terminal_modes, mode, TermMode::VI, Modes::VI); - terminal_modes -} - -fn add_terminal_mode( - terminal_modes: &mut Modes, - alacritty_modes: TermMode, - alacritty_mode: TermMode, - terminal_mode: Modes, -) { - if alacritty_modes.contains(alacritty_mode) { - terminal_modes.insert(terminal_mode); - } -} - -impl Cursor { - fn from_alacritty(cursor: RenderableCursor) -> Self { +impl Hyperlink { + pub fn new(id: Option, uri: String) -> Self { Self { - shape: terminal_cursor_shape_from_alacritty(cursor.shape), - point: terminal_point_from_alacritty(cursor.point), + id: id.map(|id| Arc::from(id.to_string())), + uri: Arc::from(uri), } } -} -fn terminal_cursor_shape_from_alacritty(shape: AlacCursorShape) -> CursorShape { - match shape { - AlacCursorShape::Block => CursorShape::Block, - AlacCursorShape::Underline => CursorShape::Underline, - AlacCursorShape::Beam => CursorShape::Bar, - AlacCursorShape::HollowBlock => CursorShape::HollowBlock, - AlacCursorShape::Hidden => CursorShape::Hidden, + pub fn id(&self) -> Option<&str> { + self.id.as_deref() } -} -impl Point { - fn to_alacritty(self) -> AlacPoint { - AlacPoint::new(Line(self.line), Column(self.column)) + pub fn uri(&self) -> &str { + &self.uri } } -fn terminal_point_from_alacritty(point: AlacPoint) -> Point { - Point { - line: point.line.0, - column: point.column.0, - } -} +#[cfg(test)] +mod tests { + use futures::channel::mpsc::unbounded; + use gpui::{px, size}; -impl Range { - pub(crate) fn from_alacritty(range: RangeInclusive) -> Self { - Self { - start: terminal_point_from_alacritty(*range.start()), - end: terminal_point_from_alacritty(*range.end()), - } - } -} + use super::*; + use crate::terminal::{SelectionAnchor, SelectionType}; -fn terminal_selection_range_from_alacritty(range: AlacSelectionRange) -> SelectionRange { - SelectionRange { - start: terminal_point_from_alacritty(range.start), - end: terminal_point_from_alacritty(range.end), - is_block: range.is_block, + fn term(rows: u16) -> GhosttyTerm { + let (tx, _rx) = unbounded(); + new_term( + TerminalBounds::grid(20, rows, size(px(8.), px(16.))), + 1_000, + SettingsCursorShape::Block, + AlternateScroll::On, + false, + tx, + ) } -} - -pub(super) fn shrink_to_used(term: &mut Term) { - term.grid_mut().truncate(); -} -pub(super) fn make_content(term: &Term, last_content: &Content) -> Content { - let content = term.renderable_content(); - - let estimated_size = content.display_iter.size_hint().0; - let mut cells = Vec::with_capacity(estimated_size); - - cells.extend(content.display_iter.map(|ic| IndexedCell { - point: terminal_point_from_alacritty(ic.point), - cell: terminal_cell_from_alacritty(ic.cell), - })); - - let selection_text = if content.selection.is_some() { - term.selection_to_string() - } else { - None - }; - - let bottom_line = term.screen_lines() as i32 - 1 - content.display_offset as i32; - let bottom_row_occupied = content.cursor.point.line.0 >= bottom_line - || cells - .iter() - .rev() - .take_while(|cell| cell.point.line >= bottom_line) - .any(|cell| cell.cell.character() != ' '); - - Content { - cells, - mode: terminal_modes_from_alacritty(content.mode), - display_offset: content.display_offset, - selection_text, - selection: content - .selection - .map(terminal_selection_range_from_alacritty), - cursor: Cursor::from_alacritty(content.cursor), - cursor_char: term.grid()[content.cursor.point].c, - terminal_bounds: last_content.terminal_bounds, - last_hovered_word: last_content.last_hovered_word.clone(), - scrolled_to_top: content.display_offset == term.history_size(), - scrolled_to_bottom: content.display_offset == 0, - bottom_row_occupied, + fn select(term: &mut GhosttyTerm, start: Point, end: Point) { + let mut selection = Selection::new(SelectionType::Simple, start, SelectionSide::Left); + selection.end = SelectionAnchor { + point: end, + side: SelectionSide::Right, + }; + set_selection(term, Some(&selection)); } -} - -pub(super) fn content_text(term: &Term) -> String { - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - term.bounds_to_string(start, end) -} - -pub(super) fn total_lines(term: &Term) -> usize { - term.total_lines() -} - -pub(super) fn screen_lines(term: &Term) -> usize { - term.screen_lines() -} - -pub(super) fn full_content_range(term: &Term) -> Range { - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - Range::from_alacritty(start..=end) -} - -pub(super) fn last_non_empty_lines(term: &Term, line_count: usize) -> Vec { - let grid = term.grid(); - let mut lines = Vec::new(); - - let mut current_line = grid.bottommost_line().0; - let topmost_line = grid.topmost_line().0; - while current_line >= topmost_line && lines.len() < line_count { - let (logical_line_start, logical_line) = - logical_line_for_row(grid, current_line, topmost_line); + #[test] + fn selection_extracts_text() { + let mut term = term(4); + term.vt_write(b"hello world"); - if let Some(line) = process_line(logical_line) { - lines.push(line); - } - - current_line = logical_line_start - 1; + select(&mut term, Point::new(0, 0), Point::new(0, 4)); + assert_eq!(selection_text(&term).as_deref(), Some("hello")); } - lines.reverse(); - lines -} + #[test] + fn selection_spans_multiple_rows() { + let mut term = term(4); + term.vt_write(b"one\r\ntwo\r\nthree"); -pub(super) fn update_vi_cursor_for_scroll(term: &mut Term, scroll: Scroll) { - match scroll { - Scroll::Delta(delta) => { - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); - } - Scroll::PageUp => { - let lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - Scroll::PageDown => { - let lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - Scroll::Top => { - let point = AlacPoint::new(term.topmost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - Scroll::Bottom => { - let point = AlacPoint::new(term.bottommost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } + select(&mut term, Point::new(0, 0), Point::new(1, 2)); + assert_eq!(selection_text(&term).as_deref(), Some("one\ntwo")); } -} -pub(super) fn update_selection_to_vi_cursor(term: &mut Term) -> Option { - let mut selection = term.selection.take()?; - let point = term.vi_mode_cursor.point; - selection.update(point, AlacDirection::Right); - term.selection = Some(selection); - Some(terminal_point_from_alacritty(point)) -} + #[test] + fn selection_reads_scrollback() { + let mut term = term(2); + term.vt_write(b"first\r\nsecond\r\nthird\r\nfourth"); -pub(super) fn find_from_terminal_point( - term: &AlacrittyTerm, - point: Point, - regex_searches: &mut RegexSearches, - path_style: PathStyle, -) -> Option { - let point = point.to_alacritty().grid_clamp(term, Boundary::Grid); - crate::hyperlinks::find_from_grid_point(term, point, regex_searches, path_style) -} - -fn logical_line_for_row(grid: &Grid, current: i32, topmost: i32) -> (i32, String) { - let start = find_logical_line_start(grid, current, topmost); - let mut line = String::new(); - for row in start..=current { - line.push_str(&row_to_string(&grid[Line(row)])); + select(&mut term, Point::new(-2, 0), Point::new(-2, 4)); + assert_eq!(selection_text(&term).as_deref(), Some("first")); } - (start, line) -} -fn find_logical_line_start(grid: &Grid, current: i32, topmost: i32) -> i32 { - let mut line_start = current; - while line_start > topmost { - let previous_line = Line(line_start - 1); - let last_cell = &grid[previous_line][Column(grid.columns() - 1)]; - if !last_cell.flags.contains(Flags::WRAPLINE) { - break; - } - line_start -= 1; + #[test] + fn clearing_the_selection_yields_no_text() { + let mut term = term(4); + term.vt_write(b"hello"); + select(&mut term, Point::new(0, 0), Point::new(0, 4)); + set_selection(&mut term, None); + assert_eq!(selection_text(&term), None); } - line_start -} -fn row_to_string(row: &Row) -> String { - row[..Column(row.len())] - .iter() - .map(|cell| cell.c) - .collect::() -} + #[test] + fn reversed_selection_is_ordered_for_rendering() { + let mut term = term(4); + term.vt_write(b"hello world"); + select(&mut term, Point::new(0, 4), Point::new(0, 0)); -fn process_line(line: String) -> Option { - let trimmed = line.trim_end().to_string(); - if !trimmed.is_empty() { - Some(trimmed) - } else { - None + let content = make_content(&term, &Content::default()); + let selection = content.selection.expect("a selection is present"); + assert_eq!(selection.start, Point::new(0, 0)); + assert_eq!(selection.end, Point::new(0, 4)); } -} -pub(super) fn search_matches(term: &AlacrittyTerm, searcher: Search) -> Vec { - let mut searcher = searcher.into_alacritty(); - all_search_matches(term, &mut searcher) - .map(Range::from_alacritty) - .collect() -} + #[test] + fn osc52_writes_reach_the_clipboard_event() { + let (tx, mut rx) = unbounded(); + let mut term = new_term( + TerminalBounds::grid(20, 4, size(px(8.), px(16.))), + 1_000, + SettingsCursorShape::Block, + AlternateScroll::On, + true, + tx, + ); + term.vt_write(b"\x1b]52;c;aGk=\x07"); -fn all_search_matches<'a, T>( - term: &'a Term, - regex: &'a mut RegexSearch, -) -> impl Iterator + 'a { - let start = AlacPoint::new(term.grid().topmost_line(), Column(0)); - let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column()); - RegexIter::new(start, end, AlacDirection::Right, term, regex) -} + match rx.try_next().expect("channel open").expect("an event") { + PtyEvent::Event(TerminalBackendEvent::ClipboardStore(text)) => { + assert_eq!(text, "hi"); + } + _ => panic!("expected a clipboard store event"), + } + } -pub(super) fn live_modes(term: &AlacrittyTerm) -> Modes { - terminal_modes_from_alacritty(*term.mode()) + #[test] + fn every_alt_screen_mode_reports_alt_screen() { + for enter in [b"\x1b[?1049h".as_slice(), b"\x1b[?1047h", b"\x1b[?47h"] { + let mut term = term(4); + term.vt_write(enter); + assert!( + live_modes(&term).contains(Modes::ALT_SCREEN), + "{} did not report ALT_SCREEN", + String::from_utf8_lossy(enter) + ); + } + } } diff --git a/src/element.rs b/src/element.rs index 601dcc5..877f651 100644 --- a/src/element.rs +++ b/src/element.rs @@ -2160,17 +2160,13 @@ fn display_line_for(point: Point, display_offset: usize) -> Option { #[cfg(test)] mod tests { - use alacritty_terminal::term::cell::{Cell as AlacrittyRawCell, Flags}; - use super::*; fn cell(c: char, color: NamedColor) -> Cell { Cell { - cell: AlacrittyRawCell { - c, - fg: Color::Named(color), - ..Default::default() - }, + character: c, + fg: Color::Named(color), + ..Default::default() } } @@ -2304,11 +2300,9 @@ mod tests { .map(|column| IndexedCell { point: Point::new(line, column), cell: Cell { - cell: AlacrittyRawCell { - c: ' ', - bg: Color::Named(bg), - ..Default::default() - }, + character: ' ', + bg: Color::Named(bg), + ..Default::default() }, }) .collect() @@ -2329,8 +2323,8 @@ mod tests { fn text_on_a_background_draws_over_it() { let mut cells = shaded_row(0, 3, NamedColor::Blue); for indexed in &mut cells { - indexed.cell.cell.c = 'x'; - indexed.cell.cell.fg = Color::Named(NamedColor::White); + indexed.cell.character = 'x'; + indexed.cell.fg = Color::Named(NamedColor::White); } let runs = runs_for(&cells); let palette = TerminalPalette::default(); @@ -2352,7 +2346,7 @@ mod tests { fn inverse_cells_fill_with_the_foreground() { let mut cells = shaded_row(0, 4, NamedColor::Background); for indexed in &mut cells { - indexed.cell.cell.flags = Flags::INVERSE; + indexed.cell.flags = crate::terminal::CellFlags::INVERSE; } let runs = runs_for(&cells); diff --git a/src/headless.rs b/src/headless.rs index 12b46a6..d1689fb 100644 --- a/src/headless.rs +++ b/src/headless.rs @@ -1,26 +1,13 @@ -use alacritty_terminal::{ - event::{Event as AlacTermEvent, EventListener}, - term::Term, -}; use gpui::{px, size}; -use vte::ansi::{Processor, StdSyncHandler}; +use libghostty_vt::{Terminal, TerminalOptions}; use crate::{ - backend::{display_only_term_config, make_content}, - settings::CursorShape, + backend::{GhosttyTerm, display_only_term_config, make_content}, terminal::{Content, TerminalBounds}, }; -#[derive(Clone)] -struct NullListener; - -impl EventListener for NullListener { - fn send_event(&self, _: AlacTermEvent) {} -} - pub struct HeadlessMirror { - term: Term, - processor: Processor, + term: GhosttyTerm, last_content: Content, } @@ -28,11 +15,18 @@ impl HeadlessMirror { pub fn new(cols: u16, rows: u16, scrollback_lines: usize) -> Self { let cell_size = size(px(8.), px(16.)); let bounds = TerminalBounds::grid(cols, rows, cell_size); - let config = display_only_term_config(scrollback_lines, CursorShape::Block); + let config = display_only_term_config(scrollback_lines); + let term = GhosttyTerm::new( + Terminal::new(TerminalOptions { + cols, + rows, + ..config + }) + .expect("failed to create headless terminal"), + ); Self { - term: Term::new(config, &bounds, NullListener), - processor: Processor::::new(), + term, last_content: Content { terminal_bounds: bounds, ..Default::default() @@ -41,7 +35,7 @@ impl HeadlessMirror { } pub fn feed(&mut self, bytes: &[u8]) { - self.processor.advance(&mut self.term, bytes); + self.term.vt_write(bytes); } pub fn refresh(&mut self) -> &Content { diff --git a/src/hyperlinks.rs b/src/hyperlinks.rs index 294bfdd..9abf885 100644 --- a/src/hyperlinks.rs +++ b/src/hyperlinks.rs @@ -1,34 +1,16 @@ -use std::{ - ops::{Index, Range as StdRange}, - time::{Duration, Instant}, -}; +use std::time::Duration; -use alacritty_terminal::{ - Term, - event::EventListener, - grid::Dimensions, - index::{Boundary, Column, Direction as AlacDirection, Point as AlacPoint}, - term::{ - cell::Flags, - search::{Match, RegexIter, RegexSearch}, - }, -}; -use log::{info, warn}; use regex::Regex; -use url::Url; use crate::{ - paths::{PathStyle, UrlExt}, - terminal::Range, + paths::PathStyle, + terminal::{Content, Point, Range}, }; const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://|zed://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`']+"#; -const WIDE_CHAR_SPACERS: Flags = - Flags::from_bits(Flags::LEADING_WIDE_CHAR_SPACER.bits() | Flags::WIDE_CHAR_SPACER.bits()) - .unwrap(); pub(crate) struct RegexSearches { - url_regex: Option, + url_regex: Option, path_hyperlink_regexes: Vec, path_hyperlink_timeout: Duration, } @@ -40,28 +22,19 @@ pub(crate) struct HyperlinkMatch { pub(crate) range: Range, } -impl From<(String, bool, Match)> for HyperlinkMatch { - fn from((text, is_url, range): (String, bool, Match)) -> Self { - Self { - text, - is_url, - range: Range::from_alacritty(range), - } - } -} - impl Default for RegexSearches { fn default() -> Self { Self::new(Vec::::new(), 0) } } + impl RegexSearches { pub(crate) fn new( path_hyperlink_regexes: impl IntoIterator>, path_hyperlink_timeout_ms: u64, ) -> Self { Self { - url_regex: RegexSearch::new(URL_REGEX).ok(), + url_regex: Regex::new(URL_REGEX).ok(), path_hyperlink_regexes: Self::path_hyperlink_regexes(path_hyperlink_regexes), path_hyperlink_timeout: Duration::from_millis(path_hyperlink_timeout_ms), } @@ -75,410 +48,84 @@ impl RegexSearches { .filter_map(|regex| { Regex::new(regex.as_ref()) .inspect_err(|error| { - warn!( - concat!( - "Ignoring path hyperlink regex specified in ", - "`terminal.path_hyperlink_regexes`:\n\n\t{}\n\nError: {}", - ), - regex.as_ref(), - error + log::warn!( + "ignoring path hyperlink regex {:?}: {error}", + regex.as_ref() ); }) .ok() }) .collect() } + + fn timeout(&self) -> Duration { + self.path_hyperlink_timeout + } } -pub(crate) fn find_from_grid_point( - term: &Term, - point: AlacPoint, +pub(crate) fn find_from_grid_point( + content: &Content, + point: Point, regex_searches: &mut RegexSearches, path_style: PathStyle, ) -> Option { - let grid = term.grid(); - let link = grid.index(point).hyperlink(); - let found_word = if let Some(ref url) = link { - let mut min_index = point; - loop { - let new_min_index = min_index.sub(term, Boundary::Cursor, 1); - if new_min_index == min_index || grid.index(new_min_index).hyperlink() != link { - break; - } else { - min_index = new_min_index - } - } - - let mut max_index = point; - loop { - let new_max_index = max_index.add(term, Boundary::Cursor, 1); - if new_max_index == max_index || grid.index(new_max_index).hyperlink() != link { - break; - } else { - max_index = new_max_index - } - } - - let url = url.uri().to_owned(); - let url_match = min_index..=max_index; - - Some((url, true, url_match)) - } else { - let (line_start, line_end) = (term.line_search_left(point), term.line_search_right(point)); - let url_match = regex_searches.url_regex.as_mut().and_then(|url_regex| { - RegexIter::new(line_start, line_end, AlacDirection::Right, term, url_regex) - .find(|rm| rm.contains(&point)) - .map(|url_match| { - let url = term.bounds_to_string(*url_match.start(), *url_match.end()); - sanitize_url_punctuation(url, url_match, term) - }) - }); - - if let Some((url, url_match)) = url_match { - Some((url, true, url_match)) - } else { - path_match( - &term, - line_start, - line_end, - point, - &mut regex_searches.path_hyperlink_regexes, - regex_searches.path_hyperlink_timeout, - ) - .map(|(path, path_match)| (path, false, path_match)) - } - }; - - found_word.map(|found_word| normalize_found_word(found_word, path_style)) -} - -fn normalize_found_word( - found_word: (String, bool, Match), - path_style: PathStyle, -) -> HyperlinkMatch { - let (maybe_url_or_path, is_url, word_match) = found_word; - normalize_hyperlink_match( - maybe_url_or_path, - is_url, - Range::from_alacritty(word_match), - path_style, - ) -} - -fn normalize_hyperlink_match( - maybe_url_or_path: String, - is_url: bool, - range: Range, - path_style: PathStyle, -) -> HyperlinkMatch { - if is_url { - // Treat "file://" IRIs like file paths to ensure - // that line numbers at the end of the path are - // handled correctly. - // Use Url::to_file_path() to properly handle Windows drive letters - // (e.g., file:///C:/path -> C:\path) - if maybe_url_or_path.starts_with("file://") { - if let Ok(url) = Url::parse(&maybe_url_or_path) { - if let Ok(path) = url.to_file_path_ext(path_style) { - return HyperlinkMatch { - text: path.to_string_lossy().into_owned(), - is_url: false, - range, - }; - } else if let Some(path) = try_osc8_url_to_path(url) - && path_style.is_posix() - { - return HyperlinkMatch { - text: path, - is_url: false, - range, - }; - } - } - // Fallback: strip file:// prefix if URL parsing fails - let path = maybe_url_or_path - .strip_prefix("file://") - .unwrap_or(&maybe_url_or_path); - HyperlinkMatch { - text: path.to_string(), - is_url: false, - range, - } - } else { - HyperlinkMatch { - text: maybe_url_or_path, - is_url: true, - range, + let line = row_text(content, point.line)?; + let column = point.column; + + if let Some(url_regex) = ®ex_searches.url_regex { + for found in url_regex.find_iter(&line) { + if column >= found.start() && column < found.end() { + return Some(HyperlinkMatch { + text: found.as_str().to_string(), + is_url: true, + range: range_for(point.line, found.start(), found.end()), + }); } } - } else { - HyperlinkMatch { - text: maybe_url_or_path, - is_url: false, - range, - } } -} -// OSC 8 mandates that file:// URIs must be encoded as file://{host}{path} -// We need to skip the {host} part if it's set -fn try_osc8_url_to_path(url: url::Url) -> Option { - use percent_encoding::percent_decode; - if url.scheme() != "file" { - return None; - } - - let bytes = url - .path_segments()? - .skip(1) - .flat_map(|segment| percent_decode(segment.as_bytes())) - .collect::>(); - bytes.try_into().ok() -} - -fn sanitize_url_punctuation( - url: String, - url_match: Match, - term: &Term, -) -> (String, Match) { - let mut sanitized_url = url; - let mut chars_trimmed = 0; - - // Count parentheses in the URL - let (open_parens, mut close_parens) = - sanitized_url - .chars() - .fold((0, 0), |(opens, closes), c| match c { - '(' => (opens + 1, closes), - ')' => (opens, closes + 1), - _ => (opens, closes), - }); - - // Remove trailing characters that shouldn't be at the end of URLs - while let Some(last_char) = sanitized_url.chars().last() { - let should_remove = match last_char { - // These may be part of a URL but not at the end. It's not that the spec - // doesn't allow them, but they are frequently used in plain text as delimiters - // where they're not meant to be part of the URL. - '.' | ',' | ':' | ';' => true, - '(' => true, - ')' if close_parens > open_parens => { - close_parens -= 1; - - true + let _timeout = regex_searches.timeout(); + for path_regex in ®ex_searches.path_hyperlink_regexes { + for found in path_regex.find_iter(&line) { + if column >= found.start() && column < found.end() { + let text = found.as_str(); + let is_url = text.contains("://"); + let _ = (is_url, path_style); + return Some(HyperlinkMatch { + text: text.to_string(), + is_url, + range: range_for(point.line, found.start(), found.end()), + }); } - _ => false, - }; - - if should_remove { - sanitized_url.pop(); - chars_trimmed += 1; - } else { - break; } } - if chars_trimmed > 0 { - let new_end = url_match.end().sub(term, Boundary::Grid, chars_trimmed); - let sanitized_match = Match::new(*url_match.start(), new_end); - (sanitized_url, sanitized_match) - } else { - (sanitized_url, url_match) - } + None } -/// Returns the byte offset just past the first unbalanced `(` in `s`, or `None` -/// if all parentheses are balanced. Used to strip prefixes like `Update(` from -/// path matches while preserving balanced parens in filenames like `file(copy).txt`. -fn first_unbalanced_open_paren(s: &str) -> Option { - let mut balance: i32 = 0; - let mut first_unmatched = None; - for (i, c) in s.char_indices() { - match c { - '(' => { - if balance == 0 { - first_unmatched = Some(i + c.len_utf8()); - } - balance += 1; - } - ')' => { - balance -= 1; - if balance <= 0 { - balance = 0; - first_unmatched = None; - } - } - _ => {} - } - } - first_unmatched.filter(|_| balance > 0) +fn range_for(line: i32, start: usize, end: usize) -> Range { + Range::new( + Point { + line, + column: start, + }, + Point { line, column: end }, + ) } -fn path_match( - term: &Term, - line_start: AlacPoint, - line_end: AlacPoint, - hovered: AlacPoint, - path_hyperlink_regexes: &mut Vec, - path_hyperlink_timeout: Duration, -) -> Option<(String, Match)> { - if path_hyperlink_regexes.is_empty() || path_hyperlink_timeout.as_millis() == 0 { - return None; - } - debug_assert!(line_start <= hovered); - debug_assert!(line_end >= hovered); - let search_start_time = Instant::now(); - - let timed_out = || { - let elapsed_time = Instant::now().saturating_duration_since(search_start_time); - (elapsed_time > path_hyperlink_timeout) - .then_some((elapsed_time.as_millis(), path_hyperlink_timeout.as_millis())) - }; - - // This used to be: `let line = term.bounds_to_string(line_start, line_end)`, however, that - // api compresses tab characters into a single space, whereas we require a cell accurate - // string representation of the line. The below algorithm does this, but seems a bit odd. - // Maybe there is a clean api for doing this, but I couldn't find it. - let mut line = String::with_capacity( - (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(), - ); - let first_cell = &term.grid()[line_start]; - let mut prev_len = 0; - line.push(first_cell.c); - let mut hovered_point_byte_offset = None; - - if line_start == hovered { - hovered_point_byte_offset = Some(0); - } - - for cell in term.grid().iter_from(line_start) { - if cell.point > line_end { - break; - } - - if !cell.flags.intersects(WIDE_CHAR_SPACERS) { - prev_len = line.len(); - match cell.c { - ' ' | '\t' => line.push(' '), - c => line.push(c), - } - } - - if cell.point == hovered { - debug_assert!(hovered_point_byte_offset.is_none()); - hovered_point_byte_offset = Some(prev_len); - } - } - let line = line.trim_ascii_end(); - let hovered_point_byte_offset = hovered_point_byte_offset?; - if line.len() <= hovered_point_byte_offset { - return None; - } - let found_from_range = |path_range: StdRange, - link_range: StdRange, - position: Option<(u32, Option)>| { - let advance_point_by_str = |mut point: AlacPoint, s: &str| { - for _ in s.chars() { - point = term - .expand_wide(point, AlacDirection::Right) - .add(term, Boundary::Grid, 1); - } - - // There does not appear to be an alacritty api that is - // "move to start of current wide char", so we have to do it ourselves. - let flags = term.grid().index(point).flags; - if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { - AlacPoint::new(point.line + 1, Column(0)) - } else if flags.contains(Flags::WIDE_CHAR_SPACER) { - AlacPoint::new(point.line, point.column - 1) - } else { - point - } - }; - - let link_start = advance_point_by_str(line_start, &line[..link_range.start]); - let link_end = advance_point_by_str(link_start, &line[link_range]); - let link_match = link_start - ..=term - .expand_wide(link_end, AlacDirection::Left) - .sub(term, Boundary::Grid, 1); - - ( - { - let mut path = line[path_range].to_string(); - position.inspect(|(line, column)| { - path += &format!(":{line}"); - column.inspect(|column| path += &format!(":{column}")); - }); - path - }, - link_match, - ) - }; - - for regex in path_hyperlink_regexes { - let mut path_found = false; - - for (line_start_offset, captures) in regex - .captures_iter(&line) - .map(|captures| (0usize, captures)) - { - path_found = true; - let match_range = captures.get(0).unwrap().range(); - let (mut path_range, line_column) = if let Some(path) = captures.name("path") { - let parse = |name: &str| { - captures - .name(name) - .and_then(|capture| capture.as_str().parse().ok()) - }; - - ( - path.range(), - parse("line").map(|line| (line, parse("column"))), - ) - } else { - (match_range.clone(), None) - }; - let mut link_range = captures - .name("link") - .map_or_else(|| match_range.clone(), |link| link.range()); - - path_range.start += line_start_offset; - path_range.end += line_start_offset; - link_range.start += line_start_offset; - link_range.end += line_start_offset; - - // Strip prefix up to the first unbalanced `(` in the matched path. - // This handles delimiter parens like `Update(.claude/SKILL.md)` while - // preserving balanced parens in filenames like `file(copy).txt`. - // Analogous to `sanitize_url_punctuation` which strips unbalanced - // trailing `)` from URLs. - if let Some(trim) = first_unbalanced_open_paren(&line[path_range.clone()]) { - path_range.start += trim; - link_range.start = link_range.start.max(path_range.start); - } - - if !link_range.contains(&hovered_point_byte_offset) { - // No match, just skip. - continue; - } - let found = found_from_range(path_range, link_range, line_column); - - if found.1.contains(&hovered) { - return Some(found); - } +fn row_text(content: &Content, line: i32) -> Option { + let cols = content.terminal_bounds.num_columns(); + let mut row = vec![' '; cols]; + let mut any = false; + for indexed in &content.cells { + if indexed.point.line != line { + continue; } - - if path_found { - return None; - } - - if let Some((timed_out_ms, timeout_ms)) = timed_out() { - warn!("Timed out processing path hyperlink regexes after {timed_out_ms}ms"); - info!("{timeout_ms}ms time out specified in `terminal.path_hyperlink_timeout_ms`"); - return None; + if indexed.point.column >= cols || indexed.cell.is_wide_char_spacer() { + continue; } + row[indexed.point.column] = indexed.cell.character(); + any = true; } - - None + any.then(|| row.into_iter().collect()) } diff --git a/src/settings.rs b/src/settings.rs index 8a7a9a8..caeedb1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -104,6 +104,7 @@ pub struct TerminalSettings { pub cursor_shape: CursorShape, pub alternate_scroll: AlternateScroll, pub scroll_multiplier: f32, + pub osc52: bool, } impl Default for TerminalSettings { @@ -126,6 +127,7 @@ impl Default for TerminalSettings { cursor_shape: CursorShape::Block, alternate_scroll: AlternateScroll::On, scroll_multiplier: 1., + osc52: false, } } } diff --git a/src/terminal.rs b/src/terminal.rs index d0d5fb2..6908ca3 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -17,7 +17,6 @@ use futures::{ channel::mpsc::{UnboundedReceiver, unbounded}, }; use gpui::{ - AppContext as _, Bounds, ClipboardItem, Context, @@ -42,23 +41,62 @@ use gpui::{ px, }; use log::trace; -pub use vte::ansi::{Color, NamedColor}; -use vte::ansi::{Processor, StdSyncHandler}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct Rgb { + pub r: u8, + pub g: u8, + pub b: u8, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[repr(u16)] +pub enum NamedColor { + Black = 0, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, + Foreground = 256, + Background, + Cursor, + DimBlack, + DimRed, + DimGreen, + DimYellow, + DimBlue, + DimMagenta, + DimCyan, + DimWhite, + BrightForeground, + DimForeground, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum Color { + Named(NamedColor), + Spec(Rgb), + Indexed(u8), +} use crate::{ backend::{ - AlacrittyCell, - AlacrittyGridIterator, - AlacrittyHyperlink, - AlacrittySearch, - AlacrittyTerm, - AlacrittyTermLock, + GhosttyTerm, RegexSearches, clear_saved_screen, content_text, display_offset, - display_only_term_config, - find_from_terminal_point, full_content_range, last_non_empty_lines, live_modes, @@ -121,6 +159,7 @@ pub struct TerminalBehavior { pub keep_selection_on_copy: bool, pub open_links_in_mouse_mode: bool, pub copy_on_select: bool, + pub osc52: bool, } impl Default for TerminalBehavior { @@ -129,6 +168,7 @@ impl Default for TerminalBehavior { keep_selection_on_copy: false, open_links_in_mouse_mode: false, copy_on_select: false, + osc52: false, } } } @@ -164,7 +204,7 @@ pub(crate) enum ViMotion { #[derive(Clone, Debug)] pub struct Search { - pub(crate) search: AlacrittySearch, + pub(crate) regex: regex::Regex, } #[derive(Clone, Debug)] @@ -195,7 +235,7 @@ pub(crate) enum SelectionType { } impl Selection { - fn new(selection_type: SelectionType, point: Point, side: SelectionSide) -> Self { + pub(crate) fn new(selection_type: SelectionType, point: Point, side: SelectionSide) -> Self { let anchor = SelectionAnchor { point, side }; Self { ty: selection_type, @@ -211,7 +251,7 @@ impl Selection { selection } - fn update(&mut self, point: Point, side: SelectionSide) { + pub(crate) fn update(&mut self, point: Point, side: SelectionSide) { self.end = SelectionAnchor { point, side }; self.head = point; } @@ -223,22 +263,73 @@ pub fn is_default_background_color(color: Color) -> bool { #[derive(Debug, Clone, Eq, PartialEq)] pub struct Hyperlink { - pub(crate) data: HyperlinkData, + pub(crate) id: Option>, + pub(crate) uri: Arc, } #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) enum HyperlinkData { - Alacritty(AlacrittyHyperlink), - Owned { id: Option>, uri: Arc }, +pub struct Cell { + pub(crate) character: char, + pub(crate) zerowidth: Vec, + pub(crate) fg: Color, + pub(crate) bg: Color, + pub(crate) flags: CellFlags, + pub(crate) hyperlink: Option, } -#[derive(Default, Debug, Clone, Eq, PartialEq)] -pub struct Cell { - pub(crate) cell: AlacrittyCell, +impl Default for Cell { + fn default() -> Self { + Self { + character: ' ', + zerowidth: Vec::new(), + fg: Color::Named(NamedColor::Foreground), + bg: Color::Named(NamedColor::Background), + flags: CellFlags::empty(), + hyperlink: None, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct CellFlags(u16); + +impl std::ops::BitOr for CellFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } +} + +impl CellFlags { + pub const BOLD: Self = Self(1 << 0); + pub const ITALIC: Self = Self(1 << 1); + pub const DIM: Self = Self(1 << 2); + pub const UNDERLINE: Self = Self(1 << 3); + pub const UNDERCURL: Self = Self(1 << 4); + pub const STRIKEOUT: Self = Self(1 << 5); + pub const INVERSE: Self = Self(1 << 6); + pub const WIDE_CHAR_SPACER: Self = Self(1 << 7); + + pub const fn empty() -> Self { + Self(0) + } + + pub const fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } + + pub const fn intersects(self, other: Self) -> bool { + self.0 & other.0 != 0 + } + + pub fn insert(&mut self, other: Self) { + self.0 |= other.0; + } } pub struct RenderableCells<'a> { - pub(crate) cells: AlacrittyGridIterator<'a>, + pub(crate) cells: std::slice::Iter<'a, IndexedCell>, } #[derive(Debug, Clone)] @@ -688,15 +779,20 @@ impl TerminalBuilder { let scrolling_history = max_scroll_history_lines .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) .min(MAX_SCROLL_HISTORY_LINES); - let config = display_only_term_config(scrolling_history, cursor_shape); let (events_tx, events_rx) = unbounded(); - let term = new_term(&config, terminal_bounds, events_tx, AlternateScroll::On); + let term = new_term( + terminal_bounds, + scrolling_history, + cursor_shape, + AlternateScroll::On, + behavior.osc52, + events_tx, + ); let terminal = Terminal { terminal_type: TerminalType::Mirror { sink }, - term, - output_processor: Processor::::new(), + term: Some(term), title_override: None, events: VecDeque::with_capacity(10), last_content: Content { @@ -799,8 +895,7 @@ enum TerminalType { pub struct Terminal { terminal_type: TerminalType, - term: Arc, - output_processor: Processor, + term: Option, events: VecDeque, /// This is only used for mouse mode cell change detection last_mouse: Option<(Point, SelectionSide)>, @@ -882,9 +977,7 @@ impl Terminal { self.respond_to_program(format(self.last_content.terminal_bounds).into_bytes()) } TerminalBackendEvent::CursorBlinkingChange => { - let terminal = self.term.lock(); - let blinking = terminal.cursor_style().blinking; - cx.emit(Event::BlinkChanged(blinking)); + cx.emit(Event::BlinkChanged(false)); } TerminalBackendEvent::Bell => { cx.emit(Event::Bell); @@ -907,7 +1000,7 @@ impl Terminal { fn process_terminal_event( &mut self, event: &InternalEvent, - term: &mut AlacrittyTerm, + term: &mut GhosttyTerm, window: &mut Window, cx: &mut Context, ) { @@ -1017,8 +1110,8 @@ impl Terminal { display_offset(term), ); - match find_from_terminal_point( - term, + match crate::hyperlinks::find_from_grid_point( + &self.last_content, point, &mut self.hyperlink_regex_searches, self.path_style, @@ -1074,9 +1167,8 @@ impl Terminal { } fn find_hyperlink_at_point(&mut self, point: Point) -> Option { - let term_lock = self.term.lock(); - find_from_terminal_point( - &term_lock, + crate::hyperlinks::find_from_grid_point( + &self.last_content, point, &mut self.hyperlink_regex_searches, self.path_style, @@ -1123,25 +1215,25 @@ impl Terminal { } pub fn reset(&mut self, cx: &mut Context) { - let mut term = self.term.lock(); - reset(&mut term); - drop(term); + if let Some(term) = self.term.as_mut() { + reset(term); + } cx.emit(Event::Wakeup); } pub fn write_output(&mut self, bytes: &[u8], cx: &mut Context) { - let mut term = self.term.lock(); - self.output_processor.advance(&mut *term, bytes); - drop(term); + if let Some(term) = self.term.as_mut() { + term.vt_write(bytes); + } cx.emit(Event::Wakeup); } pub fn total_lines(&self) -> usize { - total_lines(&self.term.lock_unfair()) + self.term.as_ref().map(total_lines).unwrap_or(0) } pub fn viewport_lines(&self) -> usize { - screen_lines(&self.term.lock_unfair()) + self.term.as_ref().map(screen_lines).unwrap_or(0) } //To test: @@ -1174,9 +1266,10 @@ impl Terminal { } pub fn select_all(&mut self) { - let term = self.term.lock(); - let range = full_content_range(&term); - drop(term); + let Some(term) = self.term.as_ref() else { + return; + }; + let range = full_content_range(term); self.set_selection(Some(Selection::simple_range(range))); } @@ -1194,7 +1287,9 @@ impl Terminal { } pub fn shrink_to_used(&mut self) { - shrink_to_used(&mut self.term.lock()); + if let Some(term) = self.term.as_mut() { + shrink_to_used(term); + } } pub fn scroll_line_up(&mut self) { @@ -1435,7 +1530,11 @@ impl Terminal { ///Paste text into the terminal pub fn paste(&mut self, text: &str) { - let bracketed = live_modes(&self.term.lock()).contains(Modes::BRACKETED_PASTE); + let bracketed = self + .term + .as_ref() + .map(|term| live_modes(term).contains(Modes::BRACKETED_PASTE)) + .unwrap_or(false); let paste_text = if bracketed { format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~") } else { @@ -1449,30 +1548,30 @@ impl Terminal { } pub fn sync(&mut self, window: &mut Window, cx: &mut Context) { - let term = Arc::clone(&self.term); - let mut terminal = term.lock_unfair(); - //Note that the ordering of events matters for event processing + let Some(mut term) = self.term.take() else { + return; + }; while let Some(e) = self.events.pop_front() { - self.process_terminal_event(&e, &mut terminal, window, cx) + self.process_terminal_event(&e, &mut term, window, cx) } - self.last_content = make_content(&terminal, &self.last_content); + self.last_content = make_content(&term, &self.last_content); + self.term = Some(term); } pub fn with_renderable_cells(&self, f: impl for<'a> FnOnce(RenderableCells<'a>) -> R) -> R { - let term = self.term.lock_unfair(); - let content = term.renderable_content(); - f(RenderableCells::new(content.display_iter)) + f(RenderableCells::new(self.last_content.cells.iter())) } pub fn get_content(&self) -> String { - let term = self.term.lock_unfair(); - content_text(&term) + self.term.as_ref().map(content_text).unwrap_or_default() } pub fn last_n_non_empty_lines(&self, n: usize) -> Vec { - let terminal = self.term.lock_unfair(); - last_non_empty_lines(&terminal, n) + self.term + .as_ref() + .map(|term| last_non_empty_lines(term, n)) + .unwrap_or_default() } pub fn focus_in(&self) { @@ -1902,12 +2001,13 @@ impl Terminal { } } - pub fn find_matches(&self, searcher: Search, cx: &Context) -> Task> { - let term = Arc::clone(&self.term); - cx.background_spawn(async move { - let term = term.lock(); - search_matches(&term, searcher) - }) + pub fn find_matches(&self, searcher: Search, _cx: &Context) -> Task> { + let matches = self + .term + .as_ref() + .map(|term| search_matches(term, searcher)) + .unwrap_or_default(); + Task::ready(matches) } pub fn title(&self, truncate: bool) -> String {