Skip to content

Commit

Permalink
regex hyperlinks
Browse files Browse the repository at this point in the history
  • Loading branch information
leb-kuchen authored and jackpot51 committed Dec 18, 2024
1 parent 77be96e commit ff42dd2
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
51 changes: 50 additions & 1 deletion src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -202,6 +214,9 @@ pub struct Terminal {
pub profile_id_opt: Option<ProfileId>,
pub tab_title_override: Option<String>,
pub term: Arc<FairMutex<Term<EventProxy>>>,
pub url_regex_search: RegexSearch,
pub regex_matches: Vec<alacritty_terminal::term::search::Match>,
pub active_regex_match: Option<alacritty_terminal::term::search::Match>,
bold_font_weight: Weight,
buffer: Arc<Buffer>,
colors: Colors,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<T>,
regex: &'a mut RegexSearch,
) -> impl Iterator<Item = alacritty_terminal::term::search::Match> + '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
Expand Down
80 changes: 80 additions & 0 deletions src/terminal_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub struct TerminalBox<'a, Message> {
opacity: Option<f32>,
mouse_inside_boundary: Option<bool>,
on_middle_click: Option<Box<dyn Fn() -> Message + 'a>>,
on_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
key_binds: HashMap<KeyBind, Action>,
}

Expand All @@ -80,6 +81,7 @@ where
mouse_inside_boundary: None,
on_middle_click: None,
key_binds: key_binds(),
on_open_hyperlink: None,
}
}

Expand Down Expand Up @@ -135,6 +137,14 @@ where
self.opacity = Some(opacity);
self
}

pub fn on_open_hyperlink(
mut self,
on_open_hyperlink: Option<Box<dyn Fn(String) -> Message + 'a>>,
) -> Self {
self.on_open_hyperlink = on_open_hyperlink;
self
}
}

pub fn terminal_box<Message>(terminal: &Mutex<Terminal>) -> TerminalBox<'_, Message>
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
}
_ => (),
Expand All @@ -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
Expand Down

0 comments on commit ff42dd2

Please sign in to comment.