embedded_cli/
cli.rs

1pub use crate::builder::CliBuilder;
2
3use core::fmt::Debug;
4
5#[cfg(not(feature = "history"))]
6use core::marker::PhantomData;
7
8use crate::{
9    buffer::Buffer,
10    builder::DEFAULT_PROMPT,
11    codes,
12    command::RawCommand,
13    editor::Editor,
14    input::{ControlInput, Input, InputGenerator},
15    service::{Autocomplete, CommandProcessor, Help, ParseError, ProcessError},
16    token::Tokens,
17    writer::{WriteExt, Writer},
18};
19
20#[cfg(feature = "autocomplete")]
21use crate::autocomplete::Request;
22
23#[cfg(feature = "help")]
24use crate::{help::HelpRequest, service::HelpError};
25
26#[cfg(feature = "history")]
27use crate::history::History;
28
29use embedded_io::{Error, Write};
30
31pub struct CliHandle<'a, W: Write<Error = E>, E: embedded_io::Error> {
32    new_prompt: Option<&'static str>,
33    writer: Writer<'a, W, E>,
34}
35
36impl<'a, W, E> CliHandle<'a, W, E>
37where
38    W: Write<Error = E>,
39    E: embedded_io::Error,
40{
41    /// Set new prompt to use in CLI
42    pub fn set_prompt(&mut self, prompt: &'static str) {
43        self.new_prompt = Some(prompt)
44    }
45
46    pub fn writer(&mut self) -> &mut Writer<'a, W, E> {
47        &mut self.writer
48    }
49
50    fn new(writer: Writer<'a, W, E>) -> Self {
51        Self {
52            new_prompt: None,
53            writer,
54        }
55    }
56}
57
58impl<'a, W, E> Debug for CliHandle<'a, W, E>
59where
60    W: Write<Error = E>,
61    E: embedded_io::Error,
62{
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        f.debug_struct("CliHandle").finish()
65    }
66}
67
68#[cfg(feature = "history")]
69enum NavigateHistory {
70    Older,
71    Newer,
72}
73
74enum NavigateInput {
75    Backward,
76    Forward,
77}
78
79#[doc(hidden)]
80pub struct Cli<W: Write<Error = E>, E: Error, CommandBuffer: Buffer, HistoryBuffer: Buffer> {
81    editor: Option<Editor<CommandBuffer>>,
82    #[cfg(feature = "history")]
83    history: History<HistoryBuffer>,
84    input_generator: Option<InputGenerator>,
85    prompt: &'static str,
86    writer: W,
87    #[cfg(not(feature = "history"))]
88    _ph: PhantomData<HistoryBuffer>,
89}
90
91impl<W, E, CommandBuffer, HistoryBuffer> Debug for Cli<W, E, CommandBuffer, HistoryBuffer>
92where
93    W: Write<Error = E>,
94    E: embedded_io::Error,
95    CommandBuffer: Buffer,
96    HistoryBuffer: Buffer,
97{
98    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        f.debug_struct("Cli")
100            .field("editor", &self.editor)
101            .field("input_generator", &self.input_generator)
102            .field("prompt", &self.prompt)
103            .finish()
104    }
105}
106
107impl<W, E, CommandBuffer, HistoryBuffer> Cli<W, E, CommandBuffer, HistoryBuffer>
108where
109    W: Write<Error = E>,
110    E: embedded_io::Error,
111    CommandBuffer: Buffer,
112    HistoryBuffer: Buffer,
113{
114    #[allow(unused_variables)]
115    #[deprecated(since = "0.2.1", note = "please use `builder` instead")]
116    pub fn new(
117        writer: W,
118        command_buffer: CommandBuffer,
119        history_buffer: HistoryBuffer,
120    ) -> Result<Self, E> {
121        let mut cli = Self {
122            editor: Some(Editor::new(command_buffer)),
123            #[cfg(feature = "history")]
124            history: History::new(history_buffer),
125            input_generator: Some(InputGenerator::new()),
126            prompt: DEFAULT_PROMPT,
127            writer,
128            #[cfg(not(feature = "history"))]
129            _ph: PhantomData,
130        };
131
132        cli.writer.flush_str(cli.prompt)?;
133
134        Ok(cli)
135    }
136
137    pub(crate) fn from_builder(
138        builder: CliBuilder<W, E, CommandBuffer, HistoryBuffer>,
139    ) -> Result<Self, E> {
140        let mut cli = Self {
141            editor: Some(Editor::new(builder.command_buffer)),
142            #[cfg(feature = "history")]
143            history: History::new(builder.history_buffer),
144            input_generator: Some(InputGenerator::new()),
145            prompt: builder.prompt,
146            writer: builder.writer,
147            #[cfg(not(feature = "history"))]
148            _ph: PhantomData,
149        };
150
151        cli.writer.flush_str(cli.prompt)?;
152
153        Ok(cli)
154    }
155
156    /// Each call to process byte can be done with different
157    /// command set and/or command processor.
158    /// In process callback you can change some outside state
159    /// so next calls will use different processor
160    pub fn process_byte<C: Autocomplete + Help, P: CommandProcessor<W, E>>(
161        &mut self,
162        b: u8,
163        processor: &mut P,
164    ) -> Result<(), E> {
165        if let (Some(mut editor), Some(mut input_generator)) =
166            (self.editor.take(), self.input_generator.take())
167        {
168            let result = input_generator
169                .accept(b)
170                .map(|input| match input {
171                    Input::Control(control) => {
172                        self.on_control_input::<C, _>(&mut editor, control, processor)
173                    }
174                    Input::Char(text) => self.on_text_input(&mut editor, text),
175                })
176                .unwrap_or(Ok(()));
177
178            self.editor = Some(editor);
179            self.input_generator = Some(input_generator);
180            result
181        } else {
182            Ok(())
183        }
184    }
185
186    /// Set new prompt to use in CLI
187    ///
188    /// Changes will apply immediately and current line
189    /// will be replaced by new prompt and input
190    pub fn set_prompt(&mut self, prompt: &'static str) -> Result<(), E> {
191        self.prompt = prompt;
192        self.clear_line(false)?;
193
194        if let Some(editor) = self.editor.as_mut() {
195            self.writer.flush_str(editor.text())?;
196        }
197
198        Ok(())
199    }
200
201    pub fn write(
202        &mut self,
203        f: impl FnOnce(&mut Writer<'_, W, E>) -> Result<(), E>,
204    ) -> Result<(), E> {
205        self.clear_line(true)?;
206
207        let mut cli_writer = Writer::new(&mut self.writer);
208
209        f(&mut cli_writer)?;
210
211        // we should write back input that was there before writing
212        if cli_writer.is_dirty() {
213            self.writer.write_str(codes::CRLF)?;
214        }
215        self.writer.write_str(self.prompt)?;
216        if let Some(editor) = self.editor.as_mut() {
217            self.writer.flush_str(editor.text())?;
218        }
219
220        Ok(())
221    }
222
223    fn clear_line(&mut self, clear_prompt: bool) -> Result<(), E> {
224        self.writer.write_str("\r")?;
225        self.writer.write_bytes(codes::CLEAR_LINE)?;
226
227        if !clear_prompt {
228            self.writer.write_str(self.prompt)?;
229        }
230
231        self.writer.flush()
232    }
233
234    fn on_text_input(&mut self, editor: &mut Editor<CommandBuffer>, text: &str) -> Result<(), E> {
235        let is_inside = editor.cursor() < editor.len();
236        if let Some(c) = editor.insert(text) {
237            if is_inside {
238                // text is always one char
239                debug_assert_eq!(c.chars().count(), 1);
240                self.writer.write_bytes(codes::INSERT_CHAR)?;
241            }
242            self.writer.flush_str(c)?;
243        }
244        Ok(())
245    }
246
247    fn on_control_input<C: Autocomplete + Help, P: CommandProcessor<W, E>>(
248        &mut self,
249        editor: &mut Editor<CommandBuffer>,
250        control: ControlInput,
251        processor: &mut P,
252    ) -> Result<(), E> {
253        match control {
254            ControlInput::Enter => {
255                self.writer.write_str(codes::CRLF)?;
256
257                #[cfg(feature = "history")]
258                self.history.push(editor.text());
259                let text = editor.text_mut();
260
261                let tokens = Tokens::new(text);
262                self.process_input::<C, _>(tokens, processor)?;
263
264                editor.clear();
265
266                self.writer.flush_str(self.prompt)?;
267            }
268            ControlInput::Tab => {
269                #[cfg(feature = "autocomplete")]
270                self.process_autocomplete::<C>(editor)?;
271            }
272            ControlInput::Backspace => {
273                if editor.move_left() {
274                    editor.remove();
275                    self.writer.flush_bytes(codes::CURSOR_BACKWARD)?;
276                    self.writer.flush_bytes(codes::DELETE_CHAR)?;
277                }
278            }
279            ControlInput::Down =>
280            {
281                #[cfg(feature = "history")]
282                self.navigate_history(editor, NavigateHistory::Newer)?
283            }
284            ControlInput::Up =>
285            {
286                #[cfg(feature = "history")]
287                self.navigate_history(editor, NavigateHistory::Older)?
288            }
289            ControlInput::Forward => self.navigate_input(editor, NavigateInput::Forward)?,
290            ControlInput::Back => self.navigate_input(editor, NavigateInput::Backward)?,
291        }
292
293        Ok(())
294    }
295
296    fn navigate_input(
297        &mut self,
298        editor: &mut Editor<CommandBuffer>,
299        dir: NavigateInput,
300    ) -> Result<(), E> {
301        match dir {
302            NavigateInput::Backward if editor.move_left() => {
303                self.writer.flush_bytes(codes::CURSOR_BACKWARD)?;
304            }
305            NavigateInput::Forward if editor.move_right() => {
306                self.writer.flush_bytes(codes::CURSOR_FORWARD)?;
307            }
308            _ => return Ok(()),
309        }
310        Ok(())
311    }
312
313    #[cfg(feature = "history")]
314    fn navigate_history(
315        &mut self,
316        editor: &mut Editor<CommandBuffer>,
317        dir: NavigateHistory,
318    ) -> Result<(), E> {
319        let history_elem = match dir {
320            NavigateHistory::Older => self.history.next_older(),
321            NavigateHistory::Newer => self.history.next_newer().or(Some("")),
322        };
323        if let Some(element) = history_elem {
324            editor.clear();
325            editor.insert(element);
326            self.clear_line(false)?;
327
328            self.writer.flush_str(editor.text())?;
329        }
330        Ok(())
331    }
332
333    #[cfg(feature = "autocomplete")]
334    fn process_autocomplete<C: Autocomplete>(
335        &mut self,
336        editor: &mut Editor<CommandBuffer>,
337    ) -> Result<(), E> {
338        let initial_cursor = editor.cursor();
339        editor.autocompletion(|request, autocompletion| {
340            C::autocomplete(request.clone(), autocompletion);
341            match request {
342                Request::CommandName(name) if "help".starts_with(name) => {
343                    // SAFETY: "help" starts with name, so name cannot be longer
344                    let autocompleted = unsafe { "help".get_unchecked(name.len()..) };
345                    autocompletion.merge_autocompletion(autocompleted)
346                }
347                _ => {}
348            }
349        });
350        if editor.cursor() > initial_cursor {
351            let autocompleted = editor.text_range(initial_cursor..);
352            self.writer.flush_str(autocompleted)?;
353        }
354        Ok(())
355    }
356
357    fn process_command<P: CommandProcessor<W, E>>(
358        &mut self,
359        command: RawCommand<'_>,
360        handler: &mut P,
361    ) -> Result<(), E> {
362        let cli_writer = Writer::new(&mut self.writer);
363        let mut handle = CliHandle::new(cli_writer);
364
365        let res = handler.process(&mut handle, command);
366
367        if let Some(prompt) = handle.new_prompt {
368            self.prompt = prompt;
369        }
370        if handle.writer.is_dirty() {
371            self.writer.write_str(codes::CRLF)?;
372        }
373        self.writer.flush()?;
374
375        match res {
376            Err(ProcessError::ParseError(err)) => self.process_error(err),
377            Err(ProcessError::WriteError(err)) => Err(err),
378            Ok(()) => Ok(()),
379        }
380    }
381
382    #[allow(clippy::extra_unused_type_parameters)]
383    fn process_input<C: Help, P: CommandProcessor<W, E>>(
384        &mut self,
385        tokens: Tokens<'_>,
386        handler: &mut P,
387    ) -> Result<(), E> {
388        if let Some(command) = RawCommand::from_tokens(&tokens) {
389            #[cfg(feature = "help")]
390            if let Some(request) = HelpRequest::from_command(&command) {
391                return self.process_help::<C>(request);
392            }
393
394            self.process_command(command, handler)?;
395        };
396
397        Ok(())
398    }
399
400    fn process_error(&mut self, error: ParseError<'_>) -> Result<(), E> {
401        self.writer.write_str("error: ")?;
402        match error {
403            ParseError::MissingRequiredArgument { name } => {
404                self.writer.write_str("missing required argument: ")?;
405                self.writer.write_str(name)?;
406            }
407            ParseError::NonAsciiShortOption => {
408                self.writer
409                    .write_str("non-ascii in short options is not supported")?;
410            }
411            ParseError::ParseValueError { value, expected } => {
412                self.writer.write_str("failed to parse '")?;
413                self.writer.write_str(value)?;
414                self.writer.write_str("', expected ")?;
415                self.writer.write_str(expected)?;
416            }
417            ParseError::UnexpectedArgument { value } => {
418                self.writer.write_str("unexpected argument: ")?;
419                self.writer.write_str(value)?;
420            }
421            ParseError::UnexpectedLongOption { name } => {
422                self.writer.write_str("unexpected option: -")?;
423                self.writer.write_str("-")?;
424                self.writer.write_str(name)?;
425            }
426            ParseError::UnexpectedShortOption { name } => {
427                // short options are guaranteed to be ascii alphabetic
428                if name.is_ascii_alphabetic() {
429                    self.writer.write_str("unexpected option: -")?;
430                    self.writer.write_bytes(&[name as u8])?;
431                }
432            }
433            ParseError::UnknownCommand => {
434                self.writer.write_str("unknown command")?;
435            }
436        }
437        self.writer.flush_str(codes::CRLF)
438    }
439
440    #[cfg(feature = "help")]
441    fn process_help<C: Help>(&mut self, request: HelpRequest<'_>) -> Result<(), E> {
442        let mut writer = Writer::new(&mut self.writer);
443
444        match request {
445            HelpRequest::All => C::list_commands(&mut writer)?,
446            HelpRequest::Command(command) => {
447                match C::command_help(&mut |_| Ok(()), command.clone(), &mut writer) {
448                    Err(HelpError::UnknownCommand) => {
449                        writer.write_str("error: ")?;
450                        writer.write_str("unknown command")?;
451                    }
452                    Err(HelpError::WriteError(err)) => return Err(err),
453                    Ok(()) => {}
454                }
455            }
456        };
457
458        if writer.is_dirty() {
459            self.writer.write_str(codes::CRLF)?;
460        }
461        self.writer.flush()?;
462
463        Ok(())
464    }
465}