Skip to main content

trail/preview/
image.rs

1//! Image file preview provider.
2//!
3//! Recognises image files by extension and hands them to
4//! `workers/image_decode.rs`, which decodes them off-thread and builds the
5//! encoder state for the terminal's inline-image protocol. The protocol itself
6//! is resolved once per process by [`crate::preview::graphics`].
7//!
8//! Only `[preview] image_protocol = "none"` takes the synchronous path, which
9//! reports metadata without decoding anything.
10
11use std::path::Path;
12
13use crate::app::state::{Entry, EntryKind};
14use crate::preview::graphics::{self, ImageProtocol};
15use crate::preview::provider::{PreviewContent, PreviewCtx, PreviewOutcome, PreviewProvider};
16
17/// Known image file extensions this provider handles.
18const IMAGE_EXTENSIONS: &[&str] = &[
19    "png", "jpg", "jpeg", "gif", "bmp", "ico", "tiff", "tif", "webp", "avif", "svg",
20];
21
22/// Preview provider for image files.
23///
24/// Delegates the decode to the async worker pool, which sends
25/// `WorkerMsg::ImageMeta` back to the UI thread. Falls back to a synchronous
26/// metadata preview only when image previews are switched off.
27pub struct ImageProvider;
28
29impl PreviewProvider for ImageProvider {
30    fn can_handle(&self, entry: &Entry) -> bool {
31        if entry.kind != EntryKind::File {
32            return false;
33        }
34        is_image_path(&entry.path)
35    }
36
37    fn preview(&self, entry: &Entry, ctx: &PreviewCtx) -> PreviewOutcome {
38        if graphics::active() == ImageProtocol::None {
39            // Previews are disabled: produce a compact metadata preview
40            // synchronously so there is no unnecessary loading flash.
41            let content = build_metadata_preview_sync(&entry.path, entry.metadata.as_ref());
42            return PreviewOutcome::Ready(content);
43        }
44
45        crate::workers::image_decode::spawn_image_decode(
46            entry.path.clone(),
47            ctx.generation,
48            ctx.worker_tx.clone(),
49        );
50        PreviewOutcome::Deferred
51    }
52}
53
54/// Returns `true` if `path` has a recognised image extension.
55pub fn is_image_path(path: &Path) -> bool {
56    path.extension()
57        .and_then(|e| e.to_str())
58        .map(|ext| IMAGE_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
59        .unwrap_or(false)
60}
61
62/// Produces a compact metadata preview without spawning a worker.
63///
64/// Used when image previews are disabled. Reads only filesystem metadata
65/// (size, extension) — does not decode the image — so it is safe to call on
66/// the UI thread.
67fn build_metadata_preview_sync(
68    path: &Path,
69    metadata: Option<&std::fs::Metadata>,
70) -> PreviewContent {
71    let ext = path
72        .extension()
73        .and_then(|e| e.to_str())
74        .unwrap_or("unknown")
75        .to_uppercase();
76
77    // Formatted by `metafmt`, the same as the binary preview and the listing's
78    // details column.
79    let size_str = metadata
80        .map(|m| crate::metafmt::size(m.len()))
81        .or_else(|| {
82            std::fs::metadata(path)
83                .map(|m| crate::metafmt::size(m.len()))
84                .ok()
85        })
86        .unwrap_or_else(|| "unknown".to_owned());
87
88    PreviewContent::Binary(vec![
89        format!("  Type  : {} image", ext),
90        format!("  Size  : {}", size_str),
91        String::new(),
92        "  (image previews disabled)".to_owned(),
93        "  Set [preview] image_protocol to re-enable them.".to_owned(),
94    ])
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn image_path_recognises_extensions() {
103        for ext in ["png", "jpg", "jpeg", "gif", "bmp", "webp"] {
104            let name = format!("image.{ext}");
105            let path = Path::new(&name);
106            assert!(is_image_path(path), "expected image path for .{ext}");
107        }
108    }
109
110    #[test]
111    fn non_image_extensions_rejected() {
112        for ext in ["rs", "txt", "md", "toml"] {
113            let name = format!("file.{ext}");
114            let path = Path::new(&name);
115            assert!(!is_image_path(path), "should not be image for .{ext}");
116        }
117    }
118
119    #[test]
120    fn metadata_preview_sync_produces_binary_lines() {
121        use tempfile::NamedTempFile;
122        let f = NamedTempFile::with_suffix(".png").unwrap();
123        let meta = std::fs::metadata(f.path()).ok();
124        let content = build_metadata_preview_sync(f.path(), meta.as_ref());
125        assert!(matches!(content, PreviewContent::Binary(_)));
126    }
127}