Skip to main content

matchmaker/ui/
results.rs

1use std::str::FromStr;
2
3use cli_boilerplate_automation::bring::split::split_on_nesting;
4use ratatui::{
5    layout::{Alignment, Rect},
6    style::{Color, Style, Stylize},
7    text::{Line, Span},
8    widgets::{Paragraph, Row, Table},
9};
10use unicode_width::UnicodeWidthStr;
11
12use crate::{
13    SSS, Selection, Selector,
14    config::{HorizontalSeparator, ResultsConfig, RowConnectionStyle, StatusConfig},
15    nucleo::{Status, Worker},
16    render::Click,
17    utils::{
18        string::{fit_width, substitute_escaped},
19        text::{clip_text_lines, expand_indents, prefix_text},
20    },
21};
22
23#[derive(Debug)]
24pub struct ResultsUI {
25    cursor: u16,
26    bottom: u32,
27    col: Option<usize>,
28
29    /// available height
30    height: u16,
31    /// available width
32    width: u16,
33    // column widths.
34    // Note that the first width includes the indentation.
35    widths: Vec<u16>,
36
37    pub hidden_columns: Vec<bool>,
38
39    pub status: Status,
40    status_template: Line<'static>,
41    pub status_config: StatusConfig,
42
43    pub config: ResultsConfig,
44
45    bottom_clip: Option<u16>,
46    cursor_above: u16,
47
48    pub cursor_disabled: bool,
49}
50
51impl ResultsUI {
52    pub fn new(config: ResultsConfig, status_config: StatusConfig) -> Self {
53        Self {
54            cursor: 0,
55            bottom: 0,
56            col: None,
57
58            widths: Vec::new(),
59            height: 0, // uninitialized, so be sure to call update_dimensions
60            width: 0,
61            hidden_columns: Default::default(),
62
63            status: Default::default(),
64            status_template: Span::from(status_config.template.clone())
65                .style(status_config.fg)
66                .add_modifier(status_config.modifier)
67                .into(),
68            status_config,
69            config,
70
71            cursor_disabled: false,
72            bottom_clip: None,
73            cursor_above: 0,
74        }
75    }
76
77    pub fn hidden_columns(&mut self, hidden_columns: Vec<bool>) {
78        self.hidden_columns = hidden_columns;
79    }
80
81    // as given by ratatui area
82    pub fn update_dimensions(&mut self, area: &Rect) {
83        let [bw, bh] = [self.config.border.height(), self.config.border.width()];
84        self.width = area.width.saturating_sub(bw);
85        self.height = area.height.saturating_sub(bh);
86        log::debug!("Updated results dimensions: {}x{}", self.width, self.height);
87    }
88
89    pub fn table_width(&self) -> u16 {
90        self.config.column_spacing.0 * self.widths().len().saturating_sub(1) as u16
91            + self.widths.iter().sum::<u16>()
92            + self.config.border.width()
93    }
94
95    // ------ config -------
96    pub fn reverse(&self) -> bool {
97        self.config.reverse == Some(true)
98    }
99    pub fn is_wrap(&self) -> bool {
100        self.config.wrap
101    }
102    pub fn wrap(&mut self, wrap: bool) {
103        self.config.wrap = wrap;
104    }
105
106    // ----- columns --------
107    // todo: support cooler things like only showing/outputting a specific column/cycling columns
108    pub fn toggle_col(&mut self, col_idx: usize) -> bool {
109        if self.col == Some(col_idx) {
110            self.col = None
111        } else {
112            self.col = Some(col_idx);
113        }
114        self.col.is_some()
115    }
116    pub fn cycle_col(&mut self) {
117        self.col = match self.col {
118            None => self.widths.is_empty().then_some(0),
119            Some(c) => {
120                let next = c + 1;
121                if next < self.widths.len() {
122                    Some(next)
123                } else {
124                    None
125                }
126            }
127        };
128    }
129
130    // ------- NAVIGATION ---------
131    fn scroll_padding(&self) -> u16 {
132        self.config.scroll_padding.min(self.height / 2)
133    }
134    pub fn end(&self) -> u32 {
135        self.status.matched_count.saturating_sub(1)
136    }
137
138    /// Index in worker snapshot of current item.
139    /// Use with worker.get_nth().
140    //  Equivalently, the cursor progress in the match list
141    pub fn index(&self) -> u32 {
142        if self.cursor_disabled {
143            u32::MAX
144        } else {
145            self.cursor as u32 + self.bottom
146        }
147    }
148    // pub fn cursor(&self) -> Option<u16> {
149    //     if self.cursor_disabled {
150    //         None
151    //     } else {
152    //         Some(self.cursor)
153    //     }
154    // }
155    pub fn cursor_prev(&mut self) {
156        log::trace!("cursor_prev: {self:?}");
157        if self.cursor_above <= self.scroll_padding() && self.bottom > 0 {
158            self.bottom -= 1;
159            self.bottom_clip = None;
160        } else if self.cursor > 0 {
161            self.cursor -= 1;
162        } else if self.config.scroll_wrap {
163            self.cursor_jump(self.end());
164        }
165    }
166    pub fn cursor_next(&mut self) {
167        if self.cursor_disabled {
168            self.cursor_disabled = false
169        }
170
171        // log::trace!(
172        //     "Cursor {} @ index {}. Status: {:?}.",
173        //     self.cursor,
174        //     self.index(),
175        //     self.status
176        // );
177        if self.cursor + 1 + self.scroll_padding() >= self.height
178            && self.bottom + (self.height as u32) < self.status.matched_count
179        {
180            self.bottom += 1; // 
181        } else if self.index() < self.end() {
182            self.cursor += 1;
183        } else if self.config.scroll_wrap {
184            self.cursor_jump(0)
185        }
186    }
187
188    pub fn cursor_jump(&mut self, index: u32) {
189        self.cursor_disabled = false;
190        self.bottom_clip = None;
191
192        let end = self.end();
193        let index = index.min(end);
194
195        if index < self.bottom as u32 || index >= self.bottom + self.height as u32 {
196            self.bottom = (end + 1)
197                .saturating_sub(self.height as u32) // don't exceed the first item of the last self.height items
198                .min(index);
199        }
200        self.cursor = (index - self.bottom) as u16;
201        log::debug!("cursor jumped to {}: {index}, end: {end}", self.cursor);
202    }
203
204    // ------- RENDERING ----------
205    pub fn indentation(&self) -> usize {
206        self.config.multi_prefix.width()
207    }
208    pub fn col(&self) -> Option<usize> {
209        self.col
210    }
211
212    /// Column widths.
213    /// Note that the first width includes the indentation.
214    pub fn widths(&self) -> &Vec<u16> {
215        &self.widths
216    }
217    // results width
218    pub fn width(&self) -> u16 {
219        self.width.saturating_sub(self.indentation() as u16)
220    }
221
222    /// Adapt the stored widths (initialized by [`Worker::results`]) to the fit within the available width (self.width)
223    /// widths <= min_wrap_width don't shrink and aren't wrapped
224    pub fn max_widths(&self) -> Vec<u16> {
225        let mut scale_total = 0;
226
227        let mut widths = vec![u16::MAX; self.widths.len().max(self.hidden_columns.len())];
228        let mut total = 0; // total current width
229        for i in 0..widths.len() {
230            if i < self.hidden_columns.len() && self.hidden_columns[i] || widths[i] == 0 {
231                widths[i] = 0;
232            } else if let Some(&w) = self.widths.get(i) {
233                total += w;
234                if w >= self.config.min_wrap_width {
235                    scale_total += w;
236                    widths[i] = w;
237                }
238            }
239        }
240
241        if !self.config.wrap || scale_total == 0 {
242            for x in &mut widths {
243                if *x != 0 {
244                    *x = u16::MAX
245                }
246            }
247            return widths;
248        }
249
250        let mut last_scalable = None;
251        let available = self.width().saturating_sub(total - scale_total); // 
252
253        let mut used_total = 0;
254        for (i, x) in widths.iter_mut().enumerate() {
255            if *x == 0 {
256                continue;
257            }
258            if *x == u16::MAX
259                && let Some(w) = self.widths.get(i)
260            {
261                used_total += w;
262                continue;
263            }
264            let new_w = *x * available / scale_total;
265            *x = new_w.max(self.config.min_wrap_width);
266            used_total += *x;
267            last_scalable = Some(x);
268        }
269
270        // give remainder to the last scalable column
271        if used_total < self.width()
272            && let Some(last) = last_scalable
273        {
274            *last += self.width() - used_total;
275        }
276
277        widths
278    }
279
280    // this updates the internal status, so be sure to call make_status afterward
281    // some janky wrapping is implemented, dunno whats causing flickering, padding is fixed going down only
282    pub fn make_table<'a, T: SSS>(
283        &mut self,
284        worker: &'a mut Worker<T>,
285        selector: &mut Selector<T, impl Selection>,
286        matcher: &mut nucleo::Matcher,
287        click: &mut Click,
288    ) -> Table<'a> {
289        let offset = self.bottom as u32;
290        let end = self.bottom + self.height as u32;
291        let hz = !self.config.stacked_columns;
292
293        let width_limits = if hz {
294            self.max_widths()
295        } else {
296            let default = if self.config.wrap {
297                self.width
298            } else {
299                u16::MAX
300            };
301
302            (0..worker.columns.len())
303                .map(|i| {
304                    if self.hidden_columns.get(i).copied().unwrap_or(false) {
305                        0
306                    } else {
307                        default
308                    }
309                })
310                .collect()
311        };
312
313        let (mut results, mut widths, status) =
314            worker.results(offset, end, &width_limits, self.match_style(), matcher);
315
316        // log::debug!("widths: {width_limits:?}, {widths:?}");
317
318        let match_count = status.matched_count;
319        self.status = status;
320
321        if match_count < self.bottom + self.cursor as u32 && !self.cursor_disabled {
322            self.cursor_jump(match_count);
323        } else {
324            self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
325        }
326
327        widths[0] += self.indentation() as u16;
328
329        let mut rows = vec![];
330        let mut total_height = 0;
331
332        if results.is_empty() {
333            return Table::new(rows, widths);
334        }
335
336        let height_of = |t: &(Vec<ratatui::text::Text<'a>>, _)| {
337            self._hr()
338                + if hz {
339                    t.0.iter()
340                        .map(|t| t.height() as u16)
341                        .max()
342                        .unwrap_or_default()
343                } else {
344                    t.0.iter().map(|t| t.height() as u16).sum::<u16>()
345                }
346        };
347
348        // log::debug!("results initial: {}, {}, {}, {}, {}", self.bottom, self.cursor, total_height, self.height, results.len());
349        let h_at_cursor = height_of(&results[self.cursor as usize]);
350        let h_after_cursor = results[self.cursor as usize + 1..]
351            .iter()
352            .map(height_of)
353            .sum();
354        let h_to_cursor = results[0..self.cursor as usize]
355            .iter()
356            .map(height_of)
357            .sum::<u16>();
358        let cursor_end_should_lt = self.height - self.scroll_padding().min(h_after_cursor);
359        // let cursor_start_should_gt = self.scroll_padding().min(h_to_cursor);
360
361        // log::debug!(
362        //     "Computed heights: {h_at_cursor}, {h_to_cursor}, {h_after_cursor}, {cursor_end_should_lt}",
363        // );
364        // begin adjustment
365        let mut start_index = 0; // the index in results of the first complete item
366
367        if h_at_cursor >= cursor_end_should_lt {
368            start_index = self.cursor;
369            self.bottom += self.cursor as u32;
370            self.cursor = 0;
371            self.cursor_above = 0;
372            self.bottom_clip = None;
373        } else
374        // increase the bottom index so that cursor_should_above is maintained
375        if let h_to_cursor_end = h_to_cursor + h_at_cursor
376            && h_to_cursor_end > cursor_end_should_lt
377        {
378            let mut trunc_height = h_to_cursor_end - cursor_end_should_lt;
379            // note that there is a funny side effect that scrolling up near the bottom can scroll up a bit, but it seems fine to me
380
381            for r in results[start_index as usize..self.cursor as usize].iter_mut() {
382                let h = height_of(r);
383                let (row, item) = r;
384                start_index += 1; // we always skip at least the first item
385
386                if trunc_height < h {
387                    let mut remaining_height = h - trunc_height;
388                    let prefix = if selector.contains(item) {
389                        self.config.multi_prefix.clone().to_string()
390                    } else {
391                        self.default_prefix(0)
392                    };
393
394                    total_height += remaining_height;
395
396                    // log::debug!("r: {remaining_height}");
397                    if hz {
398                        if h - self._hr() < remaining_height {
399                            for (_, t) in
400                                row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0)
401                            {
402                                clip_text_lines(t, remaining_height, !self.reverse());
403                            }
404                        }
405
406                        prefix_text(&mut row[0], prefix);
407
408                        let last_visible = widths
409                            .iter()
410                            .enumerate()
411                            .rev()
412                            .find_map(|(i, w)| (*w != 0).then_some(i));
413
414                        let mut row_texts: Vec<_> = row
415                            .iter()
416                            .take(last_visible.map(|x| x + 1).unwrap_or(0))
417                            .cloned()
418                            .collect();
419
420                        if self.config.right_align_last && row_texts.len() > 1 {
421                            row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
422                        }
423
424                        let row = Row::new(row_texts).height(remaining_height);
425                        rows.push(row);
426                    } else {
427                        let mut push = vec![];
428
429                        for col in row.into_iter().rev() {
430                            let mut height = col.height() as u16;
431                            if remaining_height == 0 {
432                                break;
433                            } else if remaining_height < height {
434                                clip_text_lines(col, remaining_height, !self.reverse());
435                                height = remaining_height;
436                            }
437                            remaining_height -= height;
438                            prefix_text(col, prefix.clone());
439                            push.push(Row::new(vec![col.clone()]).height(height));
440                        }
441                        rows.extend(push.into_iter().rev());
442                    }
443
444                    self.bottom += start_index as u32 - 1;
445                    self.cursor -= start_index - 1;
446                    self.bottom_clip = Some(remaining_height);
447                    break;
448                } else if trunc_height == h {
449                    self.bottom += start_index as u32;
450                    self.cursor -= start_index;
451                    self.bottom_clip = None;
452                    break;
453                }
454
455                trunc_height -= h;
456            }
457        } else if let Some(mut remaining_height) = self.bottom_clip {
458            start_index += 1;
459            // same as above
460            let h = height_of(&results[0]);
461            let (row, item) = &mut results[0];
462            let prefix = if selector.contains(item) {
463                self.config.multi_prefix.clone().to_string()
464            } else {
465                self.default_prefix(0)
466            };
467
468            total_height += remaining_height;
469
470            if hz {
471                if self._hr() + remaining_height != h {
472                    for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
473                        clip_text_lines(t, remaining_height, !self.reverse());
474                    }
475                }
476
477                prefix_text(&mut row[0], prefix);
478
479                let last_visible = widths
480                    .iter()
481                    .enumerate()
482                    .rev()
483                    .find_map(|(i, w)| (*w != 0).then_some(i));
484
485                let mut row_texts: Vec<_> = row
486                    .iter()
487                    .take(last_visible.map(|x| x + 1).unwrap_or(0))
488                    .cloned()
489                    .collect();
490
491                if self.config.right_align_last && row_texts.len() > 1 {
492                    row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
493                }
494
495                let row = Row::new(row_texts).height(remaining_height);
496                rows.push(row);
497            } else {
498                let mut push = vec![];
499
500                for col in row.into_iter().rev() {
501                    let mut height = col.height() as u16;
502                    if remaining_height == 0 {
503                        break;
504                    } else if remaining_height < height {
505                        clip_text_lines(col, remaining_height, !self.reverse());
506                        height = remaining_height;
507                    }
508                    remaining_height -= height;
509                    prefix_text(col, prefix.clone());
510                    push.push(Row::new(vec![col.clone()]).height(height));
511                }
512                rows.extend(push.into_iter().rev());
513            }
514        }
515
516        // topside padding is non-flexible, and does its best to stay at 2 full items without obscuring cursor.
517        // One option is we move enforcement from cursor_prev to
518
519        let mut remaining_height = self.height.saturating_sub(total_height);
520
521        for (mut i, (mut row, item)) in results.drain(start_index as usize..).enumerate() {
522            i += self.bottom_clip.is_some() as usize;
523
524            // this is technically one step out of sync but idc
525            if let Click::ResultPos(c) = click
526                && self.height - remaining_height > *c
527            {
528                let idx = self.bottom as u32 + i as u32 - 1;
529                log::debug!("Mapped click position to index: {c} -> {idx}",);
530                *click = Click::ResultIdx(idx);
531            }
532            if self.is_current(i) {
533                self.cursor_above = self.height - remaining_height;
534            }
535
536            // insert hr
537            if let Some(hr) = self.hr()
538                && remaining_height > 0
539            {
540                rows.push(hr);
541                remaining_height -= 1;
542            }
543            if remaining_height == 0 {
544                break;
545            }
546
547            // set prefix
548            let prefix = if selector.contains(item) {
549                self.config.multi_prefix.clone().to_string()
550            } else {
551                self.default_prefix(i)
552            };
553
554            if hz {
555                let mut height = row
556                    .iter()
557                    .map(|t| t.height() as u16)
558                    .max()
559                    .unwrap_or_default();
560
561                if remaining_height < height {
562                    height = remaining_height;
563
564                    for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
565                        clip_text_lines(t, height, self.reverse());
566                    }
567                }
568                remaining_height -= height;
569
570                prefix_text(&mut row[0], prefix);
571
572                // same as above
573                let last_visible = widths
574                    .iter()
575                    .enumerate()
576                    .rev()
577                    .find_map(|(i, w)| (*w != 0).then_some(i));
578
579                let mut row_texts: Vec<_> = row
580                    .iter()
581                    .take(last_visible.map(|x| x + 1).unwrap_or(0))
582                    .cloned()
583                    // highlight
584                    .enumerate()
585                    .map(|(x, t)| {
586                        if self.is_current(i)
587                            && (self.col.is_none()
588                                && matches!(
589                                    self.config.row_connection_style,
590                                    RowConnectionStyle::Disjoint
591                                )
592                                || self.col == Some(x))
593                        {
594                            t.style(self.current_style())
595                        } else {
596                            t
597                        }
598                    })
599                    .collect();
600
601                if self.config.right_align_last && row_texts.len() > 1 {
602                    row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
603                }
604
605                // push
606                let mut row = Row::new(row_texts).height(height);
607
608                if self.is_current(i)
609                    && self.col.is_none()
610                    && !matches!(
611                        self.config.row_connection_style,
612                        RowConnectionStyle::Disjoint
613                    )
614                {
615                    row = row.style(self.current_style())
616                }
617
618                rows.push(row);
619            } else {
620                let mut push = vec![];
621
622                for (x, mut col) in row.into_iter().enumerate() {
623                    let mut height = col.height() as u16;
624
625                    if remaining_height == 0 {
626                        break;
627                    } else if remaining_height < height {
628                        height = remaining_height;
629                        clip_text_lines(&mut col, remaining_height, self.reverse());
630                    }
631                    remaining_height -= height;
632
633                    prefix_text(&mut col, prefix.clone());
634
635                    // push
636                    let mut row = Row::new(vec![col]).height(height);
637
638                    if self.is_current(i) && (self.col.is_none() || self.col == Some(x)) {
639                        row = row.style(self.current_style())
640                    }
641
642                    push.push(row);
643                }
644                rows.extend(push);
645            }
646        }
647
648        if self.reverse() {
649            rows.reverse();
650            if remaining_height > 0 {
651                rows.insert(0, Row::new(vec![vec![]]).height(remaining_height));
652            }
653        }
654
655        // up to the last nonempty row position
656
657        if hz {
658            self.widths = {
659                let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
660                let mut widths = widths[..pos].to_vec();
661                if pos > 2 && self.config.right_align_last {
662                    let used = widths.iter().take(widths.len() - 1).sum();
663                    widths[pos - 1] = self.width().saturating_sub(used);
664                }
665                widths
666            };
667        }
668
669        // why does the row highlight apply beyond the table width?
670        let mut table = Table::new(
671            rows,
672            if hz {
673                self.widths.clone()
674            } else {
675                vec![self.width]
676            },
677        )
678        .column_spacing(self.config.column_spacing.0)
679        .style(self.config.fg)
680        .add_modifier(self.config.modifier);
681
682        table = table.block(self.config.border.as_static_block());
683        table
684    }
685}
686
687impl ResultsUI {
688    pub fn make_status(&self, full_width: u16) -> Paragraph<'_> {
689        let status_config = &self.status_config;
690        let replacements = [
691            ('r', self.index().to_string()),
692            ('m', self.status.matched_count.to_string()),
693            ('t', self.status.item_count.to_string()),
694        ];
695
696        // sub replacements into line
697        let mut new_spans = Vec::new();
698
699        if status_config.match_indent {
700            new_spans.push(Span::raw(" ".repeat(self.indentation())));
701        }
702
703        for span in &self.status_template {
704            let subbed = substitute_escaped(&span.content, &replacements);
705            new_spans.push(Span::styled(subbed, span.style));
706        }
707
708        let substituted_line = Line::from(new_spans);
709
710        // sub whitespace expansions
711        let effective_width = match self.status_config.row_connection_style {
712            RowConnectionStyle::Full => full_width,
713            _ => self.width,
714        } as usize;
715        let expanded = expand_indents(substituted_line, r"\s", effective_width)
716            .style(status_config.fg)
717            .add_modifier(status_config.modifier);
718
719        Paragraph::new(expanded)
720    }
721
722    pub fn set_status_line(&mut self, template: Option<Line<'static>>) {
723        let status_config = &self.status_config;
724
725        self.status_template = template
726            .unwrap_or(status_config.template.clone().into())
727            .style(status_config.fg)
728            .add_modifier(status_config.modifier)
729            .into()
730    }
731}
732
733// helpers
734impl ResultsUI {
735    fn default_prefix(&self, i: usize) -> String {
736        let substituted = substitute_escaped(
737            &self.config.default_prefix,
738            &[
739                ('d', &(i + 1).to_string()),                        // cursor index
740                ('r', &(i + 1 + self.bottom as usize).to_string()), // absolute index
741            ],
742        );
743
744        fit_width(&substituted, self.indentation())
745    }
746
747    fn current_style(&self) -> Style {
748        Style::from(self.config.current_fg)
749            .bg(self.config.current_bg)
750            .add_modifier(self.config.current_modifier)
751    }
752
753    fn is_current(&self, i: usize) -> bool {
754        !self.cursor_disabled && self.cursor == i as u16
755    }
756
757    pub fn match_style(&self) -> Style {
758        Style::default()
759            .fg(self.config.match_fg)
760            .add_modifier(self.config.match_modifier)
761    }
762
763    fn hr(&self) -> Option<Row<'static>> {
764        let sep = self.config.horizontal_separator;
765
766        if matches!(sep, HorizontalSeparator::None) {
767            return None;
768        }
769
770        // todo: support non_stacked by doing a seperate rendering pass
771        if !self.config.stacked_columns && self.widths.len() > 1 {
772            return Some(Row::new(vec![vec![]]));
773        }
774
775        let unit = sep.as_str();
776        let line = unit.repeat(self.width as usize);
777
778        Some(Row::new(vec![line]))
779    }
780
781    fn _hr(&self) -> u16 {
782        !matches!(self.config.horizontal_separator, HorizontalSeparator::None) as u16
783    }
784}
785
786pub struct StatusUI {}
787
788impl StatusUI {
789    pub fn parse_template_to_status_line(s: &str) -> Line<'static> {
790        let parts = match split_on_nesting(&s, ['{', '}']) {
791            Ok(x) => x,
792            Err(n) => {
793                if n > 0 {
794                    log::error!("Encountered {} unclosed parentheses", n)
795                } else {
796                    log::error!("Extra closing parenthesis at index {}", -n)
797                }
798                return Line::from(s.to_string());
799            }
800        };
801
802        let mut spans = Vec::new();
803        let mut in_nested = !s.starts_with('{');
804        for part in parts {
805            in_nested = !in_nested;
806            let content = part.as_str();
807
808            if in_nested {
809                let inner = &content[1..content.len() - 1];
810
811                if let Some((color_name, text)) = inner.split_once(':') {
812                    if let Ok(color) = Color::from_str(color_name) {
813                        spans.push(Span::styled(text.to_string(), Style::default().fg(color)));
814                        continue;
815                    }
816                }
817            }
818
819            spans.push(Span::raw(content.to_string()));
820        }
821
822        Line::from(spans)
823    }
824}