1use crate::{editor::Action, input::Event, util::normalize_line_endings};
4use std::{
5 env, fmt,
6 io::{self, BufRead, BufReader, Read, Write},
7 path::Path,
8 process::{Child, Command, Stdio},
9 sync::mpsc::Sender,
10 thread::spawn,
11};
12use tracing::info;
13
14pub trait System: fmt::Debug {
16 fn set_clipboard(&mut self, s: &str) -> io::Result<()>;
18
19 fn read_clipboard(&self) -> io::Result<String>;
21
22 fn store_child_handle(&mut self, cmd: &str, child: Child);
24
25 fn running_children(&self) -> Vec<String>;
27
28 fn n_running_children(&self) -> usize {
30 self.running_children().len()
31 }
32
33 fn cleanup_child(&mut self, id: u32);
35
36 fn kill_child(&mut self, idx: usize);
38
39 fn run_command_blocking(&self, cmd: &str, cwd: &Path, bufid: usize) -> io::Result<String> {
41 run_command_blocking(cmd, cwd, bufid)
42 }
43
44 fn run_command(
48 &mut self,
49 cmd: &str,
50 cwd: &Path,
51 bufid: usize,
52 tx: Sender<Event>,
53 ) -> io::Result<()> {
54 let child = run_command(cmd, cwd, bufid, tx)?;
55 self.store_child_handle(cmd, child);
56
57 Ok(())
58 }
59
60 fn pipe_through_command(
62 &self,
63 cmd: &str,
64 input: &str,
65 cwd: &Path,
66 bufid: usize,
67 ) -> io::Result<String> {
68 pipe_through_command(cmd, input, cwd, bufid)
69 }
70}
71
72#[derive(Debug, Clone)]
73struct ClipboardProvider {
74 copy_cmd: &'static str,
75 copy_args: Vec<&'static str>,
76 paste_cmd: &'static str,
77 paste_args: Vec<&'static str>,
78}
79
80impl ClipboardProvider {
81 pub fn try_from_env() -> Option<Self> {
82 let paths = env::var("PATH").expect("path not set");
83 let exists = |cmd: &str| env::split_paths(&paths).any(|dir| dir.join(cmd).is_file());
84
85 let (copy_cmd, copy_args, paste_cmd, paste_args) = if exists("pbcopy") {
86 info!("clipboard provider found: pbcopy");
87 ("pbcopy", vec![], "pbpaste", vec![])
88 } else if env::var("WAYLAND_DISPLAY").is_ok() && exists("wl-copy") && exists("wl-paste") {
89 info!("clipboard provider found: wl-copy");
90 (
91 "wl-copy",
92 vec!["--foreground", "--type", "text/plain"],
93 "wl-paste",
94 vec!["--no-newline"],
95 )
96 } else if env::var("DISPLAY").is_ok() && exists("xclip") {
97 info!("clipboard provider found: xclip");
98 (
99 "xclip",
100 vec!["-i", "-selection", "clipboard"],
101 "xclip",
102 vec!["-o", "-selection", "clipboard"],
103 )
104 } else {
105 info!("no clipboard provider found");
106 return None;
107 };
108
109 Some(Self {
110 copy_cmd,
111 copy_args,
112 paste_cmd,
113 paste_args,
114 })
115 }
116}
117
118#[derive(Debug)]
120pub struct DefaultSystem {
121 selection: String,
122 cp: Option<ClipboardProvider>,
123 running_children: Vec<(String, Child)>,
124}
125
126impl DefaultSystem {
127 pub fn from_env() -> Self {
128 Self {
129 selection: String::new(),
130 cp: ClipboardProvider::try_from_env(),
131 running_children: Vec::new(),
132 }
133 }
134
135 pub fn without_clipboard_provider() -> Self {
136 Self {
137 selection: String::new(),
138 cp: None,
139 running_children: Vec::new(),
140 }
141 }
142}
143
144impl System for DefaultSystem {
145 fn set_clipboard(&mut self, s: &str) -> io::Result<()> {
146 match &self.cp {
147 Some(cp) => {
148 let mut child = Command::new(cp.copy_cmd)
149 .args(&cp.copy_args)
150 .stdin(Stdio::piped())
151 .spawn()?;
152
153 child.stdin.take().unwrap().write_all(s.as_bytes())
154 }
155
156 None => {
157 self.selection = s.to_string();
158 Ok(())
159 }
160 }
161 }
162
163 fn read_clipboard(&self) -> io::Result<String> {
164 match &self.cp {
165 Some(cp) => {
166 let output = Command::new(cp.paste_cmd).args(&cp.paste_args).output()?;
167
168 Ok(String::from_utf8(output.stdout).unwrap_or_default())
169 }
170
171 None => Ok(self.selection.clone()),
172 }
173 }
174
175 fn store_child_handle(&mut self, cmd: &str, child: Child) {
176 self.running_children.push((cmd.to_owned(), child));
177 }
178
179 fn running_children(&self) -> Vec<String> {
180 self.running_children
181 .iter()
182 .map(|(cmd, _)| cmd.clone())
183 .collect()
184 }
185
186 fn n_running_children(&self) -> usize {
187 self.running_children.len()
188 }
189
190 fn cleanup_child(&mut self, id: u32) {
191 for (_, child) in self.running_children.iter_mut() {
192 if child.id() == id {
193 _ = child.wait();
194 }
195 }
196
197 self.running_children.retain(|(_, child)| child.id() != id);
198 }
199
200 fn kill_child(&mut self, idx: usize) {
201 let (_, mut child) = self.running_children.remove(idx);
202 _ = child.kill();
203 _ = child.wait();
204 }
205}
206
207fn prepare_command(cmd: &str, cwd: &Path, bufid: usize) -> Command {
208 let mut args: Vec<&str> = cmd.split_whitespace().collect();
209 if args.is_empty() {
210 return Command::new("");
211 }
212
213 let cmd = args.remove(0);
214 let path = env::var("PATH").unwrap();
215 let home = env::var("HOME").unwrap();
216 let mut command = Command::new(cmd);
217 command
218 .env("PATH", format!("{home}/.ad/bin:{path}"))
219 .env("AD_PID", crate::pid().to_string())
220 .env("AD_BUFID", bufid.to_string())
221 .current_dir(cwd)
222 .args(args);
223
224 command
225}
226
227fn run_command_blocking(cmd: &str, cwd: &Path, bufid: usize) -> io::Result<String> {
228 let output = prepare_command(cmd, cwd, bufid).output()?;
229 let mut stdout = String::from_utf8(output.stdout).unwrap_or_default();
230 let stderr = String::from_utf8(output.stderr).unwrap_or_default();
231 stdout.push_str(&stderr);
232
233 Ok(normalize_line_endings(stdout))
234}
235
236fn run_command(cmd: &str, cwd: &Path, bufid: usize, tx: Sender<Event>) -> io::Result<Child> {
237 let mut child = prepare_command(cmd, cwd, bufid)
238 .stdout(Stdio::piped())
239 .stderr(Stdio::piped())
240 .spawn()?;
241
242 let stdout = BufReader::new(child.stdout.take().unwrap());
243 let stderr = BufReader::new(child.stderr.take().unwrap());
244 let id = child.id();
245
246 spawn(move || {
247 let tx2 = tx.clone();
248 spawn(move || send_lines(bufid, stderr.lines(), tx2));
249 send_lines(bufid, stdout.lines(), tx.clone());
250 _ = tx.send(Event::Action(Action::CleanupChild { id }));
251 });
252
253 Ok(child)
254}
255
256fn send_lines(bufid: usize, it: impl Iterator<Item = io::Result<String>>, tx: Sender<Event>) {
257 for res in it {
258 match res {
259 Ok(mut line) => {
260 line.push('\n');
261 _ = tx.send(Event::Action(Action::AppendToOutputBuffer {
262 bufid,
263 content: normalize_line_endings(line),
264 }));
265 }
266 Err(_) => break,
267 }
268 }
269}
270
271pub fn pipe_through_command(
273 cmd: &str,
274 input: &str,
275 cwd: &Path,
276 bufid: usize,
277) -> io::Result<String> {
278 let mut child = prepare_command(cmd, cwd, bufid)
279 .stdin(Stdio::piped())
280 .stdout(Stdio::piped())
281 .stderr(Stdio::piped())
282 .spawn()?;
283
284 let mut buf = String::new();
285 child.stdin.take().unwrap().write_all(input.as_bytes())?;
286 child.stdout.take().unwrap().read_to_string(&mut buf)?;
287 child.stderr.take().unwrap().read_to_string(&mut buf)?;
288 _ = child.wait();
289
290 Ok(normalize_line_endings(buf))
291}