binocular/preview/request/path/
registry.rs1use super::fallback::build_text_or_binary_preview;
2use crate::preview::structured_log;
3use crate::preview::types::PreviewContent;
4use crate::preview::{archive, directory, image, media, pdf, sqlite, types};
5use ratatui::text::Text;
6use ratatui_image::picker::Picker;
7use std::path::Path;
8
9pub(crate) struct PreviewBuildContext<'a> {
10 pub picker: &'a Picker,
11 pub log_max_entries: usize,
12}
13
14pub(crate) trait PathPreviewer: Sync {
15 fn try_build(&self, path: &Path, ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent>;
16}
17
18struct DirectoryPreviewer;
19struct ImagePreviewer;
20struct MediaPreviewer;
21struct ArchivePreviewer;
22struct SqlitePreviewer;
23struct PdfPreviewer;
24struct StructuredLogPreviewer;
25struct TextOrBinaryPreviewer;
26
27static DIRECTORY_PREVIEWER: DirectoryPreviewer = DirectoryPreviewer;
28static IMAGE_PREVIEWER: ImagePreviewer = ImagePreviewer;
29static MEDIA_PREVIEWER: MediaPreviewer = MediaPreviewer;
30static ARCHIVE_PREVIEWER: ArchivePreviewer = ArchivePreviewer;
31static SQLITE_PREVIEWER: SqlitePreviewer = SqlitePreviewer;
32static PDF_PREVIEWER: PdfPreviewer = PdfPreviewer;
33static STRUCTURED_LOG_PREVIEWER: StructuredLogPreviewer = StructuredLogPreviewer;
34static TEXT_OR_BINARY_PREVIEWER: TextOrBinaryPreviewer = TextOrBinaryPreviewer;
35
36static PREVIEWERS: [&dyn PathPreviewer; 8] = [
37 &DIRECTORY_PREVIEWER,
38 &IMAGE_PREVIEWER,
39 &MEDIA_PREVIEWER,
40 &ARCHIVE_PREVIEWER,
41 &SQLITE_PREVIEWER,
42 &PDF_PREVIEWER,
43 &STRUCTURED_LOG_PREVIEWER,
44 &TEXT_OR_BINARY_PREVIEWER,
45];
46
47pub(crate) fn build_path_preview(
48 path_str: &str,
49 picker: &Picker,
50 log_max_entries: usize,
51) -> PreviewContent {
52 let path = Path::new(path_str);
53 if !path.exists() {
54 return PreviewContent::PlainText(Text::default());
55 }
56
57 let ctx = PreviewBuildContext {
58 picker,
59 log_max_entries,
60 };
61 for previewer in PREVIEWERS {
62 if let Some(preview) = previewer.try_build(path, &ctx) {
63 return preview;
64 }
65 }
66
67 PreviewContent::PlainText(Text::default())
68}
69
70impl PathPreviewer for DirectoryPreviewer {
71 fn try_build(&self, path: &Path, _ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
72 path.is_dir()
73 .then(|| PreviewContent::PlainText(directory::generate_preview(path)))
74 }
75}
76
77impl PathPreviewer for ImagePreviewer {
78 fn try_build(&self, path: &Path, ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
79 if !path.is_file() || !image::is_image_extension(path) {
80 return None;
81 }
82
83 let (protocol, metadata) = image::load_image(path, ctx.picker)?;
84 let metadata_line_count = metadata.lines.len();
85 Some(PreviewContent::Image(types::ImagePreview {
86 protocol,
87 metadata,
88 metadata_line_count,
89 }))
90 }
91}
92
93impl PathPreviewer for MediaPreviewer {
94 fn try_build(&self, path: &Path, ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
95 let kind = media::detect_media_kind(path)?;
96 let preview = media::generate_preview(path, kind);
97 let metadata_line_count = preview.text.lines.len();
98 let artwork = preview
99 .artwork_bytes
100 .as_deref()
101 .and_then(|bytes| ::image::load_from_memory(bytes).ok())
102 .map(|img| ctx.picker.new_resize_protocol(img));
103
104 Some(PreviewContent::Media(types::MediaPreview {
105 metadata: preview.text,
106 metadata_line_count,
107 artwork,
108 }))
109 }
110}
111
112impl PathPreviewer for ArchivePreviewer {
113 fn try_build(&self, path: &Path, _ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
114 archive::detect_archive_kind(path)
115 .map(|kind| PreviewContent::PlainText(archive::generate_preview(path, kind)))
116 }
117}
118
119impl PathPreviewer for SqlitePreviewer {
120 fn try_build(&self, path: &Path, _ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
121 sqlite::is_sqlite(path).then(|| PreviewContent::PlainText(sqlite::generate_preview(path)))
122 }
123}
124
125impl PathPreviewer for PdfPreviewer {
126 fn try_build(&self, path: &Path, _ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
127 pdf::is_pdf(path).then(|| PreviewContent::PlainText(pdf::generate_preview(path)))
128 }
129}
130
131impl PathPreviewer for StructuredLogPreviewer {
132 fn try_build(&self, path: &Path, ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
133 let format = structured_log::detect_structured_log(path)?;
134 let (entries, total_lines, all_fields) =
135 structured_log::parse_initial(path, &format, ctx.log_max_entries);
136 Some(structured_log::preview_content(
137 structured_log::StructuredLog {
138 entries,
139 total_lines,
140 all_fields,
141 format,
142 },
143 ))
144 }
145}
146
147impl PathPreviewer for TextOrBinaryPreviewer {
148 fn try_build(&self, path: &Path, _ctx: &PreviewBuildContext<'_>) -> Option<PreviewContent> {
149 if !path.is_file() {
150 return None;
151 }
152
153 Some(build_text_or_binary_preview(path))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use std::path::PathBuf;
161 use std::time::{SystemTime, UNIX_EPOCH};
162
163 fn unique_temp_path(name: &str, ext: &str) -> PathBuf {
164 let nanos = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .unwrap_or_default()
167 .as_nanos();
168 std::env::temp_dir().join(format!("binocular-preview-{name}-{nanos}.{ext}"))
169 }
170
171 #[test]
172 fn structured_log_preview_wins_before_text_fallback() {
173 let path = unique_temp_path("structured", "log");
174 std::fs::write(&path, "{\"level\":\"info\",\"msg\":\"hello\"}\n").unwrap();
175
176 let preview =
177 build_path_preview(&path.display().to_string(), &Picker::halfblocks(), 10_000);
178
179 assert!(matches!(preview, PreviewContent::StructuredLog(_)));
180 let _ = std::fs::remove_file(path);
181 }
182
183 #[test]
184 fn text_fallback_handles_plain_text_files() {
185 let path = unique_temp_path("text", "txt");
186 std::fs::write(&path, "hello world\n").unwrap();
187
188 let preview =
189 build_path_preview(&path.display().to_string(), &Picker::halfblocks(), 10_000);
190
191 assert!(matches!(preview, PreviewContent::RichText(_)));
192 let _ = std::fs::remove_file(path);
193 }
194}