Skip to main content

binocular/preview/
mod.rs

1pub mod archive;
2pub mod binary;
3pub mod diff;
4pub mod directory;
5pub mod doc;
6pub mod encoding;
7pub mod image;
8pub mod media;
9pub mod pdf;
10pub mod protocol;
11pub mod request;
12pub mod rich_text;
13pub mod sqlite;
14pub mod structured_log;
15pub mod types;
16pub mod worker;
17
18use ratatui_image::picker::Picker;
19use std::time::Duration;
20
21const PREVIEW_COMMAND_POLL_INTERVAL: Duration = Duration::from_millis(50);
22const PREVIEW_COMMAND_TIMEOUT: Duration = Duration::from_secs(2);
23
24/// Spawn the preview worker thread.
25///
26/// This function spawns a background thread that receives file paths and
27/// generates preview content, sending results back through the provided channel.
28pub fn spawn_previewer(
29    rx_request: impl crate::infra::channel::Receiver<PreviewRequest> + 'static,
30    tx_preview: impl crate::infra::channel::Sender<(PreviewSource, PreviewContent)> + 'static,
31    tx_log: impl crate::infra::channel::Sender<(String, Vec<LogEntry>)> + 'static,
32    picker: Picker,
33    preview_command: Option<String>,
34    delimiter: String,
35    log_max_entries: usize,
36) {
37    std::thread::spawn(move || {
38        // SECURITY: isolate the preview worker so a panic in any parsing dependency
39        // (image, PDF, ZIP, tree-sitter, etc.) cannot crash the entire application.
40        // This requires panic = "unwind" in the release profile.
41        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
42            let executor = worker::executor::PreviewExecutor::new(
43                picker,
44                preview_command,
45                delimiter,
46                log_max_entries,
47            );
48            let mut orchestrator = worker::orchestrator::PreviewOrchestrator::new(
49                rx_request, tx_preview, tx_log, executor,
50            );
51            orchestrator.run();
52        }));
53
54        if let Err(panic_info) = result {
55            let msg = if let Some(s) = panic_info.downcast_ref::<String>() {
56                s.clone()
57            } else if let Some(s) = panic_info.downcast_ref::<&str>() {
58                s.to_string()
59            } else {
60                "unknown panic in preview worker".to_string()
61            };
62            eprintln!("Preview worker panicked: {}", msg);
63        }
64    });
65}
66
67fn build_path_preview(path_str: &str, picker: &Picker, log_max_entries: usize) -> PreviewContent {
68    request::path::registry::build_path_preview(path_str, picker, log_max_entries)
69}
70
71/// Applies `{}` and `{N}` placeholder substitutions in a single command argument.
72///
73/// - `{N}` is replaced with `parts[N]` (item split by delimiter).
74/// - `{}` is replaced with the full item string.
75///
76/// # Security
77/// Substitutions are raw string replacements with **no shell escaping**. If the
78/// user wraps their preview command in a shell (e.g. `sh -c "echo {}"`), shell
79/// metacharacters in the selected item can be interpreted by the shell. This is
80/// by design — we pass arguments directly to `std::process::Command` without a
81/// shell wrapper. Only use shell wrappers with trusted input.
82pub(super) fn apply_param_substitutions(arg: &str, item: &str, parts: &[&str]) -> String {
83    let mut result = arg.to_string();
84    for (i, part) in parts.iter().enumerate() {
85        result = result.replace(&format!("{{{i}}}"), part);
86    }
87    result.replace("{}", item)
88}
89
90pub use protocol::{PreviewRequest, PreviewSource};
91pub use rich_text::syntax::{
92    detect_language, get_configs, get_highlighter, get_style, SyntaxRegistry,
93};
94pub use rich_text::RichTextDocument;
95pub use rich_text::{
96    apply_text_edit, create_rich_text_document, edit_content_delete_char,
97    edit_content_delete_char_at, edit_content_delete_range, edit_content_insert_char,
98    edit_content_insert_text, generate_plain_lines_for_range, regenerate_lines,
99};
100pub use structured_log::LogEntry;
101pub use types::{DiffPreview, ImagePreview, LogPreview, MediaPreview, PreviewContent};