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 set_prompt(&mut self, prompt: Box<dyn Prompt>) {
94 self.prompt = prompt;
95 }
96
97 pub fn read_command(&mut self) -> ReadCommandOutput<C> {
98 let line = match self.rl.read_line(&*self.prompt) {
99 Ok(Signal::Success(buffer)) => buffer,
100 Ok(Signal::CtrlC) => return ReadCommandOutput::CtrlC,
101 Ok(Signal::CtrlD) => return ReadCommandOutput::CtrlD,
102 Err(e) => return ReadCommandOutput::ReedlineError(e),
103 };
104 if line.trim().is_empty() {
105 return ReadCommandOutput::EmptyLine;
106 }
107
108 match shlex::split(&line) {
111 Some(split) => {
112 match C::try_parse_from(std::iter::once("").chain(split.iter().map(String::as_str)))
113 {
114 Ok(c) => ReadCommandOutput::Command(c),
115 Err(e) => ReadCommandOutput::ClapError(e),
116 }
117 }
118 None => ReadCommandOutput::ShlexError,
119 }
120 }
121
122 pub fn repl(mut self, mut handler: impl FnMut(C)) {
123 loop {
124 match self.read_command() {
125 ReadCommandOutput::Command(c) => handler(c),
126 ReadCommandOutput::EmptyLine => (),
127 ReadCommandOutput::ClapError(e) => {
128 e.print().unwrap();
129 }
130 ReadCommandOutput::ShlexError => {
131 println!(
132 "{} input was not valid and could not be processed",
133 style("Error:").red().bold()
134 );
135 }
136 ReadCommandOutput::ReedlineError(e) => {
137 panic!("{e}");
138 }
139 ReadCommandOutput::CtrlC => continue,
140 ReadCommandOutput::CtrlD => break,
141 }
142 }
143 }
144
145 #[cfg(feature = "async")]
146 pub async fn repl_async(mut self, mut handler: impl AsyncFnMut(C)) {
147 loop {
148 match self.read_command() {
149 ReadCommandOutput::Command(c) => handler(c).await,
150 ReadCommandOutput::EmptyLine => (),
151 ReadCommandOutput::ClapError(e) => {
152 e.print().unwrap();
153 }
154 ReadCommandOutput::ShlexError => {
155 println!(
156 "{} input was not valid and could not be processed",
157 style("Error:").red().bold()
158 );
159 }
160 ReadCommandOutput::ReedlineError(e) => {
161 panic!("{e}");
162 }
163 ReadCommandOutput::CtrlC => continue,
164 ReadCommandOutput::CtrlD => break,
165 }
166 }
167 }
168}