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