ad_editor/
system.rs

1//! An abstraction around system interactions to support testing and
2//! platform specific behaviour
3use 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
14/// Wrapper around storing system interactions
15pub trait System: fmt::Debug {
16    /// Set the clipboard to the given string
17    fn set_clipboard(&mut self, s: &str) -> io::Result<()>;
18
19    /// Read the current contents of the clipboard
20    fn read_clipboard(&self) -> io::Result<String>;
21
22    /// Store a handle to a running [Child] followinga  call to [System::run_command].
23    fn store_child_handle(&mut self, cmd: &str, child: Child);
24
25    /// Provide an ordered list of currently running child processes by their command string
26    fn running_children(&self) -> Vec<String>;
27
28    /// The number of currently running child processes
29    fn n_running_children(&self) -> usize {
30        self.running_children().len()
31    }
32
33    /// Cleanup any resources associated with a child process that is now complete
34    fn cleanup_child(&mut self, id: u32);
35
36    /// Kill a child process by its index in the list returned from [System::running_children].
37    fn kill_child(&mut self, idx: usize);
38
39    /// Run an external command and collect its output.
40    fn run_command_blocking(&self, cmd: &str, cwd: &Path, bufid: usize) -> io::Result<String> {
41        run_command_blocking(cmd, cwd, bufid)
42    }
43
44    /// Run an external command and append its output to the output buffer for `bufid` from a
45    /// background thread. If the command is successfully spawned then a [Child] should be stored
46    /// for later resource cleanup and support for user initiated killing.
47    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    /// Pipe input text through an external command, returning the output
61    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/// A default implementation for system interactions
119#[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
271/// Pipe input text through an external command, returning the output
272pub 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}