Skip to main content

freya_terminal/
url.rs

1use alacritty_terminal::term::cell::{
2    Cell,
3    Flags,
4};
5use linkify::{
6    LinkFinder,
7    LinkKind,
8};
9
10thread_local! {
11    static FINDER: LinkFinder = {
12        let mut f = LinkFinder::new();
13        f.kinds(&[LinkKind::Url]);
14        f
15    };
16}
17
18/// Column ranges `[start_col, end_col)` of clickable runs in `row`: OSC 8
19/// hyperlinks attached by the terminal program plus plain-text URLs detected
20/// by linkify.
21pub(crate) fn link_ranges(row: &[Cell]) -> Vec<(usize, usize)> {
22    let mut ranges = Vec::new();
23
24    let mut run_start: Option<usize> = None;
25    for (col, cell) in row.iter().enumerate() {
26        if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
27            continue;
28        }
29        if cell.hyperlink().is_some() {
30            run_start.get_or_insert(col);
31        } else if let Some(start) = run_start.take() {
32            ranges.push((start, col));
33        }
34    }
35    if let Some(start) = run_start {
36        ranges.push((start, row.len()));
37    }
38
39    if row_has_url_marker(row) {
40        let (text, byte_to_col) = row_text(row);
41        FINDER.with(|f| {
42            for link in f.links(&text) {
43                ranges.push((byte_to_col[link.start()], byte_to_col[link.end() - 1] + 1));
44            }
45        });
46    }
47
48    ranges
49}
50
51/// URL at column `col` in `row`, if any.
52pub(crate) fn url_at(row: &[Cell], col: usize) -> Option<String> {
53    if !row_has_url_marker(row) {
54        return None;
55    }
56    let (text, byte_to_col) = row_text(row);
57    FINDER.with(|f| {
58        f.links(&text).find_map(|link| {
59            let start = byte_to_col[link.start()];
60            let end = byte_to_col[link.end() - 1] + 1;
61            (col >= start && col < end).then(|| link.as_str().to_owned())
62        })
63    })
64}
65
66/// Cheap pre-scan: skips the row-text allocation when no `://` triplet exists in `row`.
67fn row_has_url_marker(row: &[Cell]) -> bool {
68    let (mut a, mut b) = ('\0', '\0');
69    for cell in row
70        .iter()
71        .filter(|c| !c.flags.contains(Flags::WIDE_CHAR_SPACER))
72    {
73        if a == ':' && b == '/' && cell.c == '/' {
74            return true;
75        }
76        a = b;
77        b = cell.c;
78    }
79    false
80}
81
82/// Visible text for a row paired with a byte→column map. Wide-char spacers
83/// are skipped to mirror the renderer's text layout.
84fn row_text(row: &[Cell]) -> (String, Vec<usize>) {
85    let mut text = String::with_capacity(row.len());
86    let mut byte_to_col = Vec::with_capacity(row.len());
87    for (col, cell) in row.iter().enumerate() {
88        if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
89            continue;
90        }
91        let c = match cell.c {
92            '\0' | '\t' => ' ',
93            c => c,
94        };
95        text.push(c);
96        byte_to_col.resize(text.len(), col);
97    }
98    (text, byte_to_col)
99}