Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add limited cursor support (left / right) #45

Merged
merged 12 commits into from
Jan 16, 2024
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (navigate inside input with left and right keypress)
* Any byte-stream interface is supported (for example, UART)
* Single-header distribution

Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion examples/arduino-cli/arduino-cli.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions examples/win32-example/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
118 changes: 106 additions & 12 deletions lib/src/embedded_cli.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "embedded_cli.h"

Expand Down Expand Up @@ -58,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;
Expand Down Expand Up @@ -160,6 +171,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 {
Expand Down Expand Up @@ -194,6 +211,29 @@ static const uint16_t cliInternalBindingCount = 1;

static const char *lineBreak = "\r\n";

/* 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";

/** 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 (ICH) */
static const char *escSeqInsertChar = "\x1B[@";

/** Escape sequence - Cursor delete character (DCH) */
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.
Expand Down Expand Up @@ -299,6 +339,14 @@ 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
* @param direction: true = forward (right), false = backward (left)
*/
static void moveCursor(EmbeddedCli* cli, uint16_t count, bool direction);

/**
* Returns true if provided char is a supported control char:
* \r, \n, \b or 0x7F (treated as \b)
Expand Down Expand Up @@ -459,6 +507,7 @@ EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config) {
impl->maxBindingsCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount);
impl->lastChar = '\0';
impl->invitation = config->invitation;
impl->cursorPos = 0;

initInternalBindings(cli);

Expand Down Expand Up @@ -533,10 +582,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);
Expand All @@ -546,6 +601,7 @@ void embeddedCliPrint(EmbeddedCli *cli, const char *string) {
writeToOutput(cli, impl->invitation);
writeToOutput(cli, impl->cmdBuffer);
impl->inputLineLength = impl->cmdSize;
moveCursor(cli, impl->cursorPos, CURSOR_DIRECTION_BACKWARD);

printLiveAutocompletion(cli);
}
Expand Down Expand Up @@ -677,6 +733,7 @@ static void navigateHistory(EmbeddedCli *cli, bool navigateUp) {

writeToOutput(cli, impl->cmdBuffer);
impl->inputLineLength = impl->cmdSize;
impl->cursorPos = 0;

printLiveAutocompletion(cli);
}
Expand All @@ -693,6 +750,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);
}
}
}

Expand All @@ -703,9 +770,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);
}
Expand All @@ -730,16 +804,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) {
} else if ((c == '\b' || c == 0x7F) && ((impl->cmdSize - impl->cursorPos) > 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);
}
Expand Down Expand Up @@ -971,6 +1046,11 @@ static void printLiveAutocompletion(EmbeddedCli *cli) {
cmd.autocompletedLen = impl->cmdSize;
}

// save cursor location
writeToOutput(cli, escSeqCursorSave);

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) {
cli->writeChar(cli, cmd.firstCandidate[i]);
Expand All @@ -980,10 +1060,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) {
Expand All @@ -1003,9 +1082,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;
}

Expand Down Expand Up @@ -1042,6 +1122,8 @@ static void clearCurrentLine(EmbeddedCli *cli) {
}
cli->writeChar(cli, '\r');
impl->inputLineLength = 0;

impl->cursorPos = 0;
}

static void writeToOutput(EmbeddedCli *cli, const char *str) {
Expand All @@ -1052,6 +1134,18 @@ 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[2] : escSeqCursorLeft[2];
sprintf(escBuffer, "\x1B[%u%c", count, dirChar);
writeToOutput(cli, escBuffer);
}

static bool isControlChar(char c) {
return c == '\r' || c == '\n' || c == '\b' || c == '\t' || c == 0x7F;
}
Expand Down
Loading