diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 0af3143..945dd88 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -19,6 +19,7 @@ macros = ["embedded-cli-macros"] autocomplete = ["embedded-cli-macros/autocomplete"] help = ["embedded-cli-macros/help"] history = [] +del_is_bs = [] [dependencies] embedded-cli-macros = { version = "0.2.1", path = "../embedded-cli-macros", optional = true } diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index f2d91b7..c7a342c 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -276,6 +276,12 @@ where self.writer.flush_bytes(codes::DELETE_CHAR)?; } } + ControlInput::Delete => { + if editor.cursor() < editor.len() { + editor.remove(); + self.writer.flush_bytes(codes::DELETE_CHAR)?; + } + } ControlInput::Down => { #[cfg(feature = "history")] diff --git a/embedded-cli/src/codes.rs b/embedded-cli/src/codes.rs index 5adb862..0d42c0e 100644 --- a/embedded-cli/src/codes.rs +++ b/embedded-cli/src/codes.rs @@ -3,6 +3,7 @@ pub const TABULATION: u8 = 0x09; pub const LINE_FEED: u8 = 0x0A; pub const CARRIAGE_RETURN: u8 = 0x0D; pub const ESCAPE: u8 = 0x1B; +pub const DELETE: u8 = 0x7F; pub const CRLF: &str = "\r\n"; diff --git a/embedded-cli/src/input.rs b/embedded-cli/src/input.rs index d4efaab..4136ed7 100644 --- a/embedded-cli/src/input.rs +++ b/embedded-cli/src/input.rs @@ -5,6 +5,7 @@ use crate::{codes, utf8::Utf8Accum}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ControlInput { Backspace, + Delete, Down, Enter, Back, @@ -50,7 +51,7 @@ impl InputGenerator { let last_byte = self.last_byte; self.last_byte = byte; if self.flags.contains(Flags::CSI_STARTED) { - self.process_csi(byte).map(Input::Control) + self.process_csi(byte, last_byte).map(Input::Control) } else if last_byte == codes::ESCAPE && byte == b'[' { self.flags.set(Flags::CSI_STARTED, true); None @@ -59,15 +60,16 @@ impl InputGenerator { } } - fn process_csi(&mut self, byte: u8) -> Option { + fn process_csi(&mut self, byte: u8, last_byte: u8) -> Option { // skip all parameter bytes and process only last byte in CSI sequence if (0x40..=0x7E).contains(&byte) { self.flags.set(Flags::CSI_STARTED, false); - let control = match byte { - b'A' => ControlInput::Up, - b'B' => ControlInput::Down, - b'C' => ControlInput::Forward, - b'D' => ControlInput::Back, + let control = match (last_byte, byte) { + (_, b'A') => ControlInput::Up, + (_, b'B') => ControlInput::Down, + (_, b'C') => ControlInput::Forward, + (_, b'D') => ControlInput::Back, + (b'3', b'~') => ControlInput::Delete, _ => return None, }; Some(control) @@ -79,6 +81,10 @@ impl InputGenerator { fn process_single(&mut self, byte: u8, last_byte: u8) -> Option> { let control = match byte { codes::BACKSPACE => ControlInput::Backspace, + #[cfg(feature = "del_is_bs")] + codes::DELETE => ControlInput::Backspace, + #[cfg(not(feature = "del_is_bs"))] + codes::DELETE => ControlInput::Delete, // ignore \r if \n already received (and converted to Enter) codes::CARRIAGE_RETURN if last_byte != codes::LINE_FEED => ControlInput::Enter, @@ -109,6 +115,7 @@ mod tests { #[case(b"\x1B[24B", ControlInput::Down)] #[case(b"\x1B[C", ControlInput::Forward)] #[case(b"\x1B[D", ControlInput::Back)] + #[case(b"\x1B[3~", ControlInput::Delete)] fn process_csi_control(#[case] bytes: &[u8], #[case] expected: ControlInput) { let mut accum = InputGenerator::new(); @@ -134,6 +141,24 @@ mod tests { ) } + #[test] + #[cfg(feature = "del_is_bs")] + fn process_delete_as_bs() { + assert_eq!( + InputGenerator::new().accept(crate::codes::DELETE), + Some(Input::Control(ControlInput::Backspace)) + ) + } + + #[test] + #[cfg(not(feature = "del_is_bs"))] + fn process_delete_as_del() { + assert_eq!( + InputGenerator::new().accept(crate::codes::DELETE), + Some(Input::Control(ControlInput::Delete)) + ) + } + #[test] fn process_crlf() { let mut accum = InputGenerator::new(); diff --git a/embedded-cli/tests/cli/base.rs b/embedded-cli/tests/cli/base.rs index e296c86..55ad8b6 100644 --- a/embedded-cli/tests/cli/base.rs +++ b/embedded-cli/tests/cli/base.rs @@ -54,6 +54,30 @@ fn delete_with_backspace() { assert_terminal!(cli.terminal(), 2, vec!["$"]); } +#[test] +fn delete_with_delete_key() { + let mut cli = CliWrapper::default(); + + cli.process_str("set"); + assert_terminal!(cli.terminal(), 5, vec!["$ set"]); + + cli.send_left(); + cli.send_left(); + assert_terminal!(cli.terminal(), 3, vec!["$ set"]); + + // forward delete 'e' + cli.send_delete(); + assert_terminal!(cli.terminal(), 3, vec!["$ st"]); + + // forward delete 't' + cli.send_delete(); + assert_terminal!(cli.terminal(), 3, vec!["$ s"]); + + // try forward delete at the end of the editor (should do nothing) + cli.send_delete(); + assert_terminal!(cli.terminal(), 3, vec!["$ s"]); +} + #[test] fn move_insert() { let mut cli = CliWrapper::default(); diff --git a/embedded-cli/tests/cli/wrapper.rs b/embedded-cli/tests/cli/wrapper.rs index eb59e33..dff2ec4 100644 --- a/embedded-cli/tests/cli/wrapper.rs +++ b/embedded-cli/tests/cli/wrapper.rs @@ -234,6 +234,10 @@ impl CliWrapper { self.process_str("\x08") } + pub fn send_delete(&mut self) { + self.process_str("\x1B[3~") + } + pub fn send_down(&mut self) { self.process_str("\x1B[B") }