/* * MIT License * * Copyright (c) 2019 Stefan Strobel * * 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. */ /** * @file shellmatta.c * @brief Main implementation of the Shellmatta terminal implementation * @author Stefan Strobel */ /** * @addtogroup shellmatta * @{ */ #include "shellmatta.h" #include #include #include #include #define SHELLMATTA_MIN(a,b) (((a) > (b)) ? (b) : (a)) #define SHELLMATTA_MAX(a,b) (((a) > (b)) ? (b) : (a)) static void writeEcho( shellmatta_instance_t *inst, const uint8_t *data, uint32_t length) { if(true == inst->echoEnabled) { inst->write(data, length); } } static void appendHistoryByte(shellmatta_instance_t *inst, uint8_t byte) { inst->historyEnd ++; if(inst->historyEnd >= inst->historyBufferSize) { inst->historyEnd = 0u; } inst->historyBuffer[inst->historyEnd] = byte; if(inst->historyEnd == inst->historyStart) { /* move the start pointer to the next termination (0) */ do { inst->historyStart ++; if(inst->historyStart >= inst->historyBufferSize) { inst->historyStart = 0u; } }while(0u != inst->historyBuffer[inst->historyStart]); } } static bool getHistoryByte(shellmatta_instance_t *inst, uint8_t *byte) { bool ret = false; if(inst->historyRead != inst->historyStart) { *byte = inst->historyBuffer[inst->historyRead]; if(0u == inst->historyRead) { inst->historyRead = inst->historyBufferSize; } inst->historyRead --; ret = true; } return ret; } static void storeHistoryCmd(shellmatta_instance_t *inst) { if( (inst->historyBufferSize > inst->inputCount) && (0u != inst->inputCount) && (true == inst->dirty)) { appendHistoryByte(inst, 0u); for(uint32_t i = inst->inputCount; i > 0u; i --) { appendHistoryByte(inst, inst->buffer[i - 1u]); } } inst->dirty = false; } static bool navigateHistoryCmd(shellmatta_instance_t *inst, int32_t cnt) { bool ret = true; uint32_t tempReadIdx = 0u; while((cnt > 0) && (true == ret)) { if(inst->historyRead != inst->historyEnd) { inst->historyRead ++; } while(inst->historyRead != inst->historyEnd) { inst->historyRead ++; if(inst->historyRead >= inst->historyBufferSize) { inst->historyRead = 0u; } if( (inst->historyRead != inst->historyEnd) && (0u == inst->historyBuffer[inst->historyRead])) { if(0u == inst->historyRead) { inst->historyRead = inst->historyBufferSize; } inst->historyRead --; cnt -= 1; break; } } if(inst->historyRead == inst->historyEnd) { ret = false; } } while((cnt < 0) && (true == ret)) { tempReadIdx = inst->historyRead; while(inst->historyRead != inst->historyStart) { if(0u == inst->historyRead) { inst->historyRead = inst->historyBufferSize; } inst->historyRead --; if( (inst->historyRead != inst->historyStart) && (0u == inst->historyBuffer[inst->historyRead])) { if(0u == inst->historyRead) { inst->historyRead = inst->historyBufferSize; } inst->historyRead --; cnt += 1; break; } } if(inst->historyRead == inst->historyStart) { inst->historyRead = tempReadIdx; inst->historyReadUp = false; ret = false; } } return ret; } static void restoreHistoryCmd(shellmatta_instance_t *inst) { uint8_t byte; bool ret = true; ret = getHistoryByte(inst, &byte); while((ret == true) && (byte != 0u)) { inst->buffer[inst->inputCount] = byte; inst->inputCount ++; inst->cursor ++; ret = getHistoryByte(inst, &byte); } writeEcho(inst, inst->buffer, inst->inputCount); navigateHistoryCmd(inst, 1); inst->dirty = false; } static void resetHistoryBuffer(shellmatta_instance_t *inst) { inst->historyRead = inst->historyEnd; inst->historyReadUp = true; } static void saveCursorPos(shellmatta_instance_t *inst) { writeEcho(inst, "\e[s", 3u); } static void restoreCursorPos(shellmatta_instance_t *inst) { writeEcho(inst, "\e[u", 3u); } static void eraseLine(shellmatta_instance_t *inst) { writeEcho(inst, "\e[K", 3u); } static void rewindCursor(shellmatta_instance_t *inst, uint32_t length) { uint8_t terminalCmd[16]; size_t size; length = SHELLMATTA_MIN (length, inst->cursor); if(length > 0u) { size = snprintf(terminalCmd, sizeof(terminalCmd), "\e[%uD", length); writeEcho(inst, terminalCmd, size); inst->cursor -= length; } } static void forwardCursor(shellmatta_instance_t *inst, uint32_t length) { uint8_t terminalCmd[16]; size_t size; length = SHELLMATTA_MAX (length, (inst->inputCount - inst->cursor)); if (length > 0u) { size = snprintf(terminalCmd, sizeof(terminalCmd), "\e[%uC", length); writeEcho(inst, terminalCmd, size); inst->cursor -= length; } } static void insertChars(shellmatta_instance_t *inst, uint8_t *data, uint32_t length) { /* check if we have to move chars in the buffer */ if( (inst->cursor != inst->inputCount) && (SHELLMATTA_MODE_INSERT == inst->mode)) { /* move the existing chars */ for ( uint32_t i = inst->inputCount; i > inst->cursor; i --) { inst->buffer[i + length - 1] = inst->buffer[i - 1]; } /* store and print the new chars */ memcpy(&(inst->buffer[inst->cursor]), data, length); writeEcho(inst, data, length); /* print the other chars and restore the cursor to this position */ eraseLine(inst); saveCursorPos(inst); writeEcho( inst, &(inst->buffer[inst->cursor + length]), inst->inputCount - inst->cursor); restoreCursorPos(inst); } /* just overwrite/append the chars */ else { memcpy(&(inst->buffer[inst->cursor]), data, length); writeEcho(inst, data, length); } inst->inputCount += length; inst->cursor += length; } static void removeChars(shellmatta_instance_t *inst, uint32_t length, bool backspace) { length = SHELLMATTA_MIN (length, inst->cursor); if(0u != length) { if(true == backspace) { rewindCursor(inst, length); } /* delete the char at the cursor position */ for ( uint32_t i = inst->cursor; i < (inst->inputCount - length); i++) { inst->buffer[i] = inst->buffer[i + length]; } eraseLine(inst); saveCursorPos(inst); writeEcho( inst, &(inst->buffer[inst->cursor]), (inst->inputCount - inst->cursor - length)); restoreCursorPos(inst); inst->inputCount -= length; } } static void clearInput(shellmatta_instance_t *inst) { rewindCursor(inst, inst->cursor); eraseLine(inst); inst->inputCount = 0u; inst->dirty = false; } static shellmatta_retCode_t processArrowKeys(shellmatta_instance_t *inst) { shellmatta_retCode_t ret = SHELLMATTA_USE_FAULT; if ('[' == inst->escapeChars[0]) { ret = SHELLMATTA_OK; switch (inst->escapeChars[1]) { case 'A': /* arrow up */ storeHistoryCmd(inst); if(false == inst->historyReadUp) { navigateHistoryCmd(inst, -1); } inst->historyReadUp = true; clearInput(inst); restoreHistoryCmd(inst); navigateHistoryCmd(inst, -1); break; case 'B': /* arrow down */ if((inst->historyRead != inst->historyEnd)) { storeHistoryCmd(inst); if(true == inst->historyReadUp) { navigateHistoryCmd(inst, 1); } inst->historyReadUp = false; navigateHistoryCmd(inst, 1); clearInput(inst); restoreHistoryCmd(inst); } break; case 'C': /* arrow right */ forwardCursor(inst, 1u); break; case 'D': /* arrow left */ rewindCursor(inst, 1u); break; default: /** ignore unknown escape */ ret = SHELLMATTA_USE_FAULT; break; } } return ret; } void handleEscapeSequence(shellmatta_instance_t *inst, uint8_t *data) { switch (inst->escapeCounter) { case 1u: inst->escapeChars[inst->escapeCounter - 1] = *data; inst->escapeCounter ++; break; case 2u: inst->escapeChars[inst->escapeCounter - 1] = *data; if(SHELLMATTA_OK == processArrowKeys(inst)) { inst->escapeCounter = 0u; } else if ( (0x4fu == inst->escapeChars[0]) && (0x46u == inst->escapeChars[1])) { forwardCursor(inst, inst->inputCount - inst->cursor); inst->escapeCounter = 0u; } else if(0u == (0x40u & *data)) { inst->escapeCounter ++; } else { inst->escapeCounter = 0u; } break; case 3u: inst->escapeChars[inst->escapeCounter - 1] = *data; if( (0x5bu == inst->escapeChars[0]) && (0x33u == inst->escapeChars[1]) && (0x7eu == inst->escapeChars[2])) { removeChars(inst, 1u, false); inst->escapeCounter = 0u; } /* pos1 */ else if ( (0x5bu == inst->escapeChars[0]) && (0x31u == inst->escapeChars[1]) && (0x7eu == inst->escapeChars[2])) { rewindCursor(inst, inst->cursor); inst->escapeCounter = 0u; } else if(0u == (0x40u & *data)) { inst->escapeCounter ++; } else { inst->escapeCounter = 0u; } break; default: inst->escapeCounter = 0u; break; } } static void terminateInput(shellmatta_instance_t *inst) { inst->inputCount = 0u; inst->cursor = 0u; shellmatta_printf(inst, "\n\r%s", inst->prompt); } static void doAutocomplete(shellmatta_instance_t *inst) { shellmatta_cmd_t *cmd = inst->cmdList; uint8_t *tempCmd = NULL; uint32_t minLen = 0u; uint32_t sizeDiff = 0u; bool exactMatch = true; uint32_t printedLen = 0u; inst->tabCounter++; /* on douple tab show all matching commands */ if (1u < inst->tabCounter) { inst->tabCounter = 0u; while (NULL != cmd) { /* check if command matches the input */ if( (strlen(cmd->cmd) > inst->cursor) && (0u == memcmp(cmd->cmd, inst->buffer, inst->cursor))) { /* add newline on first find */ if(0u == printedLen) { saveCursorPos(inst); inst->write("\n\r", 2u); } inst->write(cmd->cmd, strlen(cmd->cmd)); printedLen += strlen(cmd->cmd); inst->write(" ", 4u); printedLen += 4u; } /* check if command matches the input */ if( (strlen(cmd->cmdAlias) > inst->cursor) && (0u == memcmp(cmd->cmdAlias, inst->buffer, inst->cursor))) { /* add newline on first find */ if(0u == printedLen) { saveCursorPos(inst); inst->write("\n\r", 2u); } inst->write(cmd->cmdAlias, strlen(cmd->cmdAlias)); printedLen += strlen(cmd->cmdAlias); inst->write(" ", 4u); printedLen += 4u; } cmd = cmd->next; } /* print input again if a commands was found */ if(printedLen > 0u) { writeEcho(inst, "\n\r", 2u); writeEcho(inst, inst->prompt, strlen(inst->prompt)); writeEcho(inst, inst->buffer, inst->inputCount); restoreCursorPos(inst); } } /* on single tab autocomplete as far as possible */ else if(0u != inst->cursor) { /* loop through all registered commands */ while (NULL != cmd) { /* check if command matches the input */ if( (strlen(cmd->cmd) > inst->cursor) && (0u == memcmp(cmd->cmd, inst->buffer, inst->cursor))) { /* store first match */ if(NULL == tempCmd) { tempCmd = cmd->cmd; minLen = strlen(cmd->cmd); } /* find common part of the matches */ else { exactMatch = false; minLen = SHELLMATTA_MIN(strlen(cmd->cmd), minLen); for(uint32_t i = 0u; i < minLen; i++) { if(cmd->cmd[i] != tempCmd[i]) { minLen = i; } } } } /* check if command Alias matches the input */ if( (strlen(cmd->cmdAlias) > inst->cursor) && (0u == memcmp(cmd->cmdAlias, inst->buffer, inst->cursor))) { /* store first match */ if(NULL == tempCmd) { tempCmd = cmd->cmdAlias; minLen = strlen(cmd->cmdAlias); } /* find common part of the matches */ else { exactMatch = false; minLen = SHELLMATTA_MIN(strlen(cmd->cmdAlias), minLen); for(uint32_t i = 0u; i < minLen; i++) { if(cmd->cmdAlias[i] != tempCmd[i]) { minLen = i; } } } } cmd = cmd->next; } /* autocomplete found command */ if(NULL != tempCmd) { /* calculate the size of the command to be inserted */ sizeDiff = minLen - inst->cursor; /* copy the found part into the buffer and display it */ insertChars(inst, &(tempCmd[inst->cursor]), sizeDiff); /* on exact match there is no need to double Tab to display all */ if(true == exactMatch) { inst->tabCounter = 0u; } } } else { /* nothing to do here */ } } /** * @brief initialize the shellmatta terminal and provide all callbacks * @param[in,out] inst pointer to a shellmatta instance * @param[in] buffer pointer to the input buffer to use * @param[in] bufferSize size of the provided input buffer * @param[in] historyBuffer pointer to the history buffer to use * NULL in case of no history buffer * @param[in] historyBufferSize size of the history buffer * 0 in case of no history buffer * @param[in] prompt pointer to prompt string - is printed * after each command * @param[in] writeFct function pointer to output function */ shellmatta_retCode_t shellmatta_doInit( shellmatta_instance_t *inst, uint8_t *buffer, uint32_t bufferSize, uint8_t *historyBuffer, uint32_t historyBufferSize, const char *prompt, shellmatta_write_t writeFct) { /** - copy all provided buffers into the shellmatta instance */ inst->buffer = buffer; inst->bufferSize = bufferSize; inst->inputCount = 0u; inst->cursor = 0u; inst->cmdList = NULL; inst->historyBuffer = historyBuffer; inst->historyBufferSize = historyBufferSize; inst->historyStart = 0u; inst->historyEnd = 0u; inst->historyRead = 0u; inst->historyReadUp = true; inst->write = writeFct; inst->prompt = prompt; inst->echoEnabled = true; inst->dirty = false; inst->tabCounter = 0u; inst->escapeCounter = 0u; inst->mode = SHELLMATTA_MODE_INSERT; terminateInput(inst); return SHELLMATTA_OK; } /** * @brief adds a command to the command list alphabetically ordered * @param[in] inst pointer to a shellmatta instance * @param[in] cmd pointer to the command to add type #shellmatta_cmd_t */ shellmatta_retCode_t shellmatta_addCmd(shellmatta_instance_t *inst, shellmatta_cmd_t *cmd) { shellmatta_cmd_t *tempCmd = inst->cmdList; shellmatta_cmd_t **prevCmd = &inst->cmdList; bool cmdPlaced = false; shellmatta_retCode_t ret = SHELLMATTA_OK; int cmdDiff = 0; int aliasDiff = 0; /* register first command as list entry */ if (NULL == tempCmd) { inst->cmdList = cmd; cmd->next = NULL; } /* append the new command sorted */ else { while ((false == cmdPlaced) && (SHELLMATTA_OK == ret)) { cmdDiff = strcmp(tempCmd->cmd, cmd->cmd); aliasDiff = strcmp(tempCmd->cmdAlias, cmd->cmdAlias); /* check for a duplicate command */ if((0u == cmdDiff) || (0u == aliasDiff)) { ret = SHELLMATTA_DUPLICATE; } else if(0 < cmdDiff) { cmd->next = tempCmd; *prevCmd = cmd; cmdPlaced = true; } else if(NULL == tempCmd->next) { tempCmd->next = cmd; cmd->next = NULL; cmdPlaced = true; } else { /* nothing to do */ } prevCmd = &tempCmd; tempCmd = tempCmd->next; } } return ret; } void shellmatta_doTask(shellmatta_instance_t *inst, uint32_t time) { } /** * @brief processes the passed amount of data * @param[in] inst pointer to a shellmatta instance * @param[in] data pointer to input data to process * @param[in] size length of input data to process */ void shellmatta_processData(shellmatta_instance_t *inst, char *data, uint32_t size) { /* process byte wise */ for (uint32_t i = 0u; i < size; i++) { /* handle escape sequences */ if(inst->escapeCounter != 0u) { handleEscapeSequence(inst, data); } /* handle return as start of processing the command */ else if ('\r' == *data) { shellmatta_cmd_t *cmd = inst->cmdList; uint8_t cmdExecuted = 0u; inst->buffer[inst->inputCount] = 0u; /* store the current command and reset the history buffer */ inst->dirty = true; storeHistoryCmd(inst); resetHistoryBuffer(inst); while (NULL != cmd) { if ( (0 == strcmp(inst->buffer, cmd->cmd)) || (0 == strcmp(inst->buffer, cmd->cmdAlias))) { inst->write("\n\r", 2u); cmdExecuted = 1u; char *blubb[10]; blubb[0] = inst->buffer; cmd->cmdFct(1, blubb); cmd = NULL; } else { cmd = cmd->next; } } if ((cmdExecuted == 0u) && (inst->inputCount > 0)) { inst->buffer[inst->inputCount] = '\0'; shellmatta_printf( inst, "\n\rCommand: %s not found", inst->buffer); } terminateInput(inst); } /* tabulator key - auto complete */ else if('\t' == *data) { inst->dirty = true; doAutocomplete(inst); } /* cancel - terminate current input and print prompt again */ else if(3 == *data) { inst->dirty = false; resetHistoryBuffer(inst); terminateInput(inst); } /* backspace */ else if('\b' == *data) { inst->dirty = true; removeChars(inst, 1u, true); } /* check for delete key */ else if(0x7eu == *data) { inst->dirty = true; removeChars(inst, 1u, false); } /* start of escape sequence */ else if('\e' == *data) { inst->escapeCounter = 1u; } else { inst->dirty = true; insertChars(inst, data, 1); } /* reset tab counter on not a tab */ if ('\t' != *data) { inst->tabCounter = 0u; } data ++; } } void shellmatta_printf(shellmatta_instance_t *inst, const char *fmt, ...) { char outputBuffer[1024u]; va_list arg; va_start(arg, fmt); int length = vsnprintf(outputBuffer, 1024, fmt, arg); va_end(arg); inst->write(outputBuffer, length); } void shellmatta_getArg(uint32_t cnt, uint8_t *arg) { } /** @} */