1use crate::colors::{Colorscheme, ResultsColorscheme};
2use crate::layout::InputPosition;
3use color_eyre::eyre::Result;
4use ratatui::layout::{Alignment, Rect};
5use ratatui::prelude::{Color, Line, Span, Style};
6use ratatui::style::Stylize;
7use ratatui::widgets::{
8 Block, BorderType, Borders, List, ListDirection, ListState, Padding,
9};
10use ratatui::Frame;
11use rustc_hash::{FxHashMap, FxHashSet};
12use std::str::FromStr;
13use television_channels::entry::Entry;
14use television_utils::strings::{
15 make_matched_string_printable, next_char_boundary,
16 slice_at_char_boundaries,
17};
18
19const POINTER_SYMBOL: &str = "> ";
20const SELECTED_SYMBOL: &str = "● ";
21const DESLECTED_SYMBOL: &str = " ";
22
23pub fn build_results_list<'a, 'b>(
24 results_block: Block<'b>,
25 entries: &'a [Entry],
26 selected_entries: Option<&FxHashSet<Entry>>,
27 list_direction: ListDirection,
28 use_icons: bool,
29 icon_color_cache: &mut FxHashMap<String, Color>,
30 colorscheme: &ResultsColorscheme,
31) -> List<'a>
32where
33 'b: 'a,
34{
35 List::new(entries.iter().map(|entry| {
36 let mut spans = Vec::new();
37 if let Some(selected_entries) = selected_entries {
39 if !selected_entries.is_empty() {
40 spans.push(if selected_entries.contains(entry) {
41 Span::styled(
42 SELECTED_SYMBOL,
43 Style::default().fg(colorscheme.result_selected_fg),
44 )
45 } else {
46 Span::from(DESLECTED_SYMBOL)
47 });
48 }
49 }
50 if let Some(icon) = entry.icon.as_ref() {
52 if use_icons {
53 if let Some(icon_color) = icon_color_cache.get(icon.color) {
54 spans.push(Span::styled(
55 icon.to_string(),
56 Style::default().fg(*icon_color),
57 ));
58 } else {
59 let icon_color = Color::from_str(icon.color).unwrap();
60 icon_color_cache
61 .insert(icon.color.to_string(), icon_color);
62 spans.push(Span::styled(
63 icon.to_string(),
64 Style::default().fg(icon_color),
65 ));
66 }
67
68 spans.push(Span::raw(" "));
69 }
70 }
71 let (entry_name, name_match_ranges) = make_matched_string_printable(
73 &entry.name,
74 entry.name_match_ranges.as_deref(),
75 );
76 let mut last_match_end = 0;
77 for (start, end) in name_match_ranges
78 .iter()
79 .map(|(s, e)| (*s as usize, *e as usize))
80 {
81 spans.push(Span::styled(
83 slice_at_char_boundaries(&entry_name, last_match_end, start)
84 .to_string(),
85 Style::default().fg(colorscheme.result_name_fg),
86 ));
87 spans.push(Span::styled(
89 slice_at_char_boundaries(&entry_name, start, end).to_string(),
90 Style::default().fg(colorscheme.match_foreground_color),
91 ));
92 last_match_end = end;
93 }
94 let next_boundary = next_char_boundary(&entry_name, last_match_end);
97 if next_boundary < entry_name.len() {
98 let remainder = entry_name[next_boundary..].to_string();
99 spans.push(Span::styled(
100 remainder,
101 Style::default().fg(colorscheme.result_name_fg),
102 ));
103 }
104 if let Some(line_number) = entry.line_number {
106 spans.push(Span::styled(
107 format!(":{line_number}"),
108 Style::default().fg(colorscheme.result_line_number_fg),
109 ));
110 }
111 if let Some(preview) = &entry.value {
113 spans.push(Span::raw(": "));
114
115 let (preview, preview_match_ranges) =
116 make_matched_string_printable(
117 preview,
118 entry.value_match_ranges.as_deref(),
119 );
120 let mut last_match_end = 0;
121 for (start, end) in preview_match_ranges
122 .iter()
123 .map(|(s, e)| (*s as usize, *e as usize))
124 {
125 spans.push(Span::styled(
126 slice_at_char_boundaries(&preview, last_match_end, start)
127 .to_string(),
128 Style::default().fg(colorscheme.result_preview_fg),
129 ));
130 spans.push(Span::styled(
131 slice_at_char_boundaries(&preview, start, end).to_string(),
132 Style::default().fg(colorscheme.match_foreground_color),
133 ));
134 last_match_end = end;
135 }
136 let next_boundary = next_char_boundary(&preview, last_match_end);
137 if next_boundary < preview.len() {
138 spans.push(Span::styled(
139 preview[next_boundary..].to_string(),
140 Style::default().fg(colorscheme.result_preview_fg),
141 ));
142 }
143 }
144 Line::from(spans)
145 }))
146 .direction(list_direction)
147 .highlight_style(
148 Style::default().bg(colorscheme.result_selected_bg).bold(),
149 )
150 .highlight_symbol(POINTER_SYMBOL)
151 .block(results_block)
152}
153
154#[allow(clippy::too_many_arguments)]
155pub fn draw_results_list(
156 f: &mut Frame,
157 rect: Rect,
158 entries: &[Entry],
159 selected_entries: &FxHashSet<Entry>,
160 relative_picker_state: &mut ListState,
161 input_bar_position: InputPosition,
162 use_nerd_font_icons: bool,
163 icon_color_cache: &mut FxHashMap<String, Color>,
164 colorscheme: &Colorscheme,
165 help_keybinding: &str,
166 preview_keybinding: &str,
167) -> Result<()> {
168 let results_block = Block::default()
169 .title_top(Line::from(" Results ").alignment(Alignment::Center))
170 .title_bottom(
171 Line::from(format!(
172 " help: <{help_keybinding}> preview: <{preview_keybinding}> "
173 ))
174 .alignment(Alignment::Center),
175 )
176 .borders(Borders::ALL)
177 .border_type(BorderType::Rounded)
178 .border_style(Style::default().fg(colorscheme.general.border_fg))
179 .style(
180 Style::default()
181 .bg(colorscheme.general.background.unwrap_or_default()),
182 )
183 .padding(Padding::right(1));
184
185 let results_list = build_results_list(
186 results_block,
187 entries,
188 Some(selected_entries),
189 match input_bar_position {
190 InputPosition::Bottom => ListDirection::BottomToTop,
191 InputPosition::Top => ListDirection::TopToBottom,
192 },
193 use_nerd_font_icons,
194 icon_color_cache,
195 &colorscheme.results,
196 );
197
198 f.render_stateful_widget(results_list, rect, relative_picker_state);
199 Ok(())
200}