1use std::fmt;
7
8use unicode_width::UnicodeWidthStr;
9
10use crate::align::AlignMethod;
11use crate::style::Style;
12
13#[derive(Debug, Clone, PartialEq)]
19pub struct Span {
20 pub start: usize,
21 pub end: usize,
22 pub style: Style,
23}
24
25impl Span {
26 pub fn new(start: usize, end: usize, style: Style) -> Self {
28 Self { start, end, style }
29 }
30
31 pub fn is_empty(&self) -> bool {
33 self.end <= self.start
34 }
35
36 pub fn split(&self, offset: usize) -> (Self, Option<Self>) {
39 if offset <= self.start || offset >= self.end {
40 return (self.clone(), None);
41 }
42 let span1 = Self::new(self.start, self.end.min(offset), self.style.clone());
43 let span2 = Self::new(span1.end, self.end, self.style.clone());
44 (span1, Some(span2))
45 }
46
47 pub fn move_by(&self, offset: isize) -> Self {
49 let start = (self.start as isize + offset).max(0) as usize;
50 let end = (self.end as isize + offset).max(0) as usize;
51 Self::new(start, end, self.style.clone())
52 }
53
54 pub fn right_crop(&self, offset: usize) -> Self {
56 if offset >= self.end {
57 self.clone()
58 } else {
59 Self::new(self.start, self.end.min(offset), self.style.clone())
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
70pub struct Text {
71 pub plain: String,
73 pub spans: Vec<Span>,
75 pub style: Style,
77 pub justify: JustifyMethod,
79 pub end: String,
81 pub overflow: OverflowMethod,
83 pub no_wrap: bool,
85}
86
87pub type JustifyMethod = crate::align::AlignMethod;
88pub type OverflowMethod = crate::console::OverflowMethod;
89
90impl Text {
91 pub fn new(plain: impl Into<String>) -> Self {
93 Self {
94 plain: plain.into(),
95 spans: Vec::new(),
96 style: Style::new(),
97 justify: JustifyMethod::Left,
98 end: "\n".to_string(),
99 overflow: OverflowMethod::Fold,
100 no_wrap: false,
101 }
102 }
103
104 pub fn styled(plain: impl Into<String>, style: Style) -> Self {
106 Self {
107 plain: plain.into(),
108 spans: Vec::new(),
109 style,
110 justify: JustifyMethod::Left,
111 end: "\n".to_string(),
112 overflow: OverflowMethod::Fold,
113 no_wrap: false,
114 }
115 }
116
117 pub fn style(mut self, style: Style) -> Self {
119 self.style = style;
120 self
121 }
122
123 pub fn justify(mut self, justify: JustifyMethod) -> Self {
125 self.justify = justify;
126 self
127 }
128
129 pub fn end(mut self, end: impl Into<String>) -> Self {
131 self.end = end.into();
132 self
133 }
134
135 pub fn overflow(mut self, overflow: OverflowMethod) -> Self {
137 self.overflow = overflow;
138 self
139 }
140
141 pub fn append(&mut self, text: impl Into<Text>, style: Option<Style>) {
143 let text: Text = text.into();
144 let offset = self.plain.len();
145 self.plain.push_str(&text.plain);
146
147 for span in &text.spans {
149 let mut s = span.clone();
150 s.start += offset;
151 s.end += offset;
152 self.spans.push(s);
153 }
154
155 if let Some(st) = style {
157 self.spans.push(Span::new(
158 offset,
159 offset + text.plain.len(),
160 st,
161 ));
162 }
163 }
164
165 pub fn append_styled(&mut self, text: impl Into<String>, style: Style) {
167 let text = text.into();
168 let offset = self.plain.len();
169 self.plain.push_str(&text);
170 self.spans.push(Span::new(offset, offset + text.len(), style));
171 }
172
173 pub fn cell_len(&self) -> usize {
175 UnicodeWidthStr::width(self.plain.as_str())
176 }
177
178 pub fn style_at(&self, position: usize) -> Style {
180 let mut style = self.style.clone();
181 for span in &self.spans {
182 if position >= span.start && position < span.end {
183 style = style.combine(&span.style);
184 }
185 }
186 style
187 }
188
189 pub fn truncate(&mut self, max_width: usize, overflow: OverflowMethod) {
191 let w = self.cell_len();
192 if w <= max_width {
193 return;
194 }
195
196 match overflow {
197 OverflowMethod::Ellipsis => {
198 let ellipsis = "…";
199 let ellip_w = UnicodeWidthStr::width(ellipsis);
200 if max_width <= ellip_w {
201 self.plain = ellipsis[..max_width].to_string();
202 self.spans.clear();
203 return;
204 }
205 let target = max_width - ellip_w;
207 let _char_count = 0usize;
208 let mut byte_pos = 0usize;
209 let mut w_count = 0usize;
210 for (i, ch) in self.plain.char_indices() {
211 let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
212 if w_count + cw > target {
213 break;
214 }
215 w_count += cw;
216 byte_pos = i + ch.len_utf8();
217 }
218 self.plain.truncate(byte_pos);
219 self.plain.push_str(ellipsis);
220 let crop_at = byte_pos;
222 self.spans.retain(|s| s.start < crop_at);
223 for s in &mut self.spans {
224 if s.end > crop_at {
225 s.end = crop_at;
226 }
227 }
228 }
229 OverflowMethod::Crop => {
230 let mut w_count = 0usize;
232 let mut byte_pos = 0usize;
233 for (i, ch) in self.plain.char_indices() {
234 let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
235 if w_count + cw > max_width {
236 break;
237 }
238 w_count += cw;
239 byte_pos = i + ch.len_utf8();
240 }
241 self.plain.truncate(byte_pos);
242 let crop_at = byte_pos;
243 self.spans.retain(|s| s.start < crop_at);
244 for s in &mut self.spans {
245 if s.end > crop_at {
246 s.end = crop_at;
247 }
248 }
249 }
250 _ => {} }
252 }
253
254 pub fn expand_tabs(&mut self) {
256 let tab_width = 8;
257 let mut result = String::new();
258 let mut col = 0usize;
259 for ch in self.plain.chars() {
260 if ch == '\t' {
261 let spaces = tab_width - (col % tab_width);
262 result.push_str(&" ".repeat(spaces));
263 col += spaces;
264 } else {
265 result.push(ch);
266 col += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
267 }
268 }
269 self.plain = result;
270 }
271
272 pub fn split_lines(&self) -> Vec<Text> {
274 self.plain
275 .split('\n')
276 .map(|line| Text::new(line.to_string()))
277 .collect()
278 }
279
280 pub fn render(&self) -> String {
282 if self.spans.is_empty() && self.style.is_plain() {
284 return self.plain.clone();
285 }
286
287 let mut out = String::new();
288 let chars: Vec<(usize, char)> = self.plain.char_indices().collect();
289 let default_ansi = self.style.to_ansi();
290 let reset = if default_ansi.is_empty() { "" } else { "\x1b[0m" };
291
292 if !default_ansi.is_empty() {
293 out.push_str(&default_ansi);
294 }
295
296 for (byte_pos, ch) in &chars {
297 let mut applied = String::new();
299 for span in &self.spans {
300 if span.start == *byte_pos {
301 applied.push_str(&span.style.to_ansi());
302 }
303 }
304 out.push_str(&applied);
305
306 out.push(*ch);
308
309 let char_end = byte_pos + ch.len_utf8();
311 let mut ended = false;
312 for span in &self.spans {
313 if span.end == char_end {
314 out.push_str("\x1b[0m");
315 ended = true;
316 }
317 }
318 if ended && !default_ansi.is_empty() {
320 out.push_str(&default_ansi);
321 }
322 }
323
324 if !reset.is_empty() {
325 out.push_str(reset);
326 }
327
328 out
329 }
330
331 pub fn pad(&mut self, count: usize, character: char) {
333 self.plain = format!(
334 "{}{}{}",
335 character.to_string().repeat(count),
336 self.plain,
337 character.to_string().repeat(count)
338 );
339 for span in &mut self.spans {
341 span.start += count;
342 span.end += count;
343 }
344 }
345
346 pub fn pad_left(&mut self, count: usize, character: char) {
348 self.plain = format!("{}{}", character.to_string().repeat(count), self.plain);
349 for span in &mut self.spans {
350 span.start += count;
351 span.end += count;
352 }
353 }
354
355 pub fn pad_right(&mut self, count: usize, character: char) {
357 self.plain = format!("{}{}", self.plain, character.to_string().repeat(count));
358 }
359
360 pub fn align(&mut self, method: AlignMethod, width: usize) {
362 let current = self.cell_len();
363 if current >= width {
364 return;
365 }
366 let padding = width - current;
367 match method {
368 AlignMethod::Left => self.pad_right(padding, ' '),
369 AlignMethod::Right => self.pad_left(padding, ' '),
370 AlignMethod::Center => {
371 let left = padding / 2;
372 self.pad_left(left, ' ');
373 self.pad_right(padding - left, ' ');
374 }
375 AlignMethod::Full => {} }
377 }
378
379 pub fn stylize(&mut self, style: Style, start: usize, end: Option<usize>) {
382 let end = end.unwrap_or(self.plain.len());
383 if start < end && start < self.plain.len() {
384 self.spans.push(Span::new(start, end, style));
385 }
386 }
387
388 pub fn highlight_regex(&mut self, pattern: &str, style: Style) -> usize {
391 let re = regex::Regex::new(pattern);
392 let re = match re {
393 Ok(r) => r,
394 Err(_) => return 0,
395 };
396
397 let mut count = 0usize;
398 let matches: Vec<(usize, usize)> = re
400 .find_iter(&self.plain)
401 .map(|m| (m.start(), m.end()))
402 .collect();
403
404 for (start, end) in matches {
405 self.spans.push(Span::new(start, end, style.clone()));
406 count += 1;
407 }
408 count
409 }
410
411 pub fn wrap(&self, width: usize) -> Vec<Text> {
415 let mut lines: Vec<Text> = Vec::new();
416 let mut current = Text::new("");
417
418 for word in self.plain.split_whitespace() {
419 let word_w = unicode_width::UnicodeWidthStr::width(word);
420 let cur_w = current.cell_len();
421
422 if cur_w == 0 {
423 current = Text::new(word);
425 } else if cur_w + 1 + word_w <= width {
426 current.plain.push(' ');
428 current.plain.push_str(word);
429 } else {
430 if !current.plain.is_empty() {
432 lines.push(current);
433 }
434 current = Text::new(word);
435 }
436 }
437
438 if !current.plain.is_empty() {
439 lines.push(current);
440 }
441
442 lines
443 }
444}
445
446impl fmt::Display for Text {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 write!(f, "{}", self.render())
449 }
450}
451
452impl From<&str> for Text {
453 fn from(s: &str) -> Self {
454 Self::new(s)
455 }
456}
457
458impl From<String> for Text {
459 fn from(s: String) -> Self {
460 Self::new(s)
461 }
462}
463
464#[derive(Debug, Clone)]
470pub enum TextType {
471 Plain(String),
472 Rich(Text),
473}
474
475impl TextType {
476 pub fn render(&self) -> String {
477 match self {
478 Self::Plain(s) => s.clone(),
479 Self::Rich(t) => t.render(),
480 }
481 }
482}
483
484impl From<&str> for TextType {
485 fn from(s: &str) -> Self {
486 Self::Plain(s.to_string())
487 }
488}
489
490impl From<String> for TextType {
491 fn from(s: String) -> Self {
492 Self::Plain(s)
493 }
494}
495
496impl From<Text> for TextType {
497 fn from(t: Text) -> Self {
498 Self::Rich(t)
499 }
500}
501
502impl fmt::Display for TextType {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 match self {
505 Self::Plain(s) => write!(f, "{s}"),
506 Self::Rich(t) => write!(f, "{t}"),
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn test_text_append() {
517 let mut t = Text::new("Hello");
518 t.append_styled(" World", Style::new().bold(true));
519 assert_eq!(t.plain, "Hello World");
520 assert_eq!(t.spans.len(), 1);
521 assert_eq!(t.spans[0].start, 5);
522 assert_eq!(t.spans[0].end, 11);
523 }
524
525 #[test]
526 fn test_text_truncate() {
527 let mut t = Text::new("Hello World");
528 t.truncate(5, OverflowMethod::Ellipsis);
529 assert!(t.plain.contains('…'));
530 }
531}