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::{allocate_widths, 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 height(&self) -> u16 {
90 self.height
91 }
92
93 pub fn reverse(&self) -> bool {
95 self.config.reverse == Some(true)
96 }
97 pub fn is_wrap(&self) -> bool {
98 self.config.wrap
99 }
100 pub fn wrap(&mut self, wrap: bool) {
101 self.config.wrap = wrap;
102 }
103
104 pub fn toggle_col(&mut self, col_idx: usize) -> bool {
107 self.reset_current_scroll();
108
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.reset_current_scroll();
118
119 self.col = match self.col {
120 None => self.widths.is_empty().then_some(0),
121 Some(c) => {
122 let next = c + 1;
123 if next < self.widths.len() {
124 Some(next)
125 } else {
126 None
127 }
128 }
129 };
130 }
131
132 fn scroll_padding(&self) -> u16 {
134 self.config.scroll_padding.min(self.height / 2)
135 }
136 pub fn end(&self) -> u32 {
137 self.status.matched_count.saturating_sub(1)
138 }
139
140 pub fn index(&self) -> u32 {
144 if self.cursor_disabled {
145 u32::MAX
146 } else {
147 self.cursor as u32 + self.bottom
148 }
149 }
150 pub fn cursor_prev(&mut self) {
158 self.reset_current_scroll();
159
160 log::trace!("cursor_prev: {self:?}");
161 if self.cursor_above <= self.scroll_padding() && self.bottom > 0 {
162 self.bottom -= 1;
163 self.bottom_clip = None;
164 } else if self.cursor > 0 {
165 self.cursor -= 1;
166 } else if self.config.scroll_wrap {
167 self.cursor_jump(self.end());
168 }
169 }
170 pub fn cursor_next(&mut self) {
171 self.reset_current_scroll();
172
173 if self.cursor_disabled {
174 self.cursor_disabled = false
175 }
176
177 if self.cursor + 1 + self.scroll_padding() >= self.height
184 && self.bottom + (self.height as u32) < self.status.matched_count
185 {
186 self.bottom += 1; } else if self.index() < self.end() {
188 self.cursor += 1;
189 } else if self.config.scroll_wrap {
190 self.cursor_jump(0)
191 }
192 }
193
194 pub fn cursor_jump(&mut self, index: u32) {
195 self.reset_current_scroll();
196
197 self.cursor_disabled = false;
198 self.bottom_clip = None;
199
200 let end = self.end();
201 let index = index.min(end);
202
203 if index < self.bottom as u32 || index >= self.bottom + self.height as u32 {
204 self.bottom = (end + 1)
205 .saturating_sub(self.height as u32) .min(index);
207 }
208 self.cursor = (index - self.bottom) as u16;
209 log::debug!("cursor jumped to {}: {index}, end: {end}", self.cursor);
210 }
211
212 pub fn current_scroll(&mut self, x: i8, horizontal: bool) {
213 if horizontal {
214 self.hscroll = if x == 0 {
215 0
216 } else {
217 self.hscroll.saturating_add(x)
218 };
219 } else {
220 self.vscroll = if x == 0 {
221 0
222 } else if x.is_negative() {
223 self.vscroll.saturating_sub(x.unsigned_abs())
224 } else {
225 self.vscroll.saturating_add(x as u8)
226 };
227 }
228 }
229
230 pub fn reset_current_scroll(&mut self) {
231 self.hscroll = 0;
232 self.vscroll = 0;
233 }
234
235 pub fn indentation(&self) -> usize {
237 self.config.multi_prefix.width()
238 }
239 pub fn col(&self) -> Option<usize> {
240 self.col
241 }
242
243 pub fn widths(&self) -> &Vec<u16> {
246 &self.widths
247 }
248
249 pub fn max_widths(&self) -> Vec<u16> {
252 let mut base_widths = self.widths.clone();
253
254 if base_widths.is_empty() {
255 return base_widths;
256 }
257 base_widths.resize(self.hidden_columns.len().max(base_widths.len()), 0);
258
259 for (i, is_hidden) in self.hidden_columns.iter().enumerate() {
260 if *is_hidden {
261 base_widths[i] = 0;
262 }
263 }
264
265 let target = self.content_width();
266 let sum: u16 = base_widths
267 .iter()
268 .map(|x| {
269 (*x != 0)
270 .then_some(*x.max(&self.config.min_wrap_width))
271 .unwrap_or_default()
272 })
273 .sum();
274
275 if sum < target {
276 let nonzero_count = base_widths.iter().filter(|w| **w > 0).count();
277 if nonzero_count > 0 {
278 let extra_per_column = (target - sum) / nonzero_count as u16;
279 let mut remainder = (target - sum) % nonzero_count as u16;
280
281 for w in base_widths.iter_mut().filter(|w| **w > 0) {
282 *w += extra_per_column;
283 if remainder > 0 {
284 *w += 1;
285 remainder -= 1;
286 }
287 }
288 }
289 }
290
291 match allocate_widths(&base_widths, target, self.config.min_wrap_width) {
292 Ok(s) | Err(s) => s,
293 }
294 }
295
296 pub fn content_width(&self) -> u16 {
297 self.width
298 .saturating_sub(self.indentation() as u16)
299 .saturating_sub(self.column_spacing_width())
300 }
301
302 pub fn column_spacing_width(&self) -> u16 {
303 let pos = self.widths.iter().rposition(|&x| x != 0);
304 self.config.column_spacing.0 * (pos.unwrap_or_default() as u16)
305 }
306
307 pub fn table_width(&self) -> u16 {
308 if self.config.stacked_columns {
309 self.width
310 } else {
311 self.widths.iter().sum::<u16>()
312 + self.config.border.width()
313 + self.indentation() as u16
314 + self.column_spacing_width()
315 }
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 = self.width.saturating_sub(self.indentation() as u16);
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.config.wrap,
353 self.match_style(),
354 matcher,
355 self.config.autoscroll,
356 self.hscroll,
357 );
358
359 widths[0] += self.indentation() as u16;
360 for x in widths.iter_mut().zip(&self.hidden_columns) {
362 if *x.1 {
363 *x.0 = 0
364 }
365 }
366 let widths = widths;
367
368 let match_count = status.matched_count;
369 self.status = status;
370
371 if match_count < self.bottom + self.cursor as u32 && !self.cursor_disabled {
372 self.cursor_jump(match_count);
373 } else {
374 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
375 }
376
377 let mut rows = vec![];
378 let mut total_height = 0;
379
380 if results.is_empty() {
381 return Table::new(rows, widths);
382 }
383
384 let height_of = |t: &(Vec<ratatui::text::Text<'a>>, _)| {
385 self._hr()
386 + if hz {
387 t.0.iter()
388 .map(|t| t.height() as u16)
389 .max()
390 .unwrap_or_default()
391 } else {
392 t.0.iter().map(|t| t.height() as u16).sum::<u16>()
393 }
394 };
395
396 let h_at_cursor = height_of(&results[self.cursor as usize]);
398 let h_after_cursor = results[self.cursor as usize + 1..]
399 .iter()
400 .map(height_of)
401 .sum();
402 let h_to_cursor = results[0..self.cursor as usize]
403 .iter()
404 .map(height_of)
405 .sum::<u16>();
406 let cursor_end_should_lte = self.height - self.scroll_padding().min(h_after_cursor);
407 let mut start_index = 0; if h_at_cursor >= cursor_end_should_lte {
418 start_index = self.cursor;
419 self.bottom += self.cursor as u32;
420 self.cursor = 0;
421 self.cursor_above = 0;
422 self.bottom_clip = None;
423 } else
424 if let h_to_cursor_end = h_to_cursor + h_at_cursor
426 && h_to_cursor_end > cursor_end_should_lte
427 {
428 let mut trunc_height = h_to_cursor_end - cursor_end_should_lte;
429 for r in results[start_index as usize..self.cursor as usize].iter_mut() {
432 let h = height_of(r);
433 let (row, item) = r;
434 start_index += 1; if trunc_height < h {
437 let mut remaining_height = h - trunc_height;
438 let prefix = if selector.contains(item) {
439 self.config.multi_prefix.clone().to_string()
440 } else {
441 self.default_prefix(0)
442 };
443
444 total_height += remaining_height;
445
446 if hz {
448 if h - self._hr() < remaining_height {
449 for (_, t) in
450 row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0)
451 {
452 clip_text_lines(t, remaining_height, !self.reverse());
453 }
454 }
455
456 prefix_text(&mut row[0], prefix);
457
458 let last_visible = widths
459 .iter()
460 .enumerate()
461 .rev()
462 .find_map(|(i, w)| (*w != 0).then_some(i));
463
464 let mut row_texts: Vec<_> = row
465 .iter()
466 .take(last_visible.map(|x| x + 1).unwrap_or(0))
467 .cloned()
468 .collect();
469
470 if self.config.right_align_last && row_texts.len() > 1 {
471 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
472 }
473
474 let row = Row::new(row_texts).height(remaining_height);
475 rows.push(row);
476 } else {
477 let mut push = vec![];
478
479 for col in row.into_iter().rev() {
480 let mut height = col.height() as u16;
481 if remaining_height == 0 {
482 break;
483 } else if remaining_height < height {
484 clip_text_lines(col, remaining_height, !self.reverse());
485 height = remaining_height;
486 }
487 remaining_height -= height;
488 prefix_text(col, prefix.clone());
489 push.push(Row::new(vec![col.clone()]).height(height));
490 }
491 rows.extend(push.into_iter().rev());
492 }
493
494 self.bottom += start_index as u32 - 1;
495 self.cursor -= start_index - 1;
496 self.bottom_clip = Some(remaining_height);
497 break;
498 } else if trunc_height == h {
499 self.bottom += start_index as u32;
500 self.cursor -= start_index;
501 self.bottom_clip = None;
502 break;
503 }
504
505 trunc_height -= h;
506 }
507 } else if let Some(mut remaining_height) = self.bottom_clip {
508 start_index += 1;
509 let h = height_of(&results[0]);
511 let (row, item) = &mut results[0];
512 let prefix = if selector.contains(item) {
513 self.config.multi_prefix.clone().to_string()
514 } else {
515 self.default_prefix(0)
516 };
517
518 total_height += remaining_height;
519
520 if hz {
521 if self._hr() + remaining_height != h {
522 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
523 clip_text_lines(t, remaining_height, !self.reverse());
524 }
525 }
526
527 prefix_text(&mut row[0], prefix);
528
529 let last_visible = widths
530 .iter()
531 .enumerate()
532 .rev()
533 .find_map(|(i, w)| (*w != 0).then_some(i));
534
535 let mut row_texts: Vec<_> = row
536 .iter()
537 .take(last_visible.map(|x| x + 1).unwrap_or(0))
538 .cloned()
539 .collect();
540
541 if self.config.right_align_last && row_texts.len() > 1 {
542 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
543 }
544
545 let row = Row::new(row_texts).height(remaining_height);
546 rows.push(row);
547 } else {
548 let mut push = vec![];
549
550 for col in row.into_iter().rev() {
551 let mut height = col.height() as u16;
552 if remaining_height == 0 {
553 break;
554 } else if remaining_height < height {
555 clip_text_lines(col, remaining_height, !self.reverse());
556 height = remaining_height;
557 }
558 remaining_height -= height;
559 prefix_text(col, prefix.clone());
560 push.push(Row::new(vec![col.clone()]).height(height));
561 }
562 rows.extend(push.into_iter().rev());
563 }
564 }
565
566 let mut remaining_height = self.height.saturating_sub(total_height);
568
569 for (mut i, (mut row, item)) in results.drain(start_index as usize..).enumerate() {
570 i += self.bottom_clip.is_some() as usize;
571
572 if let Click::ResultPos(c) = click
574 && self.height - remaining_height > *c
575 {
576 let idx = self.bottom as u32 + i as u32 - 1;
577 log::debug!("Mapped click position to index: {c} -> {idx}",);
578 *click = Click::ResultIdx(idx);
579 }
580 if self.is_current(i) {
581 self.cursor_above = self.height - remaining_height;
582 }
583
584 if let Some(hr) = self.hr()
586 && remaining_height > 0
587 {
588 rows.push(hr);
589 remaining_height -= 1;
590 }
591 if remaining_height == 0 {
592 break;
593 }
594
595 let prefix = if selector.contains(item) {
597 self.config.multi_prefix.clone().to_string()
598 } else {
599 self.default_prefix(i)
600 };
601
602 if hz {
603 if self.is_current(i) && self.vscroll > 0 {
605 for (x, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
606 if self.col.is_none() || self.col() == Some(x) {
607 let scroll = self.vscroll as usize;
608
609 if scroll < t.lines.len() {
610 t.lines = t.lines.split_off(scroll);
611 } else {
612 t.lines.clear();
613 }
614 }
615 }
616 }
617
618 let mut height = row
619 .iter()
620 .map(|t| t.height() as u16)
621 .max()
622 .unwrap_or_default();
623
624 if remaining_height < height {
625 height = remaining_height;
626
627 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
628 clip_text_lines(t, height, self.reverse());
629 }
630 }
631 remaining_height -= height;
632
633 let last_visible = widths
635 .iter()
636 .enumerate()
637 .rev()
638 .find_map(|(i, w)| (*w != 0).then_some(i));
639
640 let mut row_texts: Vec<_> = row
641 .iter()
642 .take(last_visible.map(|x| x + 1).unwrap_or(0))
643 .cloned()
644 .enumerate()
646 .map(|(x, mut t)| {
647 let is_active_col = active_column == x;
648 let is_current_row = self.is_current(i);
649
650 if is_current_row && is_active_col {
651 }
653
654 match self.config.row_connection {
655 RowConnectionStyle::Disjoint => {
656 if is_active_col {
657 t = t.style(if is_current_row {
658 self.current_style()
659 } else {
660 self.active_style()
661 });
662 } else {
663 t = t.style(if is_current_row {
664 self.inactive_current_style()
665 } else {
666 self.inactive_style()
667 });
668 }
669 }
670 RowConnectionStyle::Capped => {
671 if is_active_col {
672 t = t.style(if is_current_row {
673 self.current_style()
674 } else {
675 self.active_style()
676 });
677 }
678 }
679 RowConnectionStyle::Full => {}
680 }
681
682 if x == 0 {
684 prefix_text(&mut t, prefix.clone());
685 };
686 t
687 })
688 .collect();
689
690 if self.config.right_align_last && row_texts.len() > 1 {
691 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
692 }
693
694 let mut row = Row::new(row_texts).height(height);
696
697 if self.is_current(i) {
698 match self.config.row_connection {
699 RowConnectionStyle::Capped => {
700 row = row.style(self.inactive_current_style())
701 }
702 RowConnectionStyle::Full => row = row.style(self.current_style()),
703 _ => {}
704 }
705 }
706
707 rows.push(row);
708 } else {
709 let mut push = vec![];
710 let mut vscroll_to_skip = if self.is_current(i) {
711 self.vscroll as usize
712 } else {
713 0
714 };
715
716 for (x, mut col) in row.into_iter().enumerate() {
717 if vscroll_to_skip > 0 {
718 let col_height = col.lines.len();
719 if vscroll_to_skip >= col_height {
720 vscroll_to_skip -= col_height;
721 continue;
722 } else {
723 col.lines = col.lines.split_off(vscroll_to_skip);
724 vscroll_to_skip = 0;
725 }
726 }
727
728 let mut height = col.height() as u16;
729
730 if remaining_height == 0 {
731 break;
732 } else if remaining_height < height {
733 height = remaining_height;
734 clip_text_lines(&mut col, remaining_height, self.reverse());
735 }
736 remaining_height -= height;
737
738 prefix_text(&mut col, prefix.clone());
739
740 let is_active_col = active_column == x;
741 let is_current_row = self.is_current(i);
742
743 match self.config.row_connection {
744 RowConnectionStyle::Disjoint => {
745 if is_active_col {
746 col = col.style(if is_current_row {
747 self.current_style()
748 } else {
749 self.active_style()
750 });
751 } else {
752 col = col.style(if is_current_row {
753 self.inactive_current_style()
754 } else {
755 self.inactive_style()
756 });
757 }
758 }
759 RowConnectionStyle::Capped => {
760 if is_active_col {
761 col = col.style(if is_current_row {
762 self.current_style()
763 } else {
764 self.active_style()
765 });
766 }
767 }
768 RowConnectionStyle::Full => {}
769 }
770
771 let mut row = Row::new(vec![col]).height(height);
773 if is_current_row {
774 match self.config.row_connection {
775 RowConnectionStyle::Capped => {
776 row = row.style(self.inactive_current_style())
777 }
778 RowConnectionStyle::Full => row = row.style(self.current_style()),
779 _ => {}
780 }
781 }
782 push.push(row);
783 }
784 rows.extend(push);
785 }
786 }
787
788 if self.reverse() {
789 rows.reverse();
790 if remaining_height > 0 {
791 rows.insert(0, Row::new(vec![vec![]]).height(remaining_height));
792 }
793 }
794
795 let table_widths = if hz {
797 let pos = widths.iter().rposition(|&x| x != 0);
799 let mut widths: Vec<_> = widths[..pos.map_or(0, |x| x + 1)].to_vec();
801 if let Some(pos) = pos
802 && pos > 0
803 && self.config.right_align_last
804 {
805 let used = widths.iter().take(widths.len() - 1).sum();
806 widths[pos] = self.width.saturating_sub(used);
807 }
808 if let Some(s) = widths.get_mut(0) {
809 *s -= self.indentation() as u16
810 }
811 self.widths = widths.clone();
812
813 if !self.config.wrap {
814 widths
815 .iter_mut()
816 .zip(width_limits.iter())
817 .for_each(|(w, &limit)| {
818 *w = (*w).min(limit);
819 });
820 }
821
822 if let Some(s) = widths.get_mut(0) {
823 *s += self.indentation() as u16;
824 }
825
826 widths
827 } else {
828 vec![self.width]
829 };
830
831 let mut table = Table::new(rows, table_widths).column_spacing(self.config.column_spacing.0);
837
838 table = match self.config.row_connection {
839 RowConnectionStyle::Full => table.style(self.active_style()),
840 RowConnectionStyle::Capped => table.style(self.inactive_style()),
841 _ => table,
842 };
843
844 table = table.block(self.config.border.as_static_block());
845 table
846 }
847}
848
849impl ResultsUI {
850 pub fn make_status(&self, full_width: u16) -> Paragraph<'_> {
851 let status_config = &self.status_config;
852 let replacements = [
853 ('r', self.index().to_string()),
854 ('m', self.status.matched_count.to_string()),
855 ('t', self.status.item_count.to_string()),
856 ];
857
858 let mut new_spans = Vec::new();
860
861 if status_config.match_indent {
862 new_spans.push(Span::raw(" ".repeat(self.indentation())));
863 }
864
865 for span in &self.status_template {
866 let subbed = substitute_escaped(&span.content, &replacements);
867 new_spans.push(Span::styled(subbed, span.style));
868 }
869
870 let substituted_line = Line::from(new_spans);
871
872 let effective_width = match self.status_config.row_connection {
874 RowConnectionStyle::Full => full_width,
875 _ => self.width,
876 } as usize;
877 let expanded = expand_indents(substituted_line, r"\s", r"\S", effective_width)
878 .style(status_config.base_style());
879
880 Paragraph::new(expanded)
881 }
882
883 pub fn set_status_line(&mut self, template: Option<Line<'static>>) {
884 let status_config = &self.status_config;
885 log::trace!("status line: {template:?}");
886
887 self.status_template = template
888 .unwrap_or(status_config.template.clone().into())
889 .style(status_config.base_style())
890 .into()
891 }
892}
893
894impl ResultsUI {
896 fn default_prefix(&self, i: usize) -> String {
897 let substituted = substitute_escaped(
898 &self.config.default_prefix,
899 &[
900 ('d', &(i + 1).to_string()), ('r', &(i + 1 + self.bottom as usize).to_string()), ],
903 );
904
905 fit_width(&substituted, self.indentation())
906 }
907
908 fn current_style(&self) -> Style {
909 Style::from(self.config.current_fg)
910 .bg(self.config.current_bg)
911 .add_modifier(self.config.current_modifier)
912 }
913
914 fn active_style(&self) -> Style {
915 Style::from(self.config.fg)
916 .bg(self.config.bg)
917 .add_modifier(self.config.modifier)
918 }
919
920 fn inactive_style(&self) -> Style {
921 Style::from(self.config.inactive_fg)
922 .bg(self.config.inactive_bg)
923 .add_modifier(self.config.inactive_modifier)
924 }
925
926 fn inactive_current_style(&self) -> Style {
927 Style::from(self.config.inactive_current_fg)
928 .bg(self.config.inactive_current_bg)
929 .add_modifier(self.config.inactive_current_modifier)
930 }
931
932 fn is_current(&self, i: usize) -> bool {
933 !self.cursor_disabled && self.cursor == i as u16
934 }
935
936 pub fn match_style(&self) -> Style {
937 Style::default()
938 .fg(self.config.match_fg)
939 .add_modifier(self.config.match_modifier)
940 }
941
942 fn hr(&self) -> Option<Row<'static>> {
943 let sep = self.config.horizontal_separator;
944
945 if matches!(sep, HorizontalSeparator::None) {
946 return None;
947 }
948
949 let unit = sep.as_str();
950 let line = unit.repeat(self.width as usize);
951
952 if !self.config.stacked_columns && self.widths.len() > 1 {
954 Some(Row::new(vec![line; self.widths().len()]))
956 } else {
957 Some(Row::new(vec![line]))
958 }
959 }
960
961 fn _hr(&self) -> u16 {
962 !matches!(self.config.horizontal_separator, HorizontalSeparator::None) as u16
963 }
964}
965
966pub struct StatusUI {}
967
968impl StatusUI {
969 pub fn parse_template_to_status_line(s: &str) -> Line<'static> {
970 let parts = match split_on_nesting(&s, ['{', '}']) {
971 Ok(x) => x,
972 Err(n) => {
973 if n > 0 {
974 log::error!("Encountered {} unclosed parentheses", n)
975 } else {
976 log::error!("Extra closing parenthesis at index {}", -n)
977 }
978 return Line::from(s.to_string());
979 }
980 };
981
982 let mut spans = Vec::new();
983 let mut in_nested = !s.starts_with('{');
984 for part in parts {
985 in_nested = !in_nested;
986 let content = part.as_str();
987
988 if in_nested {
989 let inner = &content[1..content.len() - 1];
990
991 spans.push(Self::span_from_template(inner));
993 } else {
994 spans.push(Span::raw(content.to_string()));
995 }
996 }
997
998 Line::from(spans)
999 }
1000
1001 pub fn span_from_template(inner: &str) -> Span<'static> {
1025 use std::str::FromStr;
1026
1027 let (style_part, text) = inner.split_once(':').unwrap_or(("", inner));
1028
1029 let mut style = Style::default();
1030 let mut fg_set = false;
1031 let mut bg_set = false;
1032 let mut unknown_tokens = Vec::new();
1033
1034 for token in style_part.split(',') {
1035 let token = token.trim();
1036 if token.is_empty() {
1037 continue;
1038 }
1039
1040 if !fg_set {
1041 if let Ok(color) = Color::from_str(token) {
1042 style = style.fg(color);
1043 fg_set = true;
1044 continue;
1045 }
1046 }
1047
1048 if !bg_set {
1049 if let Ok(color) = Color::from_str(token) {
1050 style = style.bg(color);
1051 bg_set = true;
1052 continue;
1053 }
1054 }
1055
1056 match token.to_lowercase().as_str() {
1057 "bold" => {
1058 style = style.add_modifier(Modifier::BOLD);
1059 }
1060 "dim" => {
1061 style = style.add_modifier(Modifier::DIM);
1062 }
1063 "italic" => {
1064 style = style.add_modifier(Modifier::ITALIC);
1065 }
1066 "underlined" => {
1067 style = style.add_modifier(Modifier::UNDERLINED);
1068 }
1069 "slow_blink" => {
1070 style = style.add_modifier(Modifier::SLOW_BLINK);
1071 }
1072 "rapid_blink" => {
1073 style = style.add_modifier(Modifier::RAPID_BLINK);
1074 }
1075 "reversed" => {
1076 style = style.add_modifier(Modifier::REVERSED);
1077 }
1078 "hidden" => {
1079 style = style.add_modifier(Modifier::HIDDEN);
1080 }
1081 "crossed_out" => {
1082 style = style.add_modifier(Modifier::CROSSED_OUT);
1083 }
1084 _ => unknown_tokens.push(token.to_string()),
1085 };
1086 }
1087
1088 if !unknown_tokens.is_empty() {
1089 log::warn!("Unknown style tokens: {:?}", unknown_tokens);
1090 }
1091
1092 Span::styled(text.to_string(), style)
1093 }
1094}