| /** | |
| * This header was automatically built using | |
| * embedded_cli.h and embedded_cli.c | |
| * @date 2022-11-03 | |
| * | |
| * MIT License | |
| * | |
| * Copyright (c) 2021 Sviatoslav Kokurin (funbiscuit) | |
| * | |
| * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| * of this software and associated documentation files (the "Software"), to deal | |
| * in the Software without restriction, including without limitation the rights | |
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| * copies of the Software, and to permit persons to whom the Software is | |
| * furnished to do so, subject to the following conditions: | |
| * | |
| * The above copyright notice and this permission notice shall be included in all | |
| * copies or substantial portions of the Software. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| * SOFTWARE. | |
| */ | |
| #ifndef EMBEDDED_CLI_H | |
| #define EMBEDDED_CLI_H | |
| #ifdef __cplusplus | |
| extern "C" { | |
| #else | |
| #include <stdbool.h> | |
| #endif | |
| // cstdint is available only since C++11, so use C header | |
| #include <stdint.h> | |
| // used for proper alignment of cli buffer | |
| #if UINTPTR_MAX == 0xFFFF | |
| #define CLI_UINT uint16_t | |
| #elif UINTPTR_MAX == 0xFFFFFFFF | |
| #define CLI_UINT uint32_t | |
| #elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFFu | |
| #define CLI_UINT uint64_t | |
| #else | |
| #error unsupported pointer size | |
| #endif | |
| #define CLI_UINT_SIZE (sizeof(CLI_UINT)) | |
| // convert size in bytes to size in terms of CLI_UINTs (rounded up | |
| // if bytes is not divisible by size of single CLI_UINT) | |
| #define BYTES_TO_CLI_UINTS(bytes) \ | |
| (((bytes) + CLI_UINT_SIZE - 1)/CLI_UINT_SIZE) | |
| typedef struct CliCommand CliCommand; | |
| typedef struct CliCommandBinding CliCommandBinding; | |
| typedef struct EmbeddedCli EmbeddedCli; | |
| typedef struct EmbeddedCliConfig EmbeddedCliConfig; | |
| struct CliCommand { | |
| /** | |
| * Name of the command. | |
| * In command "set led 1 1" "set" is name | |
| */ | |
| const char *name; | |
| /** | |
| * String of arguments of the command. | |
| * In command "set led 1 1" "led 1 1" is string of arguments | |
| * Is ended with double 0x00 char | |
| * Use tokenize functions to easily get individual tokens | |
| */ | |
| char *args; | |
| }; | |
| /** | |
| * Struct to describe binding of command to function and | |
| */ | |
| struct CliCommandBinding { | |
| /** | |
| * Name of command to bind. Should not be NULL. | |
| */ | |
| const char *name; | |
| /** | |
| * Help string that will be displayed when "help <cmd>" is executed. | |
| * Can have multiple lines separated with "\r\n" | |
| * Can be NULL if no help is provided. | |
| */ | |
| const char *help; | |
| /** | |
| * Flag to perform tokenization before calling binding function. | |
| */ | |
| bool tokenizeArgs; | |
| /** | |
| * Pointer to any specific app context that is required for this binding. | |
| * It will be provided in binding callback. | |
| */ | |
| void *context; | |
| /** | |
| * Binding function for when command is received. | |
| * If null, default callback (onCommand) will be called. | |
| * @param cli - pointer to cli that is calling this binding | |
| * @param args - string of args (if tokenizeArgs is false) or tokens otherwise | |
| * @param context | |
| */ | |
| void (*binding)(EmbeddedCli *cli, char *args, void *context); | |
| }; | |
| struct EmbeddedCli { | |
| /** | |
| * Should write char to connection | |
| * @param cli - pointer to cli that executed this function | |
| * @param c - actual character to write | |
| */ | |
| void (*writeChar)(EmbeddedCli *cli, char c); | |
| /** | |
| * Called when command is received and command not found in list of | |
| * command bindings (or binding function is null). | |
| * @param cli - pointer to cli that executed this function | |
| * @param command - pointer to received command | |
| */ | |
| void (*onCommand)(EmbeddedCli *cli, CliCommand *command); | |
| /** | |
| * Can be used by for any application context | |
| */ | |
| void *appContext; | |
| /** | |
| * Pointer to actual implementation, do not use. | |
| */ | |
| void *_impl; | |
| }; | |
| /** | |
| * Configuration to create CLI | |
| */ | |
| struct EmbeddedCliConfig { | |
| /** | |
| * Size of buffer that is used to store characters until they're processed | |
| */ | |
| uint16_t rxBufferSize; | |
| /** | |
| * Size of buffer that is used to store current input that is not yet | |
| * sended as command (return not pressed yet) | |
| */ | |
| uint16_t cmdBufferSize; | |
| /** | |
| * Size of buffer that is used to store previously entered commands | |
| * Only unique commands are stored in buffer. If buffer is smaller than | |
| * entered command (including arguments), command is discarded from history | |
| */ | |
| uint16_t historyBufferSize; | |
| /** | |
| * Maximum amount of bindings that can be added via addBinding function. | |
| * Cli increases takes extra bindings for internal commands: | |
| * - help | |
| */ | |
| uint16_t maxBindingCount; | |
| /** | |
| * Buffer to use for cli and all internal structures. If NULL, memory will | |
| * be allocated dynamically. Otherwise this buffer is used and no | |
| * allocations are made | |
| */ | |
| CLI_UINT *cliBuffer; | |
| /** | |
| * Size of buffer for cli and internal structures (in bytes). | |
| */ | |
| uint16_t cliBufferSize; | |
| /** | |
| * Whether autocompletion should be enabled. | |
| * If false, autocompletion is disabled but you still can use 'tab' to | |
| * complete current command manually. | |
| */ | |
| bool enableAutoComplete; | |
| }; | |
| /** | |
| * Returns pointer to default configuration for cli creation. It is safe to | |
| * modify it and then send to embeddedCliNew(). | |
| * Returned structure is always the same so do not free and try to use it | |
| * immediately. | |
| * Default values: | |
| * <ul> | |
| * <li>rxBufferSize = 64</li> | |
| * <li>cmdBufferSize = 64</li> | |
| * <li>historyBufferSize = 128</li> | |
| * <li>cliBuffer = NULL (use dynamic allocation)</li> | |
| * <li>cliBufferSize = 0</li> | |
| * <li>maxBindingCount = 8</li> | |
| * <li>enableAutoComplete = true</li> | |
| * </ul> | |
| * @return configuration for cli creation | |
| */ | |
| EmbeddedCliConfig *embeddedCliDefaultConfig(void); | |
| /** | |
| * Returns how many space in config buffer is required for cli creation | |
| * If you provide buffer with less space, embeddedCliNew will return NULL | |
| * This amount will always be divisible by CLI_UINT_SIZE so allocated buffer | |
| * and internal structures can be properly aligned | |
| * @param config | |
| * @return | |
| */ | |
| uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config); | |
| /** | |
| * Create new CLI. | |
| * Memory is allocated dynamically if cliBuffer in config is NULL. | |
| * After CLI is created, override function pointers to start using it | |
| * @param config - config for cli creation | |
| * @return pointer to created CLI | |
| */ | |
| EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config); | |
| /** | |
| * Same as calling embeddedCliNew with default config. | |
| * @return | |
| */ | |
| EmbeddedCli *embeddedCliNewDefault(void); | |
| /** | |
| * Receive character and put it to internal buffer | |
| * Actual processing is done inside embeddedCliProcess | |
| * You can call this function from something like interrupt service routine, | |
| * just make sure that you call it only from single place. Otherwise input | |
| * might get corrupted | |
| * @param cli | |
| * @param c - received char | |
| */ | |
| void embeddedCliReceiveChar(EmbeddedCli *cli, char c); | |
| /** | |
| * Process rx/tx buffers. Command callbacks are called from here | |
| * @param cli | |
| */ | |
| void embeddedCliProcess(EmbeddedCli *cli); | |
| /** | |
| * Add specified binding to list of bindings. If list is already full, binding | |
| * is not added and false is returned | |
| * @param cli | |
| * @param binding | |
| * @return true if binding was added, false otherwise | |
| */ | |
| bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding); | |
| /** | |
| * Print specified string and account for currently entered but not submitted | |
| * command. | |
| * Current command is deleted, provided string is printed (with new line) after | |
| * that current command is printed again, so user can continue typing it. | |
| * @param cli | |
| * @param string | |
| */ | |
| void embeddedCliPrint(EmbeddedCli *cli, const char *string); | |
| /** | |
| * Free allocated for cli memory | |
| * @param cli | |
| */ | |
| void embeddedCliFree(EmbeddedCli *cli); | |
| /** | |
| * Perform tokenization of arguments string. Original string is modified and | |
| * should not be used directly (only inside other token functions). | |
| * Individual tokens are separated by single 0x00 char, double 0x00 is put at | |
| * the end of token list. After calling this function, you can use other | |
| * token functions to get individual tokens and token count. | |
| * | |
| * Important: Call this function only once. Otherwise information will be lost if | |
| * more than one token existed | |
| * @param args - string to tokenize (must have extra writable char after 0x00) | |
| * @return | |
| */ | |
| void embeddedCliTokenizeArgs(char *args); | |
| /** | |
| * Return specific token from tokenized string | |
| * @param tokenizedStr | |
| * @param pos (counted from 1) | |
| * @return token | |
| */ | |
| const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos); | |
| /** | |
| * Same as embeddedCliGetToken but works on non-const buffer | |
| * @param tokenizedStr | |
| * @param pos (counted from 1) | |
| * @return token | |
| */ | |
| char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos); | |
| /** | |
| * Find token in provided tokens string and return its position (counted from 1) | |
| * If no such token is found - 0 is returned. | |
| * @param tokenizedStr | |
| * @param token - token to find | |
| * @return position (increased by 1) or zero if no such token found | |
| */ | |
| uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token); | |
| /** | |
| * Return number of tokens in tokenized string | |
| * @param tokenizedStr | |
| * @return number of tokens | |
| */ | |
| uint16_t embeddedCliGetTokenCount(const char *tokenizedStr); | |
| #ifdef __cplusplus | |
| } | |
| #endif | |
| #endif //EMBEDDED_CLI_H | |
| #ifdef EMBEDDED_CLI_IMPL | |
| #ifndef EMBEDDED_CLI_IMPL_GUARD | |
| #define EMBEDDED_CLI_IMPL_GUARD | |
| #ifdef __cplusplus | |
| extern "C" { | |
| #endif | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #define CLI_TOKEN_NPOS 0xffff | |
| #define UNUSED(x) (void)x | |
| #define PREPARE_IMPL(t) \ | |
| EmbeddedCliImpl* impl = (EmbeddedCliImpl*)t->_impl | |
| #define IS_FLAG_SET(flags, flag) (((flags) & (flag)) != 0) | |
| #define SET_FLAG(flags, flag) ((flags) |= (flag)) | |
| #define UNSET_U8FLAG(flags, flag) ((flags) &= (uint8_t) ~(flag)) | |
| /** | |
| * Marks binding as candidate for autocompletion | |
| * This flag is updated each time getAutocompletedCommand is called | |
| */ | |
| #define BINDING_FLAG_AUTOCOMPLETE 1u | |
| /** | |
| * Indicates that rx buffer overflow happened. In such case last command | |
| * that wasn't finished (no \r or \n were received) will be discarded | |
| */ | |
| #define CLI_FLAG_OVERFLOW 0x01u | |
| /** | |
| * Indicates that initialization is completed. Initialization is completed in | |
| * first call to process and needed, for example, to print invitation message. | |
| */ | |
| #define CLI_FLAG_INIT_COMPLETE 0x02u | |
| /** | |
| * Indicates that CLI structure and internal structures were allocated with | |
| * malloc and should bre freed | |
| */ | |
| #define CLI_FLAG_ALLOCATED 0x04u | |
| /** | |
| * Indicates that CLI structure and internal structures were allocated with | |
| * malloc and should bre freed | |
| */ | |
| #define CLI_FLAG_ESCAPE_MODE 0x08u | |
| /** | |
| * Indicates that CLI in mode when it will print directly to output without | |
| * clear of current command and printing it back | |
| */ | |
| #define CLI_FLAG_DIRECT_PRINT 0x10u | |
| /** | |
| * Indicates that live autocompletion is enabled | |
| */ | |
| #define CLI_FLAG_AUTOCOMPLETE_ENABLED 0x20u | |
| typedef struct EmbeddedCliImpl EmbeddedCliImpl; | |
| typedef struct AutocompletedCommand AutocompletedCommand; | |
| typedef struct FifoBuf FifoBuf; | |
| typedef struct CliHistory CliHistory; | |
| struct FifoBuf { | |
| char *buf; | |
| /** | |
| * Position of first element in buffer. From this position elements are taken | |
| */ | |
| uint16_t front; | |
| /** | |
| * Position after last element. At this position new elements are inserted | |
| */ | |
| uint16_t back; | |
| /** | |
| * Size of buffer | |
| */ | |
| uint16_t size; | |
| }; | |
| struct CliHistory { | |
| /** | |
| * Items in buffer are separated by null-chars | |
| */ | |
| char *buf; | |
| /** | |
| * Total size of buffer | |
| */ | |
| uint16_t bufferSize; | |
| /** | |
| * Index of currently selected element. This allows to navigate history | |
| * After command is sent, current element is reset to 0 (no element) | |
| */ | |
| uint16_t current; | |
| /** | |
| * Number of items in buffer | |
| * Items are counted from top to bottom (and are 1 based). | |
| * So the most recent item is 1 and the oldest is itemCount. | |
| */ | |
| uint16_t itemsCount; | |
| }; | |
| struct EmbeddedCliImpl { | |
| /** | |
| * Invitation string. Is printed at the beginning of each line with user | |
| * input | |
| */ | |
| const char *invitation; | |
| CliHistory history; | |
| /** | |
| * Buffer for storing received chars. | |
| * Chars are stored in FIFO mode. | |
| */ | |
| FifoBuf rxBuffer; | |
| /** | |
| * Buffer for current command | |
| */ | |
| char *cmdBuffer; | |
| /** | |
| * Size of current command | |
| */ | |
| uint16_t cmdSize; | |
| /** | |
| * Total size of command buffer | |
| */ | |
| uint16_t cmdMaxSize; | |
| CliCommandBinding *bindings; | |
| /** | |
| * Flags for each binding. Sizes are the same as for bindings array | |
| */ | |
| uint8_t *bindingsFlags; | |
| uint16_t bindingsCount; | |
| uint16_t maxBindingsCount; | |
| /** | |
| * Total length of input line. This doesn't include invitation but | |
| * includes current command and its live autocompletion | |
| */ | |
| uint16_t inputLineLength; | |
| /** | |
| * Stores last character that was processed. | |
| */ | |
| char lastChar; | |
| /** | |
| * Flags are defined as CLI_FLAG_* | |
| */ | |
| uint8_t flags; | |
| }; | |
| struct AutocompletedCommand { | |
| /** | |
| * Name of autocompleted command (or first candidate for autocompletion if | |
| * there are multiple candidates). | |
| * NULL if autocomplete not possible. | |
| */ | |
| const char *firstCandidate; | |
| /** | |
| * Number of characters that can be completed safely. For example, if there | |
| * are two possible commands "get-led" and "get-adc", then for prefix "g" | |
| * autocompletedLen will be 4. If there are only one candidate, this number | |
| * is always equal to length of the command. | |
| */ | |
| uint16_t autocompletedLen; | |
| /** | |
| * Total number of candidates for autocompletion | |
| */ | |
| uint16_t candidateCount; | |
| }; | |
| static EmbeddedCliConfig defaultConfig; | |
| /** | |
| * Number of commands that cli adds. Commands: | |
| * - help | |
| */ | |
| static const uint16_t cliInternalBindingCount = 1; | |
| static const char *lineBreak = "\r\n"; | |
| /** | |
| * Navigate through command history back and forth. If navigateUp is true, | |
| * navigate to older commands, otherwise navigate to newer. | |
| * When history end is reached, nothing happens. | |
| * @param cli | |
| * @param navigateUp | |
| */ | |
| static void navigateHistory(EmbeddedCli *cli, bool navigateUp); | |
| /** | |
| * Process escaped character. After receiving ESC+[ sequence, all chars up to | |
| * ending character are sent to this function | |
| * @param cli | |
| * @param c | |
| */ | |
| static void onEscapedInput(EmbeddedCli *cli, char c); | |
| /** | |
| * Process input character. Character is valid displayable char and should be | |
| * added to current command string and displayed to client. | |
| * @param cli | |
| * @param c | |
| */ | |
| static void onCharInput(EmbeddedCli *cli, char c); | |
| /** | |
| * Process control character (like \r or \n) possibly altering state of current | |
| * command or executing onCommand callback. | |
| * @param cli | |
| * @param c | |
| */ | |
| static void onControlInput(EmbeddedCli *cli, char c); | |
| /** | |
| * Parse command in buffer and execute callback | |
| * @param cli | |
| */ | |
| static void parseCommand(EmbeddedCli *cli); | |
| /** | |
| * Setup bindings for internal commands, like help | |
| * @param cli | |
| */ | |
| static void initInternalBindings(EmbeddedCli *cli); | |
| /** | |
| * Show help for given tokens (or default help if no tokens) | |
| * @param cli | |
| * @param tokens | |
| * @param context - not used | |
| */ | |
| static void onHelp(EmbeddedCli *cli, char *tokens, void *context); | |
| /** | |
| * Show error about unknown command | |
| * @param cli | |
| * @param name | |
| */ | |
| static void onUnknownCommand(EmbeddedCli *cli, const char *name); | |
| /** | |
| * Return autocompleted command for given prefix. | |
| * Prefix is compared to all known command bindings and autocompleted result | |
| * is returned | |
| * @param cli | |
| * @param prefix | |
| * @return | |
| */ | |
| static AutocompletedCommand getAutocompletedCommand(EmbeddedCli *cli, const char *prefix); | |
| /** | |
| * Prints autocompletion result while keeping current command unchanged | |
| * Prints only if autocompletion is present and only one candidate exists. | |
| * @param cli | |
| */ | |
| static void printLiveAutocompletion(EmbeddedCli *cli); | |
| /** | |
| * Handles autocomplete request. If autocomplete possible - fills current | |
| * command with autocompleted command. When multiple commands satisfy entered | |
| * prefix, they are printed to output. | |
| * @param cli | |
| */ | |
| static void onAutocompleteRequest(EmbeddedCli *cli); | |
| /** | |
| * Removes all input from current line (replaces it with whitespaces) | |
| * And places cursor at the beginning of the line | |
| * @param cli | |
| */ | |
| static void clearCurrentLine(EmbeddedCli *cli); | |
| /** | |
| * Write given string to cli output | |
| * @param cli | |
| * @param str | |
| */ | |
| static void writeToOutput(EmbeddedCli *cli, const char *str); | |
| /** | |
| * Returns true if provided char is a supported control char: | |
| * \r, \n, \b or 0x7F (treated as \b) | |
| * @param c | |
| * @return | |
| */ | |
| static bool isControlChar(char c); | |
| /** | |
| * Returns true if provided char is a valid displayable character: | |
| * a-z, A-Z, 0-9, whitespace, punctuation, etc. | |
| * Currently only ASCII is supported | |
| * @param c | |
| * @return | |
| */ | |
| static bool isDisplayableChar(char c); | |
| /** | |
| * How many elements are currently available in buffer | |
| * @param buffer | |
| * @return number of elements | |
| */ | |
| static uint16_t fifoBufAvailable(FifoBuf *buffer); | |
| /** | |
| * Return first character from buffer and remove it from buffer | |
| * Buffer must be non-empty, otherwise 0 is returned | |
| * @param buffer | |
| * @return | |
| */ | |
| static char fifoBufPop(FifoBuf *buffer); | |
| /** | |
| * Push character into fifo buffer. If there is no space left, character is | |
| * discarded and false is returned | |
| * @param buffer | |
| * @param a - character to add | |
| * @return true if char was added to buffer, false otherwise | |
| */ | |
| static bool fifoBufPush(FifoBuf *buffer, char a); | |
| /** | |
| * Copy provided string to the history buffer. | |
| * If it is already inside history, it will be removed from it and added again. | |
| * So after addition, it will always be on top | |
| * If available size is not enough (and total size is enough) old elements will | |
| * be removed from history so this item can be put to it | |
| * @param history | |
| * @param str | |
| * @return true if string was put in history | |
| */ | |
| static bool historyPut(CliHistory *history, const char *str); | |
| /** | |
| * Get item from history. Items are counted from 1 so if item is 0 or greater | |
| * than itemCount, NULL is returned | |
| * @param history | |
| * @param item | |
| * @return true if string was put in history | |
| */ | |
| static const char *historyGet(CliHistory *history, uint16_t item); | |
| /** | |
| * Remove specific item from history | |
| * @param history | |
| * @param str - string to remove | |
| * @return | |
| */ | |
| static void historyRemove(CliHistory *history, const char *str); | |
| /** | |
| * Return position (index of first char) of specified token | |
| * @param tokenizedStr - tokenized string (separated by \0 with | |
| * \0\0 at the end) | |
| * @param pos - token position (counted from 1) | |
| * @return index of first char of specified token | |
| */ | |
| static uint16_t getTokenPosition(const char *tokenizedStr, uint16_t pos); | |
| EmbeddedCliConfig *embeddedCliDefaultConfig(void) { | |
| defaultConfig.rxBufferSize = 64; | |
| defaultConfig.cmdBufferSize = 64; | |
| defaultConfig.historyBufferSize = 128; | |
| defaultConfig.cliBuffer = NULL; | |
| defaultConfig.cliBufferSize = 0; | |
| defaultConfig.maxBindingCount = 8; | |
| defaultConfig.enableAutoComplete = true; | |
| return &defaultConfig; | |
| } | |
| uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config) { | |
| uint16_t bindingCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); | |
| return (uint16_t) (CLI_UINT_SIZE * ( | |
| BYTES_TO_CLI_UINTS(sizeof(EmbeddedCli)) + | |
| BYTES_TO_CLI_UINTS(sizeof(EmbeddedCliImpl)) + | |
| BYTES_TO_CLI_UINTS(config->rxBufferSize * sizeof(char)) + | |
| BYTES_TO_CLI_UINTS(config->cmdBufferSize * sizeof(char)) + | |
| BYTES_TO_CLI_UINTS(config->historyBufferSize * sizeof(char)) + | |
| BYTES_TO_CLI_UINTS(bindingCount * sizeof(CliCommandBinding)) + | |
| BYTES_TO_CLI_UINTS(bindingCount * sizeof(uint8_t)))); | |
| } | |
| EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config) { | |
| EmbeddedCli *cli = NULL; | |
| uint16_t bindingCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); | |
| size_t totalSize = embeddedCliRequiredSize(config); | |
| bool allocated = false; | |
| if (config->cliBuffer == NULL) { | |
| // config->cliBuffer = (CLI_UINT *) malloc(totalSize); // malloc guarantees alignment. | |
| if (config->cliBuffer == NULL) | |
| return NULL; | |
| allocated = true; | |
| } else if (config->cliBufferSize < totalSize) { | |
| return NULL; | |
| } | |
| CLI_UINT *buf = config->cliBuffer; | |
| memset(buf, 0, totalSize); | |
| cli = (EmbeddedCli *) buf; | |
| buf += BYTES_TO_CLI_UINTS(sizeof(EmbeddedCli)); | |
| cli->_impl = (EmbeddedCliImpl *) buf; | |
| buf += BYTES_TO_CLI_UINTS(sizeof(EmbeddedCliImpl)); | |
| PREPARE_IMPL(cli); | |
| impl->rxBuffer.buf = (char *) buf; | |
| buf += BYTES_TO_CLI_UINTS(config->rxBufferSize * sizeof(char)); | |
| impl->cmdBuffer = (char *) buf; | |
| buf += BYTES_TO_CLI_UINTS(config->cmdBufferSize * sizeof(char)); | |
| impl->bindings = (CliCommandBinding *) buf; | |
| buf += BYTES_TO_CLI_UINTS(bindingCount * sizeof(CliCommandBinding)); | |
| impl->bindingsFlags = (uint8_t *) buf; | |
| buf += BYTES_TO_CLI_UINTS(bindingCount); | |
| impl->history.buf = (char *) buf; | |
| impl->history.bufferSize = config->historyBufferSize; | |
| if (allocated) | |
| SET_FLAG(impl->flags, CLI_FLAG_ALLOCATED); | |
| if (config->enableAutoComplete) | |
| SET_FLAG(impl->flags, CLI_FLAG_AUTOCOMPLETE_ENABLED); | |
| impl->rxBuffer.size = config->rxBufferSize; | |
| impl->rxBuffer.front = 0; | |
| impl->rxBuffer.back = 0; | |
| impl->cmdMaxSize = config->cmdBufferSize; | |
| impl->bindingsCount = 0; | |
| impl->maxBindingsCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); | |
| impl->lastChar = '\0'; | |
| impl->invitation = "> "; | |
| initInternalBindings(cli); | |
| return cli; | |
| } | |
| EmbeddedCli *embeddedCliNewDefault(void) { | |
| return embeddedCliNew(embeddedCliDefaultConfig()); | |
| } | |
| void embeddedCliReceiveChar(EmbeddedCli *cli, char c) { | |
| PREPARE_IMPL(cli); | |
| if (!fifoBufPush(&impl->rxBuffer, c)) { | |
| SET_FLAG(impl->flags, CLI_FLAG_OVERFLOW); | |
| } | |
| } | |
| void embeddedCliProcess(EmbeddedCli *cli) { | |
| if (cli->writeChar == NULL) | |
| return; | |
| PREPARE_IMPL(cli); | |
| if (!IS_FLAG_SET(impl->flags, CLI_FLAG_INIT_COMPLETE)) { | |
| SET_FLAG(impl->flags, CLI_FLAG_INIT_COMPLETE); | |
| writeToOutput(cli, impl->invitation); | |
| } | |
| while (fifoBufAvailable(&impl->rxBuffer)) { | |
| char c = fifoBufPop(&impl->rxBuffer); | |
| if (IS_FLAG_SET(impl->flags, CLI_FLAG_ESCAPE_MODE)) { | |
| onEscapedInput(cli, c); | |
| } else if (impl->lastChar == 0x1B && c == '[') { | |
| //enter escape mode | |
| SET_FLAG(impl->flags, CLI_FLAG_ESCAPE_MODE); | |
| } else if (isControlChar(c)) { | |
| onControlInput(cli, c); | |
| } else if (isDisplayableChar(c)) { | |
| onCharInput(cli, c); | |
| } | |
| printLiveAutocompletion(cli); | |
| impl->lastChar = c; | |
| } | |
| // discard unfinished command if overflow happened | |
| if (IS_FLAG_SET(impl->flags, CLI_FLAG_OVERFLOW)) { | |
| impl->cmdSize = 0; | |
| impl->cmdBuffer[impl->cmdSize] = '\0'; | |
| UNSET_U8FLAG(impl->flags, CLI_FLAG_OVERFLOW); | |
| } | |
| } | |
| bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding) { | |
| PREPARE_IMPL(cli); | |
| if (impl->bindingsCount == impl->maxBindingsCount) | |
| return false; | |
| impl->bindings[impl->bindingsCount] = binding; | |
| ++impl->bindingsCount; | |
| return true; | |
| } | |
| void embeddedCliPrint(EmbeddedCli *cli, const char *string) { | |
| if (cli->writeChar == NULL) | |
| return; | |
| PREPARE_IMPL(cli); | |
| // remove chars for autocompletion and live command | |
| if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) | |
| clearCurrentLine(cli); | |
| // print provided string | |
| writeToOutput(cli, string); | |
| writeToOutput(cli, lineBreak); | |
| // print current command back to screen | |
| if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) { | |
| writeToOutput(cli, impl->invitation); | |
| writeToOutput(cli, impl->cmdBuffer); | |
| impl->inputLineLength = impl->cmdSize; | |
| printLiveAutocompletion(cli); | |
| } | |
| } | |
| void embeddedCliFree(EmbeddedCli *cli) { | |
| PREPARE_IMPL(cli); | |
| if (IS_FLAG_SET(impl->flags, CLI_FLAG_ALLOCATED)) { | |
| // allocation is done in single call to malloc, so need only single free | |
| // free(cli); | |
| } | |
| } | |
| void embeddedCliTokenizeArgs(char *args) { | |
| if (args == NULL) | |
| return; | |
| // for now only space, but can add more later | |
| const char *separators = " "; | |
| // indicates that arg is quoted so separators are copied as is | |
| bool quotesEnabled = false; | |
| // indicates that previous char was a slash, so next char is copied as is | |
| bool escapeActivated = false; | |
| int insertPos = 0; | |
| int i = 0; | |
| char currentChar; | |
| while ((currentChar = args[i]) != '\0') { | |
| ++i; | |
| if (escapeActivated) { | |
| escapeActivated = false; | |
| } else if (currentChar == '\\') { | |
| escapeActivated = true; | |
| continue; | |
| } else if (currentChar == '"') { | |
| quotesEnabled = !quotesEnabled; | |
| currentChar = '\0'; | |
| } else if (!quotesEnabled && strchr(separators, currentChar) != NULL) { | |
| currentChar = '\0'; | |
| } | |
| // null chars are only copied once and not copied to the beginning | |
| if (currentChar != '\0' || (insertPos > 0 && args[insertPos - 1] != '\0')) { | |
| args[insertPos] = currentChar; | |
| ++insertPos; | |
| } | |
| } | |
| // make args double null-terminated source buffer must be big enough to contain extra spaces | |
| args[insertPos] = '\0'; | |
| args[insertPos + 1] = '\0'; | |
| } | |
| const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos) { | |
| uint16_t i = getTokenPosition(tokenizedStr, pos); | |
| if (i != CLI_TOKEN_NPOS) | |
| return &tokenizedStr[i]; | |
| else | |
| return NULL; | |
| } | |
| char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos) { | |
| uint16_t i = getTokenPosition(tokenizedStr, pos); | |
| if (i != CLI_TOKEN_NPOS) | |
| return &tokenizedStr[i]; | |
| else | |
| return NULL; | |
| } | |
| uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token) { | |
| if (tokenizedStr == NULL || token == NULL) | |
| return 0; | |
| uint16_t size = embeddedCliGetTokenCount(tokenizedStr); | |
| for (uint16_t i = 1; i <= size; ++i) { | |
| if (strcmp(embeddedCliGetToken(tokenizedStr, i), token) == 0) | |
| return i; | |
| } | |
| return 0; | |
| } | |
| uint16_t embeddedCliGetTokenCount(const char *tokenizedStr) { | |
| if (tokenizedStr == NULL || tokenizedStr[0] == '\0') | |
| return 0; | |
| int i = 0; | |
| uint16_t tokenCount = 1; | |
| while (true) { | |
| if (tokenizedStr[i] == '\0') { | |
| if (tokenizedStr[i + 1] == '\0') | |
| break; | |
| ++tokenCount; | |
| } | |
| ++i; | |
| } | |
| return tokenCount; | |
| } | |
| static void navigateHistory(EmbeddedCli *cli, bool navigateUp) { | |
| PREPARE_IMPL(cli); | |
| if (impl->history.itemsCount == 0 || | |
| (navigateUp && impl->history.current == impl->history.itemsCount) || | |
| (!navigateUp && impl->history.current == 0)) | |
| return; | |
| clearCurrentLine(cli); | |
| writeToOutput(cli, impl->invitation); | |
| if (navigateUp) | |
| ++impl->history.current; | |
| else | |
| --impl->history.current; | |
| const char *item = historyGet(&impl->history, impl->history.current); | |
| // simple way to handle empty command the same way as others | |
| if (item == NULL) | |
| item = ""; | |
| uint16_t len = (uint16_t) strlen(item); | |
| memcpy(impl->cmdBuffer, item, len); | |
| impl->cmdBuffer[len] = '\0'; | |
| impl->cmdSize = len; | |
| writeToOutput(cli, impl->cmdBuffer); | |
| impl->inputLineLength = impl->cmdSize; | |
| printLiveAutocompletion(cli); | |
| } | |
| static void onEscapedInput(EmbeddedCli *cli, char c) { | |
| PREPARE_IMPL(cli); | |
| if (c >= 64 && c <= 126) { | |
| // handle escape sequence | |
| UNSET_U8FLAG(impl->flags, CLI_FLAG_ESCAPE_MODE); | |
| if (c == 'A' || c == 'B') { | |
| // treat \e[..A as cursor up and \e[..B as cursor down | |
| // there might be extra chars between [ and A/B, just ignore them | |
| navigateHistory(cli, c == 'A'); | |
| } | |
| } | |
| } | |
| static void onCharInput(EmbeddedCli *cli, char c) { | |
| PREPARE_IMPL(cli); | |
| // have to reserve two extra chars for command ending (used in tokenization) | |
| if (impl->cmdSize + 2 >= impl->cmdMaxSize) | |
| return; | |
| impl->cmdBuffer[impl->cmdSize] = c; | |
| ++impl->cmdSize; | |
| impl->cmdBuffer[impl->cmdSize] = '\0'; | |
| cli->writeChar(cli, c); | |
| } | |
| static void onControlInput(EmbeddedCli *cli, char c) { | |
| PREPARE_IMPL(cli); | |
| // process \r\n and \n\r as single \r\n command | |
| if ((impl->lastChar == '\r' && c == '\n') || | |
| (impl->lastChar == '\n' && c == '\r')) | |
| return; | |
| if (c == '\r' || c == '\n') { | |
| // try to autocomplete command and then process it | |
| onAutocompleteRequest(cli); | |
| writeToOutput(cli, lineBreak); | |
| if (impl->cmdSize > 0) | |
| parseCommand(cli); | |
| impl->cmdSize = 0; | |
| impl->cmdBuffer[impl->cmdSize] = '\0'; | |
| impl->inputLineLength = 0; | |
| impl->history.current = 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'); | |
| // and from buffer | |
| --impl->cmdSize; | |
| impl->cmdBuffer[impl->cmdSize] = '\0'; | |
| } else if (c == '\t') { | |
| onAutocompleteRequest(cli); | |
| } | |
| } | |
| static void parseCommand(EmbeddedCli *cli) { | |
| PREPARE_IMPL(cli); | |
| bool isEmpty = true; | |
| for (int i = 0; i < impl->cmdSize; ++i) { | |
| if (impl->cmdBuffer[i] != ' ') { | |
| isEmpty = false; | |
| break; | |
| } | |
| } | |
| // do not process empty commands | |
| if (isEmpty) | |
| return; | |
| // push command to history before buffer is modified | |
| historyPut(&impl->history, impl->cmdBuffer); | |
| char *cmdName = NULL; | |
| char *cmdArgs = NULL; | |
| bool nameFinished = false; | |
| // find command name and command args inside command buffer | |
| for (int i = 0; i < impl->cmdSize; ++i) { | |
| char c = impl->cmdBuffer[i]; | |
| if (c == ' ') { | |
| // all spaces between name and args are filled with zeros | |
| // so name is a correct null-terminated string | |
| if (cmdArgs == NULL) | |
| impl->cmdBuffer[i] = '\0'; | |
| if (cmdName != NULL) | |
| nameFinished = true; | |
| } else if (cmdName == NULL) { | |
| cmdName = &impl->cmdBuffer[i]; | |
| } else if (cmdArgs == NULL && nameFinished) { | |
| cmdArgs = &impl->cmdBuffer[i]; | |
| } | |
| } | |
| // we keep two last bytes in cmd buffer reserved so cmdSize is always by 2 | |
| // less than cmdMaxSize | |
| impl->cmdBuffer[impl->cmdSize + 1] = '\0'; | |
| if (cmdName == NULL) | |
| return; | |
| // try to find command in bindings | |
| for (int i = 0; i < impl->bindingsCount; ++i) { | |
| if (strcmp(cmdName, impl->bindings[i].name) == 0) { | |
| if (impl->bindings[i].binding == NULL) | |
| break; | |
| if (impl->bindings[i].tokenizeArgs) | |
| embeddedCliTokenizeArgs(cmdArgs); | |
| // currently, output is blank line, so we can just print directly | |
| SET_FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); | |
| impl->bindings[i].binding(cli, cmdArgs, impl->bindings[i].context); | |
| UNSET_U8FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); | |
| return; | |
| } | |
| } | |
| // command not found in bindings or binding was null | |
| // try to call default callback | |
| if (cli->onCommand != NULL) { | |
| CliCommand command; | |
| command.name = cmdName; | |
| command.args = cmdArgs; | |
| // currently, output is blank line, so we can just print directly | |
| SET_FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); | |
| cli->onCommand(cli, &command); | |
| UNSET_U8FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); | |
| } else { | |
| onUnknownCommand(cli, cmdName); | |
| } | |
| } | |
| static void initInternalBindings(EmbeddedCli *cli) { | |
| CliCommandBinding b = { | |
| "help", | |
| "Print list of commands", | |
| true, | |
| NULL, | |
| onHelp | |
| }; | |
| embeddedCliAddBinding(cli, b); | |
| } | |
| static void onHelp(EmbeddedCli *cli, char *tokens, void *context) { | |
| UNUSED(context); | |
| PREPARE_IMPL(cli); | |
| if (impl->bindingsCount == 0) { | |
| writeToOutput(cli, "Help is not available"); | |
| writeToOutput(cli, lineBreak); | |
| return; | |
| } | |
| uint16_t tokenCount = embeddedCliGetTokenCount(tokens); | |
| if (tokenCount == 0) { | |
| for (int i = 0; i < impl->bindingsCount; ++i) { | |
| writeToOutput(cli, " * "); | |
| writeToOutput(cli, impl->bindings[i].name); | |
| writeToOutput(cli, lineBreak); | |
| if (impl->bindings[i].help != NULL) { | |
| cli->writeChar(cli, '\t'); | |
| writeToOutput(cli, impl->bindings[i].help); | |
| writeToOutput(cli, lineBreak); | |
| } | |
| } | |
| } else if (tokenCount == 1) { | |
| // try find command | |
| const char *helpStr = NULL; | |
| const char *cmdName = embeddedCliGetToken(tokens, 1); | |
| bool found = false; | |
| for (int i = 0; i < impl->bindingsCount; ++i) { | |
| if (strcmp(impl->bindings[i].name, cmdName) == 0) { | |
| helpStr = impl->bindings[i].help; | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (found && helpStr != NULL) { | |
| writeToOutput(cli, " * "); | |
| writeToOutput(cli, cmdName); | |
| writeToOutput(cli, lineBreak); | |
| cli->writeChar(cli, '\t'); | |
| writeToOutput(cli, helpStr); | |
| writeToOutput(cli, lineBreak); | |
| } else if (found) { | |
| writeToOutput(cli, "Help is not available"); | |
| writeToOutput(cli, lineBreak); | |
| } else { | |
| onUnknownCommand(cli, cmdName); | |
| } | |
| } else { | |
| writeToOutput(cli, "Command \"help\" receives one or zero arguments"); | |
| writeToOutput(cli, lineBreak); | |
| } | |
| } | |
| static void onUnknownCommand(EmbeddedCli *cli, const char *name) { | |
| writeToOutput(cli, "Unknown command: \""); | |
| writeToOutput(cli, name); | |
| writeToOutput(cli, "\". Write \"help\" for a list of available commands"); | |
| writeToOutput(cli, lineBreak); | |
| } | |
| static AutocompletedCommand getAutocompletedCommand(EmbeddedCli *cli, const char *prefix) { | |
| AutocompletedCommand cmd = {NULL, 0, 0}; | |
| size_t prefixLen = strlen(prefix); | |
| PREPARE_IMPL(cli); | |
| if (impl->bindingsCount == 0 || prefixLen == 0) | |
| return cmd; | |
| for (int i = 0; i < impl->bindingsCount; ++i) { | |
| const char *name = impl->bindings[i].name; | |
| size_t len = strlen(name); | |
| // unset autocomplete flag | |
| UNSET_U8FLAG(impl->bindingsFlags[i], BINDING_FLAG_AUTOCOMPLETE); | |
| if (len < prefixLen) | |
| continue; | |
| // check if this command is candidate for autocomplete | |
| bool isCandidate = true; | |
| for (size_t j = 0; j < prefixLen; ++j) { | |
| if (prefix[j] != name[j]) { | |
| isCandidate = false; | |
| break; | |
| } | |
| } | |
| if (!isCandidate) | |
| continue; | |
| impl->bindingsFlags[i] |= BINDING_FLAG_AUTOCOMPLETE; | |
| if (cmd.candidateCount == 0 || len < cmd.autocompletedLen) | |
| cmd.autocompletedLen = (uint16_t) len; | |
| ++cmd.candidateCount; | |
| if (cmd.candidateCount == 1) { | |
| cmd.firstCandidate = name; | |
| continue; | |
| } | |
| for (size_t j = impl->cmdSize; j < cmd.autocompletedLen; ++j) { | |
| if (cmd.firstCandidate[j] != name[j]) { | |
| cmd.autocompletedLen = (uint16_t) j; | |
| break; | |
| } | |
| } | |
| } | |
| return cmd; | |
| } | |
| static void printLiveAutocompletion(EmbeddedCli *cli) { | |
| PREPARE_IMPL(cli); | |
| if (!IS_FLAG_SET(impl->flags, CLI_FLAG_AUTOCOMPLETE_ENABLED)) | |
| return; | |
| AutocompletedCommand cmd = getAutocompletedCommand(cli, impl->cmdBuffer); | |
| if (cmd.candidateCount == 0) { | |
| cmd.autocompletedLen = impl->cmdSize; | |
| } | |
| // 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]); | |
| } | |
| // replace with spaces previous autocompletion | |
| for (size_t i = cmd.autocompletedLen; i < impl->inputLineLength; ++i) { | |
| 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); | |
| } | |
| static void onAutocompleteRequest(EmbeddedCli *cli) { | |
| PREPARE_IMPL(cli); | |
| AutocompletedCommand cmd = getAutocompletedCommand(cli, impl->cmdBuffer); | |
| if (cmd.candidateCount == 0) | |
| return; | |
| if (cmd.candidateCount == 1 || cmd.autocompletedLen > impl->cmdSize) { | |
| // can copy from index cmdSize, but prefix is the same, so copy everything | |
| memcpy(impl->cmdBuffer, cmd.firstCandidate, cmd.autocompletedLen); | |
| if (cmd.candidateCount == 1) { | |
| impl->cmdBuffer[cmd.autocompletedLen] = ' '; | |
| ++cmd.autocompletedLen; | |
| } | |
| impl->cmdBuffer[cmd.autocompletedLen] = '\0'; | |
| writeToOutput(cli, &impl->cmdBuffer[impl->cmdSize]); | |
| impl->cmdSize = cmd.autocompletedLen; | |
| impl->inputLineLength = impl->cmdSize; | |
| return; | |
| } | |
| // with multiple candidates when we already completed to common prefix | |
| // we show all candidates and print input again | |
| // we need to completely clear current line since it begins with invitation | |
| clearCurrentLine(cli); | |
| for (int i = 0; i < impl->bindingsCount; ++i) { | |
| // autocomplete flag is set for all candidates by last call to | |
| // getAutocompletedCommand | |
| if (!(impl->bindingsFlags[i] & BINDING_FLAG_AUTOCOMPLETE)) | |
| continue; | |
| const char *name = impl->bindings[i].name; | |
| writeToOutput(cli, name); | |
| writeToOutput(cli, lineBreak); | |
| } | |
| writeToOutput(cli, impl->invitation); | |
| writeToOutput(cli, impl->cmdBuffer); | |
| impl->inputLineLength = impl->cmdSize; | |
| } | |
| static void clearCurrentLine(EmbeddedCli *cli) { | |
| PREPARE_IMPL(cli); | |
| size_t len = impl->inputLineLength + strlen(impl->invitation); | |
| cli->writeChar(cli, '\r'); | |
| for (size_t i = 0; i < len; ++i) { | |
| cli->writeChar(cli, ' '); | |
| } | |
| cli->writeChar(cli, '\r'); | |
| impl->inputLineLength = 0; | |
| } | |
| static void writeToOutput(EmbeddedCli *cli, const char *str) { | |
| size_t len = strlen(str); | |
| for (size_t i = 0; i < len; ++i) { | |
| cli->writeChar(cli, str[i]); | |
| } | |
| } | |
| static bool isControlChar(char c) { | |
| return c == '\r' || c == '\n' || c == '\b' || c == '\t' || c == 0x7F; | |
| } | |
| static bool isDisplayableChar(char c) { | |
| return (c >= 32 && c <= 126); | |
| } | |
| static uint16_t fifoBufAvailable(FifoBuf *buffer) { | |
| if (buffer->back >= buffer->front) | |
| return (uint16_t) (buffer->back - buffer->front); | |
| else | |
| return (uint16_t) (buffer->size - buffer->front + buffer->back); | |
| } | |
| static char fifoBufPop(FifoBuf *buffer) { | |
| char a = '\0'; | |
| if (buffer->front != buffer->back) { | |
| a = buffer->buf[buffer->front]; | |
| buffer->front = (uint16_t) (buffer->front + 1) % buffer->size; | |
| } | |
| return a; | |
| } | |
| static bool fifoBufPush(FifoBuf *buffer, char a) { | |
| uint16_t newBack = (uint16_t) (buffer->back + 1) % buffer->size; | |
| if (newBack != buffer->front) { | |
| buffer->buf[buffer->back] = a; | |
| buffer->back = newBack; | |
| return true; | |
| } | |
| return false; | |
| } | |
| static bool historyPut(CliHistory *history, const char *str) { | |
| size_t len = strlen(str); | |
| // each item is ended with \0 so, need to have that much space at least | |
| if (history->bufferSize < len + 1) | |
| return false; | |
| // remove str from history (if it's present) so we don't get duplicates | |
| historyRemove(history, str); | |
| size_t usedSize; | |
| // remove old items if new one can't fit into buffer | |
| while (history->itemsCount > 0) { | |
| const char *item = historyGet(history, history->itemsCount); | |
| size_t itemLen = strlen(item); | |
| usedSize = ((size_t) (item - history->buf)) + itemLen + 1; | |
| size_t freeSpace = history->bufferSize - usedSize; | |
| if (freeSpace >= len + 1) | |
| break; | |
| // space not enough, remove last element | |
| --history->itemsCount; | |
| } | |
| if (history->itemsCount > 0) { | |
| // when history not empty, shift elements so new item is first | |
| memmove(&history->buf[len + 1], history->buf, usedSize); | |
| } | |
| memcpy(history->buf, str, len + 1); | |
| ++history->itemsCount; | |
| return true; | |
| } | |
| static const char *historyGet(CliHistory *history, uint16_t item) { | |
| if (item == 0 || item > history->itemsCount) | |
| return NULL; | |
| // items are stored in the same way (separated by \0 and counted from 1), | |
| // so can use this call | |
| return embeddedCliGetToken(history->buf, item); | |
| } | |
| static void historyRemove(CliHistory *history, const char *str) { | |
| if (str == NULL || history->itemsCount == 0) | |
| return; | |
| char *item = NULL; | |
| uint16_t itemPosition; | |
| for (itemPosition = 1; itemPosition <= history->itemsCount; ++itemPosition) { | |
| // items are stored in the same way (separated by \0 and counted from 1), | |
| // so can use this call | |
| item = embeddedCliGetTokenVariable(history->buf, itemPosition); | |
| if (strcmp(item, str) == 0) { | |
| break; | |
| } | |
| item = NULL; | |
| } | |
| if (item == NULL) | |
| return; | |
| --history->itemsCount; | |
| if (itemPosition == (history->itemsCount + 1)) { | |
| // if this is a last element, nothing is remaining to move | |
| return; | |
| } | |
| size_t len = strlen(item); | |
| size_t remaining = (size_t) (history->bufferSize - (item + len + 1 - history->buf)); | |
| // move everything to the right of found item | |
| memmove(item, &item[len + 1], remaining); | |
| } | |
| static uint16_t getTokenPosition(const char *tokenizedStr, uint16_t pos) { | |
| if (tokenizedStr == NULL || pos == 0) | |
| return CLI_TOKEN_NPOS; | |
| uint16_t i = 0; | |
| uint16_t tokenCount = 1; | |
| while (true) { | |
| if (tokenCount == pos) | |
| break; | |
| if (tokenizedStr[i] == '\0') { | |
| ++tokenCount; | |
| if (tokenizedStr[i + 1] == '\0') | |
| break; | |
| } | |
| ++i; | |
| } | |
| if (tokenizedStr[i] != '\0') | |
| return i; | |
| else | |
| return CLI_TOKEN_NPOS; | |
| } | |
| #ifdef __cplusplus | |
| } | |
| #endif | |
| #endif // EMBEDDED_CLI_IMPL_GUARD | |
| #endif // EMBEDDED_CLI_IMPL |