Skip to main content

gw_render/
remote_box.rs

1//! Box around `remote:` messages, with a host-aware title, links, and branch highlighting.
2
3use crate::style::{BRANCH, DIM, LINK, link, paint, sanitize, truncate};
4use unicode_width::UnicodeWidthStr;
5
6enum Seg<'a> {
7    Plain(&'a str),
8    Url(&'a str),
9    Branch(&'a str),
10}
11
12pub fn title(lines: &[String]) -> &'static str {
13    let all = lines.join("\n").to_lowercase();
14    if all.contains("merge request") {
15        "Merge request"
16    } else if all.contains("pull request") {
17        "Pull request"
18    } else {
19        "remote"
20    }
21}
22
23fn segments<'a>(line: &'a str, branches: &[String]) -> Vec<Seg<'a>> {
24    let mut out = Vec::new();
25    let mut rest = line;
26    while !rest.is_empty() {
27        let ws = rest.find(|c: char| !c.is_whitespace()).unwrap_or(rest.len());
28        if ws > 0 {
29            out.push(Seg::Plain(&rest[..ws]));
30            rest = &rest[ws..];
31            continue;
32        }
33        let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
34        let tok = &rest[..end];
35        rest = &rest[end..];
36        if tok.starts_with("http://") || tok.starts_with("https://") {
37            let url = tok.trim_end_matches(['.', ',', ')']);
38            out.push(Seg::Url(url));
39            out.push(Seg::Plain(&tok[url.len()..]));
40            continue;
41        }
42        let core = tok.trim_matches(|c: char| matches!(c, ':' | ',' | '.' | '\'' | '"' | '(' | ')' | '`'));
43        match tok.find(core) {
44            Some(i) if !core.is_empty() && branches.iter().any(|b| b == core) => {
45                out.push(Seg::Plain(&tok[..i]));
46                out.push(Seg::Branch(core));
47                out.push(Seg::Plain(&tok[i + core.len()..]));
48            }
49            _ => out.push(Seg::Plain(tok)),
50        }
51    }
52    out
53}
54
55/// Renders the box, or an empty string when there is nothing to show.
56/// `term_width` caps the box width; `branches` are highlighted when they appear as words.
57pub fn render_box(lines: &[String], branches: &[String], term_width: usize, color: bool) -> String {
58    let clean: Vec<String> = lines.iter().map(|l| sanitize(l)).collect();
59    let lines: Vec<&str> = clean.iter().map(|l| l.trim_end()).collect();
60    let Some(first) = lines.iter().position(|l| !l.is_empty()) else { return String::new() };
61    let last = lines.iter().rposition(|l| !l.is_empty()).unwrap();
62    let lines = &lines[first..=last];
63    let owned: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
64    let title = title(&owned);
65
66    let longest = lines.iter().map(|l| l.width()).max().unwrap_or(0);
67    let total = (longest + 4).max(title.width() + 6).min(term_width.max(title.width() + 6));
68    let inner = total - 4;
69
70    let mut out = String::new();
71    let top_fill = total - 2 - (title.width() + 2);
72    out += &paint("┌", DIM, color);
73    out += &format!(" {} ", paint(title, "1", color));
74    out += &paint(&format!("{}┐", "─".repeat(top_fill)), DIM, color);
75    out.push('\n');
76
77    for line in lines {
78        let mut room = inner;
79        let mut body = String::new();
80        for seg in segments(line, branches) {
81            if room == 0 {
82                break;
83            }
84            let text = match seg {
85                Seg::Plain(s) | Seg::Url(s) | Seg::Branch(s) => s,
86            };
87            let shown = truncate(text, room);
88            room -= shown.width();
89            body += &match seg {
90                Seg::Plain(_) => shown.clone(),
91                Seg::Url(url) => link(url, &paint(&shown, LINK, color)),
92                Seg::Branch(_) => paint(&shown, BRANCH, color),
93            };
94            if shown != text {
95                break;
96            }
97        }
98        out += &paint("│", DIM, color);
99        out += &format!(" {body}{} ", " ".repeat(room));
100        out += &paint("│", DIM, color);
101        out.push('\n');
102    }
103    out += &paint(&format!("└{}┘", "─".repeat(total - 2)), DIM, color);
104    out.push('\n');
105    out
106}