binocular/preview/request/
command.rs1use crate::preview::worker::executor::PreviewExecution;
2use crate::preview::{
3 apply_param_substitutions, PreviewContent, PreviewRequest, PREVIEW_COMMAND_POLL_INTERVAL,
4 PREVIEW_COMMAND_TIMEOUT,
5};
6use ratatui::text::Text;
7use std::io::Write;
8use std::process::{Command, Stdio};
9use std::time::Instant;
10
11pub(crate) fn execute_preview_command<F>(
12 request: PreviewRequest,
13 item: &str,
14 command: &str,
15 delimiter: &str,
16 poll_replacement: &mut F,
17) -> PreviewExecution
18where
19 F: FnMut() -> Option<PreviewRequest>,
20{
21 let Some(argv) = split_command(command) else {
22 return PreviewExecution::Completed(
23 request,
24 PreviewContent::PlainText(Text::from("Invalid --preview command")),
25 );
26 };
27
28 let Some(program) = argv.first() else {
29 return PreviewExecution::Completed(
30 request,
31 PreviewContent::PlainText(Text::from("Empty --preview command")),
32 );
33 };
34
35 let parts: Vec<&str> = item.split(delimiter).collect();
36 let has_placeholder = argv.iter().skip(1).any(|a| a.contains('{'));
37
38 let mut cmd = Command::new(program);
39 for arg in argv.iter().skip(1) {
40 cmd.arg(apply_param_substitutions(arg, item, &parts));
41 }
42
43 if !has_placeholder {
44 cmd.stdin(Stdio::piped());
45 }
46
47 cmd.stdout(Stdio::piped());
48 cmd.stderr(Stdio::piped());
49 cmd.env("BINOCULAR_PREVIEW_ITEM", item);
50
51 let mut child = match cmd.spawn() {
52 Ok(child) => child,
53 Err(err) => {
54 return PreviewExecution::Completed(
55 request,
56 PreviewContent::PlainText(Text::from(format!(
57 "Failed to start preview command: {}",
58 err
59 ))),
60 );
61 }
62 };
63
64 if !has_placeholder {
65 if let Some(mut stdin) = child.stdin.take() {
66 let _ = stdin.write_all(item.as_bytes());
67 }
68 }
69
70 let started_at = Instant::now();
71 loop {
72 if let Some(next_request) = poll_replacement() {
73 let _ = child.kill();
74 let _ = child.wait();
75 return PreviewExecution::Superseded(next_request);
76 }
77
78 if started_at.elapsed() >= PREVIEW_COMMAND_TIMEOUT {
79 let _ = child.kill();
80 let _ = child.wait();
81 return PreviewExecution::Completed(
82 request,
83 PreviewContent::PlainText(Text::from(format!(
84 "Preview command timed out after {}s",
85 PREVIEW_COMMAND_TIMEOUT.as_secs()
86 ))),
87 );
88 }
89
90 match child.try_wait() {
91 Ok(Some(_)) => break,
92 Ok(None) => std::thread::sleep(PREVIEW_COMMAND_POLL_INTERVAL),
93 Err(err) => {
94 return PreviewExecution::Completed(
95 request,
96 PreviewContent::PlainText(Text::from(format!(
97 "Failed to poll preview command: {}",
98 err
99 ))),
100 );
101 }
102 }
103 }
104
105 let output = match child.wait_with_output() {
106 Ok(output) => output,
107 Err(err) => {
108 return PreviewExecution::Completed(
109 request,
110 PreviewContent::PlainText(Text::from(format!(
111 "Failed to read preview command output: {}",
112 err
113 ))),
114 );
115 }
116 };
117
118 let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
119 let stderr = String::from_utf8_lossy(&output.stderr);
120 if !stderr.trim().is_empty() {
121 if !text.is_empty() {
122 text.push_str("\n\n");
123 }
124 text.push_str(&stderr);
125 }
126
127 if text.trim().is_empty() {
128 text = if output.status.success() {
129 "Preview command produced no output".to_string()
130 } else {
131 format!("Preview command exited with status {}", output.status)
132 };
133 }
134
135 PreviewExecution::Completed(request, PreviewContent::PlainText(Text::from(text)))
136}
137
138fn split_command(input: &str) -> Option<Vec<String>> {
147 let mut words = Vec::new();
148 let mut current = String::new();
149 let mut chars = input.chars().peekable();
150 let mut in_single = false;
151 let mut in_double = false;
152 let mut in_word = false;
153
154 while let Some(ch) = chars.next() {
155 if in_single {
156 if ch == '\'' {
157 in_single = false;
158 } else {
159 current.push(ch);
160 }
161 in_word = true;
162 } else if in_double {
163 if ch == '\\' {
164 match chars.next() {
165 Some(c @ ('"' | '\\' | '$' | '`')) => current.push(c),
166 Some('\n') => {}
167 Some(next) => {
168 current.push('\\');
169 current.push(next);
170 }
171 None => return None,
172 }
173 } else if ch == '"' {
174 in_double = false;
175 } else {
176 current.push(ch);
177 }
178 in_word = true;
179 } else {
180 match ch {
181 '\\' => {
182 match chars.next() {
183 Some(next) => current.push(next),
184 None => return None,
185 }
186 in_word = true;
187 }
188 '\'' => {
189 in_single = true;
190 in_word = true;
191 }
192 '"' => {
193 in_double = true;
194 in_word = true;
195 }
196 c if c.is_whitespace() => {
197 if in_word {
198 words.push(std::mem::take(&mut current));
199 in_word = false;
200 }
201 }
202 _ => {
203 current.push(ch);
204 in_word = true;
205 }
206 }
207 }
208 }
209
210 if in_single || in_double {
211 return None;
212 }
213
214 if in_word {
215 words.push(current);
216 }
217
218 Some(words)
219}