1use std::sync::{
4 LazyLock,
5 atomic::{AtomicU8, AtomicU64, Ordering},
6};
7
8use omp_core::Str;
9use smallvec::SmallVec;
10use xutf::{Text, width_char};
11
12use crate::{
13 escape::esc,
14 frame::{Color, Style},
15 renderer::push_style_parameters,
16};
17
18pub const SPACES: &str = " ";
20const HANGUL_COMPAT_JAMO_START: char = '\u{3131}';
21const HANGUL_COMPAT_JAMO_END: char = '\u{318e}';
22const HANGUL_FILLER: char = '\u{3164}';
23const JAMO_PLATFORM: u8 = 0;
24const JAMO_UNICODE: u8 = 1;
25const JAMO_NARROW: u8 = 2;
26const JAMO_WIDE: u8 = 3;
27
28static JAMO_WIDTH: AtomicU8 = AtomicU8::new(JAMO_PLATFORM);
29static WIDTH_CONFIG_EPOCH: AtomicU64 = AtomicU64::new(0);
30
31pub fn jamo_width() -> crate::context::JamoWidth {
33 match JAMO_WIDTH.load(Ordering::Relaxed) {
34 JAMO_UNICODE => crate::context::JamoWidth::Unicode,
35 JAMO_NARROW => crate::context::JamoWidth::Narrow,
36 JAMO_WIDE => crate::context::JamoWidth::Wide,
37 _ => crate::context::JamoWidth::Platform,
38 }
39}
40
41pub fn set_jamo_width(width: crate::context::JamoWidth) -> bool {
47 let encoded = match width {
48 crate::context::JamoWidth::Platform => JAMO_PLATFORM,
49 crate::context::JamoWidth::Unicode => JAMO_UNICODE,
50 crate::context::JamoWidth::Narrow => JAMO_NARROW,
51 crate::context::JamoWidth::Wide => JAMO_WIDE,
52 };
53 if JAMO_WIDTH.swap(encoded, Ordering::Relaxed) == encoded {
54 return false;
55 }
56 WIDTH_CONFIG_EPOCH.fetch_add(1, Ordering::Relaxed);
57 true
58}
59
60pub fn width_config_epoch() -> u64 {
64 WIDTH_CONFIG_EPOCH.load(Ordering::Relaxed)
65}
66
67pub fn cell_width(text: &str) -> u16 {
74 if text.is_ascii() && text.bytes().all(|byte| !byte.is_ascii_control()) {
75 return u16::try_from(text.len()).unwrap_or(u16::MAX);
76 }
77 let mut width = text.visible_width();
78 let policy = jamo_width();
79 for character in text
80 .chars()
81 .filter(|character| (HANGUL_COMPAT_JAMO_START..=HANGUL_COMPAT_JAMO_END).contains(character))
82 {
83 let unicode_width = width_char(character);
84 let target = if character == HANGUL_FILLER {
85 0
86 } else {
87 match policy {
88 crate::context::JamoWidth::Unicode => unicode_width,
89 crate::context::JamoWidth::Narrow => 1,
90 crate::context::JamoWidth::Wide => 2,
91 crate::context::JamoWidth::Platform if cfg!(target_os = "macos") => 1,
92 crate::context::JamoWidth::Platform => unicode_width,
93 }
94 };
95 width = width.saturating_sub(unicode_width).saturating_add(target);
96 }
97 u16::try_from(width).unwrap_or(u16::MAX)
98}
99
100fn emit_spaces(sink: &mut dyn RichSink, style: Style, mut count: u16) {
101 while count != 0 {
102 let take = usize::from(count.min(SPACES.len() as u16));
103 sink.run(style, &SPACES[..take]);
104 count -= take as u16;
105 }
106}
107
108pub trait RichSink {
111 fn run(&mut self, style: Style, text: &str);
113
114 fn newline(&mut self);
116
117 fn soft_wrap(&mut self) {
125 self.newline();
126 }
127}
128
129impl RichSink for String {
134 fn run(&mut self, style: Style, text: &str) {
135 self.push_str(esc!(style_prefix));
136 let mut first = false;
137 push_style_parameters(self, style, &mut first);
138 self.push('m');
139 self.push_str(text);
140 }
141
142 fn newline(&mut self) {
143 self.push_str(esc!(style_reset, "\n"));
144 }
145}
146
147pub fn decompose(input: &str, sink: &mut dyn RichSink) {
153 let bytes = input.as_bytes();
154 let mut style = Style::new();
155 let mut clean_start = 0;
156 let mut index = 0;
157
158 while index < bytes.len() {
159 match bytes[index] {
160 b'\n' => {
161 emit_clean(input, clean_start, index, style, sink);
162 sink.newline();
163 index += 1;
164 clean_start = index;
165 },
166 b'\r' => {
167 emit_clean(input, clean_start, index, style, sink);
168 index += 1;
169 if bytes.get(index) == Some(&b'\n') {
170 sink.newline();
171 index += 1;
172 }
173 clean_start = index;
174 },
175 b'\x1b' => {
176 emit_clean(input, clean_start, index, style, sink);
177 index = consume_escape(input, index, &mut style);
178 clean_start = index;
179 },
180 _ => index += 1,
181 }
182 }
183 emit_clean(input, clean_start, index, style, sink);
184}
185
186fn emit_clean(input: &str, start: usize, end: usize, style: Style, sink: &mut dyn RichSink) {
187 if start != end {
188 sink.run(style, &input[start..end]);
189 }
190}
191
192fn consume_escape(input: &str, start: usize, style: &mut Style) -> usize {
193 let bytes = input.as_bytes();
194 let Some(&kind) = bytes.get(start + 1) else {
195 return bytes.len();
196 };
197 match kind {
198 b'[' => {
199 let mut end = start + 2;
200 while let Some(&byte) = bytes.get(end) {
201 if (0x40..=0x7e).contains(&byte) {
202 if byte == b'm' {
203 apply_sgr(&input[start + 2..end], style);
204 }
205 return end + 1;
206 }
207 end += 1;
208 }
209 bytes.len()
210 },
211 b']' => {
212 let mut end = start + 2;
213 while let Some(&byte) = bytes.get(end) {
214 if byte == b'\x07' {
215 return end + 1;
216 }
217 if byte == b'\x1b' && bytes.get(end + 1) == Some(&b'\\') {
218 return end + 2;
219 }
220 end += 1;
221 }
222 bytes.len()
223 },
224 _ => {
225 let length = input[start + 1..]
226 .chars()
227 .next()
228 .expect("escape kind exists")
229 .len_utf8();
230 start + 1 + length
231 },
232 }
233}
234
235fn apply_sgr(parameters: &str, style: &mut Style) {
236 let mut parameters = parameters.split(';');
237 while let Some(parameter) = parameters.next() {
238 let code = if parameter.is_empty() {
239 0
240 } else if let Ok(code) = parameter.parse::<u16>() {
241 code
242 } else {
243 continue;
244 };
245 match code {
246 0 => *style = Style::new(),
247 1 => style.bold = true,
248 2 => style.dim = true,
249 3 => style.italic = true,
250 4 => style.underline = true,
251 7 => style.reverse = true,
252 9 => style.strikethrough = true,
253 22 => {
254 style.bold = false;
255 style.dim = false;
256 },
257 23 => style.italic = false,
258 24 => style.underline = false,
259 27 => style.reverse = false,
260 29 => style.strikethrough = false,
261 30..=37 => style.foreground = Color::Indexed((code - 30) as u8),
262 39 => style.foreground = Color::Default,
263 40..=47 => style.background = Color::Indexed((code - 40) as u8),
264 49 => style.background = Color::Default,
265 90..=97 => style.foreground = Color::Indexed((code - 90 + 8) as u8),
266 100..=107 => style.background = Color::Indexed((code - 100 + 8) as u8),
267 38 | 48 => {
268 let background = code == 48;
269 let color = match parameters
270 .next()
271 .and_then(|value| value.parse::<u16>().ok())
272 {
273 Some(5) => parameters
274 .next()
275 .and_then(|value| value.parse::<u8>().ok())
276 .map(Color::Indexed),
277 Some(2) => {
278 let red = parameters.next().and_then(|value| value.parse::<u8>().ok());
279 let green = parameters.next().and_then(|value| value.parse::<u8>().ok());
280 let blue = parameters.next().and_then(|value| value.parse::<u8>().ok());
281 red.zip(green)
282 .zip(blue)
283 .map(|((red, green), blue)| Color::Rgb(red, green, blue))
284 },
285 _ => None,
286 };
287 if let Some(color) = color {
288 if background {
289 style.background = color;
290 } else {
291 style.foreground = color;
292 }
293 }
294 },
295 _ => {},
296 }
297 }
298}
299
300#[derive(Clone, Debug)]
301struct Run {
302 end: u32,
303 style: Style,
304}
305
306#[derive(Clone, Debug)]
307struct RowMeta {
308 run_end: u32,
309 width: u16,
310 soft: bool,
312}
313
314#[derive(Clone, Debug, Default)]
316pub struct RichText {
317 text: String,
318 runs: Vec<Run>,
319 rows: Vec<RowMeta>,
320 current_width: u16,
321 open: bool,
322}
323
324impl RichText {
325 pub fn clear(&mut self) {
327 self.text.clear();
328 self.runs.clear();
329 self.rows.clear();
330 self.current_width = 0;
331 self.open = false;
332 }
333
334 #[cfg(test)]
336 pub(crate) const fn capacities(&self) -> (usize, usize, usize) {
337 (self.text.capacity(), self.runs.capacity(), self.rows.capacity())
338 }
339
340 pub fn rows(&self) -> u16 {
342 u16::try_from(self.rows.len())
343 .unwrap_or(u16::MAX)
344 .saturating_add(u16::from(self.open))
345 }
346
347 pub fn row_width(&self, row: u16) -> u16 {
349 let index = usize::from(row);
350 if let Some(meta) = self.rows.get(index) {
351 meta.width
352 } else if self.open && index == self.rows.len() {
353 self.current_width
354 } else {
355 0
356 }
357 }
358
359 fn row_run_bounds(&self, row: u16) -> (usize, usize) {
360 let index = usize::from(row);
361 if index >= usize::from(self.rows()) {
362 return (0, 0);
363 }
364 let start = index
365 .checked_sub(1)
366 .and_then(|previous| self.rows.get(previous))
367 .map_or(0, |meta| meta.run_end as usize);
368 let end = self
369 .rows
370 .get(index)
371 .map_or(self.runs.len(), |meta| meta.run_end as usize);
372 (start, end)
373 }
374
375 pub fn row_text(&self, row: u16) -> &str {
377 let (run_start, run_end) = self.row_run_bounds(row);
378 let byte_start = run_start
379 .checked_sub(1)
380 .and_then(|index| self.runs.get(index))
381 .map_or(0, |run| run.end as usize);
382 let byte_end = run_end
383 .checked_sub(1)
384 .and_then(|index| self.runs.get(index))
385 .map_or(byte_start, |run| run.end as usize);
386 &self.text[byte_start..byte_end]
387 }
388
389 pub fn row_runs(
391 &self,
392 row: u16,
393 ) -> impl DoubleEndedIterator<Item = (Style, &str)>
394 + ExactSizeIterator
395 + Clone
396 + std::iter::FusedIterator
397 + '_ {
398 let (start, end) = self.row_run_bounds(row);
399 self.runs[start..end]
400 .iter()
401 .enumerate()
402 .map(move |(offset, run)| {
403 let index = start + offset;
404 let byte_start = index
405 .checked_sub(1)
406 .and_then(|previous| self.runs.get(previous))
407 .map_or(0, |previous| previous.end as usize);
408 (run.style, &self.text[byte_start..run.end as usize])
409 })
410 }
411
412 pub fn row_soft_wrap(&self, row: u16) -> bool {
415 self
416 .rows
417 .get(usize::from(row))
418 .is_some_and(|meta| meta.soft)
419 }
420
421 pub fn replay(&self, sink: &mut dyn RichSink) {
423 for row in 0..self.rows() {
424 self.replay_row(row, sink);
425 if usize::from(row) < self.rows.len() {
426 if self.row_soft_wrap(row) {
427 sink.soft_wrap();
428 } else {
429 sink.newline();
430 }
431 }
432 }
433 }
434
435 pub fn replay_row(&self, row: u16, sink: &mut dyn RichSink) {
437 for (style, text) in self.row_runs(row) {
438 sink.run(style, text);
439 }
440 }
441
442 pub fn widest(&self) -> u16 {
444 self
445 .rows
446 .iter()
447 .map(|row| row.width)
448 .chain(self.open.then_some(self.current_width))
449 .max()
450 .unwrap_or(0)
451 }
452}
453
454impl RichSink for RichText {
455 fn run(&mut self, style: Style, text: &str) {
456 if text.is_empty() {
457 return;
458 }
459 self.text.push_str(text);
460 let end = u32::try_from(self.text.len()).expect("rich text exceeds four gigabytes");
461 if self.open && self.runs.last().is_some_and(|run| run.style == style) {
462 self.runs.last_mut().expect("last run exists").end = end;
463 } else {
464 self.runs.push(Run { end, style });
465 }
466 self.current_width = self.current_width.saturating_add(cell_width(text));
467 self.open = true;
468 }
469
470 fn newline(&mut self) {
471 self.end_row(false);
472 }
473
474 fn soft_wrap(&mut self) {
475 self.end_row(true);
476 }
477}
478impl RichText {
479 fn end_row(&mut self, soft: bool) {
480 self.rows.push(RowMeta {
481 run_end: u32::try_from(self.runs.len()).expect("rich text has too many runs"),
482 width: self.current_width,
483 soft,
484 });
485 self.current_width = 0;
486 self.open = false;
487 }
488}
489
490#[derive(Default)]
492pub struct Measure {
493 pub rows: u16,
495 pub widest: u16,
497 current: u16,
498 open: bool,
499}
500
501impl RichSink for Measure {
502 fn run(&mut self, _style: Style, text: &str) {
503 if text.is_empty() {
504 return;
505 }
506 if !self.open {
507 self.rows = self.rows.saturating_add(1);
508 self.open = true;
509 }
510 self.current = self.current.saturating_add(cell_width(text));
511 self.widest = self.widest.max(self.current);
512 }
513
514 fn newline(&mut self) {
515 if !self.open {
516 self.rows = self.rows.saturating_add(1);
517 }
518 self.widest = self.widest.max(self.current);
519 self.current = 0;
520 self.open = false;
521 }
522}
523
524#[derive(Clone, Debug, Default)]
526pub struct Prefix {
527 text: String,
528 runs: SmallVec<(u32, Style), 2>,
529 width: u16,
530}
531
532impl Prefix {
533 pub fn push(&mut self, style: Style, text: &str) {
535 if text.is_empty() {
536 return;
537 }
538 self.text.push_str(text);
539 let end = u32::try_from(self.text.len()).expect("prefix exceeds four gigabytes");
540 if self
541 .runs
542 .last()
543 .is_some_and(|(_, run_style)| *run_style == style)
544 {
545 self.runs.last_mut().expect("last prefix run exists").0 = end;
546 } else {
547 self.runs.push((end, style));
548 }
549 self.width = self.width.saturating_add(cell_width(text));
550 }
551
552 pub const fn width(&self) -> u16 {
554 self.width
555 }
556
557 pub fn emit(&self, sink: &mut dyn RichSink) {
559 let mut start = 0;
560 for (end, style) in &self.runs {
561 sink.run(*style, &self.text[start..*end as usize]);
562 start = *end as usize;
563 }
564 }
565
566 pub const fn is_empty(&self) -> bool {
568 self.text.is_empty()
569 }
570
571 pub fn empty_ref() -> &'static Self {
573 static EMPTY: LazyLock<Prefix> = LazyLock::new(Prefix::default);
574 &EMPTY
575 }
576
577 fn emit_clipped(&self, width: u16, sink: &mut dyn RichSink) -> bool {
578 let mut used = 0_u16;
579 let mut start = 0;
580 let mut emitted = false;
581 'outer: for (end, style) in &self.runs {
582 for grapheme in self.text[start..*end as usize].graphemes() {
583 let grapheme_width = cell_width(grapheme);
584 if used.saturating_add(grapheme_width) > width {
585 break 'outer;
586 }
587 sink.run(*style, grapheme);
588 emitted = true;
589 used = used.saturating_add(grapheme_width);
590 }
591 start = *end as usize;
592 }
593 emitted
594 }
595}
596
597impl<S: RichSink + ?Sized> RichSink for &mut S {
598 fn run(&mut self, style: Style, text: &str) {
599 (**self).run(style, text);
600 }
601
602 fn newline(&mut self) {
603 (**self).newline();
604 }
605
606 fn soft_wrap(&mut self) {
607 (**self).soft_wrap();
608 }
609}
610
611pub trait Pipeline: RichSink + Sized {
613 fn clip(self, width: u16, marker: Option<char>) -> Clip<Self> {
616 Clip::new(self, width, marker)
617 }
618
619 fn rows(self, max: u16) -> Rows<Self> {
621 Rows { inner: self, max, seen: 0, truncated: false }
622 }
623
624 fn wrap(self, width: u16) -> Wrap<'static, Self> {
626 self.wrap_prefixed(width, Prefix::empty_ref(), Prefix::empty_ref())
627 }
628 fn wrap_chars(self, width: u16) -> CharWrap<Self> {
632 CharWrap { inner: self, width: width.max(1), used: 0 }
633 }
634
635 fn wrap_prefixed<'p>(self, width: u16, first: &'p Prefix, cont: &'p Prefix) -> Wrap<'p, Self> {
637 Wrap::new(self, width, first, cont)
638 }
639
640 fn tee(self, copy: &mut RichText) -> Tee<'_, Self> {
642 Tee { inner: self, copy }
643 }
644
645 fn restyle<F: Fn(Style) -> Style>(self, map: F) -> Restyle<Self, F> {
647 Restyle { inner: self, map }
648 }
649
650 fn prefixed<'p>(self, first: &'p Prefix, cont: &'p Prefix) -> Prefixed<'p, Self> {
652 Prefixed { inner: self, first, cont, row: 0, at_start: true }
653 }
654}
655
656impl<S: RichSink> Pipeline for S {}
657pub struct CharWrap<S: RichSink> {
663 inner: S,
664 width: u16,
665 used: u16,
666}
667
668impl<S: RichSink> RichSink for CharWrap<S> {
669 fn run(&mut self, style: Style, text: &str) {
670 for grapheme in text.graphemes() {
671 let grapheme_width = cell_width(grapheme);
672 if grapheme_width > self.width {
673 continue;
674 }
675 if grapheme_width > 0 && self.used.saturating_add(grapheme_width) > self.width {
676 self.inner.soft_wrap();
677 self.used = 0;
678 }
679 self.inner.run(style, grapheme);
680 self.used = self.used.saturating_add(grapheme_width);
681 }
682 }
683
684 fn newline(&mut self) {
685 self.inner.newline();
686 self.used = 0;
687 }
688
689 fn soft_wrap(&mut self) {
690 self.inner.soft_wrap();
691 self.used = 0;
692 }
693}
694
695pub struct Wrap<'p, S: RichSink> {
697 inner: S,
698 width: u16,
699 first: &'p Prefix,
700 cont: &'p Prefix,
701 word: String,
702 word_runs: SmallVec<(u32, Style), 4>,
703 word_width: u16,
704 gap_text: String,
705 gap_runs: SmallVec<(u32, Style), 2>,
706 gap_width: u16,
707 line_width: u16,
708 emitted: bool,
709 content: bool,
710 continuation: bool,
711}
712
713impl<'p, S: RichSink> Wrap<'p, S> {
714 const fn new(inner: S, width: u16, first: &'p Prefix, cont: &'p Prefix) -> Self {
715 Self {
716 inner,
717 width,
718 first,
719 cont,
720 word: String::new(),
721 word_runs: SmallVec::new(),
722 word_width: 0,
723 gap_text: String::new(),
724 gap_runs: SmallVec::new(),
725 gap_width: 0,
726 line_width: 0,
727 emitted: false,
728 content: false,
729 continuation: false,
730 }
731 }
732
733 fn start_row(&mut self) {
734 if self.emitted {
735 return;
736 }
737 let prefix = if self.continuation {
738 self.cont
739 } else {
740 self.first
741 };
742 prefix.emit_clipped(self.width, &mut self.inner);
743 self.line_width = prefix.width().min(self.width);
744 self.emitted = true;
745 }
746
747 fn break_row(&mut self, soft: bool) {
748 self.start_row();
749 if soft && self.cont.is_empty() {
752 self.inner.soft_wrap();
753 } else {
754 self.inner.newline();
755 }
756 self.line_width = 0;
757 self.emitted = false;
758 self.content = false;
759 self.continuation = true;
760 }
761
762 fn append_word_grapheme(&mut self, style: Style, grapheme: &str) {
763 self.word.push_str(grapheme);
764 let end = u32::try_from(self.word.len()).expect("wrapped word exceeds four gigabytes");
765 if self
766 .word_runs
767 .last()
768 .is_some_and(|(_, run_style)| *run_style == style)
769 {
770 self.word_runs.last_mut().expect("last word run exists").0 = end;
771 } else {
772 self.word_runs.push((end, style));
773 }
774 self.word_width = self.word_width.saturating_add(cell_width(grapheme));
775 }
776
777 fn append_gap_grapheme(&mut self, style: Style, grapheme: &str) {
778 self.gap_text.push_str(grapheme);
779 let end = u32::try_from(self.gap_text.len()).expect("wrapped gap exceeds four gigabytes");
780 if self
781 .gap_runs
782 .last()
783 .is_some_and(|(_, run_style)| *run_style == style)
784 {
785 self.gap_runs.last_mut().expect("last gap run exists").0 = end;
786 } else {
787 self.gap_runs.push((end, style));
788 }
789 self.gap_width = self.gap_width.saturating_add(cell_width(grapheme));
790 }
791
792 fn emit_gap(&mut self) {
793 if self.gap_text.is_empty() {
794 emit_spaces(&mut self.inner, Style::new(), 1);
795 return;
796 }
797 let mut start = 0;
798 for (end, style) in &self.gap_runs {
799 self.inner.run(*style, &self.gap_text[start..*end as usize]);
800 start = *end as usize;
801 }
802 }
803
804 fn clear_gap(&mut self) {
805 self.gap_text.clear();
806 self.gap_runs.clear();
807 self.gap_width = 0;
808 }
809
810 fn flush_word(&mut self) {
811 if self.word.is_empty() {
812 return;
813 }
814 let word = std::mem::take(&mut self.word);
815 let runs = std::mem::take(&mut self.word_runs);
816 let word_width = std::mem::take(&mut self.word_width);
817 self.start_row();
818 let join_gap = self.gap_width.max(1);
819 if self.content
820 && self
821 .line_width
822 .saturating_add(join_gap)
823 .saturating_add(word_width)
824 > self.width
825 {
826 self.break_row(false);
827 self.start_row();
828 } else if self.content {
829 self.emit_gap();
830 self.line_width = self.line_width.saturating_add(join_gap);
831 }
832
833 let mut start = 0;
834 for (end, style) in &runs {
835 for grapheme in word[start..*end as usize].graphemes() {
836 let grapheme_width = cell_width(grapheme);
837 if self.content && self.line_width.saturating_add(grapheme_width) > self.width {
838 self.break_row(true);
839 self.start_row();
840 }
841 if self.line_width.saturating_add(grapheme_width) <= self.width || self.width != 0 {
842 self.inner.run(*style, grapheme);
843 self.line_width = self.line_width.saturating_add(grapheme_width);
844 self.content = true;
845 }
846 }
847 start = *end as usize;
848 }
849 self.clear_gap();
850 self.word = word;
851 self.word.clear();
852 self.word_runs = runs;
853 self.word_runs.clear();
854 }
855
856 pub fn finish(mut self) -> S {
858 self.flush_word();
859 self.start_row();
860 self.inner.newline();
861 self.inner
862 }
863}
864
865impl<S: RichSink> RichSink for Wrap<'_, S> {
866 fn run(&mut self, style: Style, text: &str) {
867 for grapheme in text.graphemes() {
868 if grapheme != "\u{a0}" && grapheme.chars().all(char::is_whitespace) {
869 self.flush_word();
870 self.append_gap_grapheme(style, grapheme);
871 } else {
872 self.append_word_grapheme(style, grapheme);
873 }
874 }
875 }
876
877 fn newline(&mut self) {
878 self.flush_word();
879 self.break_row(false);
880 self.clear_gap();
881 }
882}
883
884pub struct Clip<S: RichSink> {
886 inner: S,
887 width: u16,
888 marker: Option<char>,
889 used: u16,
890 done: bool,
891 pending: Option<(Style, Str, u16)>,
892}
893
894impl<S: RichSink> Clip<S> {
895 const fn new(inner: S, width: u16, marker: Option<char>) -> Self {
896 Self { inner, width, marker, used: 0, done: false, pending: None }
897 }
898
899 fn flush_pending(&mut self) {
900 if let Some((style, text, _)) = self.pending.take() {
901 self.inner.run(style, text.as_str());
902 }
903 }
904
905 fn truncate(&mut self, fallback_style: Style) {
906 self.done = true;
907 let style = self
908 .pending
909 .as_ref()
910 .map_or(fallback_style, |pending| pending.0);
911 if let Some((_, _, width)) = self.pending.take() {
912 self.used = self.used.saturating_sub(width);
913 }
914 if let Some(marker) = self.marker {
915 let mut encoded = [0_u8; 4];
916 let marker = marker.encode_utf8(&mut encoded);
917 let marker_width = cell_width(marker);
918 if marker_width != 0 && self.used.saturating_add(marker_width) <= self.width {
919 self.inner.run(style, marker);
920 self.used = self.used.saturating_add(marker_width);
921 }
922 }
923 }
924}
925
926impl<S: RichSink> RichSink for Clip<S> {
927 fn run(&mut self, style: Style, text: &str) {
928 if self.done || text.is_empty() {
929 return;
930 }
931 for grapheme in text.graphemes() {
932 let grapheme_width = cell_width(grapheme);
933 if self.used.saturating_add(grapheme_width) > self.width {
934 self.truncate(style);
935 break;
936 }
937 if self.marker.is_some() {
938 self.flush_pending();
939 self.pending = Some((style, Str::new(grapheme), grapheme_width));
940 } else {
941 self.inner.run(style, grapheme);
942 }
943 self.used = self.used.saturating_add(grapheme_width);
944 }
945 }
946
947 fn newline(&mut self) {
948 if !self.done {
949 self.flush_pending();
950 }
951 self.inner.newline();
952 self.used = 0;
953 self.done = false;
954 self.pending = None;
955 }
956
957 fn soft_wrap(&mut self) {
958 if !self.done {
959 self.flush_pending();
960 }
961 self.inner.soft_wrap();
962 self.used = 0;
963 self.done = false;
964 self.pending = None;
965 }
966}
967
968impl<S: RichSink> Drop for Clip<S> {
969 fn drop(&mut self) {
970 if !self.done {
971 self.flush_pending();
972 }
973 }
974}
975
976pub struct Rows<S: RichSink> {
978 inner: S,
979 max: u16,
980 seen: u16,
981 truncated: bool,
982}
983
984impl<S: RichSink> Rows<S> {
985 pub const fn truncated(&self) -> bool {
987 self.truncated
988 }
989}
990
991impl<S: RichSink> RichSink for Rows<S> {
992 fn run(&mut self, style: Style, text: &str) {
993 if self.seen < self.max {
994 self.inner.run(style, text);
995 } else if !text.is_empty() {
996 self.truncated = true;
997 }
998 }
999
1000 fn newline(&mut self) {
1001 if self.seen < self.max {
1002 self.inner.newline();
1003 self.seen = self.seen.saturating_add(1);
1004 } else {
1005 self.truncated = true;
1006 }
1007 }
1008
1009 fn soft_wrap(&mut self) {
1010 if self.seen < self.max {
1011 self.inner.soft_wrap();
1012 self.seen = self.seen.saturating_add(1);
1013 } else {
1014 self.truncated = true;
1015 }
1016 }
1017}
1018
1019pub struct Tee<'b, S: RichSink> {
1021 inner: S,
1022 copy: &'b mut RichText,
1023}
1024
1025impl<S: RichSink> RichSink for Tee<'_, S> {
1026 fn run(&mut self, style: Style, text: &str) {
1027 self.inner.run(style, text);
1028 self.copy.run(style, text);
1029 }
1030
1031 fn newline(&mut self) {
1032 self.inner.newline();
1033 self.copy.newline();
1034 }
1035
1036 fn soft_wrap(&mut self) {
1037 self.inner.soft_wrap();
1038 self.copy.soft_wrap();
1039 }
1040}
1041
1042pub struct Restyle<S: RichSink, F: Fn(Style) -> Style> {
1044 inner: S,
1045 map: F,
1046}
1047
1048impl<S: RichSink, F: Fn(Style) -> Style> RichSink for Restyle<S, F> {
1049 fn run(&mut self, style: Style, text: &str) {
1050 self.inner.run((self.map)(style), text);
1051 }
1052
1053 fn newline(&mut self) {
1054 self.inner.newline();
1055 }
1056
1057 fn soft_wrap(&mut self) {
1058 self.inner.soft_wrap();
1059 }
1060}
1061
1062pub struct Prefixed<'p, S: RichSink> {
1064 inner: S,
1065 first: &'p Prefix,
1066 cont: &'p Prefix,
1067 row: u16,
1068 at_start: bool,
1069}
1070
1071impl<S: RichSink> Prefixed<'_, S> {
1072 fn prefix(&mut self) {
1073 if !self.at_start {
1074 return;
1075 }
1076 if self.row == 0 {
1077 self.first.emit(&mut self.inner);
1078 } else {
1079 self.cont.emit(&mut self.inner);
1080 }
1081 self.at_start = false;
1082 }
1083}
1084
1085impl<S: RichSink> RichSink for Prefixed<'_, S> {
1086 fn run(&mut self, style: Style, text: &str) {
1087 if text.is_empty() {
1088 return;
1089 }
1090 self.prefix();
1091 self.inner.run(style, text);
1092 }
1093
1094 fn newline(&mut self) {
1095 self.prefix();
1096 self.inner.newline();
1097 self.row = self.row.saturating_add(1);
1098 self.at_start = true;
1099 }
1100 }
1103
1104#[cfg(test)]
1105mod tests {
1106 use super::*;
1107
1108 fn texts(rich: &RichText) -> Vec<&str> {
1109 (0..rich.rows()).map(|row| rich.row_text(row)).collect()
1110 }
1111
1112 #[test]
1113 fn decompose_splits_bash_ansi_into_clean_styled_runs() {
1114 let mut output = RichText::default();
1115 decompose("plain \x1b[31;1mred\x1b[0m!\n\x1b[38;2;1;2;3mrgb", &mut output);
1116
1117 assert_eq!(texts(&output), ["plain red!", "rgb"]);
1118 assert!(texts(&output).iter().all(|text| !text.contains('\x1b')));
1119 assert_eq!(output.row_runs(0).collect::<Vec<_>>(), [
1120 (Style::new(), "plain "),
1121 (Style::new().fg(Color::Indexed(1)).bold(), "red"),
1122 (Style::new(), "!"),
1123 ]);
1124 assert_eq!(output.row_runs(1).collect::<Vec<_>>(), [(
1125 Style::new().fg(Color::Rgb(1, 2, 3)),
1126 "rgb"
1127 )]);
1128 }
1129
1130 #[test]
1131 fn string_sink_emits_a_complete_style_for_every_run() {
1132 let mut output = String::new();
1133 output.run(Style::new(), "a");
1134 output.run(Style::new().bold().fg(Color::Rgb(1, 2, 3)), "b");
1135 output.newline();
1136
1137 assert_eq!(output, "\x1b[0ma\x1b[0;1;38;2;1;2;3mb\x1b[0m\n");
1138 }
1139
1140 #[test]
1141 fn ansi_materialization_round_trips_run_structure() {
1142 let mut first = RichText::default();
1143 decompose("a\x1b[3;48;5;12mb\n\x1b[9;94mc", &mut first);
1144 let mut ansi = String::new();
1145 first.replay(&mut ansi);
1146 let mut second = RichText::default();
1147 decompose(&ansi, &mut second);
1148
1149 assert_eq!(texts(&first), texts(&second));
1150 for row in 0..RichText::rows(&first) {
1151 assert_eq!(
1152 first.row_runs(row).collect::<Vec<_>>(),
1153 second.row_runs(row).collect::<Vec<_>>()
1154 );
1155 }
1156 }
1157
1158 #[test]
1159 fn decompose_strips_osc_and_non_sgr_csi() {
1160 let mut output = RichText::default();
1161 decompose("a\x1b]0;title\x07b\x1b[2Ac\x1b]ignored\x1b\\d", &mut output);
1162 assert_eq!(texts(&output), ["abcd"]);
1163 }
1164
1165 #[test]
1166 fn decompose_measure_counts_only_visible_cells() {
1167 let mut measure = Measure::default();
1168 decompose("a\x1b[31m界\x1b[0m\x1b]0;title\x07\r\nxy\x1b[2A", &mut measure);
1169 assert_eq!(measure.rows, 2);
1170 assert_eq!(measure.widest, 3);
1171 }
1172
1173 #[test]
1174 fn chains_row_limit_and_clipping() {
1175 let mut output = RichText::default();
1176 {
1177 let mut sink = (&mut output).rows(2).clip(3, Some('…'));
1178 for _ in 0..3 {
1179 sink.run(Style::new(), "abcdef");
1180 sink.newline();
1181 }
1182 }
1183 assert_eq!(texts(&output), ["ab…", "ab…"]);
1184 }
1185
1186 #[test]
1187 fn tee_makes_identical_copies() {
1188 let mut forwarded = RichText::default();
1189 let mut copied = RichText::default();
1190 {
1191 let mut sink = (&mut forwarded).tee(&mut copied);
1192 sink.run(Style::new(), "one");
1193 sink.newline();
1194 sink.run(Style::new().bold(), "two");
1195 }
1196 assert_eq!(texts(&forwarded), texts(&copied));
1197 for row in 0..RichText::rows(&forwarded) {
1198 assert_eq!(
1199 forwarded.row_runs(row).collect::<Vec<_>>(),
1200 copied.row_runs(row).collect::<Vec<_>>()
1201 );
1202 }
1203 }
1204
1205 #[test]
1206 fn prefixes_first_and_continuation_rows() {
1207 let mut first = Prefix::default();
1208 first.push(Style::new(), "> ");
1209 let mut cont = Prefix::default();
1210 cont.push(Style::new(), " ");
1211 let mut output = RichText::default();
1212 {
1213 let mut sink = (&mut output).prefixed(&first, &cont);
1214 sink.run(Style::new(), "a");
1215 sink.newline();
1216 sink.run(Style::new(), "b");
1217 }
1218 assert_eq!(texts(&output), ["> a", " b"]);
1219 }
1220
1221 #[test]
1222 fn mid_word_overflow_records_soft_rows() {
1223 let mut output = RichText::default();
1224 let mut wrap = (&mut output).wrap(3);
1225 wrap.run(Style::new(), "abcdef gh");
1226 wrap.finish();
1227 assert_eq!(texts(&output), ["abc", "def", "gh"]);
1228 assert!(output.row_soft_wrap(0), "a width break inside a word is soft");
1229 assert!(!output.row_soft_wrap(1), "a word-boundary break collapsed whitespace");
1230 assert!(!output.row_soft_wrap(2));
1231 }
1232
1233 #[test]
1234 fn prefixed_continuations_never_record_soft_rows() {
1235 let mut cont = Prefix::default();
1236 cont.push(Style::new(), "> ");
1237 let mut output = RichText::default();
1238 let mut wrap = (&mut output).wrap_prefixed(4, Prefix::empty_ref(), &cont);
1239 wrap.run(Style::new(), "abcdefgh");
1240 wrap.finish();
1241 assert!(RichText::rows(&output) > 1);
1242 assert!((0..RichText::rows(&output)).all(|row| !output.row_soft_wrap(row)));
1243 }
1244
1245 #[test]
1246 fn char_wrap_flows_exact_and_preserves_whitespace() {
1247 let mut output = RichText::default();
1248 {
1249 let mut wrap = (&mut output).wrap_chars(3);
1250 wrap.run(Style::new(), "ab cdef");
1251 }
1252 assert_eq!(texts(&output), ["ab ", "cde", "f"]);
1253 assert!(output.row_soft_wrap(0) && output.row_soft_wrap(1));
1254 }
1255
1256 #[test]
1257 fn replay_preserves_soft_rows() {
1258 let mut original = RichText::default();
1259 {
1260 let mut wrap = (&mut original).wrap_chars(3);
1261 wrap.run(Style::new(), "abcdef");
1262 }
1263 let mut copy = RichText::default();
1264 original.replay(&mut copy);
1265 assert_eq!(texts(©), texts(&original));
1266 assert!(copy.row_soft_wrap(0));
1267 }
1268 #[test]
1269 fn restyle_composes_with_wrap() {
1270 let changed = Style::new().fg(Color::Indexed(4));
1271 let mut output = RichText::default();
1272 let mut wrap = (&mut output).restyle(|_| changed).wrap(3);
1273 wrap.run(Style::new(), "ab cd");
1274 wrap.finish();
1275 assert_eq!(texts(&output), ["ab", "cd"]);
1276 assert!(output.row_runs(0).all(|(style, _)| style == changed));
1277 }
1278
1279 #[test]
1280 fn dynamic_sink_can_be_chained() {
1281 let mut output = RichText::default();
1282 let dynamic: &mut dyn RichSink = &mut output;
1283 {
1284 let mut sink = dynamic.rows(1).clip(2, None);
1285 sink.run(Style::new(), "abc");
1286 sink.newline();
1287 }
1288 assert_eq!(texts(&output), ["ab"]);
1289 }
1290 #[test]
1291 fn jamo_profiles_filler_and_ambiguous_widths() {
1292 let original = jamo_width();
1293 let jamo = "ㅁㄴㅇㅂ";
1294
1295 set_jamo_width(crate::context::JamoWidth::Unicode);
1296 assert_eq!(cell_width(jamo), 8);
1297 assert_eq!(cell_width("\u{3164}"), 0);
1298
1299 set_jamo_width(crate::context::JamoWidth::Narrow);
1300 assert_eq!(cell_width(jamo), 4);
1301 assert_eq!(cell_width("\u{3164}"), 0);
1302
1303 set_jamo_width(crate::context::JamoWidth::Wide);
1304 assert_eq!(cell_width(jamo), 8);
1305 assert_eq!(cell_width("\u{3164}"), 0);
1306
1307 set_jamo_width(crate::context::JamoWidth::Platform);
1308 assert_eq!(cell_width(jamo), if cfg!(target_os = "macos") { 4 } else { 8 });
1309 assert_eq!(cell_width("\u{3164}"), 0);
1310 assert_eq!(cell_width("©"), 1, "East Asian Ambiguous characters stay narrow");
1311
1312 set_jamo_width(original);
1313 }
1314}