1use unicode_width::UnicodeWidthChar;
4
5pub const GREEN: &str = "32";
6pub const RED: &str = "31";
7pub const YELLOW: &str = "33";
8pub const DIM: &str = "2";
9pub const BOLD: &str = "1";
10pub const BRANCH: &str = "36";
11pub const BRANCH_BOLD: &str = "1;36";
12pub const UPSTREAM: &str = "35";
13pub const LINK: &str = "4;34";
14
15pub fn no_color_env() -> bool {
17 std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty())
18}
19
20pub fn paint(s: &str, sgr: &str, color: bool) -> String {
22 if color && !s.is_empty() { format!("\x1b[{sgr}m{s}\x1b[0m") } else { s.to_string() }
23}
24
25pub fn link(url: &str, text: &str) -> String {
27 format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")
28}
29
30pub fn sanitize(s: &str) -> String {
33 let mut out = String::with_capacity(s.len());
34 for c in s.chars() {
35 match c {
36 '\t' => out.push(' '),
37 c if c.is_control() => out += &format!("\\x{:02x}", c as u32),
38 c => out.push(c),
39 }
40 }
41 out
42}
43
44pub fn strip_escapes(s: &str) -> String {
46 let mut out = String::with_capacity(s.len());
47 let mut it = s.chars().peekable();
48 while let Some(c) = it.next() {
49 if c != '\x1b' {
50 out.push(c);
51 continue;
52 }
53 match it.next() {
54 Some('[') => {
55 for c in it.by_ref() {
56 if ('\x40'..='\x7e').contains(&c) {
57 break;
58 }
59 }
60 }
61 Some(']') => {
62 while let Some(c) = it.next() {
63 if c == '\x07' || (c == '\x1b' && it.next_if_eq(&'\\').is_some()) {
64 break;
65 }
66 }
67 }
68 _ => {}
69 }
70 }
71 out
72}
73
74pub fn visible_width(s: &str) -> usize {
76 unicode_width::UnicodeWidthStr::width(strip_escapes(s).as_str())
77}
78
79pub fn truncate(s: &str, max: usize) -> String {
81 if unicode_width::UnicodeWidthStr::width(s) <= max {
82 return s.to_string();
83 }
84 let mut out = String::new();
85 let mut w = 0;
86 for c in s.chars() {
87 let cw = c.width().unwrap_or(0);
88 if w + cw + 1 > max {
89 break;
90 }
91 out.push(c);
92 w += cw;
93 }
94 if max > 0 {
95 out.push('…');
96 }
97 out
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn no_color_has_no_sgr_but_keeps_links() {
106 let s = format!("{} {}", paint("x", GREEN, false), link("https://a.b", "a.b"));
107 assert!(!s.contains("\x1b["));
108 assert!(s.contains("\x1b]8;;https://a.b\x1b\\"));
109 }
110
111 #[test]
112 fn sanitize_controls() {
113 assert_eq!(sanitize("a\x1b]8;;x\x07b\u{9b}c\td"), "a\\x1b]8;;x\\x07b\\x9bc d");
114 assert_eq!(sanitize("ünï"), "ünï");
115 }
116
117 #[test]
118 fn width_ignores_escapes() {
119 let s = format!("{}─{}", paint("ab", BOLD, true), link("https://x.y/zz", "x…"));
120 assert_eq!(visible_width(&s), 5);
121 assert_eq!(truncate("abcdef", 4), "abc…");
122 assert_eq!(truncate("abc", 4), "abc");
123 }
124}