1use std::{ffi::OsString, marker::PhantomData, path::PathBuf, str::FromStr};
2
3use clap::{Parser, Subcommand};
4use console::style;
5
6pub use reedline;
8use reedline::{Prompt, Reedline, Signal, Span};
9use shlex::Shlex;
10
11mod builder;
12
13pub use builder::ClapEditorBuilder;
14
15pub struct ClapEditor<C: Parser + Send + Sync + 'static> {
16 rl: Reedline,
17 prompt: Box<dyn Prompt>,
18 c_phantom: PhantomData<C>,
19}
20
21struct ReedCompleter<C: Parser + Send + Sync + 'static> {
22 c_phantom: PhantomData<C>,
23}
24
25impl<C: Parser + Send + Sync + 'static> reedline::Completer for ReedCompleter<C> {
26 fn complete(&mut self, line: &str, pos: usize) -> Vec<reedline::Suggestion> {
27 let cmd = C::command();
28 let mut cmd = clap_complete::dynamic::command::CompleteCommand::augment_subcommands(cmd);
29 let args = Shlex::new(line);
30 let mut args = std::iter::once("".to_owned())
31 .chain(args)
32 .map(OsString::from)
33 .collect::<Vec<_>>();
34 if line.ends_with(' ') {
35 args.push(OsString::new());
36 }
37 let arg_index = args.len() - 1;
38 let span = Span::new(pos - args[arg_index].len(), pos);
39 let Ok(candidates) = clap_complete::dynamic::complete(
40 &mut cmd,
41 args,
42 arg_index,
43 PathBuf::from_str(".").ok().as_deref(),
44 ) else {
45 return vec![];
46 };
47 candidates
48 .into_iter()
49 .map(|c| reedline::Suggestion {
50 value: c.get_content().to_string_lossy().into_owned(),
51 description: c.get_help().map(|x| x.to_string()),
52 style: None,
53 extra: None,
54 span,
55 append_whitespace: true,
56 })
57 .collect()
58 }
59}
60
61pub enum ReadCommandOutput<C> {
62 Command(C),
64
65 EmptyLine,
67
68 ClapError(clap::error::Error),
70
71 ShlexError,
73
74 ReedlineError(std::io::Error),
76
77 CtrlC,
79
80 CtrlD,
82}
83
84impl<C: Parser + Send + Sync + 'static> ClapEditor<C> {
85 pub fn builder() -> ClapEditorBuilder<C> {
86 ClapEditorBuilder::<C>::new()
87 }
88
89 pub fn get_editor(&mut self) -> &mut Reedline {
90 &mut self.rl
91 }
92
93 pub fn read_command(&mut self) -> ReadCommandOutput<C> {
94 let line = match self.rl.read_line(&*self.prompt) {
95 Ok(Signal::Success(buffer)) => buffer,
96 Ok(Signal::CtrlC) => return ReadCommandOutput::CtrlC,
97 Ok(Signal::CtrlD) => return ReadCommandOutput::CtrlD,
98 Err(e) => return ReadCommandOutput::ReedlineError(e),
99 };
100 if line.trim().is_empty() {
101 return ReadCommandOutput::EmptyLine;
102 }
103
104 match shlex::split(&line) {
107 Some(split) => {
108 match C::try_parse_from(std::iter::once("").chain(split.iter().map(String::as_str)))
109 {
110 Ok(c) => ReadCommandOutput::Command(c),
111 Err(e) => ReadCommandOutput::ClapError(e),
112 }
113 }
114 None => ReadCommandOutput::ShlexError,
115 }
116 }
117
118 pub fn repl(mut self, mut handler: impl FnMut(C)) {
119 loop {
120 match self.read_command() {
121 ReadCommandOutput::Command(c) => handler(c),
122 ReadCommandOutput::EmptyLine => (),
123 ReadCommandOutput::ClapError(e) => {
124 e.print().unwrap();
125 }
126 ReadCommandOutput::ShlexError => {
127 println!(
128 "{} input was not valid and could not be processed",
129 style("Error:").red().bold()
130 );
131 }
132 ReadCommandOutput::ReedlineError(e) => {
133 panic!("{e}");
134 }
135 ReadCommandOutput::CtrlC => continue,
136 ReadCommandOutput::CtrlD => break,
137 }
138 }
139 }
140}