1use std::str::FromStr;
2
3use cba::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::{apply_to_lines, clip_text_lines, expand_indents, hscroll_line, prefix_text},
20 },
21};
22
23#[derive(Debug)]
24pub struct ResultsUI {
25 cursor: u16,
26 bottom: u32,
27 col: Option<usize>,
28 pub scroll: [u16; 2],
30
31 height: u16,
33 width: u16,
35 widths: Vec<u16>,
38
39 pub hidden_columns: Vec<bool>,
40
41 pub status: Status,
42 status_template: Line<'static>,
43 pub status_config: StatusConfig,
44
45 pub config: ResultsConfig,
46
47 bottom_clip: Option<u16>,
48 cursor_above: u16,
49
50 pub cursor_disabled: bool,
51}
52
53impl ResultsUI {
54 pub fn new(config: ResultsConfig, status_config: StatusConfig) -> Self {
55 Self {
56 cursor: 0,
57 bottom: 0,
58 col: None,
59 scroll: [0, 0],
60
61 widths: Vec::new(),
62 height: 0, width: 0,
64 hidden_columns: Default::default(),
65
66 status: Default::default(),
67 status_template: Span::from(status_config.template.clone())
68 .style(status_config.fg)
69 .add_modifier(status_config.modifier)
70 .into(),
71 status_config,
72 config,
73
74 cursor_disabled: false,
75 bottom_clip: None,
76 cursor_above: 0,
77 }
78 }
79
80 pub fn hidden_columns(&mut self, hidden_columns: Vec<bool>) {
81 self.hidden_columns = hidden_columns;
82 }
83
84 pub fn update_dimensions(&mut self, area: &Rect) {
86 let [bw, bh] = [self.config.border.height(), self.config.border.width()];
87 self.width = area.width.saturating_sub(bw);
88 self.height = area.height.saturating_sub(bh);
89 log::debug!("Updated results dimensions: {}x{}", self.width, self.height);
90 }
91
92 pub fn table_width(&self) -> u16 {
93 self.config.column_spacing.0 * self.widths().len().saturating_sub(1) as u16
94 + self.widths.iter().sum::<u16>()
95 + self.config.border.width()
96 }
97
98 pub fn height(&self) -> u16 {
99 self.height
100 }
101
102 pub fn reverse(&self) -> bool {
104 self.config.reverse == Some(true)
105 }
106 pub fn is_wrap(&self) -> bool {
107 self.config.wrap
108 }
109 pub fn wrap(&mut self, wrap: bool) {
110 self.config.wrap = wrap;
111 }
112
113 pub fn toggle_col(&mut self, col_idx: usize) -> bool {
116 self.reset_current_scroll();
117
118 if self.col == Some(col_idx) {
119 self.col = None
120 } else {
121 self.col = Some(col_idx);
122 }
123 self.col.is_some()
124 }
125 pub fn cycle_col(&mut self) {
126 self.reset_current_scroll();
127
128 self.col = match self.col {
129 None => self.widths.is_empty().then_some(0),
130 Some(c) => {
131 let next = c + 1;
132 if next < self.widths.len() {
133 Some(next)
134 } else {
135 None
136 }
137 }
138 };
139 }
140
141 fn scroll_padding(&self) -> u16 {
143 self.config.scroll_padding.min(self.height / 2)
144 }
145 pub fn end(&self) -> u32 {
146 self.status.matched_count.saturating_sub(1)
147 }
148
149 pub fn index(&self) -> u32 {
153 if self.cursor_disabled {
154 u32::MAX
155 } else {
156 self.cursor as u32 + self.bottom
157 }
158 }
159 pub fn cursor_prev(&mut self) {
167 self.reset_current_scroll();
168
169 log::trace!("cursor_prev: {self:?}");
170 if self.cursor_above <= self.scroll_padding() && self.bottom > 0 {
171 self.bottom -= 1;
172 self.bottom_clip = None;
173 } else if self.cursor > 0 {
174 self.cursor -= 1;
175 } else if self.config.scroll_wrap {
176 self.cursor_jump(self.end());
177 }
178 }
179 pub fn cursor_next(&mut self) {
180 self.reset_current_scroll();
181
182 if self.cursor_disabled {
183 self.cursor_disabled = false
184 }
185
186 if self.cursor + 1 + self.scroll_padding() >= self.height
193 && self.bottom + (self.height as u32) < self.status.matched_count
194 {
195 self.bottom += 1; } else if self.index() < self.end() {
197 self.cursor += 1;
198 } else if self.config.scroll_wrap {
199 self.cursor_jump(0)
200 }
201 }
202
203 pub fn cursor_jump(&mut self, index: u32) {
204 self.reset_current_scroll();
205
206 self.cursor_disabled = false;
207 self.bottom_clip = None;
208
209 let end = self.end();
210 let index = index.min(end);
211
212 if index < self.bottom as u32 || index >= self.bottom + self.height as u32 {
213 self.bottom = (end + 1)
214 .saturating_sub(self.height as u32) .min(index);
216 }
217 self.cursor = (index - self.bottom) as u16;
218 log::debug!("cursor jumped to {}: {index}, end: {end}", self.cursor);
219 }
220
221 pub fn current_scroll(&mut self, x: i8, horizontal: bool) {
222 let value = &mut self.scroll[horizontal as usize];
223 *value = if x.is_negative() {
224 value.saturating_sub(x.unsigned_abs() as u16)
225 } else if x.is_positive() {
226 value.saturating_add(x as u16)
227 } else {
228 0
229 };
230 }
232
233 pub fn reset_current_scroll(&mut self) {
234 self.scroll = [0, 0]
235 }
236
237 pub fn indentation(&self) -> usize {
239 self.config.multi_prefix.width()
240 }
241 pub fn col(&self) -> Option<usize> {
242 self.col
243 }
244
245 pub fn widths(&self) -> &Vec<u16> {
248 &self.widths
249 }
250 pub fn width(&self) -> u16 {
252 self.width.saturating_sub(self.indentation() as u16)
253 }
254
255 pub fn max_widths(&self) -> Vec<u16> {
258 let mut scale_total = 0;
259
260 let mut widths = vec![u16::MAX; self.widths.len().max(self.hidden_columns.len())];
261
262 let mut total = 0; for i in 0..widths.len() {
264 if i < self.hidden_columns.len() && self.hidden_columns[i] {
265 widths[i] = 0;
266 } else if let Some(&w) = self.widths.get(i) {
267 total += w;
268 if w >= self.config.min_wrap_width {
269 scale_total += w;
270 widths[i] = w;
271 }
272 }
273 }
274
275 if !self.config.wrap || scale_total == 0 {
276 for x in &mut widths {
277 if *x != 0 {
278 *x = u16::MAX
279 }
280 }
281 return widths;
282 }
283
284 let mut last_scalable = None;
285 let available = self.width().saturating_sub(total - scale_total); let mut used_total = 0;
288 for (i, x) in widths.iter_mut().enumerate() {
289 if *x == 0 {
290 continue;
291 }
292 if *x == u16::MAX
293 && let Some(w) = self.widths.get(i)
294 {
295 used_total += w;
296 continue;
297 }
298 let new_w = *x * available / scale_total;
299 *x = new_w.max(self.config.min_wrap_width);
300 used_total += *x;
301 last_scalable = Some(x);
302 }
303
304 if used_total < self.width()
306 && let Some(last) = last_scalable
307 {
308 *last += self.width() - used_total;
309 }
310
311 widths
312 }
313
314 pub fn make_table<'a, T: SSS>(
317 &mut self,
318 active_column: usize,
319 worker: &'a mut Worker<T>,
320 selector: &mut Selector<T, impl Selection>,
321 matcher: &mut nucleo::Matcher,
322 click: &mut Click,
323 ) -> Table<'a> {
324 let offset = self.bottom as u32;
325 let end = self.bottom + self.height as u32;
326 let hz = !self.config.stacked_columns;
327
328 let width_limits = if hz {
329 self.max_widths()
330 } else {
331 let default = if self.config.wrap {
332 self.width
333 } else {
334 u16::MAX
335 };
336
337 (0..worker.columns.len())
338 .map(|i| {
339 if self.hidden_columns.get(i).copied().unwrap_or(false) {
340 0
341 } else {
342 default
343 }
344 })
345 .collect()
346 };
347
348 let (mut results, mut widths, status) = worker.results(
349 offset,
350 end,
351 &width_limits,
352 self.match_style(),
353 matcher,
354 self.config.match_start_context,
355 );
356
357 let match_count = status.matched_count;
360 self.status = status;
361
362 if match_count < self.bottom + self.cursor as u32 && !self.cursor_disabled {
363 self.cursor_jump(match_count);
364 } else {
365 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
366 }
367
368 widths[0] += self.indentation() as u16;
369
370 let mut rows = vec![];
371 let mut total_height = 0;
372
373 if results.is_empty() {
374 return Table::new(rows, widths);
375 }
376
377 let height_of = |t: &(Vec<ratatui::text::Text<'a>>, _)| {
378 self._hr()
379 + if hz {
380 t.0.iter()
381 .map(|t| t.height() as u16)
382 .max()
383 .unwrap_or_default()
384 } else {
385 t.0.iter().map(|t| t.height() as u16).sum::<u16>()
386 }
387 };
388
389 let h_at_cursor = height_of(&results[self.cursor as usize]);
391 let h_after_cursor = results[self.cursor as usize + 1..]
392 .iter()
393 .map(height_of)
394 .sum();
395 let h_to_cursor = results[0..self.cursor as usize]
396 .iter()
397 .map(height_of)
398 .sum::<u16>();
399 let cursor_end_should_lt = self.height - self.scroll_padding().min(h_after_cursor);
400 let mut start_index = 0; if h_at_cursor >= cursor_end_should_lt {
409 start_index = self.cursor;
410 self.bottom += self.cursor as u32;
411 self.cursor = 0;
412 self.cursor_above = 0;
413 self.bottom_clip = None;
414 } else
415 if let h_to_cursor_end = h_to_cursor + h_at_cursor
417 && h_to_cursor_end > cursor_end_should_lt
418 {
419 let mut trunc_height = h_to_cursor_end - cursor_end_should_lt;
420 for r in results[start_index as usize..self.cursor as usize].iter_mut() {
423 let h = height_of(r);
424 let (row, item) = r;
425 start_index += 1; if trunc_height < h {
428 let mut remaining_height = h - trunc_height;
429 let prefix = if selector.contains(item) {
430 self.config.multi_prefix.clone().to_string()
431 } else {
432 self.default_prefix(0)
433 };
434
435 total_height += remaining_height;
436
437 if hz {
439 if h - self._hr() < remaining_height {
440 for (_, t) in
441 row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0)
442 {
443 clip_text_lines(t, remaining_height, !self.reverse());
444 }
445 }
446
447 prefix_text(&mut row[0], prefix);
448
449 let last_visible = widths
450 .iter()
451 .enumerate()
452 .rev()
453 .find_map(|(i, w)| (*w != 0).then_some(i));
454
455 let mut row_texts: Vec<_> = row
456 .iter()
457 .take(last_visible.map(|x| x + 1).unwrap_or(0))
458 .cloned()
459 .collect();
460
461 if self.config.right_align_last && row_texts.len() > 1 {
462 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
463 }
464
465 let row = Row::new(row_texts).height(remaining_height);
466 rows.push(row);
467 } else {
468 let mut push = vec![];
469
470 for col in row.into_iter().rev() {
471 let mut height = col.height() as u16;
472 if remaining_height == 0 {
473 break;
474 } else if remaining_height < height {
475 clip_text_lines(col, remaining_height, !self.reverse());
476 height = remaining_height;
477 }
478 remaining_height -= height;
479 prefix_text(col, prefix.clone());
480 push.push(Row::new(vec![col.clone()]).height(height));
481 }
482 rows.extend(push.into_iter().rev());
483 }
484
485 self.bottom += start_index as u32 - 1;
486 self.cursor -= start_index - 1;
487 self.bottom_clip = Some(remaining_height);
488 break;
489 } else if trunc_height == h {
490 self.bottom += start_index as u32;
491 self.cursor -= start_index;
492 self.bottom_clip = None;
493 break;
494 }
495
496 trunc_height -= h;
497 }
498 } else if let Some(mut remaining_height) = self.bottom_clip {
499 start_index += 1;
500 let h = height_of(&results[0]);
502 let (row, item) = &mut results[0];
503 let prefix = if selector.contains(item) {
504 self.config.multi_prefix.clone().to_string()
505 } else {
506 self.default_prefix(0)
507 };
508
509 total_height += remaining_height;
510
511 if hz {
512 if self._hr() + remaining_height != h {
513 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
514 clip_text_lines(t, remaining_height, !self.reverse());
515 }
516 }
517
518 prefix_text(&mut row[0], prefix);
519
520 let last_visible = widths
521 .iter()
522 .enumerate()
523 .rev()
524 .find_map(|(i, w)| (*w != 0).then_some(i));
525
526 let mut row_texts: Vec<_> = row
527 .iter()
528 .take(last_visible.map(|x| x + 1).unwrap_or(0))
529 .cloned()
530 .collect();
531
532 if self.config.right_align_last && row_texts.len() > 1 {
533 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
534 }
535
536 let row = Row::new(row_texts).height(remaining_height);
537 rows.push(row);
538 } else {
539 let mut push = vec![];
540
541 for col in row.into_iter().rev() {
542 let mut height = col.height() as u16;
543 if remaining_height == 0 {
544 break;
545 } else if remaining_height < height {
546 clip_text_lines(col, remaining_height, !self.reverse());
547 height = remaining_height;
548 }
549 remaining_height -= height;
550 prefix_text(col, prefix.clone());
551 push.push(Row::new(vec![col.clone()]).height(height));
552 }
553 rows.extend(push.into_iter().rev());
554 }
555 }
556
557 let mut remaining_height = self.height.saturating_sub(total_height);
561
562 for (mut i, (mut row, item)) in results.drain(start_index as usize..).enumerate() {
563 i += self.bottom_clip.is_some() as usize;
564
565 if let Click::ResultPos(c) = click
567 && self.height - remaining_height > *c
568 {
569 let idx = self.bottom as u32 + i as u32 - 1;
570 log::debug!("Mapped click position to index: {c} -> {idx}",);
571 *click = Click::ResultIdx(idx);
572 }
573 if self.is_current(i) {
574 self.cursor_above = self.height - remaining_height;
575 }
576
577 if let Some(hr) = self.hr()
579 && remaining_height > 0
580 {
581 rows.push(hr);
582 remaining_height -= 1;
583 }
584 if remaining_height == 0 {
585 break;
586 }
587
588 let prefix = if selector.contains(item) {
590 self.config.multi_prefix.clone().to_string()
591 } else {
592 self.default_prefix(i)
593 };
594
595 if hz {
596 if self.is_current(i) && self.scroll[0] > 0 {
598 for (x, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
599 if self.col.is_none() || self.col() == Some(x) {
600 let scroll = self.scroll[0] as usize;
601
602 if scroll < t.lines.len() {
603 t.lines = t.lines.split_off(scroll);
604 } else {
605 t.lines.clear();
606 }
607 }
608 }
609 }
610
611 let mut height = row
612 .iter()
613 .map(|t| t.height() as u16)
614 .max()
615 .unwrap_or_default();
616
617 if remaining_height < height {
618 height = remaining_height;
619
620 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
621 clip_text_lines(t, height, self.reverse());
622 }
623 }
624 remaining_height -= height;
625
626 let last_visible = widths
628 .iter()
629 .enumerate()
630 .rev()
631 .find_map(|(i, w)| (*w != 0).then_some(i));
632
633 let mut row_texts: Vec<_> = row
634 .iter()
635 .take(last_visible.map(|x| x + 1).unwrap_or(0))
636 .cloned()
637 .enumerate()
639 .map(|(x, mut t)| {
640 let is_active_col = active_column == x;
641 let is_current_row = self.is_current(i);
642
643 if is_current_row && is_active_col {
644 if self.scroll[1] > 0 {
645 apply_to_lines(&mut t, |line| hscroll_line(line, self.scroll[1]));
646 }
647 }
648
649 match self.config.row_connection_style {
650 RowConnectionStyle::Disjoint => {
651 if is_active_col {
652 t = t.style(if is_current_row {
653 self.current_style()
654 } else {
655 self.active_style()
656 });
657 } else {
658 t = t.style(if is_current_row {
659 self.inactive_current_style()
660 } else {
661 self.inactive_style()
662 });
663 }
664 }
665 RowConnectionStyle::Capped => {
666 if is_active_col {
667 t = t.style(if is_current_row {
668 self.current_style()
669 } else {
670 self.active_style()
671 });
672 }
673 }
674 RowConnectionStyle::Full => {}
675 }
676
677 if x == 0 {
679 prefix_text(&mut t, prefix.clone());
680 };
681 t
682 })
683 .collect();
684
685 if self.config.right_align_last && row_texts.len() > 1 {
686 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
687 }
688
689 let mut row = Row::new(row_texts).height(height);
691
692 if self.is_current(i) {
693 match self.config.row_connection_style {
694 RowConnectionStyle::Capped => {
695 row = row.style(self.inactive_current_style())
696 }
697 RowConnectionStyle::Full => row = row.style(self.current_style()),
698 _ => {}
699 }
700 }
701
702 rows.push(row);
703 } else {
704 let mut push = vec![];
705
706 for (x, mut col) in row.into_iter().enumerate() {
707 let mut height = col.height() as u16;
708
709 if remaining_height == 0 {
710 break;
711 } else if remaining_height < height {
712 height = remaining_height;
713 clip_text_lines(&mut col, remaining_height, self.reverse());
714 }
715 remaining_height -= height;
716
717 if self.is_current(i) && self.scroll[1] > 0 && active_column == x {
718 apply_to_lines(&mut col, |line| hscroll_line(line, self.scroll[1]));
719 }
720 if self.is_current(i) && self.scroll[0] > 0 && active_column == x {
721 let scroll = self.scroll[0] as usize;
722
723 if scroll < col.lines.len() {
724 col.lines = col.lines.split_off(scroll);
725 } else {
726 col.lines.clear();
727 }
728 }
729
730 prefix_text(&mut col, prefix.clone());
731
732 let is_active_col = active_column == x;
733 let is_current_row = self.is_current(i);
734
735 match self.config.row_connection_style {
736 RowConnectionStyle::Disjoint => {
737 if is_active_col {
738 col = col.style(if is_current_row {
739 self.current_style()
740 } else {
741 self.active_style()
742 });
743 } else {
744 col = col.style(if is_current_row {
745 self.inactive_current_style()
746 } else {
747 self.inactive_style()
748 });
749 }
750 }
751 RowConnectionStyle::Capped => {
752 if is_active_col {
753 col = col.style(if is_current_row {
754 self.current_style()
755 } else {
756 self.active_style()
757 });
758 }
759 }
760 RowConnectionStyle::Full => {}
761 }
762
763 let mut row = Row::new(vec![col]).height(height);
765 if is_current_row {
766 match self.config.row_connection_style {
767 RowConnectionStyle::Capped => {
768 row = row.style(self.inactive_current_style())
769 }
770 RowConnectionStyle::Full => row = row.style(self.current_style()),
771 _ => {}
772 }
773 }
774 push.push(row);
775 }
776 rows.extend(push);
777 }
778 }
779
780 if self.reverse() {
781 rows.reverse();
782 if remaining_height > 0 {
783 rows.insert(0, Row::new(vec![vec![]]).height(remaining_height));
784 }
785 }
786
787 if hz {
790 self.widths = {
791 let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
792 let mut widths = widths[..pos].to_vec();
793 if pos > 2 && self.config.right_align_last {
794 let used = widths.iter().take(widths.len() - 1).sum();
795 widths[pos - 1] = self.width().saturating_sub(used);
796 }
797 widths
798 };
799 }
800
801 let mut table = Table::new(
803 rows,
804 if hz {
805 self.widths.clone()
806 } else {
807 vec![self.width]
808 },
809 )
810 .column_spacing(self.config.column_spacing.0);
811
812 table = match self.config.row_connection_style {
813 RowConnectionStyle::Full => table.style(self.active_style()),
814 RowConnectionStyle::Capped => table.style(self.inactive_style()),
815 _ => table,
816 };
817
818 table = table.block(self.config.border.as_static_block());
819 table
820 }
821}
822
823impl ResultsUI {
824 pub fn make_status(&self, full_width: u16) -> Paragraph<'_> {
825 let status_config = &self.status_config;
826 let replacements = [
827 ('r', self.index().to_string()),
828 ('m', self.status.matched_count.to_string()),
829 ('t', self.status.item_count.to_string()),
830 ];
831
832 let mut new_spans = Vec::new();
834
835 if status_config.match_indent {
836 new_spans.push(Span::raw(" ".repeat(self.indentation())));
837 }
838
839 for span in &self.status_template {
840 let subbed = substitute_escaped(&span.content, &replacements);
841 new_spans.push(Span::styled(subbed, span.style));
842 }
843
844 let substituted_line = Line::from(new_spans);
845
846 let effective_width = match self.status_config.row_connection_style {
848 RowConnectionStyle::Full => full_width,
849 _ => self.width,
850 } as usize;
851 let expanded = expand_indents(substituted_line, r"\s", effective_width)
852 .style(status_config.fg)
853 .add_modifier(status_config.modifier);
854
855 Paragraph::new(expanded)
856 }
857
858 pub fn set_status_line(&mut self, template: Option<Line<'static>>) {
859 let status_config = &self.status_config;
860
861 self.status_template = template
862 .unwrap_or(status_config.template.clone().into())
863 .style(status_config.fg)
864 .add_modifier(status_config.modifier)
865 .into()
866 }
867}
868
869impl ResultsUI {
871 fn default_prefix(&self, i: usize) -> String {
872 let substituted = substitute_escaped(
873 &self.config.default_prefix,
874 &[
875 ('d', &(i + 1).to_string()), ('r', &(i + 1 + self.bottom as usize).to_string()), ],
878 );
879
880 fit_width(&substituted, self.indentation())
881 }
882
883 fn current_style(&self) -> Style {
884 Style::from(self.config.current_fg)
885 .bg(self.config.current_bg)
886 .add_modifier(self.config.current_modifier)
887 }
888
889 fn active_style(&self) -> Style {
890 Style::from(self.config.fg)
891 .bg(self.config.bg)
892 .add_modifier(self.config.modifier)
893 }
894
895 fn inactive_style(&self) -> Style {
896 Style::from(self.config.inactive_fg)
897 .bg(self.config.inactive_bg)
898 .add_modifier(self.config.inactive_modifier)
899 }
900
901 fn inactive_current_style(&self) -> Style {
902 Style::from(self.config.inactive_current_fg)
903 .bg(self.config.inactive_current_bg)
904 .add_modifier(self.config.inactive_current_modifier)
905 }
906
907 fn is_current(&self, i: usize) -> bool {
908 !self.cursor_disabled && self.cursor == i as u16
909 }
910
911 pub fn match_style(&self) -> Style {
912 Style::default()
913 .fg(self.config.match_fg)
914 .add_modifier(self.config.match_modifier)
915 }
916
917 fn hr(&self) -> Option<Row<'static>> {
918 let sep = self.config.horizontal_separator;
919
920 if matches!(sep, HorizontalSeparator::None) {
921 return None;
922 }
923
924 let unit = sep.as_str();
925 let line = unit.repeat(self.width as usize);
926
927 if !self.config.stacked_columns && self.widths.len() > 1 {
929 Some(Row::new(vec![line; self.widths().len()]))
931 } else {
932 Some(Row::new(vec![line]))
933 }
934 }
935
936 fn _hr(&self) -> u16 {
937 !matches!(self.config.horizontal_separator, HorizontalSeparator::None) as u16
938 }
939}
940
941pub struct StatusUI {}
942
943impl StatusUI {
944 pub fn parse_template_to_status_line(s: &str) -> Line<'static> {
945 let parts = match split_on_nesting(&s, ['{', '}']) {
946 Ok(x) => x,
947 Err(n) => {
948 if n > 0 {
949 log::error!("Encountered {} unclosed parentheses", n)
950 } else {
951 log::error!("Extra closing parenthesis at index {}", -n)
952 }
953 return Line::from(s.to_string());
954 }
955 };
956
957 let mut spans = Vec::new();
958 let mut in_nested = !s.starts_with('{');
959 for part in parts {
960 in_nested = !in_nested;
961 let content = part.as_str();
962
963 if in_nested {
964 let inner = &content[1..content.len() - 1];
965
966 if let Some((color_name, text)) = inner.split_once(':') {
967 if let Ok(color) = Color::from_str(color_name) {
968 spans.push(Span::styled(text.to_string(), Style::default().fg(color)));
969 continue;
970 }
971 }
972 }
973
974 spans.push(Span::raw(content.to_string()));
975 }
976
977 Line::from(spans)
978 }
979}