Skip to content

Commit

Permalink
feat: customize cli prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
funbiscuit committed Mar 2, 2024
1 parent 1acdacd commit 7e39c8a
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 21 deletions.
23 changes: 19 additions & 4 deletions embedded-cli/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ use crate::{buffer::Buffer, cli::Cli, writer::EmptyWriter};

pub const DEFAULT_CMD_LEN: usize = 40;
pub const DEFAULT_HISTORY_LEN: usize = 100;
pub const DEFAULT_PROMPT: &str = "$ ";

pub struct CliBuilder<W: Write<Error = E>, E: Error, CommandBuffer: Buffer, HistoryBuffer: Buffer> {
command_buffer: CommandBuffer,
history_buffer: HistoryBuffer,
writer: W,
pub(crate) command_buffer: CommandBuffer,
pub(crate) history_buffer: HistoryBuffer,
pub(crate) prompt: &'static str,
pub(crate) writer: W,
}

impl<W, E, CommandBuffer, HistoryBuffer> Debug for CliBuilder<W, E, CommandBuffer, HistoryBuffer>
Expand All @@ -36,7 +38,7 @@ where
HistoryBuffer: Buffer,
{
pub fn build(self) -> Result<Cli<W, E, CommandBuffer, HistoryBuffer>, E> {
Cli::new(self.writer, self.command_buffer, self.history_buffer)
Cli::from_builder(self)
}

pub fn command_buffer<B: Buffer>(
Expand All @@ -47,6 +49,7 @@ where
command_buffer,
history_buffer: self.history_buffer,
writer: self.writer,
prompt: self.prompt,
}
}

Expand All @@ -58,6 +61,16 @@ where
command_buffer: self.command_buffer,
history_buffer,
writer: self.writer,
prompt: self.prompt,
}
}

pub fn prompt(self, prompt: &'static str) -> Self {
CliBuilder {
command_buffer: self.command_buffer,
history_buffer: self.history_buffer,
writer: self.writer,
prompt,
}
}

Expand All @@ -69,6 +82,7 @@ where
command_buffer: self.command_buffer,
history_buffer: self.history_buffer,
writer,
prompt: self.prompt,
}
}
}
Expand All @@ -81,6 +95,7 @@ impl Default
command_buffer: [0; DEFAULT_CMD_LEN],
history_buffer: [0; DEFAULT_HISTORY_LEN],
writer: EmptyWriter,
prompt: DEFAULT_PROMPT,
}
}
}
54 changes: 50 additions & 4 deletions embedded-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use core::marker::PhantomData;

use crate::{
buffer::Buffer,
builder::DEFAULT_PROMPT,
codes,
command::RawCommand,
editor::Editor,
Expand All @@ -27,9 +28,8 @@ use crate::history::History;

use embedded_io::{Error, Write};

const PROMPT: &str = "$ ";

pub struct CliHandle<'a, W: Write<Error = E>, E: embedded_io::Error> {
new_prompt: Option<&'static str>,
writer: Writer<'a, W, E>,
}

Expand All @@ -38,12 +38,20 @@ where
W: Write<Error = E>,
E: embedded_io::Error,
{
/// Set new prompt to use in CLI
pub fn set_prompt(&mut self, prompt: &'static str) {
self.new_prompt = Some(prompt)
}

pub fn writer(&mut self) -> &mut Writer<'a, W, E> {
&mut self.writer
}

fn new(writer: Writer<'a, W, E>) -> Self {
Self { writer }
Self {
new_prompt: None,
writer,
}
}
}

Expand Down Expand Up @@ -104,6 +112,7 @@ where
HistoryBuffer: Buffer,
{
#[allow(unused_variables)]
#[deprecated(since = "0.2.1", note = "please use `builder` instead")]
pub fn new(
writer: W,
command_buffer: CommandBuffer,
Expand All @@ -114,7 +123,7 @@ where
#[cfg(feature = "history")]
history: History::new(history_buffer),
input_generator: Some(InputGenerator::new()),
prompt: PROMPT,
prompt: DEFAULT_PROMPT,
writer,
#[cfg(not(feature = "history"))]
_ph: PhantomData,
Expand All @@ -125,6 +134,25 @@ where
Ok(cli)
}

pub(crate) fn from_builder(
builder: CliBuilder<W, E, CommandBuffer, HistoryBuffer>,
) -> Result<Self, E> {
let mut cli = Self {
editor: Some(Editor::new(builder.command_buffer)),
#[cfg(feature = "history")]
history: History::new(builder.history_buffer),
input_generator: Some(InputGenerator::new()),
prompt: builder.prompt,
writer: builder.writer,
#[cfg(not(feature = "history"))]
_ph: PhantomData,
};

cli.writer.flush_str(cli.prompt)?;

Ok(cli)
}

/// Each call to process byte can be done with different
/// command set and/or command processor.
/// In process callback you can change some outside state
Expand Down Expand Up @@ -155,6 +183,21 @@ where
}
}

/// Set new prompt to use in CLI
///
/// Changes will apply immediately and current line
/// will be replaced by new prompt and input
pub fn set_prompt(&mut self, prompt: &'static str) -> Result<(), E> {
self.prompt = prompt;
self.clear_line(false)?;

if let Some(editor) = self.editor.as_mut() {
self.writer.flush_str(editor.text())?;
}

Ok(())
}

pub fn write(
&mut self,
f: impl FnOnce(&mut Writer<'_, W, E>) -> Result<(), E>,
Expand Down Expand Up @@ -321,6 +364,9 @@ where

let res = handler.process(&mut handle, command);

if let Some(prompt) = handle.new_prompt {
self.prompt = prompt;
}
if handle.writer.is_dirty() {
self.writer.write_str(codes::CRLF)?;
}
Expand Down
98 changes: 98 additions & 0 deletions embedded-cli/tests/cli/base.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use rstest::rstest;

use crate::wrapper::{Arg, CliWrapper, RawCommand};

use crate::terminal::assert_terminal;
Expand Down Expand Up @@ -88,6 +90,102 @@ fn move_insert() {
);
}

#[rstest]
#[case("#")]
#[case("###> ")]
#[case("")]
fn set_prompt_dynamic(#[case] prompt: &'static str) {
let mut cli = CliWrapper::default();
assert_terminal!(cli.terminal(), 2, vec!["$"]);

cli.set_prompt(prompt);
assert_terminal!(cli.terminal(), prompt.len(), vec![prompt.trim()]);

cli.set_prompt("$ ");
assert_terminal!(cli.terminal(), 2, vec!["$"]);

cli.set_prompt(prompt);
assert_terminal!(cli.terminal(), prompt.len(), vec![prompt.trim()]);

cli.process_str("set");
assert_terminal!(
cli.terminal(),
prompt.len() + 3,
vec![format!("{}set", prompt)]
);

cli.set_prompt("$ ");
assert_terminal!(cli.terminal(), 5, vec!["$ set"]);

cli.set_handler(move |cli, _| {
cli.set_prompt(prompt);
Ok(())
});
cli.send_enter();
assert_terminal!(cli.terminal(), prompt.len(), vec!["$ set", prompt.trim()]);

cli.set_handler(move |cli, _| {
cli.set_prompt("$ ");
Ok(())
});
cli.process_str("get");
cli.send_enter();
assert_terminal!(
cli.terminal(),
2,
vec![
"$ set".to_string(),
format!("{}get", prompt),
"$".to_string()
]
);

assert_eq!(
cli.received_commands(),
vec![
Ok(RawCommand {
name: "set".to_string(),
args: vec![],
}),
Ok(RawCommand {
name: "get".to_string(),
args: vec![],
})
]
);
}

#[rstest]
#[case("#")]
#[case("###> ")]
#[case("")]
fn set_prompt_static(#[case] prompt: &'static str) {
let mut cli = CliWrapper::builder().prompt(prompt).build();
assert_terminal!(cli.terminal(), prompt.len(), vec![prompt.trim_end()]);

cli.process_str("set");
assert_terminal!(
cli.terminal(),
prompt.len() + 3,
vec![format!("{}set", prompt)]
);

cli.send_enter();
assert_terminal!(
cli.terminal(),
prompt.len(),
vec![format!("{}set", prompt), prompt.trim().to_string()]
);

assert_eq!(
cli.received_commands(),
vec![Ok(RawCommand {
name: "set".to_string(),
args: vec![],
})]
);
}

#[test]
fn try_move_outside() {
let mut cli = CliWrapper::default();
Expand Down
59 changes: 46 additions & 13 deletions embedded-cli/tests/cli/wrapper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{cell::RefCell, convert::Infallible, fmt::Debug, rc::Rc};
use std::{cell::RefCell, convert::Infallible, fmt::Debug, marker::PhantomData, rc::Rc};

use embedded_cli::{
arguments::{Arg as CliArg, ArgError},
Expand Down Expand Up @@ -207,8 +207,17 @@ impl Default for CliWrapper<RawCommand> {
}

impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
pub fn builder() -> CliWrapperBuilder<T> {
CliWrapperBuilder {
command_size: 80,
history_size: 500,
prompt: None,
_ph: PhantomData,
}
}

pub fn new() -> Self {
Self::new_with_sizes(80, 500)
Self::builder().build()
}

pub fn process_str(&mut self, text: &str) {
Expand Down Expand Up @@ -260,6 +269,11 @@ impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
self.handler = Some(Box::new(handler));
}

pub fn set_prompt(&mut self, prompt: &'static str) {
self.cli.set_prompt(prompt).unwrap();
self.update_terminal();
}

pub fn received_commands(&self) -> Vec<Result<T, ParseError>> {
self.state.borrow().commands.to_vec()
}
Expand All @@ -273,23 +287,43 @@ impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
self.update_terminal();
}

fn new_with_sizes(command_size: usize, history_size: usize) -> Self {
fn update_terminal(&mut self) {
for byte in self.state.borrow_mut().written.drain(..) {
self.terminal.receive_byte(byte)
}
}
}

#[derive(Debug)]
pub struct CliWrapperBuilder<T: Autocomplete + Help + CommandConvert + Clone> {
command_size: usize,
history_size: usize,
prompt: Option<&'static str>,
_ph: PhantomData<T>,
}

impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapperBuilder<T> {
pub fn build(self) -> CliWrapper<T> {
let state = Rc::new(RefCell::new(State::default()));

let writer = Writer {
state: state.clone(),
};

//TODO: impl Buffer for Vec so no need to leak
let cli = CliBuilder::default()
let builder = CliBuilder::default()
.writer(writer)
.command_buffer(vec![0; command_size].leak())
.history_buffer(vec![0; history_size].leak())
.build()
.unwrap();
.command_buffer(vec![0; self.command_size].leak())
.history_buffer(vec![0; self.history_size].leak());
let builder = if let Some(prompt) = self.prompt {
builder.prompt(prompt)
} else {
builder
};
let cli = builder.build().unwrap();

let terminal = Terminal::new();
let mut wrapper = Self {
let mut wrapper = CliWrapper {
cli,
handler: None,
state,
Expand All @@ -299,10 +333,9 @@ impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
wrapper
}

fn update_terminal(&mut self) {
for byte in self.state.borrow_mut().written.drain(..) {
self.terminal.receive_byte(byte)
}
pub fn prompt(mut self, prompt: &'static str) -> Self {
self.prompt = Some(prompt);
self
}
}

Expand Down

0 comments on commit 7e39c8a

Please sign in to comment.