1use 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
55pub 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}