1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
20#![warn(missing_docs)]
21#![warn(clippy::print_stderr)]
22#![warn(clippy::print_stdout)]
23#![cfg(unix)]
24
25use std::ffi::OsStr;
26use std::ffi::OsString;
27use std::io::Read as _;
28use std::io::Write as _;
29use std::path::PathBuf;
30use std::process::Command;
31use std::time::Duration;
32
33use ptyprocess::PtyProcess;
34
35pub use completest::Runtime;
36pub use completest::RuntimeBuilder;
37pub use completest::Term;
38
39#[derive(Debug)]
41#[non_exhaustive]
42pub struct ZshRuntimeBuilder {}
43
44impl RuntimeBuilder for ZshRuntimeBuilder {
45 type Runtime = ZshRuntime;
46
47 fn name() -> &'static str {
48 "zsh"
49 }
50
51 fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
52 ZshRuntime::new(bin_root, home)
53 }
54
55 fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
56 ZshRuntime::with_home(bin_root, home)
57 }
58}
59
60#[derive(Debug)]
62#[cfg(unix)] pub struct ZshRuntime {
64 path: OsString,
65 home: PathBuf,
66 timeout: Duration,
67}
68
69impl ZshRuntime {
70 pub fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
72 std::fs::create_dir_all(&home)?;
73
74 let config_path = home.join(".zshenv");
75 let config = "\
76fpath=($fpath $ZDOTDIR/zsh)
77autoload -U +X compinit && compinit -u # bypass compaudit security checking
78precmd_functions=\"\" # avoid the prompt being overwritten
79PS1='%% '
80PROMPT='%% '
81";
82 std::fs::write(config_path, config)?;
83
84 let _ = std::fs::remove_file(home.join(".zcompdump"));
85
86 Self::with_home(bin_root, home)
87 }
88
89 pub fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
91 let path = build_path(bin_root);
92
93 Ok(Self {
94 path,
95 home,
96 timeout: Duration::from_millis(100),
97 })
98 }
99
100 pub fn home(&self) -> &std::path::Path {
102 &self.home
103 }
104
105 pub fn register(&mut self, name: &str, content: &str) -> std::io::Result<()> {
107 let path = self.home.join(format!("zsh/_{name}"));
108 std::fs::create_dir_all(path.parent().expect("path created with parent"))?;
109 std::fs::write(path, content)
110 }
111
112 pub fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
114 let mut command = Command::new("zsh");
115 command.arg("--noglobalrcs");
116 command
117 .env("PATH", &self.path)
118 .env("TERM", "xterm")
119 .env("ZDOTDIR", &self.home);
120 let echo = false;
121 comptest(command, echo, input, term, self.timeout)
122 }
123}
124
125impl Runtime for ZshRuntime {
126 fn home(&self) -> &std::path::Path {
127 self.home()
128 }
129
130 fn register(&mut self, name: &str, content: &str) -> std::io::Result<()> {
131 self.register(name, content)
132 }
133
134 fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
135 self.complete(input, term)
136 }
137}
138
139#[derive(Debug)]
141#[non_exhaustive]
142pub struct BashRuntimeBuilder {}
143
144impl RuntimeBuilder for BashRuntimeBuilder {
145 type Runtime = BashRuntime;
146
147 fn name() -> &'static str {
148 "bash"
149 }
150
151 fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
152 BashRuntime::new(bin_root, home)
153 }
154
155 fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
156 BashRuntime::with_home(bin_root, home)
157 }
158}
159
160#[derive(Debug)]
162#[cfg(unix)] pub struct BashRuntime {
164 path: OsString,
165 home: PathBuf,
166 config: PathBuf,
167 timeout: Duration,
168}
169
170impl BashRuntime {
171 pub fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
173 std::fs::create_dir_all(&home)?;
174
175 let config_path = home.join(".bashrc");
176 let inputrc_path = home.join(".inputrc");
177 let config = "\
178PS1='% '
179. /etc/bash_completion
180"
181 .to_owned();
182 std::fs::write(config_path, config)?;
183 std::fs::write(
185 inputrc_path,
186 "# expected empty file to disable loading ~/.inputrc\n",
187 )?;
188
189 Self::with_home(bin_root, home)
190 }
191
192 pub fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
194 let config_path = home.join(".bashrc");
195 let path = build_path(bin_root);
196
197 Ok(Self {
198 path,
199 home,
200 config: config_path,
201 timeout: Duration::from_millis(50),
202 })
203 }
204
205 pub fn home(&self) -> &std::path::Path {
207 &self.home
208 }
209
210 pub fn register(&mut self, _name: &str, content: &str) -> std::io::Result<()> {
212 let mut file = std::fs::OpenOptions::new()
213 .append(true)
214 .open(&self.config)?;
215 writeln!(&mut file, "{content}")?;
216 Ok(())
217 }
218
219 pub fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
221 let mut command = Command::new("bash");
222 let inputrc_path = self.home.join(".inputrc");
223 command
224 .env("PATH", &self.path)
225 .env("TERM", "xterm")
226 .env("INPUTRC", &inputrc_path)
227 .args([
228 OsStr::new("--noprofile"),
229 OsStr::new("--rcfile"),
230 self.config.as_os_str(),
231 ]);
232 let echo = !input.contains("\t\t");
233 comptest(command, echo, input, term, self.timeout)
234 }
235}
236
237impl Runtime for BashRuntime {
238 fn home(&self) -> &std::path::Path {
239 self.home()
240 }
241
242 fn register(&mut self, name: &str, content: &str) -> std::io::Result<()> {
243 self.register(name, content)
244 }
245
246 fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
247 self.complete(input, term)
248 }
249}
250
251#[derive(Debug)]
253#[non_exhaustive]
254pub struct FishRuntimeBuilder {}
255
256impl RuntimeBuilder for FishRuntimeBuilder {
257 type Runtime = FishRuntime;
258
259 fn name() -> &'static str {
260 "fish"
261 }
262
263 fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
264 FishRuntime::new(bin_root, home)
265 }
266
267 fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
268 FishRuntime::with_home(bin_root, home)
269 }
270}
271
272#[derive(Debug)]
274#[cfg(unix)] pub struct FishRuntime {
276 path: OsString,
277 home: PathBuf,
278 timeout: Duration,
279}
280
281impl FishRuntime {
282 pub fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
284 std::fs::create_dir_all(&home)?;
285
286 let config_path = home.join("fish/config.fish");
287 let config = "\
288set -U fish_greeting \"\"
289set -U fish_autosuggestion_enabled 0
290function fish_title
291end
292function fish_prompt
293 printf '%% '
294end;
295"
296 .to_owned();
297 std::fs::create_dir_all(config_path.parent().expect("path created with parent"))?;
298 std::fs::write(config_path, config)?;
299
300 Self::with_home(bin_root, home)
301 }
302
303 pub fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
305 let path = build_path(bin_root);
306
307 Ok(Self {
308 path,
309 home,
310 timeout: Duration::from_millis(50),
311 })
312 }
313
314 pub fn home(&self) -> &std::path::Path {
316 &self.home
317 }
318
319 pub fn register(&mut self, name: &str, content: &str) -> std::io::Result<()> {
321 let path = self.home.join(format!("fish/completions/{name}.fish"));
322 std::fs::create_dir_all(path.parent().expect("path created with parent"))?;
323 std::fs::write(path, content)
324 }
325
326 pub fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
328 let mut command = Command::new("fish");
329 command
330 .env("PATH", &self.path)
331 .env("TERM", "xterm")
333 .env("XDG_CONFIG_HOME", &self.home);
334 let echo = false;
335 comptest(command, echo, input, term, self.timeout)
336 }
337}
338
339impl Runtime for FishRuntime {
340 fn home(&self) -> &std::path::Path {
341 self.home()
342 }
343
344 fn register(&mut self, name: &str, content: &str) -> std::io::Result<()> {
345 self.register(name, content)
346 }
347
348 fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
349 self.complete(input, term)
350 }
351}
352
353#[derive(Debug)]
355#[non_exhaustive]
356pub struct ElvishRuntimeBuilder {}
357
358impl RuntimeBuilder for ElvishRuntimeBuilder {
359 type Runtime = ElvishRuntime;
360
361 fn name() -> &'static str {
362 "elvish"
363 }
364
365 fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
366 ElvishRuntime::new(bin_root, home)
367 }
368
369 fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self::Runtime> {
370 ElvishRuntime::with_home(bin_root, home)
371 }
372}
373
374#[derive(Debug)]
376#[cfg(unix)] pub struct ElvishRuntime {
378 path: OsString,
379 home: PathBuf,
380 config: PathBuf,
381 timeout: Duration,
382}
383
384impl ElvishRuntime {
385 pub fn new(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
387 std::fs::create_dir_all(&home)?;
388
389 let config_path = home.join("elvish/rc.elv");
390 let config = "\
391set edit:rprompt = (constantly \"\")
392set edit:prompt = (constantly \"% \")
393"
394 .to_owned();
395 std::fs::create_dir_all(config_path.parent().expect("path created with parent"))?;
396 std::fs::write(config_path, config)?;
397
398 Self::with_home(bin_root, home)
399 }
400
401 pub fn with_home(bin_root: PathBuf, home: PathBuf) -> std::io::Result<Self> {
403 let config_path = home.join("elvish/rc.elv");
404 let path = build_path(bin_root);
405
406 Ok(Self {
407 path,
408 home,
409 config: config_path,
410 timeout: Duration::from_millis(50),
411 })
412 }
413
414 pub fn home(&self) -> &std::path::Path {
416 &self.home
417 }
418
419 pub fn register(&mut self, _name: &str, content: &str) -> std::io::Result<()> {
421 let mut file = std::fs::OpenOptions::new()
422 .append(true)
423 .open(&self.config)?;
424 writeln!(&mut file, "{content}")?;
425 Ok(())
426 }
427
428 pub fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
430 let mut command = Command::new("elvish");
431 command
432 .env("PATH", &self.path)
433 .env("XDG_CONFIG_HOME", &self.home);
434 let echo = false;
435 comptest(command, echo, input, term, self.timeout)
436 }
437}
438
439impl Runtime for ElvishRuntime {
440 fn home(&self) -> &std::path::Path {
441 self.home()
442 }
443
444 fn register(&mut self, name: &str, content: &str) -> std::io::Result<()> {
445 self.register(name, content)
446 }
447
448 fn complete(&mut self, input: &str, term: &Term) -> std::io::Result<String> {
449 self.complete(input, term)
450 }
451}
452
453fn comptest(
454 command: Command,
455 echo: bool,
456 input: &str,
457 term: &Term,
458 timeout: Duration,
459) -> std::io::Result<String> {
460 #![allow(clippy::unwrap_used)] let mut process = PtyProcess::spawn(command)?;
467 process.set_window_size(term.get_width(), term.get_height())?;
468 process.set_echo(echo, None)?;
470
471 let mut parser = vt100::Parser::new(term.get_height(), term.get_width(), 0);
472
473 let mut stream = process.get_raw_handle()?;
474 write!(stream, "{}", input)?;
476 stream.flush()?;
477
478 let (snd, rcv) = std::sync::mpsc::channel();
479
480 let shutdown = std::sync::atomic::AtomicBool::new(false);
481 let shutdown_ref = &shutdown;
482 std::thread::scope(|scope| {
483 scope.spawn(move || {
484 rcv.recv().unwrap();
487 while rcv.recv_timeout(timeout).is_ok() {}
488 shutdown_ref.store(true, std::sync::atomic::Ordering::SeqCst);
489 process.exit(false).unwrap();
490 });
491
492 let mut buf = [0; 2048];
493 while let Ok(n) = stream.read(&mut buf) {
494 if shutdown.load(std::sync::atomic::Ordering::SeqCst) {
495 break;
497 }
498 let buf = &buf[..n];
499 if buf.is_empty() {
500 break;
501 }
502 let _ = snd.send(());
503 parser.process(buf);
504 }
505 });
506
507 let content = parser.screen().contents();
508 Ok(content)
509}
510
511fn build_path(bin_root: PathBuf) -> OsString {
512 let mut path = bin_root.into_os_string();
513 if let Some(existing) = std::env::var_os("PATH") {
514 path.push(":");
515 path.push(existing);
516 }
517 path
518}