use crate::{ cache::RenderedPreviewCache, colors::{Colorscheme, PreviewColorscheme}, }; use color_eyre::eyre::Result; use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap}; use ratatui::Frame; use ratatui::{ layout::{Alignment, Rect}, prelude::{Color, Line, Modifier, Span, Style, Stylize, Text}, }; use std::str::FromStr; use std::sync::{Arc, Mutex}; use television_channels::entry::Entry; use television_previewers::{ ansi::IntoText, previewers::{ Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG, }, }; use television_utils::strings::{ replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig, EMPTY_STRING, }; #[allow(dead_code)] const FILL_CHAR_SLANTED: char = '╱'; const FILL_CHAR_EMPTY: char = ' '; pub fn build_preview_paragraph( preview_block: Block<'_>, inner: Rect, preview_content: PreviewContent, target_line: Option, preview_scroll: u16, colorscheme: Colorscheme, ) -> Paragraph<'_> { match preview_content { PreviewContent::AnsiText(text) => { build_ansi_text_paragraph(text, preview_block, preview_scroll) } PreviewContent::PlainText(content) => build_plain_text_paragraph( content, preview_block, target_line, preview_scroll, colorscheme.preview, ), PreviewContent::PlainTextWrapped(content) => { build_plain_text_wrapped_paragraph( content, preview_block, colorscheme.preview, ) } PreviewContent::SyntectHighlightedText(highlighted_lines) => { build_syntect_highlighted_paragraph( highlighted_lines, preview_block, target_line, preview_scroll, colorscheme.preview, ) } // meta PreviewContent::Loading => { build_meta_preview_paragraph(inner, "Loading...", FILL_CHAR_EMPTY) .block(preview_block) .alignment(Alignment::Left) .style(Style::default().add_modifier(Modifier::ITALIC)) } PreviewContent::NotSupported => build_meta_preview_paragraph( inner, PREVIEW_NOT_SUPPORTED_MSG, FILL_CHAR_EMPTY, ) .block(preview_block) .alignment(Alignment::Left) .style(Style::default().add_modifier(Modifier::ITALIC)), PreviewContent::FileTooLarge => build_meta_preview_paragraph( inner, FILE_TOO_LARGE_MSG, FILL_CHAR_EMPTY, ) .block(preview_block) .alignment(Alignment::Left) .style(Style::default().add_modifier(Modifier::ITALIC)), PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)), } } fn build_ansi_text_paragraph( text: String, preview_block: Block, preview_scroll: u16, ) -> Paragraph { let text = replace_non_printable( text.as_bytes(), &ReplaceNonPrintableConfig { replace_line_feed: false, replace_control_characters: false, ..Default::default() }, ) .0 .into_text() .unwrap(); Paragraph::new(text) .block(preview_block) .scroll((preview_scroll, 0)) } fn build_plain_text_paragraph( text: Vec, preview_block: Block<'_>, target_line: Option, preview_scroll: u16, colorscheme: PreviewColorscheme, ) -> Paragraph<'_> { let mut lines = Vec::new(); for (i, line) in text.iter().enumerate() { lines.push(Line::from(vec![ build_line_number_span(i + 1).style(Style::default().fg( if matches!( target_line, Some(l) if l == u16::try_from(i).unwrap_or(0) + 1 ) { colorscheme.gutter_selected_fg } else { colorscheme.gutter_fg }, )), Span::styled(" │ ", Style::default().fg(colorscheme.gutter_fg).dim()), Span::styled( line.to_string(), Style::default().fg(colorscheme.content_fg).bg( if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) { colorscheme.highlight_bg } else { Color::Reset }, ), ), ])); } let text = Text::from(lines); Paragraph::new(text) .block(preview_block) .scroll((preview_scroll, 0)) } fn build_plain_text_wrapped_paragraph( text: String, preview_block: Block<'_>, colorscheme: PreviewColorscheme, ) -> Paragraph<'_> { let mut lines = Vec::new(); for line in text.lines() { lines.push(Line::styled( line.to_string(), Style::default().fg(colorscheme.content_fg), )); } let text = Text::from(lines); Paragraph::new(text) .block(preview_block) .wrap(Wrap { trim: true }) } fn build_syntect_highlighted_paragraph( highlighted_lines: Vec>, preview_block: Block, target_line: Option, preview_scroll: u16, colorscheme: PreviewColorscheme, ) -> Paragraph { compute_paragraph_from_highlighted_lines( &highlighted_lines, target_line.map(|l| l as usize), colorscheme, ) .block(preview_block) .alignment(Alignment::Left) .scroll((preview_scroll, 0)) } pub fn build_meta_preview_paragraph<'a>( inner: Rect, message: &str, fill_char: char, ) -> Paragraph<'a> { let message_len = message.len(); if message_len + 8 > inner.width as usize { return Paragraph::new(Text::from(EMPTY_STRING)); } let fill_char_str = fill_char.to_string(); let fill_line = fill_char_str.repeat(inner.width as usize); // Build the paragraph content with slanted lines and center the custom message let mut lines = Vec::new(); // Calculate the vertical center let vertical_center = inner.height as usize / 2; let horizontal_padding = (inner.width as usize - message_len) / 2 - 4; // Fill the paragraph with slanted lines and insert the centered custom message for i in 0..inner.height { if i as usize == vertical_center { // Center the message horizontally in the middle line let line = format!( "{} {} {}", fill_char_str.repeat(horizontal_padding), message, fill_char_str.repeat( inner.width as usize - horizontal_padding - message_len ) ); lines.push(Line::from(line)); } else if i as usize + 1 == vertical_center || (i as usize).saturating_sub(1) == vertical_center { let line = format!( "{} {} {}", fill_char_str.repeat(horizontal_padding), " ".repeat(message_len), fill_char_str.repeat( inner.width as usize - horizontal_padding - message_len ) ); lines.push(Line::from(line)); } else { lines.push(Line::from(fill_line.clone())); } } // Create a paragraph with the generated content Paragraph::new(Text::from(lines)) } #[allow(clippy::too_many_arguments)] pub fn draw_preview_content_block( f: &mut Frame, rect: Rect, entry: &Entry, preview: &Arc, rendered_preview_cache: &Arc>>, preview_scroll: u16, use_nerd_font_icons: bool, colorscheme: &Colorscheme, ) -> Result<()> { let mut preview_title_spans = vec![Span::from(" ")]; if preview.icon.is_some() && use_nerd_font_icons { let icon = preview.icon.as_ref().unwrap(); preview_title_spans.push(Span::styled( { let mut icon_str = String::from(icon.icon); icon_str.push(' '); icon_str }, Style::default().fg(Color::from_str(icon.color)?), )); } preview_title_spans.push(Span::styled( shrink_with_ellipsis( &replace_non_printable( preview.title.as_bytes(), &ReplaceNonPrintableConfig::default(), ) .0, rect.width.saturating_sub(4) as usize, ), Style::default().fg(colorscheme.preview.title_fg).bold(), )); preview_title_spans.push(Span::from(" ")); let preview_outer_block = Block::default() .title_top( Line::from(preview_title_spans) .alignment(Alignment::Center) .style(Style::default().fg(colorscheme.preview.title_fg)), ) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(colorscheme.general.border_fg)) .style(Style::default().bg(colorscheme.general.background)) .padding(Padding::new(0, 1, 1, 0)); let preview_inner_block = Block::default().style(Style::default()).padding(Padding { top: 0, right: 1, bottom: 0, left: 1, }); let inner = preview_outer_block.inner(rect); f.render_widget(preview_outer_block, rect); let target_line = entry.line_number.map(|l| u16::try_from(l).unwrap_or(0)); let cache_key = compute_cache_key(entry); // Check if the rendered preview content is already in the cache if let Some(preview_paragraph) = rendered_preview_cache.lock().unwrap().get(&cache_key) { let p = preview_paragraph.as_ref().clone(); f.render_widget(p.scroll((preview_scroll, 0)), inner); return Ok(()); } // If not, render the preview content and cache it if not empty let c_scheme = colorscheme.clone(); let rp = build_preview_paragraph( preview_inner_block, inner, preview.content.clone(), target_line, preview_scroll, c_scheme, ); if !preview.stale { rendered_preview_cache .lock() .unwrap() .insert(cache_key, &Arc::new(rp.clone())); } f.render_widget( Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)), inner, ); Ok(()) } fn build_line_number_span<'a>(line_number: usize) -> Span<'a> { Span::from(format!("{line_number:5} ")) } fn compute_paragraph_from_highlighted_lines( highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>], line_specifier: Option, colorscheme: PreviewColorscheme, ) -> Paragraph<'static> { let preview_lines: Vec = highlighted_lines .iter() .enumerate() .map(|(i, l)| { let line_number = build_line_number_span(i + 1).style(Style::default().fg( if line_specifier.is_some() && i == line_specifier.unwrap().saturating_sub(1) { colorscheme.gutter_selected_fg } else { colorscheme.gutter_fg }, )); Line::from_iter( std::iter::once(line_number) .chain(std::iter::once(Span::styled( " │ ", Style::default().fg(colorscheme.gutter_fg).dim(), ))) .chain(l.iter().cloned().map(|sr| { convert_syn_region_to_span( &(sr.0, sr.1), if line_specifier.is_some() && i == line_specifier .unwrap() .saturating_sub(1) { Some(colorscheme.highlight_bg) } else { None }, ) })), ) }) .collect(); Paragraph::new(preview_lines) } pub fn convert_syn_region_to_span<'a>( syn_region: &(syntect::highlighting::Style, String), background: Option, ) -> Span<'a> { let mut style = Style::default() .fg(convert_syn_color_to_ratatui_color(syn_region.0.foreground)); if let Some(background) = background { style = style.bg(background); } style = match syn_region.0.font_style { syntect::highlighting::FontStyle::BOLD => style.bold(), syntect::highlighting::FontStyle::ITALIC => style.italic(), syntect::highlighting::FontStyle::UNDERLINE => style.underlined(), _ => style, }; Span::styled(syn_region.1.clone(), style) } fn convert_syn_color_to_ratatui_color( color: syntect::highlighting::Color, ) -> Color { Color::Rgb(color.r, color.g, color.b) } fn compute_cache_key(entry: &Entry) -> String { let mut cache_key = entry.name.clone(); if let Some(line_number) = entry.line_number { cache_key.push_str(&line_number.to_string()); } cache_key }