2024-12-02 16:20:13 +01:00

230 lines
7.7 KiB
Rust

use crate::television::Television;
use crate::ui::layout::InputPosition;
use crate::ui::BORDER_COLOR;
use color_eyre::eyre::Result;
use ratatui::layout::{Alignment, Rect};
use ratatui::prelude::{Color, Line, Span, Style};
use ratatui::widgets::{
Block, BorderType, Borders, List, ListDirection, Padding,
};
use ratatui::Frame;
use std::collections::HashMap;
use std::str::FromStr;
use television_channels::channels::OnAir;
use television_channels::entry::Entry;
use television_utils::strings::{
make_matched_string_printable, next_char_boundary,
slice_at_char_boundaries,
};
// Styles
const DEFAULT_RESULT_NAME_FG: Color = Color::Blue;
const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150);
const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow;
const DEFAULT_RESULT_SELECTED_BG: Color = Color::Rgb(50, 50, 50);
pub struct ResultsListColors {
pub result_name_fg: Color,
pub result_preview_fg: Color,
pub result_line_number_fg: Color,
pub result_selected_bg: Color,
}
impl Default for ResultsListColors {
fn default() -> Self {
Self {
result_name_fg: DEFAULT_RESULT_NAME_FG,
result_preview_fg: DEFAULT_RESULT_PREVIEW_FG,
result_line_number_fg: DEFAULT_RESULT_LINE_NUMBER_FG,
result_selected_bg: DEFAULT_RESULT_SELECTED_BG,
}
}
}
#[allow(dead_code)]
impl ResultsListColors {
pub fn result_name_fg(mut self, color: Color) -> Self {
self.result_name_fg = color;
self
}
pub fn result_preview_fg(mut self, color: Color) -> Self {
self.result_preview_fg = color;
self
}
pub fn result_line_number_fg(mut self, color: Color) -> Self {
self.result_line_number_fg = color;
self
}
pub fn result_selected_bg(mut self, color: Color) -> Self {
self.result_selected_bg = color;
self
}
}
pub fn build_results_list<'a, 'b>(
results_block: Block<'b>,
entries: &'a [Entry],
list_direction: ListDirection,
results_list_colors: Option<ResultsListColors>,
use_icons: bool,
icon_color_cache: &mut HashMap<String, Color>,
) -> List<'a>
where
'b: 'a,
{
let results_list_colors = results_list_colors.unwrap_or_default();
List::new(entries.iter().map(|entry| {
let mut spans = Vec::new();
// optional icon
if let Some(icon) = entry.icon.as_ref() {
if use_icons {
if let Some(icon_color) = icon_color_cache.get(icon.color) {
spans.push(Span::styled(
icon.to_string(),
Style::default().fg(*icon_color),
));
} else {
let icon_color = Color::from_str(icon.color).unwrap();
icon_color_cache
.insert(icon.color.to_string(), icon_color);
spans.push(Span::styled(
icon.to_string(),
Style::default().fg(icon_color),
));
}
spans.push(Span::raw(" "));
}
}
// entry name
let (entry_name, name_match_ranges) = make_matched_string_printable(
&entry.name,
entry.name_match_ranges.as_deref(),
);
let mut last_match_end = 0;
for (start, end) in name_match_ranges
.iter()
.map(|(s, e)| (*s as usize, *e as usize))
{
// from the end of the last match to the start of the current one
spans.push(Span::styled(
slice_at_char_boundaries(&entry_name, last_match_end, start)
.to_string(),
Style::default().fg(results_list_colors.result_name_fg),
));
// the current match
spans.push(Span::styled(
slice_at_char_boundaries(&entry_name, start, end).to_string(),
Style::default().fg(Color::Red),
));
last_match_end = end;
}
// we need to push a span for the remainder of the entry name
// but only if there's something left
let next_boundary = next_char_boundary(&entry_name, last_match_end);
if next_boundary < entry_name.len() {
let remainder = entry_name[next_boundary..].to_string();
spans.push(Span::styled(
remainder,
Style::default().fg(results_list_colors.result_name_fg),
));
}
// optional line number
if let Some(line_number) = entry.line_number {
spans.push(Span::styled(
format!(":{line_number}"),
Style::default().fg(results_list_colors.result_line_number_fg),
));
}
// optional preview
if let Some(preview) = &entry.value {
spans.push(Span::raw(": "));
let (preview, preview_match_ranges) =
make_matched_string_printable(
preview,
entry.value_match_ranges.as_deref(),
);
let mut last_match_end = 0;
for (start, end) in preview_match_ranges
.iter()
.map(|(s, e)| (*s as usize, *e as usize))
{
spans.push(Span::styled(
slice_at_char_boundaries(&preview, last_match_end, start)
.to_string(),
Style::default().fg(results_list_colors.result_preview_fg),
));
spans.push(Span::styled(
slice_at_char_boundaries(&preview, start, end).to_string(),
Style::default().fg(Color::Red),
));
last_match_end = end;
}
let next_boundary = next_char_boundary(&preview, last_match_end);
if next_boundary < preview.len() {
spans.push(Span::styled(
preview[next_boundary..].to_string(),
Style::default().fg(results_list_colors.result_preview_fg),
));
}
}
Line::from(spans)
}))
.direction(list_direction)
.highlight_style(
Style::default().bg(results_list_colors.result_selected_bg),
)
.highlight_symbol("> ")
.block(results_block)
}
impl Television {
pub(crate) fn draw_results_list(
&mut self,
f: &mut Frame,
rect: Rect,
) -> Result<()> {
let results_block = Block::default()
.title_top(Line::from(" Results ").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(BORDER_COLOR))
.style(Style::default())
.padding(Padding::right(1));
let result_count = self.channel.result_count();
if result_count > 0 && self.results_picker.selected().is_none() {
self.results_picker.select(Some(0));
self.results_picker.relative_select(Some(0));
}
let entries = self.channel.results(
rect.height.saturating_sub(2).into(),
u32::try_from(self.results_picker.offset())?,
);
let results_list = build_results_list(
results_block,
&entries,
match self.config.ui.input_bar_position {
InputPosition::Bottom => ListDirection::BottomToTop,
InputPosition::Top => ListDirection::TopToBottom,
},
None,
self.config.ui.use_nerd_font_icons,
&mut self.icon_color_cache,
);
f.render_stateful_widget(
results_list,
rect,
&mut self.results_picker.relative_state,
);
Ok(())
}
}