binocular/preview/worker/
executor.rs1use 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}