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 autoscroll = self.config.autoscroll.then_some((
349 self.config.autoscroll_initial_preserved,
350 self.config.autoscroll_context,
351 ));
352
353 let (mut results, mut widths, status) = worker.results(
354 offset,
355 end,
356 &width_limits,
357 self.config.wrap,
358 self.match_style(),
359 matcher,
360 autoscroll,
361 self.hscroll,
362 );
363
364 widths[0] += self.indentation() as u16;
365 for x in widths.iter_mut().zip(&self.hidden_columns) {
367 if *x.1 {
368 *x.0 = 0
369 }
370 }
371 let widths = widths;
372
373 let match_count = status.matched_count;
374 self.status = status;
375
376 if match_count < self.bottom + self.cursor as u32 && !self.cursor_disabled {
377 self.cursor_jump(match_count);
378 } else {
379 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
380 }
381
382 let mut rows = vec![];
383 let mut total_height = 0;
384
385 if results.is_empty() {
386 return Table::new(rows, widths);
387 }
388
389 let height_of = |t: &(Vec<ratatui::text::Text<'a>>, _)| {
390 self._hr()
391 + if hz {
392 t.0.iter()
393 .map(|t| t.height() as u16)
394 .max()
395 .unwrap_or_default()
396 } else {
397 t.0.iter().map(|t| t.height() as u16).sum::<u16>()
398 }
399 };
400
401 let h_at_cursor = height_of(&results[self.cursor as usize]);
403 let h_after_cursor = results[self.cursor as usize + 1..]
404 .iter()
405 .map(height_of)
406 .sum();
407 let h_to_cursor = results[0..self.cursor as usize]
408 .iter()
409 .map(height_of)
410 .sum::<u16>();
411 let cursor_end_should_lt = self.height - self.scroll_padding().min(h_after_cursor);
412 let mut start_index = 0; if h_at_cursor >= cursor_end_should_lt {
421 start_index = self.cursor;
422 self.bottom += self.cursor as u32;
423 self.cursor = 0;
424 self.cursor_above = 0;
425 self.bottom_clip = None;
426 } else
427 if let h_to_cursor_end = h_to_cursor + h_at_cursor
429 && h_to_cursor_end > cursor_end_should_lt
430 {
431 let mut trunc_height = h_to_cursor_end - cursor_end_should_lt;
432 for r in results[start_index as usize..self.cursor as usize].iter_mut() {
435 let h = height_of(r);
436 let (row, item) = r;
437 start_index += 1; if trunc_height < h {
440 let mut remaining_height = h - trunc_height;
441 let prefix = if selector.contains(item) {
442 self.config.multi_prefix.clone().to_string()
443 } else {
444 self.default_prefix(0)
445 };
446
447 total_height += remaining_height;
448
449 if hz {
451 if h - self._hr() < remaining_height {
452 for (_, t) in
453 row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0)
454 {
455 clip_text_lines(t, remaining_height, !self.reverse());
456 }
457 }
458
459 prefix_text(&mut row[0], prefix);
460
461 let last_visible = widths
462 .iter()
463 .enumerate()
464 .rev()
465 .find_map(|(i, w)| (*w != 0).then_some(i));
466
467 let mut row_texts: Vec<_> = row
468 .iter()
469 .take(last_visible.map(|x| x + 1).unwrap_or(0))
470 .cloned()
471 .collect();
472
473 if self.config.right_align_last && row_texts.len() > 1 {
474 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
475 }
476
477 let row = Row::new(row_texts).height(remaining_height);
478 rows.push(row);
479 } else {
480 let mut push = vec![];
481
482 for col in row.into_iter().rev() {
483 let mut height = col.height() as u16;
484 if remaining_height == 0 {
485 break;
486 } else if remaining_height < height {
487 clip_text_lines(col, remaining_height, !self.reverse());
488 height = remaining_height;
489 }
490 remaining_height -= height;
491 prefix_text(col, prefix.clone());
492 push.push(Row::new(vec![col.clone()]).height(height));
493 }
494 rows.extend(push.into_iter().rev());
495 }
496
497 self.bottom += start_index as u32 - 1;
498 self.cursor -= start_index - 1;
499 self.bottom_clip = Some(remaining_height);
500 break;
501 } else if trunc_height == h {
502 self.bottom += start_index as u32;
503 self.cursor -= start_index;
504 self.bottom_clip = None;
505 break;
506 }
507
508 trunc_height -= h;
509 }
510 } else if let Some(mut remaining_height) = self.bottom_clip {
511 start_index += 1;
512 let h = height_of(&results[0]);
514 let (row, item) = &mut results[0];
515 let prefix = if selector.contains(item) {
516 self.config.multi_prefix.clone().to_string()
517 } else {
518 self.default_prefix(0)
519 };
520
521 total_height += remaining_height;
522
523 if hz {
524 if self._hr() + remaining_height != h {
525 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
526 clip_text_lines(t, remaining_height, !self.reverse());
527 }
528 }
529
530 prefix_text(&mut row[0], prefix);
531
532 let last_visible = widths
533 .iter()
534 .enumerate()
535 .rev()
536 .find_map(|(i, w)| (*w != 0).then_some(i));
537
538 let mut row_texts: Vec<_> = row
539 .iter()
540 .take(last_visible.map(|x| x + 1).unwrap_or(0))
541 .cloned()
542 .collect();
543
544 if self.config.right_align_last && row_texts.len() > 1 {
545 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
546 }
547
548 let row = Row::new(row_texts).height(remaining_height);
549 rows.push(row);
550 } else {
551 let mut push = vec![];
552
553 for col in row.into_iter().rev() {
554 let mut height = col.height() as u16;
555 if remaining_height == 0 {
556 break;
557 } else if remaining_height < height {
558 clip_text_lines(col, remaining_height, !self.reverse());
559 height = remaining_height;
560 }
561 remaining_height -= height;
562 prefix_text(col, prefix.clone());
563 push.push(Row::new(vec![col.clone()]).height(height));
564 }
565 rows.extend(push.into_iter().rev());
566 }
567 }
568
569 let mut remaining_height = self.height.saturating_sub(total_height);
571
572 for (mut i, (mut row, item)) in results.drain(start_index as usize..).enumerate() {
573 i += self.bottom_clip.is_some() as usize;
574
575 if let Click::ResultPos(c) = click
577 && self.height - remaining_height > *c
578 {
579 let idx = self.bottom as u32 + i as u32 - 1;
580 log::debug!("Mapped click position to index: {c} -> {idx}",);
581 *click = Click::ResultIdx(idx);
582 }
583 if self.is_current(i) {
584 self.cursor_above = self.height - remaining_height;
585 }
586
587 if let Some(hr) = self.hr()
589 && remaining_height > 0
590 {
591 rows.push(hr);
592 remaining_height -= 1;
593 }
594 if remaining_height == 0 {
595 break;
596 }
597
598 let prefix = if selector.contains(item) {
600 self.config.multi_prefix.clone().to_string()
601 } else {
602 self.default_prefix(i)
603 };
604
605 if hz {
606 if self.is_current(i) && self.vscroll > 0 {
608 for (x, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
609 if self.col.is_none() || self.col() == Some(x) {
610 let scroll = self.vscroll as usize;
611
612 if scroll < t.lines.len() {
613 t.lines = t.lines.split_off(scroll);
614 } else {
615 t.lines.clear();
616 }
617 }
618 }
619 }
620
621 let mut height = row
622 .iter()
623 .map(|t| t.height() as u16)
624 .max()
625 .unwrap_or_default();
626
627 if remaining_height < height {
628 height = remaining_height;
629
630 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
631 clip_text_lines(t, height, self.reverse());
632 }
633 }
634 remaining_height -= height;
635
636 let last_visible = widths
638 .iter()
639 .enumerate()
640 .rev()
641 .find_map(|(i, w)| (*w != 0).then_some(i));
642
643 let mut row_texts: Vec<_> = row
644 .iter()
645 .take(last_visible.map(|x| x + 1).unwrap_or(0))
646 .cloned()
647 .enumerate()
649 .map(|(x, mut t)| {
650 let is_active_col = active_column == x;
651 let is_current_row = self.is_current(i);
652
653 if is_current_row && is_active_col {
654 }
656
657 match self.config.row_connection {
658 RowConnectionStyle::Disjoint => {
659 if is_active_col {
660 t = t.style(if is_current_row {
661 self.current_style()
662 } else {
663 self.active_style()
664 });
665 } else {
666 t = t.style(if is_current_row {
667 self.inactive_current_style()
668 } else {
669 self.inactive_style()
670 });
671 }
672 }
673 RowConnectionStyle::Capped => {
674 if is_active_col {
675 t = t.style(if is_current_row {
676 self.current_style()
677 } else {
678 self.active_style()
679 });
680 }
681 }
682 RowConnectionStyle::Full => {}
683 }
684
685 if x == 0 {
687 prefix_text(&mut t, prefix.clone());
688 };
689 t
690 })
691 .collect();
692
693 if self.config.right_align_last && row_texts.len() > 1 {
694 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
695 }
696
697 let mut row = Row::new(row_texts).height(height);
699
700 if self.is_current(i) {
701 match self.config.row_connection {
702 RowConnectionStyle::Capped => {
703 row = row.style(self.inactive_current_style())
704 }
705 RowConnectionStyle::Full => row = row.style(self.current_style()),
706 _ => {}
707 }
708 }
709
710 rows.push(row);
711 } else {
712 let mut push = vec![];
713 let mut vscroll_to_skip = if self.is_current(i) {
714 self.vscroll as usize
715 } else {
716 0
717 };
718
719 for (x, mut col) in row.into_iter().enumerate() {
720 if vscroll_to_skip > 0 {
721 let col_height = col.lines.len();
722 if vscroll_to_skip >= col_height {
723 vscroll_to_skip -= col_height;
724 continue;
725 } else {
726 col.lines = col.lines.split_off(vscroll_to_skip);
727 vscroll_to_skip = 0;
728 }
729 }
730
731 let mut height = col.height() as u16;
732
733 if remaining_height == 0 {
734 break;
735 } else if remaining_height < height {
736 height = remaining_height;
737 clip_text_lines(&mut col, remaining_height, self.reverse());
738 }
739 remaining_height -= height;
740
741 prefix_text(&mut col, prefix.clone());
742
743 let is_active_col = active_column == x;
744 let is_current_row = self.is_current(i);
745
746 match self.config.row_connection {
747 RowConnectionStyle::Disjoint => {
748 if is_active_col {
749 col = col.style(if is_current_row {
750 self.current_style()
751 } else {
752 self.active_style()
753 });
754 } else {
755 col = col.style(if is_current_row {
756 self.inactive_current_style()
757 } else {
758 self.inactive_style()
759 });
760 }
761 }
762 RowConnectionStyle::Capped => {
763 if is_active_col {
764 col = col.style(if is_current_row {
765 self.current_style()
766 } else {
767 self.active_style()
768 });
769 }
770 }
771 RowConnectionStyle::Full => {}
772 }
773
774 let mut row = Row::new(vec![col]).height(height);
776 if is_current_row {
777 match self.config.row_connection {
778 RowConnectionStyle::Capped => {
779 row = row.style(self.inactive_current_style())
780 }
781 RowConnectionStyle::Full => row = row.style(self.current_style()),
782 _ => {}
783 }
784 }
785 push.push(row);
786 }
787 rows.extend(push);
788 }
789 }
790
791 if self.reverse() {
792 rows.reverse();
793 if remaining_height > 0 {
794 rows.insert(0, Row::new(vec![vec![]]).height(remaining_height));
795 }
796 }
797
798 let table_widths = if hz {
800 let pos = widths.iter().rposition(|&x| x != 0);
802 let mut widths: Vec<_> = widths[..pos.map_or(0, |x| x + 1)].to_vec();
804 if let Some(pos) = pos
805 && pos > 0
806 && self.config.right_align_last
807 {
808 let used = widths.iter().take(widths.len() - 1).sum();
809 widths[pos] = self.width.saturating_sub(used);
810 }
811 if let Some(s) = widths.get_mut(0) {
812 *s -= self.indentation() as u16
813 }
814 self.widths = widths.clone();
815
816 if !self.config.wrap {
817 widths
818 .iter_mut()
819 .zip(width_limits.iter())
820 .for_each(|(w, &limit)| {
821 *w = (*w).min(limit);
822 });
823 }
824
825 if let Some(s) = widths.get_mut(0) {
826 *s += self.indentation() as u16;
827 }
828
829 widths
830 } else {
831 vec![self.width]
832 };
833
834 let mut table = Table::new(rows, table_widths).column_spacing(self.config.column_spacing.0);
840
841 table = match self.config.row_connection {
842 RowConnectionStyle::Full => table.style(self.active_style()),
843 RowConnectionStyle::Capped => table.style(self.inactive_style()),
844 _ => table,
845 };
846
847 table = table.block(self.config.border.as_static_block());
848 table
849 }
850}
851
852impl ResultsUI {
853 pub fn make_status(&self, full_width: u16) -> Paragraph<'_> {
854 let status_config = &self.status_config;
855 let replacements = [
856 ('r', self.index().to_string()),
857 ('m', self.status.matched_count.to_string()),
858 ('t', self.status.item_count.to_string()),
859 ];
860
861 let mut new_spans = Vec::new();
863
864 if status_config.match_indent {
865 new_spans.push(Span::raw(" ".repeat(self.indentation())));
866 }
867
868 for span in &self.status_template {
869 let subbed = substitute_escaped(&span.content, &replacements);
870 new_spans.push(Span::styled(subbed, span.style));
871 }
872
873 let substituted_line = Line::from(new_spans);
874
875 let effective_width = match self.status_config.row_connection {
877 RowConnectionStyle::Full => full_width,
878 _ => self.width,
879 } as usize;
880 let expanded = expand_indents(substituted_line, r"\s", r"\S", effective_width)
881 .style(status_config.base_style());
882
883 Paragraph::new(expanded)
884 }
885
886 pub fn set_status_line(&mut self, template: Option<Line<'static>>) {
887 let status_config = &self.status_config;
888 log::trace!("status line: {template:?}");
889
890 self.status_template = template
891 .unwrap_or(status_config.template.clone().into())
892 .style(status_config.base_style())
893 .into()
894 }
895}
896
897impl ResultsUI {
899 fn default_prefix(&self, i: usize) -> String {
900 let substituted = substitute_escaped(
901 &self.config.default_prefix,
902 &[
903 ('d', &(i + 1).to_string()), ('r', &(i + 1 + self.bottom as usize).to_string()), ],
906 );
907
908 fit_width(&substituted, self.indentation())
909 }
910
911 fn current_style(&self) -> Style {
912 Style::from(self.config.current_fg)
913 .bg(self.config.current_bg)
914 .add_modifier(self.config.current_modifier)
915 }
916
917 fn active_style(&self) -> Style {
918 Style::from(self.config.fg)
919 .bg(self.config.bg)
920 .add_modifier(self.config.modifier)
921 }
922
923 fn inactive_style(&self) -> Style {
924 Style::from(self.config.inactive_fg)
925 .bg(self.config.inactive_bg)
926 .add_modifier(self.config.inactive_modifier)
927 }
928
929 fn inactive_current_style(&self) -> Style {
930 Style::from(self.config.inactive_current_fg)
931 .bg(self.config.inactive_current_bg)
932 .add_modifier(self.config.inactive_current_modifier)
933 }
934
935 fn is_current(&self, i: usize) -> bool {
936 !self.cursor_disabled && self.cursor == i as u16
937 }
938
939 pub fn match_style(&self) -> Style {
940 Style::default()
941 .fg(self.config.match_fg)
942 .add_modifier(self.config.match_modifier)
943 }
944
945 fn hr(&self) -> Option<Row<'static>> {
946 let sep = self.config.horizontal_separator;
947
948 if matches!(sep, HorizontalSeparator::None) {
949 return None;
950 }
951
952 let unit = sep.as_str();
953 let line = unit.repeat(self.width as usize);
954
955 if !self.config.stacked_columns && self.widths.len() > 1 {
957 Some(Row::new(vec![line; self.widths().len()]))
959 } else {
960 Some(Row::new(vec![line]))
961 }
962 }
963
964 fn _hr(&self) -> u16 {
965 !matches!(self.config.horizontal_separator, HorizontalSeparator::None) as u16
966 }
967}
968
969pub struct StatusUI {}
970
971impl StatusUI {
972 pub fn parse_template_to_status_line(s: &str) -> Line<'static> {
973 let parts = match split_on_nesting(&s, ['{', '}']) {
974 Ok(x) => x,
975 Err(n) => {
976 if n > 0 {
977 log::error!("Encountered {} unclosed parentheses", n)
978 } else {
979 log::error!("Extra closing parenthesis at index {}", -n)
980 }
981 return Line::from(s.to_string());
982 }
983 };
984
985 let mut spans = Vec::new();
986 let mut in_nested = !s.starts_with('{');
987 for part in parts {
988 in_nested = !in_nested;
989 let content = part.as_str();
990
991 if in_nested {
992 let inner = &content[1..content.len() - 1];
993
994 spans.push(Self::span_from_template(inner));
996 } else {
997 spans.push(Span::raw(content.to_string()));
998 }
999 }
1000
1001 Line::from(spans)
1002 }
1003
1004 pub fn span_from_template(inner: &str) -> Span<'static> {
1028 use std::str::FromStr;
1029
1030 let (style_part, text) = inner.split_once(':').unwrap_or(("", inner));
1031
1032 let mut style = Style::default();
1033 let mut fg_set = false;
1034 let mut bg_set = false;
1035 let mut unknown_tokens = Vec::new();
1036
1037 for token in style_part.split(',') {
1038 let token = token.trim();
1039 if token.is_empty() {
1040 continue;
1041 }
1042
1043 if !fg_set {
1044 if let Ok(color) = Color::from_str(token) {
1045 style = style.fg(color);
1046 fg_set = true;
1047 continue;
1048 }
1049 }
1050
1051 if !bg_set {
1052 if let Ok(color) = Color::from_str(token) {
1053 style = style.bg(color);
1054 bg_set = true;
1055 continue;
1056 }
1057 }
1058
1059 match token.to_lowercase().as_str() {
1060 "bold" => {
1061 style = style.add_modifier(Modifier::BOLD);
1062 }
1063 "dim" => {
1064 style = style.add_modifier(Modifier::DIM);
1065 }
1066 "italic" => {
1067 style = style.add_modifier(Modifier::ITALIC);
1068 }
1069 "underlined" => {
1070 style = style.add_modifier(Modifier::UNDERLINED);
1071 }
1072 "slow_blink" => {
1073 style = style.add_modifier(Modifier::SLOW_BLINK);
1074 }
1075 "rapid_blink" => {
1076 style = style.add_modifier(Modifier::RAPID_BLINK);
1077 }
1078 "reversed" => {
1079 style = style.add_modifier(Modifier::REVERSED);
1080 }
1081 "hidden" => {
1082 style = style.add_modifier(Modifier::HIDDEN);
1083 }
1084 "crossed_out" => {
1085 style = style.add_modifier(Modifier::CROSSED_OUT);
1086 }
1087 _ => unknown_tokens.push(token.to_string()),
1088 };
1089 }
1090
1091 if !unknown_tokens.is_empty() {
1092 log::warn!("Unknown style tokens: {:?}", unknown_tokens);
1093 }
1094
1095 Span::styled(text.to_string(), style)
1096 }
1097}