Skip to main content

binocular/preview/worker/
executor.rs

1use crate::preview::request::command::execute_preview_command;
2use crate::preview::request::diff::build_diff_preview;
3use crate::preview::request::git::{
4    branch::build_git_branch_preview, commit::build_git_commit_preview,
5    history::build_history_preview,
6};
7use crate::preview::{build_path_preview, PreviewContent, PreviewRequest};
8use ratatui::text::Text;
9use ratatui_image::picker::Picker;
10
11pub(crate) enum PreviewExecution {
12    Completed(PreviewRequest, PreviewContent),
13    Superseded(PreviewRequest),
14}
15pub(crate) struct PreviewExecutor {
16    picker: Picker,
17    preview_command: Option<String>,
18    delimiter: String,
19    log_max_entries: usize,
20}
21
22impl PreviewExecutor {
23    pub(crate) fn new(
24        picker: Picker,
25        preview_command: Option<String>,
26        delimiter: String,
27        log_max_entries: usize,
28    ) -> Self {
29        Self {
30            picker,
31            preview_command,
32            delimiter,
33            log_max_entries,
34        }
35    }
36
37    pub(crate) fn execute<F>(
38        &self,
39        request: PreviewRequest,
40        mut poll_replacement: F,
41    ) -> PreviewExecution
42    where
43        F: FnMut() -> Option<PreviewRequest>,
44    {
45        match request {
46            PreviewRequest::Path { source, path } => self.execute_builtin_preview(
47                PreviewRequest::Path {
48                    source,
49                    path: path.clone(),
50                },
51                &path,
52            ),
53            PreviewRequest::Diff {
54                source,
55                left,
56                right,
57            } => {
58                let preview = build_diff_preview(&left, &right);
59                PreviewExecution::Completed(
60                    PreviewRequest::Diff {
61                        source,
62                        left,
63                        right,
64                    },
65                    preview,
66                )
67            }
68            PreviewRequest::GitHistory {
69                source,
70                repo_root,
71                commit,
72                path,
73                line,
74            } => {
75                let preview = build_history_preview(&repo_root, &commit, &path);
76                PreviewExecution::Completed(
77                    PreviewRequest::GitHistory {
78                        source,
79                        repo_root,
80                        commit,
81                        path,
82                        line,
83                    },
84                    preview,
85                )
86            }
87            PreviewRequest::GitBranch {
88                source,
89                repo_root,
90                branch,
91            } => {
92                let preview = build_git_branch_preview(&repo_root, &branch);
93                PreviewExecution::Completed(
94                    PreviewRequest::GitBranch {
95                        source,
96                        repo_root,
97                        branch,
98                    },
99                    preview,
100                )
101            }
102            PreviewRequest::GitCommit {
103                source,
104                repo_root,
105                commit,
106            } => {
107                let preview = build_git_commit_preview(&repo_root, &commit);
108                PreviewExecution::Completed(
109                    PreviewRequest::GitCommit {
110                        source,
111                        repo_root,
112                        commit,
113                    },
114                    preview,
115                )
116            }
117            PreviewRequest::Grep {
118                source,
119                path,
120                line,
121                text,
122            } => self.execute_builtin_preview(
123                PreviewRequest::Grep {
124                    source,
125                    path: path.clone(),
126                    line,
127                    text,
128                },
129                &path,
130            ),
131            PreviewRequest::StructuredLog { source, path } => self.execute_builtin_preview(
132                PreviewRequest::StructuredLog {
133                    source,
134                    path: path.clone(),
135                },
136                &path,
137            ),
138            PreviewRequest::StdinOrCommand { source, item } => {
139                let request = PreviewRequest::StdinOrCommand {
140                    source,
141                    item: item.clone(),
142                };
143                if let Some(command) = self.preview_command.as_deref() {
144                    execute_preview_command(
145                        request,
146                        &item,
147                        command,
148                        &self.delimiter,
149                        &mut poll_replacement,
150                    )
151                } else {
152                    PreviewExecution::Completed(request, PreviewContent::PlainText(Text::default()))
153                }
154            }
155        }
156    }
157
158    fn execute_builtin_preview(&self, request: PreviewRequest, path: &str) -> PreviewExecution {
159        let preview = build_path_preview(path, &self.picker, self.log_max_entries);
160        PreviewExecution::Completed(request, preview)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::preview::request::git::ansi::parse_ansi_text;
168    use crate::preview::PreviewSource;
169    use crate::search::types::SearchItem;
170    use ratatui::style::Color;
171    use std::sync::atomic::{AtomicUsize, Ordering};
172
173    fn command_request(item: &str) -> PreviewRequest {
174        PreviewRequest::StdinOrCommand {
175            source: PreviewSource::SearchItem(SearchItem::stdin(item)),
176            item: item.to_string(),
177        }
178    }
179
180    #[test]
181    fn preview_command_replacement_returns_latest_request() {
182        let executor = PreviewExecutor::new(
183            Picker::halfblocks(),
184            Some("sh -c 'sleep 1'".to_string()),
185            ":".to_string(),
186            100_000,
187        );
188        let replacement = command_request("replacement");
189        let polls = AtomicUsize::new(0);
190
191        let outcome = executor.execute(command_request("initial"), || {
192            if polls.fetch_add(1, Ordering::Relaxed) == 0 {
193                Some(replacement.clone())
194            } else {
195                None
196            }
197        });
198
199        match outcome {
200            PreviewExecution::Superseded(request) => assert_eq!(request, replacement),
201            PreviewExecution::Completed(_, _) => panic!("expected replacement"),
202        }
203    }
204
205    #[test]
206    fn preview_command_timeout_surfaces_message() {
207        let executor = PreviewExecutor::new(
208            Picker::halfblocks(),
209            Some("sh -c 'sleep 3'".to_string()),
210            ":".to_string(),
211            100_000,
212        );
213
214        let outcome = executor.execute(command_request("initial"), || None);
215
216        match outcome {
217            PreviewExecution::Completed(_, PreviewContent::PlainText(text)) => {
218                let rendered = text
219                    .lines
220                    .iter()
221                    .map(|line| line.to_string())
222                    .collect::<Vec<_>>()
223                    .join("\n");
224                assert!(rendered.contains("timed out"));
225            }
226            PreviewExecution::Completed(_, _) => panic!("expected plain text timeout"),
227            PreviewExecution::Superseded(_) => panic!("expected timeout"),
228        }
229    }
230
231    #[test]
232    fn ansi_empty_reset_code_resets_style_before_next_line() {
233        let text = parse_ansi_text("\u{1b}[31m--\u{1b}[m\nfile.rs\n".to_string());
234
235        assert_eq!(text.lines.len(), 2);
236        assert_eq!(text.lines[0].spans.len(), 1);
237        assert_eq!(text.lines[0].spans[0].content, "--");
238        assert_eq!(text.lines[0].spans[0].style.fg, Some(Color::Red));
239
240        assert_eq!(text.lines[1].spans.len(), 1);
241        assert_eq!(text.lines[1].spans[0].content, "file.rs");
242        assert_ne!(text.lines[1].spans[0].style.fg, Some(Color::Red));
243    }
244}