1use std::{ffi::OsString, marker::PhantomData, path::PathBuf, str::FromStr};
2
3use clap::Parser;
4use reedline::{Prompt, Reedline, Signal, Span};
5use shlex::Shlex;
6use tracing::error;
7
8mod builder;
9
10pub use builder::*;
11use clap_exec::{Runner, RunnerAsync};
12
13macro_rules! handle_read_command {
14 ($self:expr) => {
15 match $self.read_command() {
16 Ok(cmd) => cmd,
17 Err(ReplError::Io(err)) => {
18 tracing::error!(?err, "Unexpected IO error");
19 return Err(ReplError::Io(err));
20 }
21 Err(ReplError::Clap(err)) => {
22 tracing::warn!(?err, "Unexpected clap parsing error");
23 continue;
24 }
25 Err(ReplError::Shlex) => {
26 tracing::warn!("Invalid input detected");
27 continue;
28 }
29 }
30 };
31}
32
33macro_rules! read_command {
34 ($s:path) => {{
35 let line = match $s.rl.read_line(&*$s.prompt) {
36 Ok(Signal::Success(buffer)) => buffer,
37 Ok(Signal::CtrlC) => return Ok(ReadCommandOutput::CtrlC),
38 Ok(Signal::CtrlD) => return Ok(ReadCommandOutput::CtrlD),
39 Err(e) => return Err(ReplError::Io(e)),
40 };
41
42 if line.trim().is_empty() {
43 return Ok(ReadCommandOutput::EmptyLine);
44 }
45
46 match shlex::split(&line) {
50 Some(split) => match C::try_parse_from(std::iter::once("").chain(split.iter().map(String::as_str))) {
51 Ok(c) => Ok(ReadCommandOutput::Command(c)),
52 Err(e) => Err(ReplError::Clap(e)),
53 },
54
55 None => Err(ReplError::Shlex),
56 }
57 }};
58}
59
60#[derive(thiserror::Error, Debug)]
61pub enum ReplError {
62 #[error(transparent)]
63 Io(#[from] std::io::Error),
64 #[error(transparent)]
65 Clap(#[from] clap::Error),
66 #[error("Could not split string")]
67 Shlex,
68}
69
70pub enum ReadCommandOutput<C> {
71 Command(C),
72 EmptyLine,
73 CtrlC,
74 CtrlD,
75}
76
77pub type Result<T> = std::result::Result<T, ReplError>;
78
79struct ReedCompleter<C: Parser + Send + Sync + 'static> {
80 c_phantom: PhantomData<C>,
81}
82
83impl<C: Parser + Send + Sync + 'static> reedline::Completer for ReedCompleter<C> {
84 fn complete(&mut self, line: &str, pos: usize) -> Vec<reedline::Suggestion> {
85 let mut cmd = C::command();
86
87 let args = Shlex::new(line);
88 let mut args = std::iter::once("".to_owned())
89 .chain(args)
90 .map(OsString::from)
91 .collect::<Vec<_>>();
92 if line.ends_with(' ') {
93 args.push(OsString::new());
94 }
95 let arg_index = args.len() - 1;
96 let arg_index_len = args[arg_index].len();
97
98 let candidates = if let Ok(candidates) =
99 clap_complete::engine::complete(&mut cmd, args, arg_index, PathBuf::from_str(".").ok().as_deref())
100 {
101 candidates
102 } else {
103 return vec![];
104 };
105
106 let span = Span::new(pos - arg_index_len, pos);
107
108 candidates
109 .into_iter()
110 .map(|c| reedline::Suggestion {
111 value: c.get_value().to_string_lossy().into_owned(),
112 description: c.get_help().map(|x| x.to_string()),
113 style: None,
114 extra: None,
115 span,
116 append_whitespace: true,
117 match_indices: None,
118 })
119 .collect()
120 }
121}
122
123pub struct ClapEditor<C: Parser + Runner + Send + Sync + 'static> {
124 rl: Reedline,
125 prompt: Box<dyn Prompt>,
126 c_phantom: PhantomData<C>,
127}
128
129impl<C: Parser + Runner + Send + Sync + 'static> ClapEditor<C> {
130 pub fn builder() -> ClapEditorBuilder<C> {
131 ClapEditorBuilder::<C>::new()
132 }
133
134 pub fn get_editor(&mut self) -> &mut Reedline {
135 &mut self.rl
136 }
137
138 pub fn set_prompt(&mut self, prompt: Box<dyn Prompt>) {
139 self.prompt = prompt;
140 }
141
142 pub fn read_command(&mut self) -> Result<ReadCommandOutput<C>> {
143 read_command!(self)
144 }
145
146 pub fn repl(mut self, state: &mut C::UserState) -> Result<()> {
147 loop {
148 let cmd = handle_read_command!(self);
149
150 match cmd {
151 ReadCommandOutput::Command(mut c) => {
152 if let Err(err) = c.run(state) {
153 error!(?err, "Command execution failed");
154 }
155 }
156 ReadCommandOutput::EmptyLine => continue,
157 ReadCommandOutput::CtrlC => continue,
158 ReadCommandOutput::CtrlD => break,
159 }
160 }
161
162 Ok(())
163 }
164}
165
166pub struct ClapAsyncEditor<C: Parser + RunnerAsync + Send + Sync + 'static> {
167 rl: Reedline,
168 prompt: Box<dyn Prompt>,
169 c_phantom: PhantomData<C>,
170}
171
172impl<C: Parser + RunnerAsync + Send + Sync + 'static> ClapAsyncEditor<C> {
173 pub fn builder() -> ClapEditorAsyncBuilder<C> {
174 ClapEditorAsyncBuilder::<C>::new()
175 }
176
177 pub fn get_editor(&mut self) -> &mut Reedline {
178 &mut self.rl
179 }
180
181 pub fn set_prompt(&mut self, prompt: Box<dyn Prompt>) {
182 self.prompt = prompt;
183 }
184
185 pub fn read_command(&mut self) -> Result<ReadCommandOutput<C>> {
186 read_command!(self)
187 }
188
189 pub async fn repl(mut self, state: &mut C::UserState) -> Result<()> {
190 loop {
191 let cmd = handle_read_command!(self);
192
193 match cmd {
194 ReadCommandOutput::Command(mut c) => {
195 if let Err(err) = c.run(state).await {
196 error!(?err, "Command execution failed");
197 }
198 }
199 ReadCommandOutput::EmptyLine => continue,
200 ReadCommandOutput::CtrlC => continue,
201 ReadCommandOutput::CtrlD => break,
202 }
203 }
204
205 Ok(())
206 }
207}