use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver, Sender};
use anyhow::Result;
use image::DynamicImage;
use ratatui_image::picker::Picker;
use ratatui_image::protocol::StatefulProtocol;
pub struct ImageJob {
pub key: PathBuf,
pub max_dim: u32,
}
pub struct DecodedImage {
pub key: PathBuf,
pub image: DynamicImage,
}
pub struct ImageCache {
map: lru::LruCache<PathBuf, ImageEntry>,
}
struct ImageEntry {
proto: Option<StatefulProtocol>,
}
impl ImageCache {
pub fn new(limit: usize) -> Self {
Self {
map: lru::LruCache::new(NonZeroUsize::new(limit.max(1)).expect("limit >= 1")),
}
}
pub fn request(&mut self, key: PathBuf, max_dim: u32, tx: &Sender<ImageJob>) {
if self.map.contains(&key) {
let _ = self.map.get_mut(&key);
return;
}
let _ = tx.send(ImageJob {
key: key.clone(),
max_dim,
});
self.map.put(key, ImageEntry { proto: None });
}
pub fn install_decoded(&mut self, img: DecodedImage, picker: &Picker) {
if let Some(entry) = self.map.get_mut(&img.key) {
let proto = picker.new_resize_protocol(img.image);
entry.proto = Some(proto);
}
}
pub fn get_proto(&mut self, key: &PathBuf) -> Option<&mut StatefulProtocol> {
self.map.get_mut(key).and_then(|entry| entry.proto.as_mut())
}
}
pub fn spawn_worker() -> (Sender<ImageJob>, Receiver<Result<DecodedImage>>) {
let (tx, rx) = mpsc::channel();
let (result_tx, result_rx) = mpsc::channel();
std::thread::Builder::new()
.name("pinstar-image-decode-worker".into())
.spawn(move || {
loop {
let first = match rx.recv() {
Ok(job) => job,
Err(_) => {
return;
}
};
process_job(first, &result_tx);
while let Ok(job) = rx.try_recv() {
process_job(job, &result_tx);
}
}
})
.expect("failed to spawn image decode worker");
(tx, result_rx)
}
fn process_job(job: ImageJob, result_tx: &Sender<Result<DecodedImage>>) {
let result = decode_image(&job.key, job.max_dim);
let _ = result_tx.send(result.map(|img| DecodedImage {
key: job.key,
image: img,
}));
}
fn decode_image(key: &PathBuf, max_dim: u32) -> Result<DynamicImage> {
let img = image::ImageReader::open(key)?
.decode()
.map_err(|e| anyhow::anyhow!("Failed to decode image {}: {e}", key.display()))?;
if max_dim > 0 {
let w = img.width();
let h = img.height();
let max_current = w.max(h);
if max_current > max_dim {
let ratio = max_dim as f64 / max_current as f64;
let new_w = (w as f64 * ratio) as u32;
let new_h = (h as f64 * ratio) as u32;
return Ok(img.resize_exact(
new_w.max(1),
new_h.max(1),
image::imageops::FilterType::Lanczos3,
));
}
}
Ok(img)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eviction() {
let mut cache = ImageCache::new(2);
let dir = std::env::temp_dir().join("pinstar_test_img_cache");
let _ = std::fs::create_dir_all(&dir);
let k1 = dir.join("a.png");
let k2 = dir.join("b.png");
let k3 = dir.join("c.png");
let (tx, _) = std::sync::mpsc::channel();
cache.request(k1.clone(), 100, &tx);
cache.request(k2.clone(), 100, &tx);
assert_eq!(cache.map.len(), 2);
cache.get_proto(&k1);
cache.request(k3.clone(), 100, &tx);
assert_eq!(cache.map.len(), 2);
assert!(cache.map.contains(&k1), "k1 should survive");
assert!(!cache.map.contains(&k2), "k2 should be evicted as oldest");
assert!(cache.map.contains(&k3), "k3 should survive");
let _ = std::fs::remove_dir_all(&dir);
}
}