Skip to content

Commit 7e39c8a

Browse files
committed
feat: customize cli prompt
1 parent 1acdacd commit 7e39c8a

4 files changed

Lines changed: 213 additions & 21 deletions

File tree

embedded-cli/src/builder.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ use crate::{buffer::Buffer, cli::Cli, writer::EmptyWriter};
66

77
pub const DEFAULT_CMD_LEN: usize = 40;
88
pub const DEFAULT_HISTORY_LEN: usize = 100;
9+
pub const DEFAULT_PROMPT: &str = "$ ";
910

1011
pub struct CliBuilder<W: Write<Error = E>, E: Error, CommandBuffer: Buffer, HistoryBuffer: Buffer> {
11-
command_buffer: CommandBuffer,
12-
history_buffer: HistoryBuffer,
13-
writer: W,
12+
pub(crate) command_buffer: CommandBuffer,
13+
pub(crate) history_buffer: HistoryBuffer,
14+
pub(crate) prompt: &'static str,
15+
pub(crate) writer: W,
1416
}
1517

1618
impl<W, E, CommandBuffer, HistoryBuffer> Debug for CliBuilder<W, E, CommandBuffer, HistoryBuffer>
@@ -36,7 +38,7 @@ where
3638
HistoryBuffer: Buffer,
3739
{
3840
pub fn build(self) -> Result<Cli<W, E, CommandBuffer, HistoryBuffer>, E> {
39-
Cli::new(self.writer, self.command_buffer, self.history_buffer)
41+
Cli::from_builder(self)
4042
}
4143

4244
pub fn command_buffer<B: Buffer>(
@@ -47,6 +49,7 @@ where
4749
command_buffer,
4850
history_buffer: self.history_buffer,
4951
writer: self.writer,
52+
prompt: self.prompt,
5053
}
5154
}
5255

@@ -58,6 +61,16 @@ where
5861
command_buffer: self.command_buffer,
5962
history_buffer,
6063
writer: self.writer,
64+
prompt: self.prompt,
65+
}
66+
}
67+
68+
pub fn prompt(self, prompt: &'static str) -> Self {
69+
CliBuilder {
70+
command_buffer: self.command_buffer,
71+
history_buffer: self.history_buffer,
72+
writer: self.writer,
73+
prompt,
6174
}
6275
}
6376

@@ -69,6 +82,7 @@ where
6982
command_buffer: self.command_buffer,
7083
history_buffer: self.history_buffer,
7184
writer,
85+
prompt: self.prompt,
7286
}
7387
}
7488
}
@@ -81,6 +95,7 @@ impl Default
8195
command_buffer: [0; DEFAULT_CMD_LEN],
8296
history_buffer: [0; DEFAULT_HISTORY_LEN],
8397
writer: EmptyWriter,
98+
prompt: DEFAULT_PROMPT,
8499
}
85100
}
86101
}

embedded-cli/src/cli.rs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use core::marker::PhantomData;
77

88
use crate::{
99
buffer::Buffer,
10+
builder::DEFAULT_PROMPT,
1011
codes,
1112
command::RawCommand,
1213
editor::Editor,
@@ -27,9 +28,8 @@ use crate::history::History;
2728

2829
use embedded_io::{Error, Write};
2930

30-
const PROMPT: &str = "$ ";
31-
3231
pub struct CliHandle<'a, W: Write<Error = E>, E: embedded_io::Error> {
32+
new_prompt: Option<&'static str>,
3333
writer: Writer<'a, W, E>,
3434
}
3535

@@ -38,12 +38,20 @@ where
3838
W: Write<Error = E>,
3939
E: embedded_io::Error,
4040
{
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+
4146
pub fn writer(&mut self) -> &mut Writer<'a, W, E> {
4247
&mut self.writer
4348
}
4449

4550
fn new(writer: Writer<'a, W, E>) -> Self {
46-
Self { writer }
51+
Self {
52+
new_prompt: None,
53+
writer,
54+
}
4755
}
4856
}
4957

@@ -104,6 +112,7 @@ where
104112
HistoryBuffer: Buffer,
105113
{
106114
#[allow(unused_variables)]
115+
#[deprecated(since = "0.2.1", note = "please use `builder` instead")]
107116
pub fn new(
108117
writer: W,
109118
command_buffer: CommandBuffer,
@@ -114,7 +123,7 @@ where
114123
#[cfg(feature = "history")]
115124
history: History::new(history_buffer),
116125
input_generator: Some(InputGenerator::new()),
117-
prompt: PROMPT,
126+
prompt: DEFAULT_PROMPT,
118127
writer,
119128
#[cfg(not(feature = "history"))]
120129
_ph: PhantomData,
@@ -125,6 +134,25 @@ where
125134
Ok(cli)
126135
}
127136

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+
128156
/// Each call to process byte can be done with different
129157
/// command set and/or command processor.
130158
/// In process callback you can change some outside state
@@ -155,6 +183,21 @@ where
155183
}
156184
}
157185

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+
158201
pub fn write(
159202
&mut self,
160203
f: impl FnOnce(&mut Writer<'_, W, E>) -> Result<(), E>,
@@ -321,6 +364,9 @@ where
321364

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

367+
if let Some(prompt) = handle.new_prompt {
368+
self.prompt = prompt;
369+
}
324370
if handle.writer.is_dirty() {
325371
self.writer.write_str(codes::CRLF)?;
326372
}

embedded-cli/tests/cli/base.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use rstest::rstest;
2+
13
use crate::wrapper::{Arg, CliWrapper, RawCommand};
24

35
use crate::terminal::assert_terminal;
@@ -88,6 +90,102 @@ fn move_insert() {
8890
);
8991
}
9092

93+
#[rstest]
94+
#[case("#")]
95+
#[case("###> ")]
96+
#[case("")]
97+
fn set_prompt_dynamic(#[case] prompt: &'static str) {
98+
let mut cli = CliWrapper::default();
99+
assert_terminal!(cli.terminal(), 2, vec!["$"]);
100+
101+
cli.set_prompt(prompt);
102+
assert_terminal!(cli.terminal(), prompt.len(), vec![prompt.trim()]);
103+
104+
cli.set_prompt("$ ");
105+
assert_terminal!(cli.terminal(), 2, vec!["$"]);
106+
107+
cli.set_prompt(prompt);
108+
assert_terminal!(cli.terminal(), prompt.len(), vec![prompt.trim()]);
109+
110+
cli.process_str("set");
111+
assert_terminal!(
112+
cli.terminal(),
113+
prompt.len() + 3,
114+
vec![format!("{}set", prompt)]
115+
);
116+
117+
cli.set_prompt("$ ");
118+
assert_terminal!(cli.terminal(), 5, vec!["$ set"]);
119+
120+
cli.set_handler(move |cli, _| {
121+
cli.set_prompt(prompt);
122+
Ok(())
123+
});
124+
cli.send_enter();
125+
assert_terminal!(cli.terminal(), prompt.len(), vec!["$ set", prompt.trim()]);
126+
127+
cli.set_handler(move |cli, _| {
128+
cli.set_prompt("$ ");
129+
Ok(())
130+
});
131+
cli.process_str("get");
132+
cli.send_enter();
133+
assert_terminal!(
134+
cli.terminal(),
135+
2,
136+
vec![
137+
"$ set".to_string(),
138+
format!("{}get", prompt),
139+
"$".to_string()
140+
]
141+
);
142+
143+
assert_eq!(
144+
cli.received_commands(),
145+
vec![
146+
Ok(RawCommand {
147+
name: "set".to_string(),
148+
args: vec![],
149+
}),
150+
Ok(RawCommand {
151+
name: "get".to_string(),
152+
args: vec![],
153+
})
154+
]
155+
);
156+
}
157+
158+
#[rstest]
159+
#[case("#")]
160+
#[case("###> ")]
161+
#[case("")]
162+
fn set_prompt_static(#[case] prompt: &'static str) {
163+
let mut cli = CliWrapper::builder().prompt(prompt).build();
164+
assert_terminal!(cli.terminal(), prompt.len(), vec![prompt.trim_end()]);
165+
166+
cli.process_str("set");
167+
assert_terminal!(
168+
cli.terminal(),
169+
prompt.len() + 3,
170+
vec![format!("{}set", prompt)]
171+
);
172+
173+
cli.send_enter();
174+
assert_terminal!(
175+
cli.terminal(),
176+
prompt.len(),
177+
vec![format!("{}set", prompt), prompt.trim().to_string()]
178+
);
179+
180+
assert_eq!(
181+
cli.received_commands(),
182+
vec![Ok(RawCommand {
183+
name: "set".to_string(),
184+
args: vec![],
185+
})]
186+
);
187+
}
188+
91189
#[test]
92190
fn try_move_outside() {
93191
let mut cli = CliWrapper::default();

embedded-cli/tests/cli/wrapper.rs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cell::RefCell, convert::Infallible, fmt::Debug, rc::Rc};
1+
use std::{cell::RefCell, convert::Infallible, fmt::Debug, marker::PhantomData, rc::Rc};
22

33
use embedded_cli::{
44
arguments::{Arg as CliArg, ArgError},
@@ -207,8 +207,17 @@ impl Default for CliWrapper<RawCommand> {
207207
}
208208

209209
impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
210+
pub fn builder() -> CliWrapperBuilder<T> {
211+
CliWrapperBuilder {
212+
command_size: 80,
213+
history_size: 500,
214+
prompt: None,
215+
_ph: PhantomData,
216+
}
217+
}
218+
210219
pub fn new() -> Self {
211-
Self::new_with_sizes(80, 500)
220+
Self::builder().build()
212221
}
213222

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

272+
pub fn set_prompt(&mut self, prompt: &'static str) {
273+
self.cli.set_prompt(prompt).unwrap();
274+
self.update_terminal();
275+
}
276+
263277
pub fn received_commands(&self) -> Vec<Result<T, ParseError>> {
264278
self.state.borrow().commands.to_vec()
265279
}
@@ -273,23 +287,43 @@ impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
273287
self.update_terminal();
274288
}
275289

276-
fn new_with_sizes(command_size: usize, history_size: usize) -> Self {
290+
fn update_terminal(&mut self) {
291+
for byte in self.state.borrow_mut().written.drain(..) {
292+
self.terminal.receive_byte(byte)
293+
}
294+
}
295+
}
296+
297+
#[derive(Debug)]
298+
pub struct CliWrapperBuilder<T: Autocomplete + Help + CommandConvert + Clone> {
299+
command_size: usize,
300+
history_size: usize,
301+
prompt: Option<&'static str>,
302+
_ph: PhantomData<T>,
303+
}
304+
305+
impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapperBuilder<T> {
306+
pub fn build(self) -> CliWrapper<T> {
277307
let state = Rc::new(RefCell::new(State::default()));
278308

279309
let writer = Writer {
280310
state: state.clone(),
281311
};
282312

283313
//TODO: impl Buffer for Vec so no need to leak
284-
let cli = CliBuilder::default()
314+
let builder = CliBuilder::default()
285315
.writer(writer)
286-
.command_buffer(vec![0; command_size].leak())
287-
.history_buffer(vec![0; history_size].leak())
288-
.build()
289-
.unwrap();
316+
.command_buffer(vec![0; self.command_size].leak())
317+
.history_buffer(vec![0; self.history_size].leak());
318+
let builder = if let Some(prompt) = self.prompt {
319+
builder.prompt(prompt)
320+
} else {
321+
builder
322+
};
323+
let cli = builder.build().unwrap();
290324

291325
let terminal = Terminal::new();
292-
let mut wrapper = Self {
326+
let mut wrapper = CliWrapper {
293327
cli,
294328
handler: None,
295329
state,
@@ -299,10 +333,9 @@ impl<T: Autocomplete + Help + CommandConvert + Clone> CliWrapper<T> {
299333
wrapper
300334
}
301335

302-
fn update_terminal(&mut self) {
303-
for byte in self.state.borrow_mut().written.drain(..) {
304-
self.terminal.receive_byte(byte)
305-
}
336+
pub fn prompt(mut self, prompt: &'static str) -> Self {
337+
self.prompt = Some(prompt);
338+
self
306339
}
307340
}
308341

0 commit comments

Comments
 (0)