mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 03:55:23 +00:00
perf: preview
This commit is contained in:
parent
d68ae21630
commit
cb77f5c663
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3093,6 +3093,7 @@ name = "television-screen"
|
|||||||
version = "0.0.21"
|
version = "0.0.21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"devicons",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -56,6 +56,7 @@ readme = "README.md"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
|
devicons = "0.6.11"
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
tokio = { version = "1.41.1", features = ["full"] }
|
tokio = { version = "1.41.1", features = ["full"] }
|
||||||
@ -104,7 +105,7 @@ copypasta = "0.10.1"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.5.1"
|
criterion = "0.5.1"
|
||||||
devicons = "0.6.11"
|
devicons = { workspace = true }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
bench = false
|
bench = false
|
||||||
|
@ -20,6 +20,7 @@ television-derive = { path = "../television-derive", version = "0.0.21" }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt"] }
|
tokio = { workspace = true, features = ["rt"] }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
devicons = { workspace = true }
|
||||||
directories = { workspace = true }
|
directories = { workspace = true }
|
||||||
color-eyre = { workspace = true }
|
color-eyre = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
@ -27,7 +28,6 @@ lazy_static = { workspace = true }
|
|||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
|
||||||
devicons = "0.6.11"
|
|
||||||
ignore = "0.4.23"
|
ignore = "0.4.23"
|
||||||
strum = { version = "0.26.3", features = ["derive"] }
|
strum = { version = "0.26.3", features = ["derive"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
@ -17,6 +17,7 @@ television-channels = { path = "../television-channels", version = "0.0.21" }
|
|||||||
television-utils = { path = "../television-utils", version = "0.0.21" }
|
television-utils = { path = "../television-utils", version = "0.0.21" }
|
||||||
|
|
||||||
syntect = { workspace = true }
|
syntect = { workspace = true }
|
||||||
|
devicons = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
color-eyre = { workspace = true }
|
color-eyre = { workspace = true }
|
||||||
@ -24,7 +25,6 @@ lazy_static = { workspace = true }
|
|||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
devicons = "0.6.11"
|
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
nom = "7.1"
|
nom = "7.1"
|
||||||
tui = { version = "0.29", default-features = false, package = "ratatui" }
|
tui = { version = "0.29", default-features = false, package = "ratatui" }
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use cache::PreviewCache;
|
||||||
use devicons::FileIcon;
|
use devicons::FileIcon;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use television_channels::entry::{Entry, PreviewType};
|
use television_channels::entry::{Entry, PreviewType};
|
||||||
|
|
||||||
pub mod basic;
|
pub mod basic;
|
||||||
@ -20,6 +25,7 @@ pub use env::EnvVarPreviewerConfig;
|
|||||||
pub use files::FilePreviewer;
|
pub use files::FilePreviewer;
|
||||||
pub use files::FilePreviewerConfig;
|
pub use files::FilePreviewerConfig;
|
||||||
use syntect::highlighting::Style;
|
use syntect::highlighting::Style;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PreviewContent {
|
pub enum PreviewContent {
|
||||||
@ -47,7 +53,6 @@ pub struct Preview {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: PreviewContent,
|
pub content: PreviewContent,
|
||||||
pub icon: Option<FileIcon>,
|
pub icon: Option<FileIcon>,
|
||||||
pub stale: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Preview {
|
impl Default for Preview {
|
||||||
@ -56,7 +61,6 @@ impl Default for Preview {
|
|||||||
title: String::new(),
|
title: String::new(),
|
||||||
content: PreviewContent::Empty,
|
content: PreviewContent::Empty,
|
||||||
icon: None,
|
icon: None,
|
||||||
stale: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,14 +76,6 @@ impl Preview {
|
|||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
icon,
|
icon,
|
||||||
stale,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stale(&self) -> Self {
|
|
||||||
Preview {
|
|
||||||
stale: true,
|
|
||||||
..self.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +101,8 @@ pub struct Previewer {
|
|||||||
file: FilePreviewer,
|
file: FilePreviewer,
|
||||||
env_var: EnvVarPreviewer,
|
env_var: EnvVarPreviewer,
|
||||||
command: CommandPreviewer,
|
command: CommandPreviewer,
|
||||||
|
requests: FxHashMap<Entry, Instant>,
|
||||||
|
cache: Arc<Mutex<PreviewCache>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[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 {
|
impl Previewer {
|
||||||
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
pub fn new(config: Option<PreviewerConfig>) -> Self {
|
||||||
let config = config.unwrap_or_default();
|
let config = config.unwrap_or_default();
|
||||||
@ -140,16 +141,54 @@ impl Previewer {
|
|||||||
file: FilePreviewer::new(Some(config.file)),
|
file: FilePreviewer::new(Some(config.file)),
|
||||||
env_var: EnvVarPreviewer::new(Some(config.env_var)),
|
env_var: EnvVarPreviewer::new(Some(config.env_var)),
|
||||||
command: CommandPreviewer::new(Some(config.command)),
|
command: CommandPreviewer::new(Some(config.command)),
|
||||||
|
requests: FxHashMap::default(),
|
||||||
|
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preview(&mut self, entry: &Entry) -> Arc<Preview> {
|
pub fn preview(&mut self, entry: &Entry) -> Option<Arc<Preview>> {
|
||||||
match &entry.preview_type {
|
// remove any requests that have timed out
|
||||||
PreviewType::Basic => self.basic.preview(entry),
|
self.requests
|
||||||
PreviewType::EnvVar => self.env_var.preview(entry),
|
.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::Files => self.file.preview(entry),
|
||||||
PreviewType::Command(cmd) => self.command.preview(entry, cmd),
|
PreviewType::Command(cmd) => {
|
||||||
PreviewType::None => Arc::new(Preview::default()),
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ pub struct CommandPreviewer {
|
|||||||
cache: Arc<Mutex<PreviewCache>>,
|
cache: Arc<Mutex<PreviewCache>>,
|
||||||
config: CommandPreviewerConfig,
|
config: CommandPreviewerConfig,
|
||||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||||
last_previewed: Arc<Mutex<Arc<Preview>>>,
|
|
||||||
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +52,6 @@ impl CommandPreviewer {
|
|||||||
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
cache: Arc::new(Mutex::new(PreviewCache::default())),
|
||||||
config,
|
config,
|
||||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
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())),
|
in_flight_previews: Arc::new(Mutex::new(FxHashSet::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,17 +60,17 @@ impl CommandPreviewer {
|
|||||||
&mut self,
|
&mut self,
|
||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
command: &PreviewCommand,
|
command: &PreviewCommand,
|
||||||
) -> Arc<Preview> {
|
) -> Option<Arc<Preview>> {
|
||||||
// do we have a preview in cache for that entry?
|
// do we have a preview in cache for that entry?
|
||||||
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
||||||
return preview.clone();
|
return Some(preview);
|
||||||
}
|
}
|
||||||
debug!("Preview cache miss for {:?}", entry.name);
|
debug!("Preview cache miss for {:?}", entry.name);
|
||||||
|
|
||||||
// are we already computing a preview in the background for that entry?
|
// are we already computing a preview in the background for that entry?
|
||||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||||
debug!("Preview already in flight for {:?}", 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)
|
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||||
@ -86,21 +82,14 @@ impl CommandPreviewer {
|
|||||||
let entry_c = entry.clone();
|
let entry_c = entry.clone();
|
||||||
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
let concurrent_tasks = self.concurrent_preview_tasks.clone();
|
||||||
let command = command.clone();
|
let command = command.clone();
|
||||||
let last_previewed = self.last_previewed.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
try_preview(
|
try_preview(&command, &entry_c, &cache, &concurrent_tasks);
|
||||||
&command,
|
|
||||||
&entry_c,
|
|
||||||
&cache,
|
|
||||||
&concurrent_tasks,
|
|
||||||
&last_previewed,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
debug!("Too many concurrent preview tasks running");
|
debug!("Too many concurrent preview tasks running");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_previewed.lock().clone()
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +138,6 @@ pub fn try_preview(
|
|||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
cache: &Arc<Mutex<PreviewCache>>,
|
cache: &Arc<Mutex<PreviewCache>>,
|
||||||
concurrent_tasks: &Arc<AtomicU8>,
|
concurrent_tasks: &Arc<AtomicU8>,
|
||||||
last_previewed: &Arc<Mutex<Arc<Preview>>>,
|
|
||||||
) {
|
) {
|
||||||
debug!("Computing preview for {:?}", entry.name);
|
debug!("Computing preview for {:?}", entry.name);
|
||||||
let command = format_command(command, entry);
|
let command = format_command(command, entry);
|
||||||
@ -170,8 +158,6 @@ pub fn try_preview(
|
|||||||
));
|
));
|
||||||
|
|
||||||
cache.lock().insert(entry.name.clone(), &preview);
|
cache.lock().insert(entry.name.clone(), &preview);
|
||||||
let mut tp = last_previewed.lock();
|
|
||||||
*tp = preview.stale().into();
|
|
||||||
} else {
|
} else {
|
||||||
let content = String::from_utf8_lossy(&output.stderr);
|
let content = String::from_utf8_lossy(&output.stderr);
|
||||||
let preview = Arc::new(Preview::new(
|
let preview = Arc::new(Preview::new(
|
||||||
|
@ -28,7 +28,6 @@ pub struct FilePreviewer {
|
|||||||
pub syntax_set: Arc<SyntaxSet>,
|
pub syntax_set: Arc<SyntaxSet>,
|
||||||
pub syntax_theme: Arc<Theme>,
|
pub syntax_theme: Arc<Theme>,
|
||||||
concurrent_preview_tasks: Arc<AtomicU8>,
|
concurrent_preview_tasks: Arc<AtomicU8>,
|
||||||
last_previewed: Arc<Mutex<Arc<Preview>>>,
|
|
||||||
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
in_flight_previews: Arc<Mutex<FxHashSet<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +71,6 @@ impl FilePreviewer {
|
|||||||
syntax_set: Arc::new(syntax_set),
|
syntax_set: Arc::new(syntax_set),
|
||||||
syntax_theme: Arc::new(theme),
|
syntax_theme: Arc::new(theme),
|
||||||
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
|
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(
|
in_flight_previews: Arc::new(Mutex::new(HashSet::with_hasher(
|
||||||
FxBuildHasher,
|
FxBuildHasher,
|
||||||
))),
|
))),
|
||||||
@ -85,17 +81,17 @@ impl FilePreviewer {
|
|||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if seeking to the start of the file fails.
|
/// Panics if seeking to the start of the file fails.
|
||||||
pub fn preview(&mut self, entry: &entry::Entry) -> Arc<Preview> {
|
pub fn preview(&mut self, entry: &entry::Entry) -> Option<Arc<Preview>> {
|
||||||
// do we have a preview in cache for that entry?
|
// do we have a preview in cache for that entry?
|
||||||
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
if let Some(preview) = self.cache.lock().get(&entry.name) {
|
||||||
return preview;
|
return Some(preview);
|
||||||
}
|
}
|
||||||
debug!("Preview cache miss for {:?}", entry.name);
|
debug!("Preview cache miss for {:?}", entry.name);
|
||||||
|
|
||||||
// are we already computing a preview in the background for that entry?
|
// are we already computing a preview in the background for that entry?
|
||||||
if self.in_flight_previews.lock().contains(&entry.name) {
|
if self.in_flight_previews.lock().contains(&entry.name) {
|
||||||
debug!("Preview already in flight for {:?}", 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)
|
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
|
||||||
@ -109,7 +105,6 @@ impl FilePreviewer {
|
|||||||
let syntax_set = self.syntax_set.clone();
|
let syntax_set = self.syntax_set.clone();
|
||||||
let syntax_theme = self.syntax_theme.clone();
|
let syntax_theme = self.syntax_theme.clone();
|
||||||
let concurrent_tasks = self.concurrent_preview_tasks.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();
|
let in_flight_previews = self.in_flight_previews.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
try_preview(
|
try_preview(
|
||||||
@ -118,13 +113,12 @@ impl FilePreviewer {
|
|||||||
&syntax_set,
|
&syntax_set,
|
||||||
&syntax_theme,
|
&syntax_theme,
|
||||||
&concurrent_tasks,
|
&concurrent_tasks,
|
||||||
&last_previewed,
|
|
||||||
&in_flight_previews,
|
&in_flight_previews,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_previewed.lock().clone()
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -139,7 +133,6 @@ pub fn try_preview(
|
|||||||
syntax_set: &Arc<SyntaxSet>,
|
syntax_set: &Arc<SyntaxSet>,
|
||||||
syntax_theme: &Arc<Theme>,
|
syntax_theme: &Arc<Theme>,
|
||||||
concurrent_tasks: &Arc<AtomicU8>,
|
concurrent_tasks: &Arc<AtomicU8>,
|
||||||
last_previewed: &Arc<Mutex<Arc<Preview>>>,
|
|
||||||
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
in_flight_previews: &Arc<Mutex<FxHashSet<String>>>,
|
||||||
) {
|
) {
|
||||||
debug!("Computing preview for {:?}", entry.name);
|
debug!("Computing preview for {:?}", entry.name);
|
||||||
@ -166,8 +159,6 @@ pub fn try_preview(
|
|||||||
);
|
);
|
||||||
cache.lock().insert(entry.name.clone(), &preview);
|
cache.lock().insert(entry.name.clone(), &preview);
|
||||||
in_flight_previews.lock().remove(&entry.name);
|
in_flight_previews.lock().remove(&entry.name);
|
||||||
let mut tp = last_previewed.lock();
|
|
||||||
*tp = preview.stale().into();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Error opening file: {:?}", e);
|
warn!("Error opening file: {:?}", e);
|
||||||
|
@ -23,6 +23,7 @@ color-eyre = { workspace = true }
|
|||||||
syntect = { workspace = true }
|
syntect = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
devicons = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@ -1,15 +1,41 @@
|
|||||||
|
use devicons::FileIcon;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use television_utils::cache::RingSet;
|
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<FileIcon>,
|
||||||
|
pub title: String,
|
||||||
|
pub paragraph: Arc<Paragraph<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CachedPreview<'a> {
|
||||||
|
pub fn new(
|
||||||
|
key: String,
|
||||||
|
icon: Option<FileIcon>,
|
||||||
|
title: String,
|
||||||
|
paragraph: Arc<Paragraph<'a>>,
|
||||||
|
) -> Self {
|
||||||
|
CachedPreview {
|
||||||
|
key,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
paragraph,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RenderedPreviewCache<'a> {
|
pub struct RenderedPreviewCache<'a> {
|
||||||
previews: FxHashMap<String, Arc<Paragraph<'a>>>,
|
previews: FxHashMap<String, CachedPreview<'a>>,
|
||||||
ring_set: RingSet<String>,
|
ring_set: RingSet<String>,
|
||||||
|
pub last_preview: Option<CachedPreview<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RenderedPreviewCache<'a> {
|
impl<'a> RenderedPreviewCache<'a> {
|
||||||
@ -17,15 +43,29 @@ impl<'a> RenderedPreviewCache<'a> {
|
|||||||
RenderedPreviewCache {
|
RenderedPreviewCache {
|
||||||
previews: FxHashMap::default(),
|
previews: FxHashMap::default(),
|
||||||
ring_set: RingSet::with_capacity(capacity),
|
ring_set: RingSet::with_capacity(capacity),
|
||||||
|
last_preview: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &str) -> Option<Arc<Paragraph<'a>>> {
|
pub fn get(&self, key: &str) -> Option<CachedPreview<'a>> {
|
||||||
self.previews.get(key).cloned()
|
self.previews.get(key).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, key: String, preview: &Arc<Paragraph<'a>>) {
|
pub fn insert(
|
||||||
self.previews.insert(key.clone(), preview.clone());
|
&mut self,
|
||||||
|
key: String,
|
||||||
|
icon: Option<FileIcon>,
|
||||||
|
title: String,
|
||||||
|
paragraph: &Arc<Paragraph<'a>>,
|
||||||
|
) {
|
||||||
|
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) {
|
if let Some(oldest_key) = self.ring_set.push(key) {
|
||||||
self.previews.remove(&oldest_key);
|
self.previews.remove(&oldest_key);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
colors::{Colorscheme, PreviewColorscheme},
|
colors::{Colorscheme, PreviewColorscheme},
|
||||||
};
|
};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use devicons::FileIcon;
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
use ratatui::widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@ -22,20 +23,28 @@ use television_utils::strings::{
|
|||||||
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
replace_non_printable, shrink_with_ellipsis, ReplaceNonPrintableConfig,
|
||||||
EMPTY_STRING,
|
EMPTY_STRING,
|
||||||
};
|
};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const FILL_CHAR_SLANTED: char = '╱';
|
const FILL_CHAR_SLANTED: char = '╱';
|
||||||
const FILL_CHAR_EMPTY: char = ' ';
|
const FILL_CHAR_EMPTY: char = ' ';
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub fn build_preview_paragraph(
|
pub fn build_preview_paragraph<'a>(
|
||||||
preview_block: Block<'_>,
|
//preview_block: Block<'_>,
|
||||||
inner: Rect,
|
inner: Rect,
|
||||||
preview_content: PreviewContent,
|
preview_content: PreviewContent,
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
preview_scroll: u16,
|
preview_scroll: u16,
|
||||||
colorscheme: Colorscheme,
|
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 {
|
match preview_content {
|
||||||
PreviewContent::AnsiText(text) => {
|
PreviewContent::AnsiText(text) => {
|
||||||
build_ansi_text_paragraph(text, preview_block, preview_scroll)
|
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))
|
Paragraph::new(Text::from(lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
fn draw_content_outer_block(
|
||||||
pub fn draw_preview_content_block(
|
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
entry: &Entry,
|
|
||||||
preview: &Arc<Preview>,
|
|
||||||
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
|
|
||||||
preview_scroll: u16,
|
|
||||||
use_nerd_font_icons: bool,
|
|
||||||
colorscheme: &Colorscheme,
|
colorscheme: &Colorscheme,
|
||||||
) -> Result<()> {
|
icon: Option<FileIcon>,
|
||||||
|
title: &str,
|
||||||
|
use_nerd_font_icons: bool,
|
||||||
|
) -> Result<Rect> {
|
||||||
let mut preview_title_spans = vec![Span::from(" ")];
|
let mut preview_title_spans = vec![Span::from(" ")];
|
||||||
if preview.icon.is_some() && use_nerd_font_icons {
|
// optional icon
|
||||||
let icon = preview.icon.as_ref().unwrap();
|
if icon.is_some() && use_nerd_font_icons {
|
||||||
|
let icon = icon.as_ref().unwrap();
|
||||||
preview_title_spans.push(Span::styled(
|
preview_title_spans.push(Span::styled(
|
||||||
{
|
{
|
||||||
let mut icon_str = String::from(icon.icon);
|
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)?),
|
Style::default().fg(Color::from_str(icon.color)?),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// preview title
|
||||||
preview_title_spans.push(Span::styled(
|
preview_title_spans.push(Span::styled(
|
||||||
shrink_with_ellipsis(
|
shrink_with_ellipsis(
|
||||||
&replace_non_printable(
|
&replace_non_printable(
|
||||||
preview.title.as_bytes(),
|
title.as_bytes(),
|
||||||
&ReplaceNonPrintableConfig::default(),
|
&ReplaceNonPrintableConfig::default(),
|
||||||
)
|
)
|
||||||
.0,
|
.0,
|
||||||
@ -279,6 +287,8 @@ pub fn draw_preview_content_block(
|
|||||||
Style::default().fg(colorscheme.preview.title_fg).bold(),
|
Style::default().fg(colorscheme.preview.title_fg).bold(),
|
||||||
));
|
));
|
||||||
preview_title_spans.push(Span::from(" "));
|
preview_title_spans.push(Span::from(" "));
|
||||||
|
|
||||||
|
// build the preview block
|
||||||
let preview_outer_block = Block::default()
|
let preview_outer_block = Block::default()
|
||||||
.title_top(
|
.title_top(
|
||||||
Line::from(preview_title_spans)
|
Line::from(preview_title_spans)
|
||||||
@ -294,47 +304,136 @@ pub fn draw_preview_content_block(
|
|||||||
)
|
)
|
||||||
.padding(Padding::new(0, 1, 1, 0));
|
.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);
|
let inner = preview_outer_block.inner(rect);
|
||||||
f.render_widget(preview_outer_block, 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);
|
|
||||||
|
|
||||||
// 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();
|
#[allow(clippy::too_many_arguments)]
|
||||||
let rp = build_preview_paragraph(
|
pub fn draw_preview_content_block(
|
||||||
preview_inner_block,
|
f: &mut Frame,
|
||||||
inner,
|
rect: Rect,
|
||||||
preview.content.clone(),
|
entry: &Entry,
|
||||||
target_line,
|
preview: &Option<Arc<Preview>>,
|
||||||
preview_scroll,
|
rendered_preview_cache: &Arc<Mutex<RenderedPreviewCache<'static>>>,
|
||||||
c_scheme,
|
preview_scroll: u16,
|
||||||
);
|
use_nerd_font_icons: bool,
|
||||||
if !preview.stale {
|
colorscheme: &Colorscheme,
|
||||||
rendered_preview_cache
|
) -> 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
|
||||||
|
let cache_key = compute_cache_key(entry);
|
||||||
|
let cached_preview = rendered_preview_cache
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(cache_key, &Arc::new(rp.clone()));
|
.get(&cache_key)
|
||||||
}
|
.into_iter();
|
||||||
f.render_widget(
|
if let Some(rp) = cached_preview.clone().next() {
|
||||||
Arc::new(rp).as_ref().clone().scroll((preview_scroll, 0)),
|
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,
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,7 +560,9 @@ impl Television {
|
|||||||
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
&& !matches!(selected_entry.preview_type, PreviewType::None)
|
||||||
{
|
{
|
||||||
// preview content
|
// preview content
|
||||||
let preview = self.previewer.preview(&selected_entry);
|
let maybe_preview = self.previewer.preview(&selected_entry);
|
||||||
|
|
||||||
|
if let Some(preview) = &maybe_preview {
|
||||||
self.current_preview_total_lines = preview.total_lines();
|
self.current_preview_total_lines = preview.total_lines();
|
||||||
// initialize preview scroll
|
// initialize preview scroll
|
||||||
self.maybe_init_preview_scroll(
|
self.maybe_init_preview_scroll(
|
||||||
@ -569,11 +571,13 @@ impl Television {
|
|||||||
.map(|l| u16::try_from(l).unwrap_or(0)),
|
.map(|l| u16::try_from(l).unwrap_or(0)),
|
||||||
layout.preview_window.unwrap().height,
|
layout.preview_window.unwrap().height,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
draw_preview_content_block(
|
draw_preview_content_block(
|
||||||
f,
|
f,
|
||||||
layout.preview_window.unwrap(),
|
layout.preview_window.unwrap(),
|
||||||
&selected_entry,
|
&selected_entry,
|
||||||
&preview,
|
&maybe_preview,
|
||||||
&self.rendered_preview_cache,
|
&self.rendered_preview_cache,
|
||||||
self.preview_scroll.unwrap_or(0),
|
self.preview_scroll.unwrap_or(0),
|
||||||
self.config.ui.use_nerd_font_icons,
|
self.config.ui.use_nerd_font_icons,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user