From 42207e7e25aa702ca638c445b8150f38fd1ec126 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Mon, 8 Jan 2024 21:50:14 +0000 Subject: [PATCH 01/12] Add left and right cursor functionality to library --- lib/src/embedded_cli.c | 92 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index 9664703..28eee93 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -1,5 +1,6 @@ #include #include +#include #include "embedded_cli.h" @@ -160,6 +161,12 @@ struct EmbeddedCliImpl { * Flags are defined as CLI_FLAG_* */ uint8_t flags; + + /** + * Cursor position for current command from right to left + * 0 = end of command + */ + uint16_t cursorPos; }; struct AutocompletedCommand { @@ -194,6 +201,26 @@ static const uint16_t cliInternalBindingCount = 1; static const char *lineBreak = "\r\n"; +/* Reference for VT100 escape sequences: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences */ + +/** Escape sequence - Cursor forward (right) */ +static const char *escSeqCursorRight = "\x1B[C"; + +/** Escape sequence - Cursor backward (left) */ +static const char *escSeqCursorLeft = "\x1B[D"; + +/** Escape sequence - Cursor save position */ +static const char *escSeqCursorSave = "\x1B[s"; + +/** Escape sequence - Cursor restore position */ +static const char *escSeqCursorRestore = "\x1B[u"; + +/** Escape sequence - Cursor insert character */ +static const char *escSeqInsertChar = "\x1B[@"; + +/** Escape sequence - Cursor delete character */ +static const char *escSeqDeleteChar = "\x1B[P"; + /** * Navigate through command history back and forth. If navigateUp is true, * navigate to older commands, otherwise navigate to newer. @@ -299,6 +326,13 @@ static void clearCurrentLine(EmbeddedCli *cli); */ static void writeToOutput(EmbeddedCli *cli, const char *str); +/** + * Move cursor forward (right) by given number of positions + * @param cli + * @param count + */ +static void moveCursorForwardBy(EmbeddedCli* cli, uint16_t count); + /** * Returns true if provided char is a supported control char: * \r, \n, \b or 0x7F (treated as \b) @@ -459,6 +493,7 @@ EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config) { impl->maxBindingsCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); impl->lastChar = '\0'; impl->invitation = config->invitation; + impl->cursorPos = 0; initInternalBindings(cli); @@ -534,8 +569,10 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) { PREPARE_IMPL(cli); // remove chars for autocompletion and live command - if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) + if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)){ + writeToOutput(cli, escSeqCursorSave); clearCurrentLine(cli); + } // print provided string writeToOutput(cli, string); @@ -546,6 +583,7 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) { writeToOutput(cli, impl->invitation); writeToOutput(cli, impl->cmdBuffer); impl->inputLineLength = impl->cmdSize; + writeToOutput(cli, escSeqCursorRestore); printLiveAutocompletion(cli); } @@ -677,6 +715,7 @@ static void navigateHistory(EmbeddedCli *cli, bool navigateUp) { writeToOutput(cli, impl->cmdBuffer); impl->inputLineLength = impl->cmdSize; + impl->cursorPos = 0; printLiveAutocompletion(cli); } @@ -693,6 +732,16 @@ static void onEscapedInput(EmbeddedCli *cli, char c) { // there might be extra chars between [ and A/B, just ignore them navigateHistory(cli, c == 'A'); } + + if (c == 'C' && impl->cursorPos > 0) { + impl->cursorPos--; + writeToOutput(cli, escSeqCursorRight); + } + + if (c == 'D' && impl->cursorPos < strlen(impl->cmdBuffer)) { + impl->cursorPos++; + writeToOutput(cli, escSeqCursorLeft); + } } } @@ -703,9 +752,16 @@ static void onCharInput(EmbeddedCli *cli, char c) { if (impl->cmdSize + 2 >= impl->cmdMaxSize) return; - impl->cmdBuffer[impl->cmdSize] = c; + size_t insertPos = strlen(impl->cmdBuffer) - impl->cursorPos; + + memmove(&impl->cmdBuffer[insertPos + 1], &impl->cmdBuffer[insertPos], impl->cursorPos + 1); + ++impl->cmdSize; - impl->cmdBuffer[impl->cmdSize] = '\0'; + ++impl->inputLineLength; + impl->cmdBuffer[insertPos] = c; + + if (impl->cursorPos > 0) + writeToOutput(cli, escSeqInsertChar); // Insert Character cli->writeChar(cli, c); } @@ -730,16 +786,17 @@ static void onControlInput(EmbeddedCli *cli, char c) { impl->cmdBuffer[impl->cmdSize] = '\0'; impl->inputLineLength = 0; impl->history.current = 0; + impl->cursorPos = 0; writeToOutput(cli, impl->invitation); } else if ((c == '\b' || c == 0x7F) && impl->cmdSize > 0) { // remove char from screen - cli->writeChar(cli, '\b'); - cli->writeChar(cli, ' '); - cli->writeChar(cli, '\b'); + writeToOutput(cli, escSeqCursorLeft); // Move cursor to left + writeToOutput(cli, escSeqDeleteChar); // And remove character // and from buffer + size_t insertPos = strlen(impl->cmdBuffer) - impl->cursorPos; + memmove(&impl->cmdBuffer[insertPos - 1], &impl->cmdBuffer[insertPos], impl->cursorPos + 1); --impl->cmdSize; - impl->cmdBuffer[impl->cmdSize] = '\0'; } else if (c == '\t') { onAutocompleteRequest(cli); } @@ -971,6 +1028,13 @@ static void printLiveAutocompletion(EmbeddedCli *cli) { cmd.autocompletedLen = impl->cmdSize; } + // save cursor location + writeToOutput(cli, escSeqCursorSave); + + if (impl->cursorPos > 0) { + moveCursorForwardBy(cli, impl->cursorPos); + } + // print live autocompletion (or nothing, if it doesn't exist) for (size_t i = impl->cmdSize; i < cmd.autocompletedLen; ++i) { cli->writeChar(cli, cmd.firstCandidate[i]); @@ -980,10 +1044,9 @@ static void printLiveAutocompletion(EmbeddedCli *cli) { cli->writeChar(cli, ' '); } impl->inputLineLength = cmd.autocompletedLen; - cli->writeChar(cli, '\r'); - // print current command again so cursor is moved to initial place - writeToOutput(cli, impl->invitation); - writeToOutput(cli, impl->cmdBuffer); + + // restore cursor + writeToOutput(cli, escSeqCursorRestore); } static void onAutocompleteRequest(EmbeddedCli *cli) { @@ -1052,6 +1115,13 @@ static void writeToOutput(EmbeddedCli *cli, const char *str) { } } +static void moveCursorForwardBy(EmbeddedCli* cli, uint16_t count) { + // 5 = uint16_t max, 3 = escape sequence, 1 = string termination + char escBuffer[5 + 3 + 1] = { 0 }; + sprintf(&escBuffer, "\x1B[%uC", count); + writeToOutput(cli, escBuffer); +} + static bool isControlChar(char c) { return c == '\r' || c == '\n' || c == '\b' || c == '\t' || c == 0x7F; } From 61031e760173e47b59def5bb321ad0b23dd5947c Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Mon, 8 Jan 2024 21:50:52 +0000 Subject: [PATCH 02/12] Update tests and example for left and right cursor functionality --- examples/win32-example/main.cpp | 18 ++++-- tests/CliWrapper.cpp | 100 +++++++++++++++++++++++++++----- tests/CliWrapper.h | 6 ++ tests/cli/BaseTest.cpp | 35 ++++++++++- tests/cli/PrintTest.cpp | 1 + 5 files changed, 141 insertions(+), 19 deletions(-) diff --git a/examples/win32-example/main.cpp b/examples/win32-example/main.cpp index 17aab1e..30a6241 100644 --- a/examples/win32-example/main.cpp +++ b/examples/win32-example/main.cpp @@ -107,16 +107,26 @@ int main() { auto key = event.wVirtualKeyCode; - // key up and down - if (key == 38 || key == 40) { + // key up, down, left and right + if (key >= 37 && key <= 40) { // send as escape sequence embeddedCliReceiveChar(cli, '\x1B'); embeddedCliReceiveChar(cli, '['); - if (key == 38) + switch (key) { + case 38: embeddedCliReceiveChar(cli, 'A'); - else + break; + case 40: embeddedCliReceiveChar(cli, 'B'); + break; + case 37: + embeddedCliReceiveChar(cli, 'D'); + break; + case 39: + embeddedCliReceiveChar(cli, 'C'); + break; + } } char aChar = event.uChar.AsciiChar; diff --git a/tests/CliWrapper.cpp b/tests/CliWrapper.cpp index 79a0595..0ebb791 100644 --- a/tests/CliWrapper.cpp +++ b/tests/CliWrapper.cpp @@ -2,6 +2,8 @@ #include "CliWrapper.h" #include +#include +#include // expand implementation when single header version is tested #define EMBEDDED_CLI_IMPL @@ -80,32 +82,95 @@ CliWrapper::Display CliWrapper::getDisplay() { std::vector output; std::string line; + std::string escapeSequenceCount; size_t cursorPosition = 0; + + enum CharacterMode { NORMAL, ESCAPE_MODE_START, ESCAPE_MODE_START_PARSE }; + CharacterMode charMode = NORMAL; + + // Variable for saving the cursor position + size_t cursorPosSave = 0; for (auto c: txQueue) { - if (c == '\b') { - if (cursorPosition > 0 && (cursorPosition - 1) < line.size()) { - line.erase(cursorPosition - 1, 1); - --cursorPosition; + switch (charMode) { + default: + case NORMAL: + if (c == '\x1B') { + charMode = ESCAPE_MODE_START; + // Clear the count in case it was constructed during the previous escape sequence + escapeSequenceCount.clear(); } - } else if (c == '\r') { - cursorPosition = 0; - } else if (c == '\n') { - cursorPosition = 0; - output.push_back(line); - line.clear(); - } else { - if (line.size() > cursorPosition) { + else if (c == '\b') { + if (cursorPosition > 0 && (cursorPosition - 1) < line.size()) { + line.erase(cursorPosition - 1, 1); + --cursorPosition; + } + } + else if (c == '\r') { + cursorPosition = 0; + } + else if (c == '\n') { + cursorPosition = 0; + output.push_back(line); + line.clear(); + } + else { + if (line.size() > cursorPosition) { + line.erase(cursorPosition, 1); + } + line.insert(cursorPosition, 1, c); + ++cursorPosition; + } + break; + case ESCAPE_MODE_START: + if (c == '[') + charMode = ESCAPE_MODE_START_PARSE; + else + charMode = NORMAL; + break; + case ESCAPE_MODE_START_PARSE: + // Check if the escape sequence has a count associated with it, for example 'ESC[C' + if (isdigit(c)) { + escapeSequenceCount.push_back(c); + break; + } + + if (c == 'C') { + // At the moment, this is the only escape sequence that takes a count (number) before the command + if (escapeSequenceCount.empty()) + cursorPosition++; + else + cursorPosition += strtoul(escapeSequenceCount.c_str(), NULL, 10); + } + else if (c == 'D') { + if (cursorPosition > 0) + cursorPosition--; + } + else if (c == 's') { + cursorPosSave = cursorPosition; + } + else if (c == 'u') { + cursorPosition = cursorPosSave; + } + else if (c == '@') { + line.insert(cursorPosition, 1, ' '); + } + else if (c == 'P') { line.erase(cursorPosition, 1); } - line.insert(cursorPosition, 1, c); - ++cursorPosition; + + // Return to normal mode + charMode = NORMAL; + + break; } } + output.push_back(line); for (auto &l: output) { trimStr(l); + removeEscSeq(l); } return Display{ @@ -118,6 +183,7 @@ std::string CliWrapper::getRawOutput() { txQueue.push_back('\0'); std::string output = txQueue.data(); txQueue.pop_back(); + removeEscSeq(output); return output; } @@ -150,6 +216,12 @@ void CliWrapper::trimStr(std::string &str) { } } +void CliWrapper::removeEscSeq(std::string& str) { + // Regex to find and delete escape sequences. NOTE This might need to be updated in the future + std::regex escapeSeqRe("\\x1b\\[[0-9]*[ABCDEFGM78dsu@PXLMJK]"); + str = regex_replace(str, escapeSeqRe, ""); +} + void CliWrapper::onBoundCommand(Command command) { calledBindings.push_back(std::move(command)); } diff --git a/tests/CliWrapper.h b/tests/CliWrapper.h index 477c833..5bb4897 100644 --- a/tests/CliWrapper.h +++ b/tests/CliWrapper.h @@ -136,6 +136,12 @@ class CliWrapper { void onBoundCommand(Command command); static void trimStr(std::string &str); + + /** + * Remove escape sequences from given string + * @param input string + */ + static void removeEscSeq(std::string& str); }; diff --git a/tests/cli/BaseTest.cpp b/tests/cli/BaseTest.cpp index 899e896..cca2a96 100644 --- a/tests/cli/BaseTest.cpp +++ b/tests/cli/BaseTest.cpp @@ -110,6 +110,39 @@ TEST_CASE("CLI. Base tests", "[cli]") { REQUIRE(commands.back().args[0] == "led"); } + SECTION("Move cursor left") { + cli.send("get lft\x1B[D\x1B[De"); + cli.process(); + + auto displayed = cli.getDisplay(); + + REQUIRE(displayed.lines.size() == 1); + REQUIRE(displayed.lines[0] == "> get left"); + REQUIRE(displayed.cursorColumn == 8); + } + + SECTION("Move cursor right") { + cli.send("get right\x1B[C\x1B[C"); + cli.process(); + + auto displayed = cli.getDisplay(); + + REQUIRE(displayed.lines.size() == 1); + REQUIRE(displayed.lines[0] == "> get right"); + REQUIRE(displayed.cursorColumn == 11); + } + + SECTION("Move cursor left then right") { + cli.send("ge oth\x1B[D\x1B[D\x1B[D\x1B[Dt\x1B[Cb"); + cli.process(); + + auto displayed = cli.getDisplay(); + + REQUIRE(displayed.lines.size() == 1); + REQUIRE(displayed.lines[0] == "> get both"); + REQUIRE(displayed.cursorColumn == 7); + } + SECTION("Unknown command") { // unknown commands are only possible when onCommand callback is not set cli.raw()->onCommand = nullptr; @@ -152,7 +185,7 @@ TEST_CASE("CLI. Base tests", "[cli]") { SECTION("Escape sequences") { SECTION("Escape sequences don't show up in output") { - cli.send("t\x1B[Ae\x1B[10As\x1B[Bt\x1B[C\x1B[D1"); + cli.send("t\x1B[Ae\x1B[10As\x1B[Bt\x1B[D\x1B[C1"); cli.process(); auto displayed = cli.getDisplay(); diff --git a/tests/cli/PrintTest.cpp b/tests/cli/PrintTest.cpp index 82cb66b..1dc3adc 100644 --- a/tests/cli/PrintTest.cpp +++ b/tests/cli/PrintTest.cpp @@ -8,6 +8,7 @@ TEST_CASE("CLI. Printing", "[cli]") { CliWrapper cli = CliBuilder().build(); SECTION("Print with no command input") { + cli.process(); cli.print("test print"); auto display = cli.getDisplay(); From 5c6ce8b6c5bc94dc4779c3664dceba384275c151 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Mon, 8 Jan 2024 21:51:11 +0000 Subject: [PATCH 03/12] Update README for left and right cursor functionality --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index caf0a37..b8a6d39 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Single-header CLI library intended for use in embedded systems (like STM32 or Ar * Live autocompletion (see demo above, can be disabled) * Tab (jump to end of current autocompletion) and backspace (remove char) support * History support (navigate with up and down keypress) +* Limited cursor support * Any byte-stream interface is supported (for example, UART) * Single-header distribution @@ -166,6 +167,7 @@ Terminal is required for correct experience. Following control sequences are res * \b removes last typed character * \t moves cursor to the end of autocompleted command * Esc[A (key up) and Esc[B (key down) navigates through history +* Esc[C (key right) and Esc[D (key left) moves the cursor left and right If you run CLI through a serial port (like on Arduino with its UART-USB converter), you can use for example PuTTY (Windows) or XTerm (Linux). From 0345efe2598c7e3486d832b655d739f05de90933 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Mon, 8 Jan 2024 22:57:30 +0000 Subject: [PATCH 04/12] Fix cursor placement in 'embeddedCliPrint()' --- lib/src/embedded_cli.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index 28eee93..9557f92 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -59,6 +59,16 @@ */ #define CLI_FLAG_AUTOCOMPLETE_ENABLED 0x20u +/** +* Indicates that cursor direction should be forward +*/ +#define CURSOR_DIRECTION_FORWARD true + +/** +* Indicates that cursor direction should be backward +*/ +#define CURSOR_DIRECTION_BACKWARD false + typedef struct EmbeddedCliImpl EmbeddedCliImpl; typedef struct AutocompletedCommand AutocompletedCommand; typedef struct FifoBuf FifoBuf; @@ -330,8 +340,9 @@ static void writeToOutput(EmbeddedCli *cli, const char *str); * Move cursor forward (right) by given number of positions * @param cli * @param count + * @param direction: true = forward (right), false = backward (left) */ -static void moveCursorForwardBy(EmbeddedCli* cli, uint16_t count); +static void moveCursor(EmbeddedCli* cli, uint16_t count, bool direction); /** * Returns true if provided char is a supported control char: @@ -570,7 +581,6 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) { // remove chars for autocompletion and live command if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)){ - writeToOutput(cli, escSeqCursorSave); clearCurrentLine(cli); } @@ -583,7 +593,7 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) { writeToOutput(cli, impl->invitation); writeToOutput(cli, impl->cmdBuffer); impl->inputLineLength = impl->cmdSize; - writeToOutput(cli, escSeqCursorRestore); + moveCursor(cli, impl->cursorPos, CURSOR_DIRECTION_BACKWARD); printLiveAutocompletion(cli); } @@ -1032,7 +1042,7 @@ static void printLiveAutocompletion(EmbeddedCli *cli) { writeToOutput(cli, escSeqCursorSave); if (impl->cursorPos > 0) { - moveCursorForwardBy(cli, impl->cursorPos); + moveCursor(cli, impl->cursorPos, CURSOR_DIRECTION_FORWARD); } // print live autocompletion (or nothing, if it doesn't exist) @@ -1115,10 +1125,11 @@ static void writeToOutput(EmbeddedCli *cli, const char *str) { } } -static void moveCursorForwardBy(EmbeddedCli* cli, uint16_t count) { +static void moveCursor(EmbeddedCli* cli, uint16_t count, bool direction) { // 5 = uint16_t max, 3 = escape sequence, 1 = string termination - char escBuffer[5 + 3 + 1] = { 0 }; - sprintf(&escBuffer, "\x1B[%uC", count); + char escBuffer[5 + 3 + 1] = { 0 }; + char dirChar = direction ? escSeqCursorRight[5] : escSeqCursorLeft[5]; + sprintf(&escBuffer, "\x1B[%u%c", count, dirChar); writeToOutput(cli, escBuffer); } From 4d13fe4e5c1e1981885bea2b3b8293a392209cd3 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Mon, 8 Jan 2024 23:00:17 +0000 Subject: [PATCH 05/12] Fix formatting --- lib/src/embedded_cli.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index 9557f92..6c7b057 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -214,22 +214,22 @@ static const char *lineBreak = "\r\n"; /* Reference for VT100 escape sequences: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences */ /** Escape sequence - Cursor forward (right) */ -static const char *escSeqCursorRight = "\x1B[C"; +static const char *escSeqCursorRight = "\x1B[C"; /** Escape sequence - Cursor backward (left) */ -static const char *escSeqCursorLeft = "\x1B[D"; +static const char *escSeqCursorLeft = "\x1B[D"; /** Escape sequence - Cursor save position */ -static const char *escSeqCursorSave = "\x1B[s"; +static const char *escSeqCursorSave = "\x1B[s"; /** Escape sequence - Cursor restore position */ -static const char *escSeqCursorRestore = "\x1B[u"; +static const char *escSeqCursorRestore = "\x1B[u"; /** Escape sequence - Cursor insert character */ -static const char *escSeqInsertChar = "\x1B[@"; +static const char *escSeqInsertChar = "\x1B[@"; /** Escape sequence - Cursor delete character */ -static const char *escSeqDeleteChar = "\x1B[P"; +static const char *escSeqDeleteChar = "\x1B[P"; /** * Navigate through command history back and forth. If navigateUp is true, @@ -580,9 +580,8 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) { PREPARE_IMPL(cli); // remove chars for autocompletion and live command - if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)){ + if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) clearCurrentLine(cli); - } // print provided string writeToOutput(cli, string); From 492be7c5640e1179456551bfb704a3f5d4c133bb Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Tue, 9 Jan 2024 11:36:31 +0000 Subject: [PATCH 06/12] Fix 'sprintf' argument (pointer vs reference to pointer) --- lib/src/embedded_cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index 6c7b057..d091d35 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -1128,7 +1128,7 @@ static void moveCursor(EmbeddedCli* cli, uint16_t count, bool direction) { // 5 = uint16_t max, 3 = escape sequence, 1 = string termination char escBuffer[5 + 3 + 1] = { 0 }; char dirChar = direction ? escSeqCursorRight[5] : escSeqCursorLeft[5]; - sprintf(&escBuffer, "\x1B[%u%c", count, dirChar); + sprintf(escBuffer, "\x1B[%u%c", count, dirChar); writeToOutput(cli, escBuffer); } From 84808201ea3e0d38375f4707d3e87759cb38c176 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Tue, 9 Jan 2024 19:58:47 +0000 Subject: [PATCH 07/12] Fix cursor movement feedback --- lib/src/embedded_cli.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index d091d35..fcc987e 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -1040,9 +1040,7 @@ static void printLiveAutocompletion(EmbeddedCli *cli) { // save cursor location writeToOutput(cli, escSeqCursorSave); - if (impl->cursorPos > 0) { - moveCursor(cli, impl->cursorPos, CURSOR_DIRECTION_FORWARD); - } + moveCursor(cli, impl->cursorPos, CURSOR_DIRECTION_FORWARD); // print live autocompletion (or nothing, if it doesn't exist) for (size_t i = impl->cmdSize; i < cmd.autocompletedLen; ++i) { @@ -1125,9 +1123,13 @@ static void writeToOutput(EmbeddedCli *cli, const char *str) { } static void moveCursor(EmbeddedCli* cli, uint16_t count, bool direction) { + // Check if we need to send any command + if (count == 0) + return; + // 5 = uint16_t max, 3 = escape sequence, 1 = string termination char escBuffer[5 + 3 + 1] = { 0 }; - char dirChar = direction ? escSeqCursorRight[5] : escSeqCursorLeft[5]; + char dirChar = direction ? escSeqCursorRight[2] : escSeqCursorLeft[2]; sprintf(escBuffer, "\x1B[%u%c", count, dirChar); writeToOutput(cli, escBuffer); } From b6fa0d98a8c513c75278596c4824518a76b010eb Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Tue, 9 Jan 2024 20:27:20 +0000 Subject: [PATCH 08/12] Add test for command that is too long --- tests/cli/BaseTest.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/cli/BaseTest.cpp b/tests/cli/BaseTest.cpp index cca2a96..17ce938 100644 --- a/tests/cli/BaseTest.cpp +++ b/tests/cli/BaseTest.cpp @@ -2,6 +2,7 @@ #include "CliBuilder.h" #include +#include TEST_CASE("CLI. Base tests", "[cli]") { @@ -143,6 +144,28 @@ TEST_CASE("CLI. Base tests", "[cli]") { REQUIRE(displayed.cursorColumn == 7); } + SECTION("Command that is too long") { + size_t cmdMax = embeddedCliDefaultConfig()->cmdBufferSize; + std::string cmdMaxTest = std::string(cmdMax/2, 'x'); + + // Split command into two to prevent getting the fifo full + cli.send(cmdMaxTest); + cli.process(); + cli.sendLine(cmdMaxTest); + cli.process(); + + auto displayed = cli.getDisplay(); + + auto xcount = std::ranges::count(cli.getRawOutput(), 'x'); + + REQUIRE(displayed.lines.size() == 2); + // We are only displaying (cmdMax - 2) 'x's, + // but two additional characters are the invitation and the space + REQUIRE(displayed.lines[0].size() == cmdMax); + // Check that the two extra x's will be dropped + REQUIRE(xcount == cmdMax - 2); + } + SECTION("Unknown command") { // unknown commands are only possible when onCommand callback is not set cli.raw()->onCommand = nullptr; From a833c67dc6208ffdc400cd4855964b9fa8ddf87c Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Tue, 9 Jan 2024 20:54:43 +0000 Subject: [PATCH 09/12] Add fix for unsupported C++20 feature --- tests/cli/BaseTest.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/cli/BaseTest.cpp b/tests/cli/BaseTest.cpp index 17ce938..272f2c3 100644 --- a/tests/cli/BaseTest.cpp +++ b/tests/cli/BaseTest.cpp @@ -156,7 +156,9 @@ TEST_CASE("CLI. Base tests", "[cli]") { auto displayed = cli.getDisplay(); - auto xcount = std::ranges::count(cli.getRawOutput(), 'x'); + std::string cliRawOutput = cli.getRawOutput(); + + auto xcount = std::count(cliRawOutput.begin(), cliRawOutput.end(), 'x'); REQUIRE(displayed.lines.size() == 2); // We are only displaying (cmdMax - 2) 'x's, From 3ee124ad969f12086f61682fb38210e5297f7c13 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Sun, 14 Jan 2024 20:52:11 +0000 Subject: [PATCH 10/12] Fix cursor positioning bugs and add more tests --- lib/src/embedded_cli.c | 13 +++++++++++-- tests/CliWrapper.cpp | 5 +++-- tests/cli/AutocompleteTest.cpp | 11 +++++++++++ tests/cli/BaseTest.cpp | 13 +++++++++++++ tests/cli/PrintTest.cpp | 14 ++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index fcc987e..fbcc32c 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -579,10 +579,16 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) { PREPARE_IMPL(cli); + // Save cursor position + uint16_t cursorPosSave = impl->cursorPos; + // remove chars for autocompletion and live command if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) clearCurrentLine(cli); + // Restore cursor position + impl->cursorPos = cursorPosSave; + // print provided string writeToOutput(cli, string); writeToOutput(cli, lineBreak); @@ -798,7 +804,7 @@ static void onControlInput(EmbeddedCli *cli, char c) { impl->cursorPos = 0; writeToOutput(cli, impl->invitation); - } else if ((c == '\b' || c == 0x7F) && impl->cmdSize > 0) { + } else if ((c == '\b' || c == 0x7F) && ((impl->cmdSize - impl->cursorPos) > 0)) { // remove char from screen writeToOutput(cli, escSeqCursorLeft); // Move cursor to left writeToOutput(cli, escSeqDeleteChar); // And remove character @@ -1073,9 +1079,10 @@ static void onAutocompleteRequest(EmbeddedCli *cli) { } impl->cmdBuffer[cmd.autocompletedLen] = '\0'; - writeToOutput(cli, &impl->cmdBuffer[impl->cmdSize]); + writeToOutput(cli, &impl->cmdBuffer[impl->cmdSize - impl->cursorPos]); impl->cmdSize = cmd.autocompletedLen; impl->inputLineLength = impl->cmdSize; + impl->cursorPos = 0; // Cursor has been moved to the end return; } @@ -1112,6 +1119,8 @@ static void clearCurrentLine(EmbeddedCli *cli) { } cli->writeChar(cli, '\r'); impl->inputLineLength = 0; + + impl->cursorPos = 0; } static void writeToOutput(EmbeddedCli *cli, const char *str) { diff --git a/tests/CliWrapper.cpp b/tests/CliWrapper.cpp index 0ebb791..cbadf01 100644 --- a/tests/CliWrapper.cpp +++ b/tests/CliWrapper.cpp @@ -136,15 +136,16 @@ CliWrapper::Display CliWrapper::getDisplay() { } if (c == 'C') { - // At the moment, this is the only escape sequence that takes a count (number) before the command if (escapeSequenceCount.empty()) cursorPosition++; else cursorPosition += strtoul(escapeSequenceCount.c_str(), NULL, 10); } else if (c == 'D') { - if (cursorPosition > 0) + if (escapeSequenceCount.empty()) cursorPosition--; + else + cursorPosition -= strtoul(escapeSequenceCount.c_str(), NULL, 10); } else if (c == 's') { cursorPosSave = cursorPosition; diff --git a/tests/cli/AutocompleteTest.cpp b/tests/cli/AutocompleteTest.cpp index 0d5537d..92bf58f 100644 --- a/tests/cli/AutocompleteTest.cpp +++ b/tests/cli/AutocompleteTest.cpp @@ -112,6 +112,17 @@ TEST_CASE("CLI. Autocomplete enabled", "[cli]") { REQUIRE(displayed.cursorColumn == 3); } + SECTION("Autocomplete when inside of input has been modified") { + cli.send("res"); + cli.sendLine("\x1B[D\x1B[D"); // Move left two characters + cli.process(); + + auto displayed = cli.getDisplay(); + + REQUIRE(displayed.lines.size() == 2); + REQUIRE(displayed.lines[0] == "> reset-"); + } + SECTION("Live autocomplete when no candidates") { cli.send("m"); cli.process(); diff --git a/tests/cli/BaseTest.cpp b/tests/cli/BaseTest.cpp index 272f2c3..a7ca3a2 100644 --- a/tests/cli/BaseTest.cpp +++ b/tests/cli/BaseTest.cpp @@ -122,6 +122,19 @@ TEST_CASE("CLI. Base tests", "[cli]") { REQUIRE(displayed.cursorColumn == 8); } + SECTION("Move cursor left and remove some chars") { + cli.send("test"); + cli.send("\x1B[D\x1B[D"); // Move left two characters + cli.send("\b\b\b\b\b"); // Try to delete more characters than remaining + cli.send("almo"); + cli.process(); + + auto displayed = cli.getDisplay(); + + REQUIRE(displayed.lines.size() == 1); + REQUIRE(displayed.lines[0] == "> almost"); + } + SECTION("Move cursor right") { cli.send("get right\x1B[C\x1B[C"); cli.process(); diff --git a/tests/cli/PrintTest.cpp b/tests/cli/PrintTest.cpp index 1dc3adc..57a0f85 100644 --- a/tests/cli/PrintTest.cpp +++ b/tests/cli/PrintTest.cpp @@ -62,4 +62,18 @@ TEST_CASE("CLI. Printing", "[cli]") { REQUIRE(displayed.lines[1] == "> get"); REQUIRE(displayed.cursorColumn == 3); } + + SECTION("Print with cursor in the middle of the input") { + cli.send("test"); + cli.send("\x1B[D\x1B[D\x1B[D"); // Move left three characters + cli.process(); + cli.print("print"); + + auto displayed = cli.getDisplay(); + + REQUIRE(displayed.lines.size() == 2); + REQUIRE(displayed.lines[0] == "print"); + REQUIRE(displayed.lines[1] == "> test"); + REQUIRE(displayed.cursorColumn == 3); + } } From 16003a74a21344fd1064a2a337629ab05b812c8f Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Sun, 14 Jan 2024 20:52:40 +0000 Subject: [PATCH 11/12] Update comments --- README.md | 2 +- lib/src/embedded_cli.c | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b8a6d39..27c7faa 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Single-header CLI library intended for use in embedded systems (like STM32 or Ar * Live autocompletion (see demo above, can be disabled) * Tab (jump to end of current autocompletion) and backspace (remove char) support * History support (navigate with up and down keypress) -* Limited cursor support +* Limited cursor support (navigate inside input with left and right keypress) * Any byte-stream interface is supported (for example, UART) * Single-header distribution diff --git a/lib/src/embedded_cli.c b/lib/src/embedded_cli.c index fbcc32c..549bb29 100644 --- a/lib/src/embedded_cli.c +++ b/lib/src/embedded_cli.c @@ -211,7 +211,10 @@ static const uint16_t cliInternalBindingCount = 1; static const char *lineBreak = "\r\n"; -/* Reference for VT100 escape sequences: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences */ +/* References for VT100 escape sequences: + * https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + * https://ecma-international.org/publications-and-standards/standards/ecma-48/ + */ /** Escape sequence - Cursor forward (right) */ static const char *escSeqCursorRight = "\x1B[C"; @@ -225,10 +228,10 @@ static const char *escSeqCursorSave = "\x1B[s"; /** Escape sequence - Cursor restore position */ static const char *escSeqCursorRestore = "\x1B[u"; -/** Escape sequence - Cursor insert character */ +/** Escape sequence - Cursor insert character (ICH) */ static const char *escSeqInsertChar = "\x1B[@"; -/** Escape sequence - Cursor delete character */ +/** Escape sequence - Cursor delete character (DCH) */ static const char *escSeqDeleteChar = "\x1B[P"; /** From 097cca53ef50d54fae5ef8549da207da5c65c872 Mon Sep 17 00:00:00 2001 From: Cezar Chirila Date: Sun, 14 Jan 2024 20:52:51 +0000 Subject: [PATCH 12/12] Fix Arduino build --- examples/arduino-cli/arduino-cli.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/arduino-cli/arduino-cli.ino b/examples/arduino-cli/arduino-cli.ino index 9368f6a..8c914bd 100644 --- a/examples/arduino-cli/arduino-cli.ino +++ b/examples/arduino-cli/arduino-cli.ino @@ -21,7 +21,7 @@ #include "embedded_cli.h" // 164 bytes is minimum size for this params on Arduino Nano -#define CLI_BUFFER_SIZE 164 +#define CLI_BUFFER_SIZE 166 #define CLI_RX_BUFFER_SIZE 16 #define CLI_CMD_BUFFER_SIZE 32 #define CLI_HISTORY_SIZE 32