libpt_cli/repl/
default.rs

1//! This module implements a default repl that fullfills the [Repl] trait
2//!
3//! You can implement your own [Repl] if you want.
4
5use std::fmt::Debug;
6
7use super::Repl;
8
9use embed_doc_image::embed_doc_image;
10
11/// [clap] help template with only usage and commands/options
12pub const REPL_HELP_TEMPLATE: &str = r"{usage-heading} {usage}
13
14{all-args}{tab}
15";
16
17use clap::{Parser, Subcommand};
18use dialoguer::{BasicHistory, Completion};
19use libpt_log::trace;
20
21#[allow(clippy::needless_doctest_main)] // It makes the example look better
22/// Default implementation for a REPL
23///
24/// Note that you need to define the commands by yourself with a Subcommands enum.
25///
26/// # Example
27///
28/// ```no_run
29/// use libpt_cli::repl::{DefaultRepl, Repl};
30/// use clap::Subcommand;
31/// use strum::EnumIter;
32///
33/// #[derive(Subcommand, Debug, EnumIter, Clone)]
34/// enum ReplCommand {
35///     /// hello world
36///     Hello,
37///     /// leave the repl
38///     Exit,
39/// }
40///
41/// fn main() {
42///     let mut repl = DefaultRepl::<ReplCommand>::default();
43///     loop {
44///         repl.step().unwrap();
45///         match repl.command().to_owned().unwrap() {
46///             ReplCommand::Hello => println!("Hello"),
47///             ReplCommand::Exit => break,
48///             _ => (),
49///         }
50///     }
51/// }
52/// ```
53/// **Screenshot**
54///
55/// ![Screenshot of an example program with a REPL][repl_screenshot]
56#[embed_doc_image("repl_screenshot", "data/media/repl.png")]
57#[derive(Parser)]
58#[command(multicall = true, help_template = REPL_HELP_TEMPLATE)]
59#[allow(clippy::module_name_repetitions)] // we can't just name it `Default`, that's part of std
60pub struct DefaultRepl<C>
61where
62    C: Debug + Subcommand + strum::IntoEnumIterator,
63{
64    /// the command you want to execute, along with its arguments
65    #[command(subcommand)]
66    command: Option<C>,
67
68    // the following fields are not to be parsed from a command, but used for the internal workings
69    // of the repl
70    #[clap(skip)]
71    buf: String,
72    #[clap(skip)]
73    buf_preparsed: Vec<String>,
74    #[clap(skip)]
75    completion: DefaultReplCompletion<C>,
76    #[clap(skip)]
77    history: BasicHistory,
78}
79
80#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
81struct DefaultReplCompletion<C>
82where
83    C: Debug + Subcommand + strum::IntoEnumIterator,
84{
85    commands: std::marker::PhantomData<C>,
86}
87
88impl<C> Repl<C> for DefaultRepl<C>
89where
90    C: Debug + Subcommand + strum::IntoEnumIterator,
91{
92    fn new() -> Self {
93        Self {
94            command: None,
95            buf_preparsed: Vec::new(),
96            buf: String::new(),
97            history: BasicHistory::new(),
98            completion: DefaultReplCompletion::new(),
99        }
100    }
101    fn command(&self) -> &Option<C> {
102        &self.command
103    }
104    fn step(&mut self) -> Result<(), super::error::Error> {
105        self.buf.clear();
106
107        // NOTE: display::Input requires some kind of lifetime that would be a bother to store in
108        // our struct. It's documentation also uses it in place, so it should be fine to do it like
109        // this.
110        //
111        // NOTE: It would be nice if we could use the Validator mechanism of dialoguer, but
112        // unfortunately we can only process our input after we've preparsed it and we need an
113        // actual output. If we could set a status after the Input is over that would be amazing,
114        // but that is currently not supported by dialoguer.
115        // Therefore, every prompt will show as success regardless.
116        self.buf = dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default())
117            .completion_with(&self.completion)
118            .history_with(&mut self.history)
119            .interact_text()?;
120
121        self.buf_preparsed = Vec::new();
122        self.buf_preparsed
123            .extend(shlex::split(&self.buf).unwrap_or_default());
124
125        trace!("read input: {:?}", self.buf_preparsed);
126        trace!("repl after step: {:#?}", self);
127
128        // HACK: find a way to not allocate a new struct for this
129        let cmds = Self::try_parse_from(&self.buf_preparsed)?;
130        self.command = cmds.command;
131        Ok(())
132    }
133}
134
135impl<C> Default for DefaultRepl<C>
136where
137    C: Debug + Subcommand + strum::IntoEnumIterator,
138{
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl<C> Debug for DefaultRepl<C>
145where
146    C: Debug + Subcommand + strum::IntoEnumIterator,
147{
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("DefaultRepl")
150            .field("command", &self.command)
151            .field("buf", &self.buf)
152            .field("buf_preparsed", &self.buf_preparsed)
153            .field("completion", &self.completion)
154            .field("history", &"(no debug)")
155            .finish()
156    }
157}
158
159impl<C> DefaultReplCompletion<C>
160where
161    C: Debug + Subcommand + strum::IntoEnumIterator,
162{
163    /// Make a new [`DefaultReplCompletion`] for the type `C`
164    pub const fn new() -> Self {
165        Self {
166            commands: std::marker::PhantomData::<C>,
167        }
168    }
169    fn commands() -> Vec<String> {
170        let mut buf = Vec::new();
171        // every crate has the help command, but it is not part of the enum
172        buf.push("help".to_string());
173        for c in C::iter() {
174            // HACK: this is a horrible way to do this
175            // I just need the names of the commands
176            buf.push(
177                format!("{c:?}")
178                    .split_whitespace()
179                    .map(str::to_lowercase)
180                    .next()
181                    .unwrap()
182                    .to_string(),
183            );
184        }
185        trace!("commands: {buf:?}");
186        buf
187    }
188}
189
190impl<C> Default for DefaultReplCompletion<C>
191where
192    C: Debug + Subcommand + strum::IntoEnumIterator,
193{
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199impl<C> Completion for DefaultReplCompletion<C>
200where
201    C: Debug + Subcommand + strum::IntoEnumIterator,
202{
203    /// Simple completion implementation based on substring
204    fn get(&self, input: &str) -> Option<String> {
205        let matches = Self::commands()
206            .into_iter()
207            .filter(|option| option.starts_with(input))
208            .collect::<Vec<_>>();
209
210        trace!("\nmatches: {matches:#?}");
211        if matches.len() == 1 {
212            Some(matches[0].to_string())
213        } else {
214            None
215        }
216    }
217}