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,
6    ffi::OsStr,
7    fmt,
8    io::{self, Read, Write},
9    path::Path,
10    process::{Command, Stdio},
11    sync::mpsc::Sender,
12    thread::spawn,
13};
14
15/// Wrapper around storing system interactions
16pub trait System: fmt::Debug {
17    /// Set the clipboard to the given string
18    fn set_clipboard(&mut self, s: &str) -> io::Result<()>;
19
20    /// Read the current contents of the clipboard
21    fn read_clipboard(&self) -> io::Result<String>;
22
23    /// Run an external command and collect its output.
24    fn run_command_blocking<I, S>(
25        &self,
26        cmd: &str,
27        args: I,
28        cwd: &Path,
29        bufid: usize,
30    ) -> io::Result<String>
31    where
32        I: IntoIterator<Item = S>,
33        S: AsRef<OsStr>,
34    {
35        run_command_blocking(cmd, args, cwd, bufid)
36    }
37
38    /// Run an external command and append its output to the output buffer for `bufid` from a
39    /// background thread.
40    fn run_command<I, S>(&self, cmd: &str, args: I, cwd: &Path, bufid: usize, tx: Sender<Event>)
41    where
42        I: IntoIterator<Item = S>,
43        S: AsRef<OsStr>,
44    {
45        run_command(cmd, args, cwd, bufid, tx)
46    }
47
48    /// Pipe input text through an external command, returning the output
49    fn pipe_through_command<I, S>(
50        &self,
51        cmd: &str,
52        args: I,
53        input: &str,
54        cwd: &Path,
55        bufid: usize,
56    ) -> io::Result<String>
57    where
58        I: IntoIterator<Item = S>,
59        S: AsRef<OsStr>,
60    {
61        pipe_through_command(cmd, args, input, cwd, bufid)
62    }
63}
64
65/// A default implementation for system interactions
66#[derive(Debug, Clone, Copy)]
67pub struct DefaultSystem;
68
69#[cfg(target_os = "linux")]
70impl System for DefaultSystem {
71    fn set_clipboard(&mut self, s: &str) -> io::Result<()> {
72        let mut child = Command::new("xclip")
73            .args(["-selection", "clipboard", "-i"])
74            .stdin(Stdio::piped())
75            .spawn()?;
76
77        child.stdin.take().unwrap().write_all(s.as_bytes())
78    }
79
80    fn read_clipboard(&self) -> io::Result<String> {
81        let output = Command::new("xclip")
82            .args(["-selection", "clipboard", "-o"])
83            .output()?;
84
85        Ok(String::from_utf8(output.stdout).unwrap_or_default())
86    }
87}
88
89#[cfg(target_os = "macos")]
90impl System for DefaultSystem {
91    fn set_clipboard(&mut self, s: &str) -> io::Result<()> {
92        let mut child = Command::new("pbcopy").stdin(Stdio::piped()).spawn()?;
93
94        child.stdin.take().unwrap().write_all(s.as_bytes())
95    }
96
97    fn read_clipboard(&self) -> io::Result<String> {
98        let output = Command::new("pbpaste").output()?;
99
100        Ok(String::from_utf8(output.stdout).unwrap_or_default())
101    }
102}
103
104fn prepare_command<I, S>(cmd: &str, args: I, cwd: &Path, bufid: usize) -> Command
105where
106    I: IntoIterator<Item = S>,
107    S: AsRef<OsStr>,
108{
109    let path = env::var("PATH").unwrap();
110    let home = env::var("HOME").unwrap();
111    let mut command = Command::new(cmd);
112    command
113        .env("PATH", format!("{home}/.ad/bin:{path}"))
114        .env("bufid", bufid.to_string())
115        .current_dir(cwd)
116        .args(args);
117
118    command
119}
120
121fn run_command_blocking<I, S>(cmd: &str, args: I, cwd: &Path, bufid: usize) -> io::Result<String>
122where
123    I: IntoIterator<Item = S>,
124    S: AsRef<OsStr>,
125{
126    let output = prepare_command(cmd, args, cwd, bufid).output()?;
127    let mut stdout = String::from_utf8(output.stdout).unwrap_or_default();
128    let stderr = String::from_utf8(output.stderr).unwrap_or_default();
129    stdout.push_str(&stderr);
130
131    Ok(normalize_line_endings(stdout))
132}
133
134fn run_command<I, S>(cmd: &str, args: I, cwd: &Path, bufid: usize, tx: Sender<Event>)
135where
136    I: IntoIterator<Item = S>,
137    S: AsRef<OsStr>,
138{
139    let mut command = prepare_command(cmd, args, cwd, bufid);
140
141    spawn(move || {
142        let output = match command.output() {
143            Ok(output) => output,
144            Err(err) => {
145                _ = tx.send(Event::Action(Action::SetStatusMessage {
146                    message: err.to_string(),
147                }));
148                return;
149            }
150        };
151
152        let mut content = String::from_utf8(output.stdout).unwrap_or_default();
153        let stderr = String::from_utf8(output.stderr).unwrap_or_default();
154        content.push_str(&stderr);
155        if content.is_empty() {
156            return;
157        }
158        _ = tx.send(Event::Action(Action::AppendToOutputBuffer {
159            bufid,
160            content: normalize_line_endings(content),
161        }));
162    });
163}
164
165/// Pipe input text through an external command, returning the output
166pub fn pipe_through_command<I, S>(
167    cmd: &str,
168    args: I,
169    input: &str,
170    cwd: &Path,
171    bufid: usize,
172) -> io::Result<String>
173where
174    I: IntoIterator<Item = S>,
175    S: AsRef<OsStr>,
176{
177    let mut child = prepare_command(cmd, args, cwd, bufid)
178        .stdin(Stdio::piped())
179        .stdout(Stdio::piped())
180        .stderr(Stdio::piped())
181        .spawn()?;
182
183    let mut buf = String::new();
184    child.stdin.take().unwrap().write_all(input.as_bytes())?;
185    child.stdout.take().unwrap().read_to_string(&mut buf)?;
186    child.stderr.take().unwrap().read_to_string(&mut buf)?;
187    _ = child.wait();
188
189    Ok(normalize_line_endings(buf))
190}