1use cba::bring::split::split_on_nesting;
2use ratatui::{
3 layout::{Alignment, Rect},
4 style::{Color, Modifier, Style},
5 text::{Line, Span},
6 widgets::{Paragraph, Row, Table},
7};
8use unicode_width::UnicodeWidthStr;
9
10use crate::{
11 SSS, Selection, Selector,
12 config::{HorizontalSeparator, ResultsConfig, RowConnectionStyle, StatusConfig},
13 nucleo::{Status, Worker},
14 render::Click,
15 utils::{
16 string::{fit_width, substitute_escaped},
17 text::{clip_text_lines, expand_indents, prefix_text},
18 },
19};
20
21#[derive(Debug)]
22pub struct ResultsUI {
23 cursor: u16,
24 bottom: u32,
25 col: Option<usize>,
26 pub hscroll: i8,
27 pub vscroll: u8,
28
29 height: u16,
31 width: u16,
33 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 hscroll: 0,
58 vscroll: 0,
59
60 widths: Vec::new(),
61 height: 0, width: 0,
63 hidden_columns: Default::default(),
64
65 status: Default::default(),
66 status_template: Line::from(status_config.template.clone())
67 .style(status_config.base_style()),
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 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 pub fn height(&self) -> u16 {
96 self.height
97 }
98
99 pub fn reverse(&self) -> bool {
101 self.config.reverse == Some(true)
102 }
103 pub fn is_wrap(&self) -> bool {
104 self.config.wrap
105 }
106 pub fn wrap(&mut self, wrap: bool) {
107 self.config.wrap = wrap;
108 }
109
110 pub fn toggle_col(&mut self, col_idx: usize) -> bool {
113 self.reset_current_scroll();
114
115 if self.col == Some(col_idx) {
116 self.col = None
117 } else {
118 self.col = Some(col_idx);
119 }
120 self.col.is_some()
121 }
122 pub fn cycle_col(&mut self) {
123 self.reset_current_scroll();
124
125 self.col = match self.col {
126 None => self.widths.is_empty().then_some(0),
127 Some(c) => {
128 let next = c + 1;
129 if next < self.widths.len() {
130 Some(next)
131 } else {
132 None
133 }
134 }
135 };
136 }
137
138 fn scroll_padding(&self) -> u16 {
140 self.config.scroll_padding.min(self.height / 2)
141 }
142 pub fn end(&self) -> u32 {
143 self.status.matched_count.saturating_sub(1)
144 }
145
146 pub fn index(&self) -> u32 {
150 if self.cursor_disabled {
151 u32::MAX
152 } else {
153 self.cursor as u32 + self.bottom
154 }
155 }
156 pub fn cursor_prev(&mut self) {
164 self.reset_current_scroll();
165
166 log::trace!("cursor_prev: {self:?}");
167 if self.cursor_above <= self.scroll_padding() && self.bottom > 0 {
168 self.bottom -= 1;
169 self.bottom_clip = None;
170 } else if self.cursor > 0 {
171 self.cursor -= 1;
172 } else if self.config.scroll_wrap {
173 self.cursor_jump(self.end());
174 }
175 }
176 pub fn cursor_next(&mut self) {
177 self.reset_current_scroll();
178
179 if self.cursor_disabled {
180 self.cursor_disabled = false
181 }
182
183 if self.cursor + 1 + self.scroll_padding() >= self.height
190 && self.bottom + (self.height as u32) < self.status.matched_count
191 {
192 self.bottom += 1; } else if self.index() < self.end() {
194 self.cursor += 1;
195 } else if self.config.scroll_wrap {
196 self.cursor_jump(0)
197 }
198 }
199
200 pub fn cursor_jump(&mut self, index: u32) {
201 self.reset_current_scroll();
202
203 self.cursor_disabled = false;
204 self.bottom_clip = None;
205
206 let end = self.end();
207 let index = index.min(end);
208
209 if index < self.bottom as u32 || index >= self.bottom + self.height as u32 {
210 self.bottom = (end + 1)
211 .saturating_sub(self.height as u32) .min(index);
213 }
214 self.cursor = (index - self.bottom) as u16;
215 log::debug!("cursor jumped to {}: {index}, end: {end}", self.cursor);
216 }
217
218 pub fn current_scroll(&mut self, x: i8, horizontal: bool) {
219 if horizontal {
220 self.hscroll = if x == 0 {
221 0
222 } else {
223 self.hscroll.saturating_add(x)
224 };
225 } else {
226 self.vscroll = if x == 0 {
227 0
228 } else if x.is_negative() {
229 self.vscroll.saturating_sub(x.unsigned_abs())
230 } else {
231 self.vscroll.saturating_add(x as u8)
232 };
233 }
234 }
235
236 pub fn reset_current_scroll(&mut self) {
237 self.hscroll = 0;
238 self.vscroll = 0;
239 }
240
241 pub fn indentation(&self) -> usize {
243 self.config.multi_prefix.width()
244 }
245 pub fn col(&self) -> Option<usize> {
246 self.col
247 }
248
249 pub fn widths(&self) -> &Vec<u16> {
252 &self.widths
253 }
254 pub fn width(&self) -> u16 {
256 self.width.saturating_sub(self.indentation() as u16)
257 }
258
259 pub fn max_widths(&self) -> Vec<u16> {
262 let mut scale_total = 0;
263
264 let mut widths = vec![u16::MAX; self.widths.len().max(self.hidden_columns.len())];
265
266 let mut total = 0; for i in 0..widths.len() {
268 if i < self.hidden_columns.len() && self.hidden_columns[i] {
269 widths[i] = 0;
270 } else if let Some(&w) = self.widths.get(i) {
271 total += w;
272 if w >= self.config.min_wrap_width {
273 scale_total += w;
274 widths[i] = w;
275 }
276 }
277 }
278
279 if !self.config.wrap || scale_total == 0 {
280 for x in &mut widths {
281 if *x != 0 {
282 *x = u16::MAX
283 }
284 }
285 return widths;
286 }
287
288 let mut last_scalable = None;
289 let available = self.width().saturating_sub(total - scale_total); let mut used_total = 0;
292 for (i, x) in widths.iter_mut().enumerate() {
293 if *x == 0 {
294 continue;
295 }
296 if *x == u16::MAX
297 && let Some(w) = self.widths.get(i)
298 {
299 used_total += w;
300 continue;
301 }
302 let new_w = *x * available / scale_total;
303 *x = new_w.max(self.config.min_wrap_width);
304 used_total += *x;
305 last_scalable = Some(x);
306 }
307
308 if used_total < self.width()
310 && let Some(last) = last_scalable
311 {
312 *last += self.width() - used_total;
313 }
314
315 widths
316 }
317
318 pub fn make_table<'a, T: SSS>(
321 &mut self,
322 active_column: usize,
323 worker: &'a mut Worker<T>,
324 selector: &mut Selector<T, impl Selection>,
325 matcher: &mut nucleo::Matcher,
326 click: &mut Click,
327 ) -> Table<'a> {
328 let offset = self.bottom as u32;
329 let end = self.bottom + self.height as u32;
330 let hz = !self.config.stacked_columns;
331
332 let width_limits = if hz {
333 self.max_widths()
334 } else {
335 let default = if self.config.wrap {
336 self.width
337 } else {
338 u16::MAX
339 };
340
341 (0..worker.columns.len())
342 .map(|i| {
343 if self.hidden_columns.get(i).copied().unwrap_or(false) {
344 0
345 } else {
346 default
347 }
348 })
349 .collect()
350 };
351
352 let autoscroll = self.config.autoscroll.then_some((
353 self.config.autoscroll_initial_preserved,
354 self.config.autoscroll_context,
355 ));
356
357 let (mut results, mut widths, status) = worker.results(
358 offset,
359 end,
360 &width_limits,
361 self.match_style(),
362 matcher,
363 autoscroll,
364 self.hscroll,
365 );
366
367 let match_count = status.matched_count;
370 self.status = status;
371
372 if match_count < self.bottom + self.cursor as u32 && !self.cursor_disabled {
373 self.cursor_jump(match_count);
374 } else {
375 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
376 }
377
378 widths[0] += self.indentation() as u16;
379
380 let mut rows = vec![];
381 let mut total_height = 0;
382
383 if results.is_empty() {
384 return Table::new(rows, widths);
385 }
386
387 let height_of = |t: &(Vec<ratatui::text::Text<'a>>, _)| {
388 self._hr()
389 + if hz {
390 t.0.iter()
391 .map(|t| t.height() as u16)
392 .max()
393 .unwrap_or_default()
394 } else {
395 t.0.iter().map(|t| t.height() as u16).sum::<u16>()
396 }
397 };
398
399 let h_at_cursor = height_of(&results[self.cursor as usize]);
401 let h_after_cursor = results[self.cursor as usize + 1..]
402 .iter()
403 .map(height_of)
404 .sum();
405 let h_to_cursor = results[0..self.cursor as usize]
406 .iter()
407 .map(height_of)
408 .sum::<u16>();
409 let cursor_end_should_lt = self.height - self.scroll_padding().min(h_after_cursor);
410 let mut start_index = 0; if h_at_cursor >= cursor_end_should_lt {
419 start_index = self.cursor;
420 self.bottom += self.cursor as u32;
421 self.cursor = 0;
422 self.cursor_above = 0;
423 self.bottom_clip = None;
424 } else
425 if let h_to_cursor_end = h_to_cursor + h_at_cursor
427 && h_to_cursor_end > cursor_end_should_lt
428 {
429 let mut trunc_height = h_to_cursor_end - cursor_end_should_lt;
430 for r in results[start_index as usize..self.cursor as usize].iter_mut() {
433 let h = height_of(r);
434 let (row, item) = r;
435 start_index += 1; if trunc_height < h {
438 let mut remaining_height = h - trunc_height;
439 let prefix = if selector.contains(item) {
440 self.config.multi_prefix.clone().to_string()
441 } else {
442 self.default_prefix(0)
443 };
444
445 total_height += remaining_height;
446
447 if hz {
449 if h - self._hr() < remaining_height {
450 for (_, t) in
451 row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0)
452 {
453 clip_text_lines(t, remaining_height, !self.reverse());
454 }
455 }
456
457 prefix_text(&mut row[0], prefix);
458
459 let last_visible = widths
460 .iter()
461 .enumerate()
462 .rev()
463 .find_map(|(i, w)| (*w != 0).then_some(i));
464
465 let mut row_texts: Vec<_> = row
466 .iter()
467 .take(last_visible.map(|x| x + 1).unwrap_or(0))
468 .cloned()
469 .collect();
470
471 if self.config.right_align_last && row_texts.len() > 1 {
472 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
473 }
474
475 let row = Row::new(row_texts).height(remaining_height);
476 rows.push(row);
477 } else {
478 let mut push = vec![];
479
480 for col in row.into_iter().rev() {
481 let mut height = col.height() as u16;
482 if remaining_height == 0 {
483 break;
484 } else if remaining_height < height {
485 clip_text_lines(col, remaining_height, !self.reverse());
486 height = remaining_height;
487 }
488 remaining_height -= height;
489 prefix_text(col, prefix.clone());
490 push.push(Row::new(vec![col.clone()]).height(height));
491 }
492 rows.extend(push.into_iter().rev());
493 }
494
495 self.bottom += start_index as u32 - 1;
496 self.cursor -= start_index - 1;
497 self.bottom_clip = Some(remaining_height);
498 break;
499 } else if trunc_height == h {
500 self.bottom += start_index as u32;
501 self.cursor -= start_index;
502 self.bottom_clip = None;
503 break;
504 }
505
506 trunc_height -= h;
507 }
508 } else if let Some(mut remaining_height) = self.bottom_clip {
509 start_index += 1;
510 let h = height_of(&results[0]);
512 let (row, item) = &mut results[0];
513 let prefix = if selector.contains(item) {
514 self.config.multi_prefix.clone().to_string()
515 } else {
516 self.default_prefix(0)
517 };
518
519 total_height += remaining_height;
520
521 if hz {
522 if self._hr() + remaining_height != h {
523 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
524 clip_text_lines(t, remaining_height, !self.reverse());
525 }
526 }
527
528 prefix_text(&mut row[0], prefix);
529
530 let last_visible = widths
531 .iter()
532 .enumerate()
533 .rev()
534 .find_map(|(i, w)| (*w != 0).then_some(i));
535
536 let mut row_texts: Vec<_> = row
537 .iter()
538 .take(last_visible.map(|x| x + 1).unwrap_or(0))
539 .cloned()
540 .collect();
541
542 if self.config.right_align_last && row_texts.len() > 1 {
543 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
544 }
545
546 let row = Row::new(row_texts).height(remaining_height);
547 rows.push(row);
548 } else {
549 let mut push = vec![];
550
551 for col in row.into_iter().rev() {
552 let mut height = col.height() as u16;
553 if remaining_height == 0 {
554 break;
555 } else if remaining_height < height {
556 clip_text_lines(col, remaining_height, !self.reverse());
557 height = remaining_height;
558 }
559 remaining_height -= height;
560 prefix_text(col, prefix.clone());
561 push.push(Row::new(vec![col.clone()]).height(height));
562 }
563 rows.extend(push.into_iter().rev());
564 }
565 }
566
567 let mut remaining_height = self.height.saturating_sub(total_height);
569
570 for (mut i, (mut row, item)) in results.drain(start_index as usize..).enumerate() {
571 i += self.bottom_clip.is_some() as usize;
572
573 if let Click::ResultPos(c) = click
575 && self.height - remaining_height > *c
576 {
577 let idx = self.bottom as u32 + i as u32 - 1;
578 log::debug!("Mapped click position to index: {c} -> {idx}",);
579 *click = Click::ResultIdx(idx);
580 }
581 if self.is_current(i) {
582 self.cursor_above = self.height - remaining_height;
583 }
584
585 if let Some(hr) = self.hr()
587 && remaining_height > 0
588 {
589 rows.push(hr);
590 remaining_height -= 1;
591 }
592 if remaining_height == 0 {
593 break;
594 }
595
596 let prefix = if selector.contains(item) {
598 self.config.multi_prefix.clone().to_string()
599 } else {
600 self.default_prefix(i)
601 };
602
603 if hz {
604 if self.is_current(i) && self.vscroll > 0 {
606 for (x, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
607 if self.col.is_none() || self.col() == Some(x) {
608 let scroll = self.vscroll as usize;
609
610 if scroll < t.lines.len() {
611 t.lines = t.lines.split_off(scroll);
612 } else {
613 t.lines.clear();
614 }
615 }
616 }
617 }
618
619 let mut height = row
620 .iter()
621 .map(|t| t.height() as u16)
622 .max()
623 .unwrap_or_default();
624
625 if remaining_height < height {
626 height = remaining_height;
627
628 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
629 clip_text_lines(t, height, self.reverse());
630 }
631 }
632 remaining_height -= height;
633
634 let last_visible = widths
636 .iter()
637 .enumerate()
638 .rev()
639 .find_map(|(i, w)| (*w != 0).then_some(i));
640
641 let mut row_texts: Vec<_> = row
642 .iter()
643 .take(last_visible.map(|x| x + 1).unwrap_or(0))
644 .cloned()
645 .enumerate()
647 .map(|(x, mut t)| {
648 let is_active_col = active_column == x;
649 let is_current_row = self.is_current(i);
650
651 if is_current_row && is_active_col {
652 }
654
655 match self.config.row_connection_style {
656 RowConnectionStyle::Disjoint => {
657 if is_active_col {
658 t = t.style(if is_current_row {
659 self.current_style()
660 } else {
661 self.active_style()
662 });
663 } else {
664 t = t.style(if is_current_row {
665 self.inactive_current_style()
666 } else {
667 self.inactive_style()
668 });
669 }
670 }
671 RowConnectionStyle::Capped => {
672 if is_active_col {
673 t = t.style(if is_current_row {
674 self.current_style()
675 } else {
676 self.active_style()
677 });
678 }
679 }
680 RowConnectionStyle::Full => {}
681 }
682
683 if x == 0 {
685 prefix_text(&mut t, prefix.clone());
686 };
687 t
688 })
689 .collect();
690
691 if self.config.right_align_last && row_texts.len() > 1 {
692 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
693 }
694
695 let mut row = Row::new(row_texts).height(height);
697
698 if self.is_current(i) {
699 match self.config.row_connection_style {
700 RowConnectionStyle::Capped => {
701 row = row.style(self.inactive_current_style())
702 }
703 RowConnectionStyle::Full => row = row.style(self.current_style()),
704 _ => {}
705 }
706 }
707
708 rows.push(row);
709 } else {
710 let mut push = vec![];
711 let mut vscroll_to_skip = if self.is_current(i) {
712 self.vscroll as usize
713 } else {
714 0
715 };
716
717 for (x, mut col) in row.into_iter().enumerate() {
718 if vscroll_to_skip > 0 {
719 let col_height = col.lines.len();
720 if vscroll_to_skip >= col_height {
721 vscroll_to_skip -= col_height;
722 continue;
723 } else {
724 col.lines = col.lines.split_off(vscroll_to_skip);
725 vscroll_to_skip = 0;
726 }
727 }
728
729 let mut height = col.height() as u16;
730
731 if remaining_height == 0 {
732 break;
733 } else if remaining_height < height {
734 height = remaining_height;
735 clip_text_lines(&mut col, remaining_height, self.reverse());
736 }
737 remaining_height -= height;
738
739 prefix_text(&mut col, prefix.clone());
740
741 let is_active_col = active_column == x;
742 let is_current_row = self.is_current(i);
743
744 match self.config.row_connection_style {
745 RowConnectionStyle::Disjoint => {
746 if is_active_col {
747 col = col.style(if is_current_row {
748 self.current_style()
749 } else {
750 self.active_style()
751 });
752 } else {
753 col = col.style(if is_current_row {
754 self.inactive_current_style()
755 } else {
756 self.inactive_style()
757 });
758 }
759 }
760 RowConnectionStyle::Capped => {
761 if is_active_col {
762 col = col.style(if is_current_row {
763 self.current_style()
764 } else {
765 self.active_style()
766 });
767 }
768 }
769 RowConnectionStyle::Full => {}
770 }
771
772 let mut row = Row::new(vec![col]).height(height);
774 if is_current_row {
775 match self.config.row_connection_style {
776 RowConnectionStyle::Capped => {
777 row = row.style(self.inactive_current_style())
778 }
779 RowConnectionStyle::Full => row = row.style(self.current_style()),
780 _ => {}
781 }
782 }
783 push.push(row);
784 }
785 rows.extend(push);
786 }
787 }
788
789 if self.reverse() {
790 rows.reverse();
791 if remaining_height > 0 {
792 rows.insert(0, Row::new(vec![vec![]]).height(remaining_height));
793 }
794 }
795
796 if hz {
799 self.widths = {
800 let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
801 let mut widths = widths[..pos].to_vec();
802 if pos > 2 && self.config.right_align_last {
803 let used = widths.iter().take(widths.len() - 1).sum();
804 widths[pos - 1] = self.width().saturating_sub(used);
805 }
806 widths
807 };
808 }
809
810 let mut table = Table::new(
812 rows,
813 if hz {
814 self.widths.clone()
815 } else {
816 vec![self.width]
817 },
818 )
819 .column_spacing(self.config.column_spacing.0);
820
821 table = match self.config.row_connection_style {
822 RowConnectionStyle::Full => table.style(self.active_style()),
823 RowConnectionStyle::Capped => table.style(self.inactive_style()),
824 _ => table,
825 };
826
827 table = table.block(self.config.border.as_static_block());
828 table
829 }
830}
831
832impl ResultsUI {
833 pub fn make_status(&self, full_width: u16) -> Paragraph<'_> {
834 let status_config = &self.status_config;
835 let replacements = [
836 ('r', self.index().to_string()),
837 ('m', self.status.matched_count.to_string()),
838 ('t', self.status.item_count.to_string()),
839 ];
840
841 let mut new_spans = Vec::new();
843
844 if status_config.match_indent {
845 new_spans.push(Span::raw(" ".repeat(self.indentation())));
846 }
847
848 for span in &self.status_template {
849 let subbed = substitute_escaped(&span.content, &replacements);
850 new_spans.push(Span::styled(subbed, span.style));
851 }
852
853 let substituted_line = Line::from(new_spans);
854
855 let effective_width = match self.status_config.row_connection_style {
857 RowConnectionStyle::Full => full_width,
858 _ => self.width,
859 } as usize;
860 let expanded = expand_indents(substituted_line, r"\s", r"\S", effective_width)
861 .style(status_config.base_style());
862
863 Paragraph::new(expanded)
864 }
865
866 pub fn set_status_line(&mut self, template: Option<Line<'static>>) {
867 let status_config = &self.status_config;
868
869 self.status_template = template
870 .unwrap_or(status_config.template.clone().into())
871 .style(status_config.base_style())
872 .into()
873 }
874}
875
876impl ResultsUI {
878 fn default_prefix(&self, i: usize) -> String {
879 let substituted = substitute_escaped(
880 &self.config.default_prefix,
881 &[
882 ('d', &(i + 1).to_string()), ('r', &(i + 1 + self.bottom as usize).to_string()), ],
885 );
886
887 fit_width(&substituted, self.indentation())
888 }
889
890 fn current_style(&self) -> Style {
891 Style::from(self.config.current_fg)
892 .bg(self.config.current_bg)
893 .add_modifier(self.config.current_modifier)
894 }
895
896 fn active_style(&self) -> Style {
897 Style::from(self.config.fg)
898 .bg(self.config.bg)
899 .add_modifier(self.config.modifier)
900 }
901
902 fn inactive_style(&self) -> Style {
903 Style::from(self.config.inactive_fg)
904 .bg(self.config.inactive_bg)
905 .add_modifier(self.config.inactive_modifier)
906 }
907
908 fn inactive_current_style(&self) -> Style {
909 Style::from(self.config.inactive_current_fg)
910 .bg(self.config.inactive_current_bg)
911 .add_modifier(self.config.inactive_current_modifier)
912 }
913
914 fn is_current(&self, i: usize) -> bool {
915 !self.cursor_disabled && self.cursor == i as u16
916 }
917
918 pub fn match_style(&self) -> Style {
919 Style::default()
920 .fg(self.config.match_fg)
921 .add_modifier(self.config.match_modifier)
922 }
923
924 fn hr(&self) -> Option<Row<'static>> {
925 let sep = self.config.horizontal_separator;
926
927 if matches!(sep, HorizontalSeparator::None) {
928 return None;
929 }
930
931 let unit = sep.as_str();
932 let line = unit.repeat(self.width as usize);
933
934 if !self.config.stacked_columns && self.widths.len() > 1 {
936 Some(Row::new(vec![line; self.widths().len()]))
938 } else {
939 Some(Row::new(vec![line]))
940 }
941 }
942
943 fn _hr(&self) -> u16 {
944 !matches!(self.config.horizontal_separator, HorizontalSeparator::None) as u16
945 }
946}
947
948pub struct StatusUI {}
949
950impl StatusUI {
951 pub fn parse_template_to_status_line(s: &str) -> Line<'static> {
952 let parts = match split_on_nesting(&s, ['{', '}']) {
953 Ok(x) => x,
954 Err(n) => {
955 if n > 0 {
956 log::error!("Encountered {} unclosed parentheses", n)
957 } else {
958 log::error!("Extra closing parenthesis at index {}", -n)
959 }
960 return Line::from(s.to_string());
961 }
962 };
963
964 let mut spans = Vec::new();
965 let mut in_nested = !s.starts_with('{');
966 for part in parts {
967 in_nested = !in_nested;
968 let content = part.as_str();
969
970 if in_nested {
971 let inner = &content[1..content.len() - 1];
972
973 spans.push(Self::span_from_template(inner));
975 } else {
976 spans.push(Span::raw(content.to_string()));
977 }
978 }
979
980 Line::from(spans)
981 }
982
983 pub fn span_from_template(inner: &str) -> Span<'static> {
1007 use std::str::FromStr;
1008
1009 let (style_part, text) = inner.split_once(':').unwrap_or(("", inner));
1010
1011 let mut style = Style::default();
1012 let mut fg_set = false;
1013 let mut bg_set = false;
1014 let mut unknown_tokens = Vec::new();
1015
1016 for token in style_part.split(',') {
1017 let token = token.trim();
1018 if token.is_empty() {
1019 continue;
1020 }
1021
1022 if !fg_set {
1023 if let Ok(color) = Color::from_str(token) {
1024 style = style.fg(color);
1025 fg_set = true;
1026 continue;
1027 }
1028 }
1029
1030 if !bg_set {
1031 if let Ok(color) = Color::from_str(token) {
1032 style = style.bg(color);
1033 bg_set = true;
1034 continue;
1035 }
1036 }
1037
1038 match token.to_lowercase().as_str() {
1039 "bold" => {
1040 style = style.add_modifier(Modifier::BOLD);
1041 }
1042 "dim" => {
1043 style = style.add_modifier(Modifier::DIM);
1044 }
1045 "italic" => {
1046 style = style.add_modifier(Modifier::ITALIC);
1047 }
1048 "underlined" => {
1049 style = style.add_modifier(Modifier::UNDERLINED);
1050 }
1051 "slow_blink" => {
1052 style = style.add_modifier(Modifier::SLOW_BLINK);
1053 }
1054 "rapid_blink" => {
1055 style = style.add_modifier(Modifier::RAPID_BLINK);
1056 }
1057 "reversed" => {
1058 style = style.add_modifier(Modifier::REVERSED);
1059 }
1060 "hidden" => {
1061 style = style.add_modifier(Modifier::HIDDEN);
1062 }
1063 "crossed_out" => {
1064 style = style.add_modifier(Modifier::CROSSED_OUT);
1065 }
1066 _ => unknown_tokens.push(token.to_string()),
1067 };
1068 }
1069
1070 if !unknown_tokens.is_empty() {
1071 log::warn!("Unknown style tokens: {:?}", unknown_tokens);
1072 }
1073
1074 Span::styled(text.to_string(), style)
1075 }
1076}