From 264df243e37e11d0760d2af228c8d048227e1327 Mon Sep 17 00:00:00 2001 From: Alexey Gynu Date: Mon, 24 Aug 2026 11:45:15 +0300 Subject: [PATCH 1/2] make OSC 8 hyperlinks follow matching hint rules --- frontends/rioterm/src/hints.rs | 31 ++++++++++--- frontends/rioterm/src/screen/mod.rs | 68 +++++++++++++---------------- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/frontends/rioterm/src/hints.rs b/frontends/rioterm/src/hints.rs index 107b54e6e4..25ea416f53 100644 --- a/frontends/rioterm/src/hints.rs +++ b/frontends/rioterm/src/hints.rs @@ -86,16 +86,19 @@ impl HintState { } }; - // Find regex matches if regex is specified - if let Some(regex_pattern) = &hint.regex { - if let Ok(regex) = onig::Regex::new(regex_pattern) { - self.find_regex_matches(term, ®ex, hint.clone()); - } + let regex = match hint.regex.as_deref().map(onig::Regex::new) { + Some(Ok(regex)) => Some(regex), + Some(Err(_)) => return, + None => None, + }; + + if let Some(regex) = ®ex { + self.find_regex_matches(term, regex, hint.clone()); } // Find OSC 8 hyperlinks if enabled if hint.hyperlinks { - self.find_hyperlink_matches(term, hint.clone()); + self.find_hyperlink_matches(term, regex.as_ref(), hint.clone()); } // Cancel hint mode if no matches found @@ -252,6 +255,7 @@ impl HintState { fn find_hyperlink_matches( &mut self, term: &rio_backend::crosswords::Crosswords, + regex: Option<&onig::Regex>, hint: Rc, ) { // Walk the visible region looking for OSC 8 hyperlink spans. @@ -295,6 +299,10 @@ impl HintState { // Look up the URI once for the whole span. if let Some(hyperlink) = term.cell_hyperlink(line, Column(start_col)) { + if !hyperlink_matches_rule(regex, hyperlink.uri()) { + col = end_col; + continue; + } let mut uri = hyperlink.uri().to_string(); if hint.post_processing { uri = post_process_hyperlink_uri(&uri); @@ -338,6 +346,10 @@ impl HintState { } } +pub(crate) fn hyperlink_matches_rule(regex: Option<&onig::Regex>, uri: &str) -> bool { + regex.is_none_or(|regex| regex.find(uri).is_some()) +} + /// Generates hint labels using the specified alphabet struct LabelGenerator { alphabet: Vec, @@ -753,6 +765,13 @@ mod tests { use super::*; use rio_backend::config::hints::{HintAction, HintInternalAction}; + #[test] + fn hyperlink_rule_filters_osc8_uri() { + let file = onig::Regex::new(r"^file://").unwrap(); + assert!(hyperlink_matches_rule(Some(&file), "file://host/tmp/a.md")); + assert!(!hyperlink_matches_rule(Some(&file), "https://example.com")); + } + #[test] fn test_label_generator() { let mut gen = LabelGenerator::new("abc"); diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index 8d6c2254ec..c68b8f5581 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -2150,36 +2150,40 @@ impl Screen<'_> { continue; } + let regex = match hint_config.regex.as_deref() { + Some(pattern) => match self.compiled_hint_regex(pattern) { + Some(regex) => Some(regex), + None => continue, + }, + None => None, + }; + // Check hyperlinks if enabled if hint_config.hyperlinks { - if let Some(hyperlink_match) = - self.find_hyperlink_at_point(terminal, point) - { + if let Some(hyperlink_match) = self.find_hyperlink_at_point( + terminal, + point, + regex.as_deref(), + hint_config.clone(), + ) { return Some(hyperlink_match); } } // Check regex patterns if specified - if let Some(regex_pattern) = &hint_config.regex { - if let Some(regex) = self.compiled_hint_regex(regex_pattern) { - let line = logical_line.get_or_insert_with(|| { - crate::hints::LogicalLine::extract(terminal, point) + if let Some(regex) = ®ex { + let line = logical_line.get_or_insert_with(|| { + crate::hints::LogicalLine::extract(terminal, point) + }); + if let Some(m) = line.as_ref().and_then(|line| { + line.match_at(terminal, point, regex, hint_config.post_processing) + }) { + return Some(crate::hints::HintMatch { + text: m.text, + start: m.start, + end: m.end, + hint: hint_config.clone(), }); - if let Some(m) = line.as_ref().and_then(|line| { - line.match_at( - terminal, - point, - ®ex, - hint_config.post_processing, - ) - }) { - return Some(crate::hints::HintMatch { - text: m.text, - start: m.start, - end: m.end, - hint: hint_config.clone(), - }); - } } } } @@ -2192,6 +2196,8 @@ impl Screen<'_> { &self, terminal: &rio_backend::crosswords::Crosswords, point: rio_backend::crosswords::pos::Pos, + regex: Option<&onig::Regex>, + hint_config: std::rc::Rc, ) -> Option { let grid = &terminal.grid; @@ -2207,6 +2213,9 @@ impl Screen<'_> { // a different id while belonging to the same link) to find the // span boundaries. let hyperlink = terminal.cell_hyperlink(point.row, point.col)?; + if !crate::hints::hyperlink_matches_rule(regex, hyperlink.uri()) { + return None; + } let mut start_col = point.col; let mut end_col = point.col; @@ -2228,21 +2237,6 @@ impl Screen<'_> { } } - // Build a synthetic hint config so the rest of the hint - // pipeline (highlighting, click action) treats this just like - // a regex/url match. - let hint_config = std::rc::Rc::new(rio_backend::config::hints::Hint { - regex: None, - hyperlinks: true, - post_processing: true, - persist: false, - action: rio_backend::config::hints::HintAction::Action { - action: rio_backend::config::hints::HintInternalAction::Open, - }, - mouse: rio_backend::config::hints::HintMouse::default(), - binding: None, - }); - let mut uri = hyperlink.uri().to_string(); if hint_config.post_processing { uri = post_process_hyperlink_uri(&uri); From d40140da1b9321ad9411db52cc426942f68d2a29 Mon Sep 17 00:00:00 2001 From: Alexey Gynu Date: Mon, 24 Aug 2026 11:45:40 +0300 Subject: [PATCH 2/2] treat empty hint cells as whitespace --- frontends/rioterm/src/hints.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontends/rioterm/src/hints.rs b/frontends/rioterm/src/hints.rs index 25ea416f53..60576157d7 100644 --- a/frontends/rioterm/src/hints.rs +++ b/frontends/rioterm/src/hints.rs @@ -330,7 +330,7 @@ impl HintState { for col in 0..grid.columns() { let cell = &grid[line][Column(col)]; - text.push(cell.c()); + text.push(cell_char_for_regex(cell.c())); } text.trim_end().to_string() @@ -346,6 +346,14 @@ impl HintState { } } +fn cell_char_for_regex(c: char) -> char { + if c == '\0' { + ' ' + } else { + c + } +} + pub(crate) fn hyperlink_matches_rule(regex: Option<&onig::Regex>, uri: &str) -> bool { regex.is_none_or(|regex| regex.find(uri).is_some()) } @@ -765,6 +773,18 @@ mod tests { use super::*; use rio_backend::config::hints::{HintAction, HintInternalAction}; + #[test] + fn empty_grid_cells_are_regex_whitespace() { + let line: String = "https://example.com" + .chars() + .chain(['\0', '\0']) + .map(cell_char_for_regex) + .collect(); + let regex = onig::Regex::new(r"https?://[^\s]+").unwrap(); + + assert_eq!(regex.find_iter(&line).next(), Some((0, 19))); + } + #[test] fn hyperlink_rule_filters_osc8_uri() { let file = onig::Regex::new(r"^file://").unwrap();