OVMS3/OVMS.V3/components/microrl/microrl.c

795 lines
20 KiB
C

/*
Author: Samoylov Eugene aka Helius (ghelius@gmail.com)
BUGS and TODO:
-- add echo_off feature
-- rewrite history for use more than 256 byte buffer
*/
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include "microrl.h"
#ifdef _USE_LIBC_STDIO
#include <stdio.h>
#endif
//#define DBG(...) fprintf(stderr, "\033[33m");fprintf(stderr,__VA_ARGS__);fprintf(stderr,"\033[0m");
char * prompt_default = _PROMPT_DEFAULT;
#ifdef _USE_HISTORY
#ifdef _HISTORY_DEBUG
//*****************************************************************************
// print buffer content on screen
static void print_hist (ring_history_t * pThis)
{
printf ("\n");
for (int i = 0; i < _RING_HISTORY_LEN; i++) {
if (i == pThis->begin)
printf ("b");
else
printf (" ");
}
printf ("\n");
for (int i = 0; i < _RING_HISTORY_LEN; i++) {
if (isalpha(pThis->ring_buf[i]))
printf ("%c", pThis->ring_buf[i]);
else
printf ("%d", pThis->ring_buf[i]);
}
printf ("\n");
for (int i = 0; i < _RING_HISTORY_LEN; i++) {
if (i == pThis->end)
printf ("e");
else
printf (" ");
}
printf ("\n");
}
#endif
//*****************************************************************************
// remove older message from ring buffer
static void hist_erase_older (ring_history_t * pThis)
{
int new_pos = pThis->begin + pThis->ring_buf [pThis->begin] + 1;
if (new_pos >= _RING_HISTORY_LEN)
new_pos = new_pos - _RING_HISTORY_LEN;
pThis->begin = new_pos;
}
//*****************************************************************************
// check space for new line, remove older while not space
static int hist_is_space_for_new (ring_history_t * pThis, int len)
{
if (pThis->ring_buf [pThis->begin] == 0)
return true;
if (pThis->end >= pThis->begin) {
if (_RING_HISTORY_LEN - pThis->end + pThis->begin - 1 > len)
return true;
} else {
if (pThis->begin - pThis->end - 1> len)
return true;
}
return false;
}
//*****************************************************************************
// put line to ring buffer
static void hist_save_line (ring_history_t * pThis, char * line, int len)
{
if (len > _RING_HISTORY_LEN - 2)
return;
while (!hist_is_space_for_new (pThis, len)) {
hist_erase_older (pThis);
}
// if it's first line
if (pThis->ring_buf [pThis->begin] == 0)
pThis->ring_buf [pThis->begin] = len;
// store line
if (len < _RING_HISTORY_LEN-pThis->end-1)
memcpy (pThis->ring_buf + pThis->end + 1, line, len);
else {
int part_len = _RING_HISTORY_LEN-pThis->end-1;
memcpy (pThis->ring_buf + pThis->end + 1, line, part_len);
memcpy (pThis->ring_buf, line + part_len, len - part_len);
}
pThis->ring_buf [pThis->end] = len;
pThis->end = pThis->end + len + 1;
if (pThis->end >= _RING_HISTORY_LEN)
pThis->end -= _RING_HISTORY_LEN;
pThis->ring_buf [pThis->end] = 0;
pThis->cur = 0;
#ifdef _HISTORY_DEBUG
print_hist (pThis);
#endif
}
//*****************************************************************************
// copy saved line to 'line' and return size of line
static int hist_restore_line (ring_history_t * pThis, char * line, int dir)
{
int cnt = 0;
// count history record
int header = pThis->begin;
while (pThis->ring_buf [header] != 0) {
header += pThis->ring_buf [header] + 1;
if (header >= _RING_HISTORY_LEN)
header -= _RING_HISTORY_LEN;
cnt++;
}
if (dir == _HIST_UP) {
if (cnt >= pThis->cur) {
int header = pThis->begin;
int j = 0;
// found record for 'pThis->cur' index
while ((pThis->ring_buf [header] != 0) && (cnt - j -1 != pThis->cur)) {
header += pThis->ring_buf [header] + 1;
if (header >= _RING_HISTORY_LEN)
header -= _RING_HISTORY_LEN;
j++;
}
if (pThis->ring_buf[header]) {
pThis->cur++;
// obtain saved line
if (pThis->ring_buf [header] + header < _RING_HISTORY_LEN) {
memset (line, 0, _COMMAND_LINE_LEN);
memcpy (line, pThis->ring_buf + header + 1, pThis->ring_buf[header]);
} else {
int part0 = _RING_HISTORY_LEN - header - 1;
memset (line, 0, _COMMAND_LINE_LEN);
memcpy (line, pThis->ring_buf + header + 1, part0);
memcpy (line + part0, pThis->ring_buf, pThis->ring_buf[header] - part0);
}
return pThis->ring_buf[header];
}
}
} else {
if (pThis->cur > 0) {
pThis->cur--;
int header = pThis->begin;
int j = 0;
while ((pThis->ring_buf [header] != 0) && (cnt - j != pThis->cur)) {
header += pThis->ring_buf [header] + 1;
if (header >= _RING_HISTORY_LEN)
header -= _RING_HISTORY_LEN;
j++;
}
if (pThis->ring_buf [header] + header < _RING_HISTORY_LEN) {
memcpy (line, pThis->ring_buf + header + 1, pThis->ring_buf[header]);
} else {
int part0 = _RING_HISTORY_LEN - header - 1;
memcpy (line, pThis->ring_buf + header + 1, part0);
memcpy (line + part0, pThis->ring_buf, pThis->ring_buf[header] - part0);
}
return pThis->ring_buf[header];
} else {
/* empty line */
return 0;
}
}
return -1;
}
#endif
#ifdef _USE_QUOTING
//*****************************************************************************
// restore end quote marks in cmdline
static void restore (microrl_t * pThis)
{
int iq;
for (iq = 0; iq < _QUOTED_TOKEN_NMB; ++iq) {
if (pThis->quotes[iq].end == 0)
break;
*pThis->quotes[iq].end = *pThis->quotes[iq].begin;
pThis->quotes[iq].begin = 0;
pThis->quotes[iq].end = 0;
}
}
#endif
//*****************************************************************************
// split cmdline to tkn array and return nmb of token
static int split (microrl_t * pThis, int limit, char const ** tkn_arr)
{
int i = 0;
int ind = 0;
#ifdef _USE_QUOTING
int iq = 0;
char quote = 0;
for (iq = 0; iq < _QUOTED_TOKEN_NMB; ++iq) {
pThis->quotes[iq].begin = 0;
pThis->quotes[iq].end = 0;
}
iq = 0;
#endif
while (1) {
// go to the first NOT whitespace (not zerro for us)
while ((pThis->cmdline [ind] == '\0') && (ind < limit)) {
ind++;
}
if (!(ind < limit)) return i;
#ifdef _USE_QUOTING
if (pThis->cmdline [ind] == '\'' || pThis->cmdline [ind] == '"') {
if (iq >= _QUOTED_TOKEN_NMB) {
restore (pThis);
return -1;
}
quote = pThis->cmdline [ind];
pThis->quotes[iq].begin = pThis->cmdline + ind;
ind++;
}
#endif
tkn_arr[i++] = pThis->cmdline + ind;
if (i >= _COMMAND_TOKEN_NMB) {
#ifdef _USE_QUOTING
restore (pThis);
#endif
return -1;
}
// go to the first whitespace (zerro for us)
while (ind < limit) {
if (pThis->cmdline [ind] == '\0') {
#ifdef _USE_QUOTING
if (!quote)
#endif
break;
#ifdef _USE_QUOTING
pThis->cmdline [ind] = ' ';
} else if (pThis->cmdline [ind] == quote) {
if (pThis->cmdline [ind + 1] != '\0') {
restore (pThis);
return -1;
}
quote = 0;
pThis->quotes[iq++].end = pThis->cmdline + ind;
pThis->cmdline [ind++] = '\0';
break;
#endif
}
ind++;
}
if (!(ind < limit)) {
#ifdef _USE_QUOTING
if (quote) {
restore (pThis);
return -1;
}
#endif
return i;
}
}
return i;
}
//*****************************************************************************
inline static void print_prompt (microrl_t * pThis)
{
pThis->print (pThis, pThis->prompt_str);
}
//*****************************************************************************
inline static void terminal_backspace (microrl_t * pThis)
{
pThis->print (pThis, "\033[D \033[D");
}
//*****************************************************************************
inline static void terminal_newline (microrl_t * pThis)
{
pThis->print (pThis, "\r\n");
}
//*****************************************************************************
// set cursor at current position + offset (positive or negative).
// the provided buffer must be at least 7 bytes long.
static char* generate_move_cursor (char* str, int offset)
{
char c = 'C';
if (offset > 999)
offset = 999;
if (offset < -999)
offset = -999;
if (offset < 0) {
offset = -offset;
c = 'D';
} else if (offset == 0) {
*str = '\0';
return str;
}
#ifdef _USE_LIBC_STDIO
str += sprintf (str, "\033[%d%c", offset, c);
#else
*str++ = '\033';
*str++ = '[';
char tmp_str [4] = {0,};
int i = 0, j;
while (offset > 0) {
tmp_str[i++] = (offset % 10) + '0';
offset /=10;
}
for (j = 0; j < i; ++j)
*str++ = tmp_str [i-j-1];
*str++ = c;
*str = '\0';
#endif
return str;
}
//*****************************************************************************
// set cursor at current position + offset (positive or negative)
static void terminal_move_cursor (microrl_t * pThis, int offset)
{
char str[16] = {0,};
if (offset != 0) {
generate_move_cursor (str, offset);
pThis->print (pThis, str);
}
}
//*****************************************************************************
// print cmdline to screen, replace '\0' to whitespace
static void terminal_print_line (microrl_t * pThis, int pos, int reset)
{
char str[_PRINT_BUFFER_LEN];
char *j = str;
if (reset) {
#ifdef _USE_CARRIAGE_RETURN
*j++ = '\r';
j = generate_move_cursor(j, _PROMPT_LEN + pos);
#else
j = generate_move_cursor(j, -(_COMMAND_LINE_LEN + _PROMPT_LEN + 2));
j = generate_move_cursor(j, _PROMPT_LEN + pos);
#endif
}
for (int i = pos; i < pThis->cmdlen; i++) {
*j++ = (pThis->cmdline [i] == '\0') ? ' ' : pThis->cmdline [i];
if (j-str == sizeof(str)-1) {
*j = '\0';
pThis->print (pThis, str);
j = str;
}
}
if (j - str + 3+6+1 > _PRINT_BUFFER_LEN) {
*j = '\0';
pThis->print (pThis, str);
j = str;
}
*j++ = '\033'; // delete all past end of text
*j++ = '[';
*j++ = 'K';
generate_move_cursor (j, pThis->cursor - pThis->cmdlen);
pThis->print (pThis, str);
}
//*****************************************************************************
void microrl_init (microrl_t * pThis, void (*print) (microrl_t*, const char *), void (*error_print) (microrl_t*, const char *))
{
memset(pThis, 0, sizeof(microrl_t));
pThis->prompt_str = prompt_default;
pThis->print = print;
pThis->error_print = error_print;
#ifdef _ENABLE_INIT_PROMPT
print_prompt (pThis);
#endif
}
//*****************************************************************************
void microrl_set_complete_callback (microrl_t * pThis, char ** (*get_completion)(microrl_t*, int, const char* const*))
{
pThis->get_completion = get_completion;
}
//*****************************************************************************
void microrl_set_execute_callback (microrl_t * pThis, int (*execute)(microrl_t*, int, const char* const*))
{
pThis->execute = execute;
}
#ifdef _USE_CTLR_C
//*****************************************************************************
void microrl_set_sigint_callback (microrl_t * pThis, void (*sigintf)(microrl_t*))
{
pThis->sigint = sigintf;
}
#endif
#ifdef _USE_HISTORY
static void hist_search (microrl_t * pThis, int dir)
{
int len = hist_restore_line (&pThis->ring_hist, pThis->cmdline, dir);
if (len >= 0) {
pThis->cmdline[len] = '\0';
pThis->cursor = pThis->cmdlen = len;
terminal_print_line (pThis, 0, 1);
}
}
#endif
#ifdef _USE_ESC_SEQ
//*****************************************************************************
// handling escape sequences
static int escape_process (microrl_t * pThis, char ch)
{
if (ch == '[') {
pThis->escape_seq = _ESC_BRACKET;
return 0;
} else if (pThis->escape_seq == _ESC_BRACKET) {
if (ch == 'A') {
#ifdef _USE_HISTORY
hist_search (pThis, _HIST_UP);
#endif
return 1;
} else if (ch == 'B') {
#ifdef _USE_HISTORY
hist_search (pThis, _HIST_DOWN);
#endif
return 1;
} else if (ch == 'C') {
if (pThis->cursor < pThis->cmdlen) {
terminal_move_cursor (pThis, 1);
pThis->cursor++;
}
return 1;
} else if (ch == 'D') {
if (pThis->cursor > 0) {
terminal_move_cursor (pThis, -1);
pThis->cursor--;
}
return 1;
} else if (ch == '7') {
pThis->escape_seq = _ESC_HOME;
return 0;
} else if (ch == '8') {
pThis->escape_seq = _ESC_END;
return 0;
}
} else if (ch == '~') {
if (pThis->escape_seq == _ESC_HOME) {
terminal_move_cursor (pThis, -pThis->cursor);
pThis->cursor = 0;
return 1;
} else if (pThis->escape_seq == _ESC_END) {
terminal_move_cursor (pThis, pThis->cmdlen-pThis->cursor);
pThis->cursor = pThis->cmdlen;
return 1;
}
}
/* unknown escape sequence, stop */
return 1;
}
#endif
//*****************************************************************************
// insert len char of text at cursor position
static int microrl_insert_text (microrl_t * pThis, char * text, int len)
{
int i;
if (pThis->cmdlen + len < _COMMAND_LINE_LEN) {
memmove (pThis->cmdline + pThis->cursor + len,
pThis->cmdline + pThis->cursor,
pThis->cmdlen - pThis->cursor);
for (i = 0; i < len; i++) {
pThis->cmdline [pThis->cursor + i] = text [i];
if (pThis->cmdline [pThis->cursor + i] == ' ') {
pThis->cmdline [pThis->cursor + i] = 0;
}
}
pThis->cursor += len;
pThis->cmdlen += len;
pThis->cmdline [pThis->cmdlen] = '\0';
return true;
}
return false;
}
//*****************************************************************************
// remove len chars backwards at cursor
static void microrl_backspace (microrl_t * pThis, int len)
{
if (pThis->cursor >= len) {
memmove (pThis->cmdline + pThis->cursor-len,
pThis->cmdline + pThis->cursor,
pThis->cmdlen-pThis->cursor+len);
pThis->cursor -= len;
pThis->cmdline [pThis->cmdlen] = '\0';
pThis->cmdlen -= len;
}
}
//*****************************************************************************
// remove one char forward at cursor
static void microrl_delete (microrl_t * pThis)
{
if (pThis->cmdlen > 0) {
memmove (pThis->cmdline + pThis->cursor,
pThis->cmdline + pThis->cursor+1,
pThis->cmdlen-pThis->cursor+1);
pThis->cmdline [pThis->cmdlen] = '\0';
pThis->cmdlen--;
}
}
#ifdef _USE_COMPLETE
//*****************************************************************************
static int common_len (char ** arr)
{
int i;
int j;
char *shortest = arr[0];
int shortlen = strlen(shortest);
for (i = 0; arr[i] != NULL; ++i)
if (strlen(arr[i]) < shortlen) {
shortest = arr[i];
shortlen = strlen(shortest);
}
for (i = 0; i < shortlen; ++i)
for (j = 0; arr[j] != 0; ++j)
if (shortest[i] != arr[j][i])
return i;
return i;
}
//*****************************************************************************
static void microrl_get_complite (microrl_t * pThis)
{
char const * tkn_arr[_COMMAND_TOKEN_NMB];
char ** compl_token;
if (pThis->get_completion == NULL) // callback was not set
return;
int status = split (pThis, pThis->cursor, tkn_arr);
if (status < 0)
return;
if (pThis->cmdline[pThis->cursor-1] == '\0')
tkn_arr[status++] = "";
compl_token = pThis->get_completion (pThis, status, tkn_arr);
#ifdef _USE_QUOTING
restore (pThis);
#endif
if (compl_token[0] != NULL) {
int i = 0;
int len;
int pos = pThis->cursor;
if (compl_token[1] == NULL) {
len = strlen (compl_token[0]);
} else {
char str[_PRINT_BUFFER_LEN];
char *j = str;
*j++ = '\r';
*j++ = '\n';
while (compl_token [i] != NULL) {
int l = strlen(compl_token[i]);
// 4 is for ' ' plus ending \r\n plus \0
if ((j > str) && (j - str + l + 4 > sizeof(str))) {
*j = '\0';
pThis->print (pThis, str);
j = str;
}
if (l + 4 > sizeof(str)) {
pThis->print (pThis, compl_token[i]);
pThis->print (pThis, " ");
} else {
j = stpcpy(j, compl_token[i]);
*j++ = ' ';
}
i++;
}
*j++ = '\r';
*j++ = '\n';
*j = '\0';
pThis->print (pThis, str);
print_prompt (pThis);
len = common_len (compl_token);
pos = 0;
}
if (len) {
microrl_insert_text (pThis, compl_token[0] + strlen(tkn_arr[status-1]),
len - strlen(tkn_arr[status-1]));
if (compl_token[1] == NULL)
microrl_insert_text (pThis, " ", 1);
}
terminal_print_line (pThis, pos, 0);
}
}
#endif
//*****************************************************************************
void new_line_handler(microrl_t * pThis){
char const * tkn_arr [_COMMAND_TOKEN_NMB];
int status;
terminal_newline (pThis);
#ifdef _USE_HISTORY
if (pThis->cmdlen > 0)
hist_save_line (&pThis->ring_hist, pThis->cmdline, pThis->cmdlen);
#endif
status = split (pThis, pThis->cmdlen, tkn_arr);
if (status == -1){
// pThis->print ("ERROR: Max token amount exseed\n");
#ifdef _USE_QUOTING
pThis->error_print (pThis, "ERROR:too many tokens or invalid quoting" ENDL);
#else
pThis->error_print (pThis, "ERROR:too many tokens" ENDL);
#endif
}
if ((status > 0) && (pThis->execute != NULL))
pThis->execute (pThis, status, tkn_arr);
print_prompt (pThis);
pThis->cmdlen = 0;
pThis->cursor = 0;
memset(pThis->cmdline, 0, _COMMAND_LINE_LEN);
#ifdef _USE_HISTORY
pThis->ring_hist.cur = 0;
#endif
}
//*****************************************************************************
void microrl_insert_char (microrl_t * pThis, int ch)
{
#ifdef _USE_ESC_SEQ
if (pThis->escape) {
if (escape_process(pThis, ch))
pThis->escape = 0;
} else {
#endif
switch (ch) {
//-----------------------------------------------------
#ifdef _ENDL_CR
case KEY_CR:
new_line_handler(pThis);
break;
case KEY_LF:
break;
#elif defined(_ENDL_CRLF)
case KEY_CR:
pThis->tmpch = KEY_CR;
break;
case KEY_LF:
if (pThis->tmpch == KEY_CR)
new_line_handler(pThis);
break;
#elif defined(_ENDL_LFCR)
case KEY_LF:
pThis->tmpch = KEY_LF;
break;
case KEY_CR:
if (pThis->tmpch == KEY_LF)
new_line_handler(pThis);
break;
#else
case KEY_CR:
break;
case KEY_LF:
new_line_handler(pThis);
break;
#endif
//-----------------------------------------------------
#ifdef _USE_COMPLETE
case KEY_HT:
microrl_get_complite (pThis);
break;
#endif
//-----------------------------------------------------
case KEY_ESC:
#ifdef _USE_ESC_SEQ
pThis->escape = 1;
#endif
break;
//-----------------------------------------------------
case KEY_NAK: // ^U
if (pThis->cursor > 0)
microrl_backspace (pThis, pThis->cursor);
terminal_print_line (pThis, 0, 1);
break;
//-----------------------------------------------------
case KEY_VT: // ^K
pThis->print (pThis,"\033[K");
pThis->cmdlen = pThis->cursor;
break;
//-----------------------------------------------------
case KEY_ENQ: // ^E
terminal_move_cursor (pThis, pThis->cmdlen-pThis->cursor);
pThis->cursor = pThis->cmdlen;
break;
//-----------------------------------------------------
case KEY_SOH: // ^A
terminal_move_cursor (pThis, -pThis->cursor);
pThis->cursor = 0;
break;
//-----------------------------------------------------
case KEY_ACK: // ^F
if (pThis->cursor < pThis->cmdlen) {
terminal_move_cursor (pThis, 1);
pThis->cursor++;
}
break;
//-----------------------------------------------------
case KEY_STX: // ^B
if (pThis->cursor) {
terminal_move_cursor (pThis, -1);
pThis->cursor--;
}
break;
//-----------------------------------------------------
case KEY_DLE: //^P
#ifdef _USE_HISTORY
hist_search (pThis, _HIST_UP);
#endif
break;
//-----------------------------------------------------
case KEY_SO: //^N
#ifdef _USE_HISTORY
hist_search (pThis, _HIST_DOWN);
#endif
break;
//-----------------------------------------------------
case KEY_DEL: // Backspace
case KEY_BS: // ^H
if (pThis->cursor > 0) {
microrl_backspace (pThis, 1);
if (pThis->cursor == pThis->cmdlen)
terminal_backspace (pThis);
else
terminal_print_line (pThis, pThis->cursor, 1);
}
break;
//-----------------------------------------------------
case KEY_EOT: // ^D
microrl_delete (pThis);
terminal_print_line (pThis, pThis->cursor, 0);
break;
//-----------------------------------------------------
case KEY_DC2: // ^R
terminal_newline (pThis);
print_prompt (pThis);
terminal_print_line (pThis, 0, 0);
break;
//-----------------------------------------------------
#ifdef _USE_CTLR_C
case KEY_ETX:
if (pThis->sigint != NULL)
pThis->sigint(pThis);
break;
#endif
//-----------------------------------------------------
default:
if (((ch == ' ') && (pThis->cmdlen == 0)) || IS_CONTROL_CHAR(ch))
break;
if (microrl_insert_text (pThis, (char*)&ch, 1)) {
if (pThis->cursor == pThis->cmdlen) {
char nch [] = {0,0};
nch[0] = ch;
pThis->print (pThis, nch);
} else {
terminal_print_line (pThis, pThis->cursor-1, 0);
}
}
break;
}
#ifdef _USE_ESC_SEQ
}
#endif
}