1use unicode_width::UnicodeWidthStr;
4
5#[must_use]
16pub fn string_width(s: &str) -> usize {
17 s.width()
18}
19
20#[must_use]
31pub fn truncate_string(s: &str, max_width: usize) -> alloc::string::String {
32 let width = s.width();
33 if width <= max_width {
34 return s.to_string();
35 }
36
37 if max_width < 3 {
38 return alloc::string::String::from("...");
39 }
40
41 let mut result = alloc::string::String::new();
42 let mut current_width = 0;
43 let target_width = max_width - 3; for grapheme in unicode_segmentation::UnicodeSegmentation::graphemes(s, true) {
46 let grapheme_width = grapheme.width();
47 if current_width + grapheme_width > target_width {
48 break;
49 }
50 result.push_str(grapheme);
51 current_width += grapheme_width;
52 }
53
54 result.push_str("...");
55 result
56}
57
58#[must_use]
62pub fn wrap_text(text: &str, width: usize) -> alloc::vec::Vec<alloc::string::String> {
63 let mut lines = alloc::vec::Vec::new();
64 let mut current_line = alloc::string::String::new();
65 let mut current_width = 0;
66
67 for word in text.split_whitespace() {
68 let word_width = word.width();
69
70 if current_width + word_width + 1 > width && !current_line.is_empty() {
71 lines.push(current_line);
72 current_line = alloc::string::String::new();
73 current_width = 0;
74 }
75
76 if !current_line.is_empty() {
77 current_line.push(' ');
78 current_width += 1;
79 }
80
81 current_line.push_str(word);
82 current_width += word_width;
83 }
84
85 if !current_line.is_empty() {
86 lines.push(current_line);
87 }
88
89 if lines.is_empty() {
90 lines.push(alloc::string::String::new());
91 }
92
93 lines
94}
95
96#[must_use]
100pub fn supports_truecolor() -> bool {
101 #[cfg(feature = "std")]
102 {
103 if let Ok(colorterm) = std::env::var("COLORTERM") {
104 if colorterm == "truecolor" || colorterm == "24bit" {
105 return true;
106 }
107 }
108
109 if let Ok(term) = std::env::var("TERM") {
110 if term.contains("256color") || term.contains("24bit") {
111 return true;
112 }
113 }
114 }
115
116 false
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ColorSupport {
122 None,
124 Ansi16,
126 Ansi256,
128 TrueColor,
130}
131
132#[must_use]
134pub fn detect_color_support() -> ColorSupport {
135 #[cfg(feature = "std")]
136 {
137 if supports_truecolor() {
138 return ColorSupport::TrueColor;
139 }
140
141 if let Ok(term) = std::env::var("TERM") {
142 if term.contains("256") {
143 return ColorSupport::Ansi256;
144 }
145 if term != "dumb" && !term.is_empty() {
146 return ColorSupport::Ansi16;
147 }
148 }
149 }
150
151 ColorSupport::Ansi16
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_string_width() {
160 assert_eq!(string_width("Hello"), 5);
161 assert_eq!(string_width(""), 0);
162 }
163
164 #[test]
165 fn test_truncate_string() {
166 assert_eq!(truncate_string("Hello, world!", 8), "Hello...");
167 assert_eq!(truncate_string("Hi", 10), "Hi");
168 }
169
170 #[test]
171 fn test_wrap_text() {
172 let lines = wrap_text("Hello world this is a test", 10);
173 assert!(lines.len() > 1);
174 assert!(lines[0].width() <= 10);
175 }
176
177 #[test]
178 fn test_color_support() {
179 let support = detect_color_support();
180 assert!(matches!(
181 support,
182 ColorSupport::None
183 | ColorSupport::Ansi16
184 | ColorSupport::Ansi256
185 | ColorSupport::TrueColor
186 ));
187 }
188}