Skip to main content

clap_exec_repl/
lib.rs

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		// TODO: the following line
47		// _ = $s.rl.add_history_entry(line.as_str());
48
49		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}