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 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        // _ = self.rl.add_history_entry(line.as_str());
105
106        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}