clap_repl/
lib.rs

1use std::{ffi::OsString, marker::PhantomData, path::PathBuf, str::FromStr};
2
3use clap::{Parser, Subcommand};
4use console::style;
5
6// reexport reedline to prevent version mixups
7pub 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    /// Input parsed successfully.
63    Command(C),
64
65    /// Input was empty.
66    EmptyLine,
67
68    /// Clap parse error happened. You should print the error manually.
69    ClapError(clap::error::Error),
70
71    /// Input was not lexically valid, for example it had odd number of `"`
72    ShlexError,
73
74    /// Reedline failed to work with stdio.
75    ReedlineError(std::io::Error),
76
77    /// User pressed ctrl+C
78    CtrlC,
79
80    /// User pressed ctrl+D
81    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        // _ = self.rl.add_history_entry(line.as_str());
109
110        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}