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