diff --git a/Cargo.lock b/Cargo.lock index e9a0529..1c6cce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3093,6 +3093,7 @@ name = "television-screen" version = "0.0.21" dependencies = [ "color-eyre", + "devicons", "ratatui", "rustc-hash", "serde", diff --git a/Cargo.toml b/Cargo.toml index a92627d..d890fcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ readme = "README.md" [workspace.dependencies] directories = "5.0.1" +devicons = "0.6.11" color-eyre = "0.6.3" lazy_static = "1.5.0" tokio = { version = "1.41.1", features = ["full"] } @@ -104,7 +105,7 @@ copypasta = "0.10.1" [dev-dependencies] criterion = "0.5.1" -devicons = "0.6.11" +devicons = { workspace = true } [[bin]] bench = false diff --git a/crates/television-channels/Cargo.toml b/crates/television-channels/Cargo.toml index b1f53f7..544b4b8 100644 --- a/crates/television-channels/Cargo.toml +++ b/crates/television-channels/Cargo.toml @@ -20,6 +20,7 @@ television-derive = { path = "../television-derive", version = "0.0.21" } tracing = { workspace = true } tokio = { workspace = true, features = ["rt"] } clap = { workspace = true, features = ["derive"] } +devicons = { workspace = true } directories = { workspace = true } color-eyre = { workspace = true } serde = { workspace = true } @@ -27,7 +28,6 @@ lazy_static = { workspace = true } toml = { workspace = true } rustc-hash = { workspace = true } -devicons = "0.6.11" ignore = "0.4.23" strum = { version = "0.26.3", features = ["derive"] } regex = "1.11.1" diff --git a/crates/television-previewers/Cargo.toml b/crates/television-previewers/Cargo.toml index 57657a2..7ebb37a 100644 --- a/crates/television-previewers/Cargo.toml +++ b/crates/television-previewers/Cargo.toml @@ -17,6 +17,7 @@ television-channels = { path = "../television-channels", version = "0.0.21" } television-utils = { path = "../television-utils", version = "0.0.21" } syntect = { workspace = true } +devicons = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } color-eyre = { workspace = true } @@ -24,7 +25,6 @@ lazy_static = { workspace = true } rustc-hash = { workspace = true } parking_lot = "0.12.3" -devicons = "0.6.11" regex = "1.11.1" nom = "7.1" tui = { version = "0.29", default-features = false, package = "ratatui" } diff --git a/crates/television-previewers/src/previewers.rs b/crates/television-previewers/src/previewers.rs index 3595259..132a703 100644 --- a/crates/television-previewers/src/previewers.rs +++ b/crates/television-previewers/src/previewers.rs @@ -1,6 +1,11 @@ use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use cache::PreviewCache; use devicons::FileIcon; +use parking_lot::Mutex; +use rustc_hash::FxHashMap; use television_channels::entry::{Entry, PreviewType}; pub mod basic; @@ -20,6 +25,7 @@ pub use env::EnvVarPreviewerConfig; pub use files::FilePreviewer; pub use files::FilePreviewerConfig; use syntect::highlighting::Style; +use tracing::debug; #[derive(Clone, Debug)] pub enum PreviewContent { @@ -47,7 +53,6 @@ pub struct Preview { pub title: String, pub content: PreviewContent, pub icon: Option, - pub stale: bool, } impl Default for Preview { @@ -56,7 +61,6 @@ impl Default for Preview { title: String::new(), content: PreviewContent::Empty, icon: None, - stale: false, } } } @@ -72,14 +76,6 @@ impl Preview { title, content, icon, - stale, - } - } - - pub fn stale(&self) -> Self { - Preview { - stale: true, - ..self.clone() } } @@ -105,6 +101,8 @@ pub struct Previewer { file: FilePreviewer, env_var: EnvVarPreviewer, command: CommandPreviewer, + requests: FxHashMap, + cache: Arc>, } #[derive(Debug, Default)] @@ -132,6 +130,9 @@ impl PreviewerConfig { } } +const DEBOUNCE_DURATION: Duration = Duration::from_millis(20); +const REQUEST_TIMEOUT: Duration = Duration::from_millis(200); + impl Previewer { pub fn new(config: Option) -> Self { let config = config.unwrap_or_default(); @@ -140,16 +141,54 @@ impl Previewer { file: FilePreviewer::new(Some(config.file)), env_var: EnvVarPreviewer::new(Some(config.env_var)), command: CommandPreviewer::new(Some(config.command)), + requests: FxHashMap::default(), + cache: Arc::new(Mutex::new(PreviewCache::default())), } } - pub fn preview(&mut self, entry: &Entry) -> Arc { - match &entry.preview_type { - PreviewType::Basic => self.basic.preview(entry), - PreviewType::EnvVar => self.env_var.preview(entry), - PreviewType::Files => self.file.preview(entry), - PreviewType::Command(cmd) => self.command.preview(entry, cmd), - PreviewType::None => Arc::new(Preview::default()), + pub fn preview(&mut self, entry: &Entry) -> Option> { + // remove any requests that have timed out + self.requests + .retain(|e, v| v.elapsed() < REQUEST_TIMEOUT || e == entry); + + // if we have a preview in cache, return it + if let Some(preview) = self.cache.lock().get(&entry.name) { + debug!("Preview already in cache"); + return Some(preview); + } + // if we've already acknowledged the request + if let Some(initial_request) = self.requests.get(entry) { + debug!("Request already acknowledged"); + // and we're past the debounce duration + if initial_request.elapsed() > DEBOUNCE_DURATION { + debug!("Past debounce duration"); + // forward the request to the appropriate previewer + let preview = match &entry.preview_type { + PreviewType::Basic => Some(self.basic.preview(entry)), + PreviewType::EnvVar => Some(self.env_var.preview(entry)), + PreviewType::Files => self.file.preview(entry), + PreviewType::Command(cmd) => { + self.command.preview(entry, cmd) + } + PreviewType::None => Some(Arc::new(Preview::default())), + }; + // if we got a preview, cache it + if let Some(preview) = preview { + self.cache.lock().insert(entry.name.clone(), &preview); + Some(preview) + } else { + None + } + } else { + debug!("Not past debounce duration"); + None + } + } + // if we haven't acknowledged the request yet + else { + debug!("Request not acknowledged, acknowledging"); + self.requests.insert(entry.clone(), Instant::now()); + None } } diff --git a/crates/television-previewers/src/previewers/command.rs b/crates/television-previewers/src/previewers/command.rs index 6db4197..9e6c03e 100644 --- a/crates/television-previewers/src/previewers/command.rs +++ b/crates/television-previewers/src/previewers/command.rs @@ -16,7 +16,6 @@ pub struct CommandPreviewer { cache: Arc>, config: CommandPreviewerConfig, concurrent_preview_tasks: Arc, - last_previewed: Arc>>, in_flight_previews: Arc>>, } @@ -53,9 +52,6 @@ impl CommandPreviewer { cache: Arc::new(Mutex::new(PreviewCache::default())), config, concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), - last_previewed: Arc::new(Mutex::new(Arc::new( - Preview::default().stale(), - ))), in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())), } } @@ -64,17 +60,17 @@ impl CommandPreviewer { &mut self, entry: &Entry, command: &PreviewCommand, - ) -> Arc { + ) -> Option> { // do we have a preview in cache for that entry? if let Some(preview) = self.cache.lock().get(&entry.name) { - return preview.clone(); + return Some(preview); } debug!("Preview cache miss for {:?}", entry.name); // are we already computing a preview in the background for that entry? if self.in_flight_previews.lock().contains(&entry.name) { debug!("Preview already in flight for {:?}", entry.name); - return self.last_previewed.lock().clone(); + return None; } if self.concurrent_preview_tasks.load(Ordering::Relaxed) @@ -86,21 +82,14 @@ impl CommandPreviewer { let entry_c = entry.clone(); let concurrent_tasks = self.concurrent_preview_tasks.clone(); let command = command.clone(); - let last_previewed = self.last_previewed.clone(); tokio::spawn(async move { - try_preview( - &command, - &entry_c, - &cache, - &concurrent_tasks, - &last_previewed, - ); + try_preview(&command, &entry_c, &cache, &concurrent_tasks); }); } else { debug!("Too many concurrent preview tasks running"); } - self.last_previewed.lock().clone() + None } } @@ -149,7 +138,6 @@ pub fn try_preview( entry: &Entry, cache: &Arc>, concurrent_tasks: &Arc, - last_previewed: &Arc>>, ) { debug!("Computing preview for {:?}", entry.name); let command = format_command(command, entry); @@ -170,8 +158,6 @@ pub fn try_preview( )); cache.lock().insert(entry.name.clone(), &preview); - let mut tp = last_previewed.lock(); - *tp = preview.stale().into(); } else { let content = String::from_utf8_lossy(&output.stderr); let preview = Arc::new(Preview::new( diff --git a/crates/television-previewers/src/previewers/files.rs b/crates/television-previewers/src/previewers/files.rs index f560368..a7f14bc 100644 --- a/crates/television-previewers/src/previewers/files.rs +++ b/crates/television-previewers/src/previewers/files.rs @@ -28,7 +28,6 @@ pub struct FilePreviewer { pub syntax_set: Arc, pub syntax_theme: Arc, concurrent_preview_tasks: Arc, - last_previewed: Arc>>, in_flight_previews: Arc>>, } @@ -72,9 +71,6 @@ impl FilePreviewer { syntax_set: Arc::new(syntax_set), syntax_theme: Arc::new(theme), concurrent_preview_tasks: Arc::new(AtomicU8::new(0)), - last_previewed: Arc::new(Mutex::new(Arc::new( - Preview::default().stale(), - ))), in_flight_previews: Arc::new(Mutex::new(HashSet::with_hasher( FxBuildHasher, ))), @@ -85,17 +81,17 @@ impl FilePreviewer { /// /// # Panics /// Panics if seeking to the start of the file fails. - pub fn preview(&mut self, entry: &entry::Entry) -> Arc { + pub fn preview(&mut self, entry: &entry::Entry) -> Option> { // do we have a preview in cache for that entry? if let Some(preview) = self.cache.lock().get(&entry.name) { - return preview; + return Some(preview); } debug!("Preview cache miss for {:?}", entry.name); // are we already computing a preview in the background for that entry? if self.in_flight_previews.lock().contains(&entry.name) { debug!("Preview already in flight for {:?}", entry.name); - return self.last_previewed.lock().clone(); + return None; } if self.concurrent_preview_tasks.load(Ordering::Relaxed) @@ -109,7 +105,6 @@ impl FilePreviewer { let syntax_set = self.syntax_set.clone(); let syntax_theme = self.syntax_theme.clone(); let concurrent_tasks = self.concurrent_preview_tasks.clone(); - let last_previewed = self.last_previewed.clone(); let in_flight_previews = self.in_flight_previews.clone(); tokio::spawn(async move { try_preview( @@ -118,13 +113,12 @@ impl FilePreviewer { &syntax_set, &syntax_theme, &concurrent_tasks, - &last_previewed, &in_flight_previews, ); }); } - self.last_previewed.lock().clone() + None } #[allow(dead_code)] @@ -139,7 +133,6 @@ pub fn try_preview( syntax_set: &Arc, syntax_theme: &Arc, concurrent_tasks: &Arc, - last_previewed: &Arc>>, in_flight_previews: &Arc>>, ) { debug!("Computing preview for {:?}", entry.name); @@ -166,8 +159,6 @@ pub fn try_preview( ); cache.lock().insert(entry.name.clone(), &preview); in_flight_previews.lock().remove(&entry.name); - let mut tp = last_previewed.lock(); - *tp = preview.stale().into(); } Err(e) => { warn!("Error opening file: {:?}", e); diff --git a/crates/television-screen/Cargo.toml b/crates/television-screen/Cargo.toml index 9ae942e..c0488d2 100644 --- a/crates/television-screen/Cargo.toml +++ b/crates/television-screen/Cargo.toml @@ -23,6 +23,7 @@ color-eyre = { workspace = true } syntect = { workspace = true } rustc-hash = { workspace = true } tracing = { workspace = true } +devicons = { workspace = true } [lints] workspace = true diff --git a/crates/television-screen/src/cache.rs b/crates/television-screen/src/cache.rs index 526f7bf..da35566 100644 --- a/crates/television-screen/src/cache.rs +++ b/crates/television-screen/src/cache.rs @@ -1,15 +1,41 @@ +use devicons::FileIcon; use rustc_hash::FxHashMap; use std::sync::Arc; use ratatui::widgets::Paragraph; use television_utils::cache::RingSet; -const DEFAULT_RENDERED_PREVIEW_CACHE_SIZE: usize = 25; +const DEFAULT_RENDERED_PREVIEW_CACHE_SIZE: usize = 10; + +#[derive(Clone, Debug)] +pub struct CachedPreview<'a> { + pub key: String, + pub icon: Option, + pub title: String, + pub paragraph: Arc>, +} + +impl<'a> CachedPreview<'a> { + pub fn new( + key: String, + icon: Option, + title: String, + paragraph: Arc>, + ) -> Self { + CachedPreview { + key, + icon, + title, + paragraph, + } + } +} #[derive(Debug)] pub struct RenderedPreviewCache<'a> { - previews: FxHashMap>>, + previews: FxHashMap>, ring_set: RingSet, + pub last_preview: Option>, } impl<'a> RenderedPreviewCache<'a> { @@ -17,15 +43,29 @@ impl<'a> RenderedPreviewCache<'a> { RenderedPreviewCache { previews: FxHashMap::default(), ring_set: RingSet::with_capacity(capacity), + last_preview: None, } } - pub fn get(&self, key: &str) -> Option>> { + pub fn get(&self, key: &str) -> Option> { self.previews.get(key).cloned() } - pub fn insert(&mut self, key: String, preview: &Arc>) { - self.previews.insert(key.clone(), preview.clone()); + pub fn insert( + &mut self, + key: String, + icon: Option, + title: String, + paragraph: &Arc>, + ) { + let cached_preview = CachedPreview::new( + key.clone(), + icon, + title.clone(), + paragraph.clone(), + ); + self.last_preview = Some(cached_preview.clone()); + self.previews.insert(key.clone(), cached_preview); if let Some(oldest_key) = self.ring_set.push(key) { self.previews.remove(&oldest_key); } diff --git a/crates/television-screen/src/preview.rs b/crates/television-screen/src/preview.rs index b1ee114..180b4aa 100644 --- a/crates/television-screen/src/preview.rs +++ b/crates/television-screen/src/preview.rs @@ -3,6 +3,7 @@ use crate::{ colors::{Colorscheme, PreviewColorscheme}, }; use color_eyre::eyre::Result; +use devicons::FileIcon; use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap}; use ratatui::Frame; use ratatui::{ @@ -22,20 +23,28 @@ use television_utils::strings::{ replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig, EMPTY_STRING, }; +use tracing::debug; #[allow(dead_code)] const FILL_CHAR_SLANTED: char = '╱'; const FILL_CHAR_EMPTY: char = ' '; #[allow(clippy::needless_pass_by_value)] -pub fn build_preview_paragraph( - preview_block: Block<'_>, +pub fn build_preview_paragraph<'a>( + //preview_block: Block<'_>, inner: Rect, preview_content: PreviewContent, target_line: Option, preview_scroll: u16, colorscheme: Colorscheme, -) -> Paragraph<'_> { +) -> Paragraph<'a> { + let preview_block = + Block::default().style(Style::default()).padding(Padding { + top: 0, + right: 1, + bottom: 0, + left: 1, + }); match preview_content { PreviewContent::AnsiText(text) => { build_ansi_text_paragraph(text, preview_block, preview_scroll) @@ -244,20 +253,18 @@ pub fn build_meta_preview_paragraph<'a>( Paragraph::new(Text::from(lines)) } -#[allow(clippy::too_many_arguments)] -pub fn draw_preview_content_block( +fn draw_content_outer_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<()> { + icon: Option, + title: &str, + use_nerd_font_icons: bool, +) -> 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(); + // optional icon + if icon.is_some() && use_nerd_font_icons { + let icon = icon.as_ref().unwrap(); preview_title_spans.push(Span::styled( { let mut icon_str = String::from(icon.icon); @@ -267,10 +274,11 @@ pub fn draw_preview_content_block( Style::default().fg(Color::from_str(icon.color)?), )); } + // preview title preview_title_spans.push(Span::styled( shrink_with_ellipsis( &replace_non_printable( - preview.title.as_bytes(), + title.as_bytes(), &ReplaceNonPrintableConfig::default(), ) .0, @@ -279,6 +287,8 @@ pub fn draw_preview_content_block( Style::default().fg(colorscheme.preview.title_fg).bold(), )); preview_title_spans.push(Span::from(" ")); + + // build the preview block let preview_outer_block = Block::default() .title_top( Line::from(preview_title_spans) @@ -294,47 +304,136 @@ pub fn draw_preview_content_block( ) .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); + Ok(inner) +} - let target_line = entry.line_number.map(|l| u16::try_from(l).unwrap_or(0)); - let cache_key = compute_cache_key(entry); +#[allow(clippy::too_many_arguments)] +pub fn draw_preview_content_block( + f: &mut Frame, + rect: Rect, + entry: &Entry, + preview: &Option>, + rendered_preview_cache: &Arc>>, + preview_scroll: u16, + use_nerd_font_icons: bool, + colorscheme: &Colorscheme, +) -> Result<()> { + if let Some(preview) = preview { + debug!("preview is Some"); + let inner = draw_content_outer_block( + f, + rect, + colorscheme, + preview.icon, + &preview.title, + use_nerd_font_icons, + )?; - // 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 + // check if the rendered preview content is already in the cache + let cache_key = compute_cache_key(entry); + let cached_preview = rendered_preview_cache .lock() .unwrap() - .insert(cache_key, &Arc::new(rp.clone())); + .get(&cache_key) + .into_iter(); + if let Some(rp) = cached_preview.clone().next() { + debug!("cached preview found"); + let p = rp.paragraph.as_ref().clone(); + f.render_widget(p.scroll((preview_scroll, 0)), inner); + return Ok(()); + } else { + debug!("cached preview not found"); + // render the preview content and cache it if not empty + let rp = build_preview_paragraph( + //preview_inner_block, + inner, + preview.content.clone(), + entry.line_number.map(|l| u16::try_from(l).unwrap_or(0)), + preview_scroll, + colorscheme.clone(), + ); + debug!("inserting into cache"); + rendered_preview_cache.lock().unwrap().insert( + cache_key, + preview.icon, + preview.title.clone(), + &Arc::new(rp.clone()), + ); + debug!("rendering widget"); + f.render_widget(rp.scroll((preview_scroll, 0)), inner); + return Ok(()); + } + // else if last_preview exists + } else { + debug!("preview is None"); + let maybe_last_preview = + &rendered_preview_cache.lock().unwrap().last_preview.clone(); + + if let Some(last_preview) = maybe_last_preview { + let inner = draw_content_outer_block( + f, + rect, + colorscheme, + last_preview.icon, + &last_preview.title, + use_nerd_font_icons, + )?; + + // check if the rendered preview content is already in the cache + let cache_key = last_preview.key.clone(); + let maybe_cached_preview = + rendered_preview_cache.lock().unwrap().get(&cache_key); + + if let Some(cached_preview) = maybe_cached_preview.clone() { + let p = cached_preview.paragraph.as_ref().clone(); + f.render_widget(p.scroll((preview_scroll, 0)), inner); + return Ok(()); + } else { + // render the preview content and cache it if not empty + let rp = build_preview_paragraph( + inner, + PreviewContent::Empty, + None, + preview_scroll, + colorscheme.clone(), + ); + rendered_preview_cache.lock().unwrap().insert( + cache_key, + last_preview.icon, + last_preview.title.clone(), + &Arc::new(rp.clone()), + ); + f.render_widget(rp.scroll((preview_scroll, 0)), inner); + } + return Ok(()); + } + // render empty preview + let inner = draw_content_outer_block( + f, + rect, + colorscheme, + None, + "", + use_nerd_font_icons, + )?; + let preview_outer_block = Block::default() + .title_top(Line::from(Span::styled( + " ", + 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)); + f.render_widget(preview_outer_block, inner); } - f.render_widget( - Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)), - inner, - ); + Ok(()) } diff --git a/crates/television/television.rs b/crates/television/television.rs index 890424a..69bec66 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -560,20 +560,24 @@ impl Television { && !matches!(selected_entry.preview_type, PreviewType::None) { // preview content - let preview = self.previewer.preview(&selected_entry); - self.current_preview_total_lines = preview.total_lines(); - // initialize preview scroll - self.maybe_init_preview_scroll( - selected_entry - .line_number - .map(|l| u16::try_from(l).unwrap_or(0)), - layout.preview_window.unwrap().height, - ); + let maybe_preview = self.previewer.preview(&selected_entry); + + if let Some(preview) = &maybe_preview { + self.current_preview_total_lines = preview.total_lines(); + // initialize preview scroll + self.maybe_init_preview_scroll( + selected_entry + .line_number + .map(|l| u16::try_from(l).unwrap_or(0)), + layout.preview_window.unwrap().height, + ); + } + draw_preview_content_block( f, layout.preview_window.unwrap(), &selected_entry, - &preview, + &maybe_preview, &self.rendered_preview_cache, self.preview_scroll.unwrap_or(0), self.config.ui.use_nerd_font_icons,