diff --git a/src/main.rs b/src/main.rs index c92dad7..0fba58d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2672,6 +2672,7 @@ impl Application for App { Message::TabContextMenu(pane, position_opt) }) .on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click))) + .on_open_hyperlink(Some(Box::new(Message::LaunchUrl))) .opacity(self.config.opacity_ratio()) .padding(space_xxs) .show_headerbar(self.config.show_headerbar); diff --git a/src/terminal.rs b/src/terminal.rs index 8aee8d7..e8ef712 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -48,6 +48,18 @@ use crate::{ /// Duplicated from alacritty pub const MIN_CURSOR_CONTRAST: f64 = 1.5; +/// Maximum number of linewraps followed outside of the viewport during search highlighting. +/// Duplicated from you guessed it. +/// A regex expression can start or end outside the visible screen. Therefore, without this constant, some regular expressions would not match at the top and bottom. +pub const MAX_SEARCH_LINES: usize = 100; + +/// https://github.com/alacritty/alacritty/blob/4a7728bf7fac06a35f27f6c4f31e0d9214e5152b/alacritty/src/config/ui_config.rs#L36-L39 +fn url_regex_search() -> RegexSearch { + let url_regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\ + [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; + RegexSearch::new(url_regex).unwrap() +} + #[derive(Clone, Copy, Debug)] pub struct Size { pub width: u32, @@ -202,6 +214,9 @@ pub struct Terminal { pub profile_id_opt: Option, pub tab_title_override: Option, pub term: Arc>>, + pub url_regex_search: RegexSearch, + pub regex_matches: Vec, + pub active_regex_match: Option, bold_font_weight: Weight, buffer: Arc, colors: Colors, @@ -288,6 +303,9 @@ impl Terminal { let _pty_join_handle = pty_event_loop.spawn(); Ok(Self { + active_regex_match: None, + url_regex_search: url_regex_search(), + regex_matches: Vec::new(), bold_font_weight: Weight(bold_font_weight), buffer: Arc::new(buffer), colors, @@ -683,6 +701,10 @@ impl Terminal { } term.reset_damage(); + let regex_match_iter = visible_regex_match_iter(&term, &mut self.url_regex_search); + self.regex_matches.clear(); + self.regex_matches.extend(regex_match_iter); + let grid = term.grid(); for indexed in grid.display_iter() { if indexed.point.line != last_point.unwrap_or(indexed.point).line { @@ -806,8 +828,17 @@ impl Terminal { .underline_color() .map(|c| convert_color(&self.colors, c)) .unwrap_or(fg); + + let mut flags = indexed.cell.flags; + + if let Some(active_match) = &self.active_regex_match { + if active_match.contains(&indexed.point) { + flags |= Flags::UNDERLINE; + } + } + let metadata = Metadata::new(bg, fg) - .with_flags(indexed.cell.flags) + .with_flags(flags) .with_underline_color(underline_color); let (meta_idx, _) = self.metadata_set.insert_full(metadata); attrs = attrs.metadata(meta_idx); @@ -932,6 +963,24 @@ impl Terminal { } } +/// Iterate over all visible regex matches. +/// This includes the screen +- 100 lines (MAX_SEARCH_LINES). +/// display/hint.rs +pub fn visible_regex_match_iter<'a, T>( + term: &'a Term, + regex: &'a mut RegexSearch, +) -> impl Iterator + 'a { + let viewport_start = Line(-(term.grid().display_offset() as i32)); + let viewport_end = viewport_start + term.bottommost_line(); + let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); + let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); + start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); + end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); + + alacritty_terminal::term::search::RegexIter::new(start, end, Direction::Right, term, regex) + .skip_while(move |rm| rm.end().line < viewport_start) + .take_while(move |rm| rm.start().line <= viewport_end) +} impl Drop for Terminal { fn drop(&mut self) { // Ensure shutdown on terminal drop diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 8c31278..a7e1250 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -58,6 +58,7 @@ pub struct TerminalBox<'a, Message> { opacity: Option, mouse_inside_boundary: Option, on_middle_click: Option Message + 'a>>, + on_open_hyperlink: Option Message + 'a>>, key_binds: HashMap, } @@ -80,6 +81,7 @@ where mouse_inside_boundary: None, on_middle_click: None, key_binds: key_binds(), + on_open_hyperlink: None, } } @@ -135,6 +137,14 @@ where self.opacity = Some(opacity); self } + + pub fn on_open_hyperlink( + mut self, + on_open_hyperlink: Option Message + 'a>>, + ) -> Self { + self.on_open_hyperlink = on_open_hyperlink; + self + } } pub fn terminal_box(terminal: &Mutex) -> TerminalBox<'_, Message> @@ -231,6 +241,19 @@ where && y >= 0.0 && y < buffer_size.1.unwrap_or(0.0) { + let col = x / terminal.size().cell_width; + let row = y / terminal.size().cell_height; + + let location = terminal + .viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize))); + if let Some(_) = terminal + .regex_matches + .iter() + .find(|bounds| bounds.contains(&location)) + { + return mouse::Interaction::Pointer; + } + return mouse::Interaction::Text; } } @@ -1008,6 +1031,22 @@ where //TODO: better calculation of position let col = x / terminal.size().cell_width; let row = y / terminal.size().cell_height; + + let location = terminal + .viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize))); + if let Some(on_open_hyperlink) = &self.on_open_hyperlink { + if let Some(match_) = terminal + .regex_matches + .iter() + .find(|bounds| bounds.contains(&location)) + { + let term = terminal.term.lock(); + let hyperlink = term.bounds_to_string(*match_.start(), *match_.end()); + shell.publish(on_open_hyperlink(hyperlink)); + status = Status::Captured; + } + } + if is_mouse_mode { terminal.report_mouse(event, &state.modifiers, col as u32, row as u32); } else { @@ -1049,6 +1088,10 @@ where //TODO: better calculation of position let col = x / terminal.size().cell_width; let row = y / terminal.size().cell_height; + let location = terminal + .viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize))); + update_active_regex_match(&mut terminal, location); + if is_mouse_mode { terminal.report_mouse(event, &state.modifiers, col as u32, row as u32); } else { @@ -1127,6 +1170,19 @@ where } } } + { + let x = p.x - self.padding.left; + let y = p.y - self.padding.top; + //TODO: better calculation of position + let col = x / terminal.size().cell_width; + let row = y / terminal.size().cell_height; + + let location = terminal.viewport_to_point(TermPoint::new( + row as usize, + TermColumn(col as usize), + )); + update_active_regex_match(&mut terminal, location); + } } } _ => (), @@ -1136,6 +1192,30 @@ where } } +fn update_active_regex_match( + terminal: &mut std::sync::MutexGuard<'_, Terminal>, + location: TermPoint, +) { + if let Some(match_) = terminal + .regex_matches + .iter() + .find(|bounds| bounds.contains(&location)) + { + 'update: { + if let Some(active_match) = &terminal.active_regex_match { + if active_match == match_ { + break 'update; + } + } + terminal.active_regex_match = Some(match_.clone()); + terminal.needs_update = true; + } + } else if terminal.active_regex_match.is_some() { + terminal.active_regex_match = None; + terminal.needs_update = true; + } +} + fn shade(color: cosmic_text::Color, is_focused: bool) -> cosmic_text::Color { if is_focused { color