1use 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};
14use tracing::info;
15
16pub trait System: fmt::Debug {
18 fn set_clipboard(&mut self, s: &str) -> io::Result<()>;
20
21 fn read_clipboard(&self) -> io::Result<String>;
23
24 fn run_command_blocking<I, S>(
26 &self,
27 cmd: &str,
28 args: I,
29 cwd: &Path,
30 bufid: usize,
31 ) -> io::Result<String>
32 where
33 I: IntoIterator<Item = S>,
34 S: AsRef<OsStr>,
35 {
36 run_command_blocking(cmd, args, cwd, bufid)
37 }
38
39 fn run_command<I, S>(&self, cmd: &str, args: I, cwd: &Path, bufid: usize, tx: Sender<Event>)
42 where
43 I: IntoIterator<Item = S>,
44 S: AsRef<OsStr>,
45 {
46 run_command(cmd, args, cwd, bufid, tx)
47 }
48
49 fn pipe_through_command<I, S>(
51 &self,
52 cmd: &str,
53 args: I,
54 input: &str,
55 cwd: &Path,
56 bufid: usize,
57 ) -> io::Result<String>
58 where
59 I: IntoIterator<Item = S>,
60 S: AsRef<OsStr>,
61 {
62 pipe_through_command(cmd, args, input, cwd, bufid)
63 }
64}
65
66#[derive(Debug, Clone)]
67struct ClipboardProvider {
68 copy_cmd: &'static str,
69 copy_args: Vec<&'static str>,
70 paste_cmd: &'static str,
71 paste_args: Vec<&'static str>,
72}
73
74impl ClipboardProvider {
75 pub fn try_from_env() -> Option<Self> {
76 let paths = env::var("PATH").expect("path not set");
77 let exists = |cmd: &str| env::split_paths(&paths).any(|dir| dir.join(cmd).is_file());
78
79 let (copy_cmd, copy_args, paste_cmd, paste_args) = if exists("pbcopy") {
80 info!("clipboard provider found: pbcopy");
81 ("pbcopy", vec![], "pbpaste", vec![])
82 } else if env::var("WAYLAND_DISPLAY").is_ok() && exists("wl-copy") && exists("wl-paste") {
83 info!("clipboard provider found: wl-copy");
84 (
85 "wl-copy",
86 vec!["--foreground", "--type", "text/plain"],
87 "wl-paste",
88 vec!["--no-newline"],
89 )
90 } else if env::var("DISPLAY").is_ok() && exists("xclip") {
91 info!("clipboard provider found: xclip");
92 (
93 "xclip",
94 vec!["-i", "-selection", "clipboard"],
95 "xclip",
96 vec!["-o", "-selection", "clipboard"],
97 )
98 } else {
99 info!("no clipboard provider found");
100 return None;
101 };
102
103 Some(Self {
104 copy_cmd,
105 copy_args,
106 paste_cmd,
107 paste_args,
108 })
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct DefaultSystem {
115 selection: String,
116 cp: Option<ClipboardProvider>,
117}
118
119impl DefaultSystem {
120 pub fn from_env() -> Self {
121 Self {
122 selection: String::new(),
123 cp: ClipboardProvider::try_from_env(),
124 }
125 }
126}
127
128impl System for DefaultSystem {
129 fn set_clipboard(&mut self, s: &str) -> io::Result<()> {
130 match &self.cp {
131 Some(cp) => {
132 let mut child = Command::new(cp.copy_cmd)
133 .args(&cp.copy_args)
134 .stdin(Stdio::piped())
135 .spawn()?;
136
137 child.stdin.take().unwrap().write_all(s.as_bytes())
138 }
139
140 None => {
141 self.selection = s.to_string();
142 Ok(())
143 }
144 }
145 }
146
147 fn read_clipboard(&self) -> io::Result<String> {
148 match &self.cp {
149 Some(cp) => {
150 let output = Command::new(cp.paste_cmd).args(&cp.paste_args).output()?;
151
152 Ok(String::from_utf8(output.stdout).unwrap_or_default())
153 }
154
155 None => Ok(self.selection.clone()),
156 }
157 }
158}
159
160fn prepare_command<I, S>(cmd: &str, args: I, cwd: &Path, bufid: usize) -> Command
161where
162 I: IntoIterator<Item = S>,
163 S: AsRef<OsStr>,
164{
165 let path = env::var("PATH").unwrap();
166 let home = env::var("HOME").unwrap();
167 let mut command = Command::new(cmd);
168 command
169 .env("PATH", format!("{home}/.ad/bin:{path}"))
170 .env("bufid", bufid.to_string())
171 .current_dir(cwd)
172 .args(args);
173
174 command
175}
176
177fn run_command_blocking<I, S>(cmd: &str, args: I, cwd: &Path, bufid: usize) -> io::Result<String>
178where
179 I: IntoIterator<Item = S>,
180 S: AsRef<OsStr>,
181{
182 let output = prepare_command(cmd, args, cwd, bufid).output()?;
183 let mut stdout = String::from_utf8(output.stdout).unwrap_or_default();
184 let stderr = String::from_utf8(output.stderr).unwrap_or_default();
185 stdout.push_str(&stderr);
186
187 Ok(normalize_line_endings(stdout))
188}
189
190fn run_command<I, S>(cmd: &str, args: I, cwd: &Path, bufid: usize, tx: Sender<Event>)
191where
192 I: IntoIterator<Item = S>,
193 S: AsRef<OsStr>,
194{
195 let mut command = prepare_command(cmd, args, cwd, bufid);
196
197 spawn(move || {
198 let output = match command.output() {
199 Ok(output) => output,
200 Err(err) => {
201 _ = tx.send(Event::Action(Action::SetStatusMessage {
202 message: err.to_string(),
203 }));
204 return;
205 }
206 };
207
208 let mut content = String::from_utf8(output.stdout).unwrap_or_default();
209 let stderr = String::from_utf8(output.stderr).unwrap_or_default();
210 content.push_str(&stderr);
211 if content.is_empty() {
212 return;
213 }
214 _ = tx.send(Event::Action(Action::AppendToOutputBuffer {
215 bufid,
216 content: normalize_line_endings(content),
217 }));
218 });
219}
220
221pub fn pipe_through_command<I, S>(
223 cmd: &str,
224 args: I,
225 input: &str,
226 cwd: &Path,
227 bufid: usize,
228) -> io::Result<String>
229where
230 I: IntoIterator<Item = S>,
231 S: AsRef<OsStr>,
232{
233 let mut child = prepare_command(cmd, args, cwd, bufid)
234 .stdin(Stdio::piped())
235 .stdout(Stdio::piped())
236 .stderr(Stdio::piped())
237 .spawn()?;
238
239 let mut buf = String::new();
240 child.stdin.take().unwrap().write_all(input.as_bytes())?;
241 child.stdout.take().unwrap().read_to_string(&mut buf)?;
242 child.stderr.take().unwrap().read_to_string(&mut buf)?;
243 _ = child.wait();
244
245 Ok(normalize_line_endings(buf))
246}