mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-09 04:55:26 +00:00
417 lines
13 KiB
Rust
417 lines
13 KiB
Rust
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<u16>,
|
||
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<String>,
|
||
preview_block: Block<'_>,
|
||
target_line: Option<u16>,
|
||
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<Vec<(syntect::highlighting::Style, String)>>,
|
||
preview_block: Block,
|
||
target_line: Option<u16>,
|
||
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<Preview>,
|
||
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
|
||
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.unwrap_or_default()),
|
||
)
|
||
.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<usize>,
|
||
colorscheme: PreviewColorscheme,
|
||
) -> Paragraph<'static> {
|
||
let preview_lines: Vec<Line> = 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<Color>,
|
||
) -> 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
|
||
}
|