/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
/** terminal prompt interface (code)
* @note allows line editing and supports some ANSI escape codes
* @file terminal.c
* @author King Kévin
* @date 2018
*/
/* standard libraries */
#include // standard integer types
#include // boolean type
#include // standard utilities
#include // string utilities
/* own libraries */
#include "global.h" // global definitions
#include "terminal.h" // own definitions
#include "print.h" // printing utilities
char* terminal_prefix = NULL;
void (*terminal_process)(char* line) = NULL;
/** buffer to store user input and keep history */
static char terminal_buffer[1024] = {0};
/** how much of the buffer is user */
static uint16_t terminal_end = 0;
/** current position in the buffer */
static uint16_t terminal_pos = 0;
/** start position or current line in the buffer */
static uint16_t terminal_line = 0;
/** is the current line the last one */
static bool terminal_last = true;
/** currently inserting or replacing characters */
static bool terminal_insert = true;
/** current escape code */
static char escape_code[8] = {0};
/** current position in the escape code */
static uint8_t escape_pos = 0;
/** remove one line from buffer start and shift rest to start
* @return if one line has been removed
*/
static bool terminal_remove_line(void)
{
if (0==terminal_line) { // be sure we are currently not on the first line
return false;
}
uint16_t line_end = strlen(&terminal_buffer[0]); // get end of line
if (terminal_end<=line_end) { // be sure there is a line to delete
return false;
}
for (uint16_t i=line_end+1; i<=terminal_end && iline_end+1) { // update buffer end
terminal_end -= line_end+1;
} else {
terminal_end = 0;
}
if (terminal_pos>line_end+1) { // update buffer position
terminal_pos -= line_end+1;
} else {
terminal_pos = 0;
}
if (terminal_line>line_end+1) { // update line position
terminal_line -= line_end+1;
} else {
terminal_line = 0;
}
if (0==terminal_line) { // update if we are on the last line
terminal_last = true;
}
return true;
}
/** shift with rotate current characters to other position
* @note this uses a recursive function
* @param[in] to_copy position of character(s) to shift
* @param[in] nb_copy number of characters to shift
* @param[in] to_shift where to shift the characters to
*/
static void terminal_shift_line(uint16_t to_copy, uint16_t nb_copy, uint16_t to_shift)
{
char c = terminal_buffer[to_copy];
terminal_buffer[to_copy] = terminal_buffer[to_shift];
if (to_shift0 ? nb_copy-1 : 0, to_shift+1); // shift next character
}
if (nb_copy>0) {
terminal_buffer[terminal_end-nb_copy] = c; // place back
}
}
/** copy current line to last line */
static void terminal_copy_line(void)
{
if (terminal_last) { // current line is already last line
return; // do nothing
}
uint16_t line_len = strlen(&terminal_buffer[terminal_line]);
while (terminal_end>=LENGTH(terminal_buffer)-line_len-1 && terminal_remove_line()); // delete line if not enough space
if (terminal_end2) { // number of cells provided
escape_code[escape_pos-1] = '\0'; // terminate string
n = atoi(&escape_code[1]); // get number of cells
}
while (n--) { // go up number of line
if (0==terminal_line) { // stop if we are already at the top line
break;
}
uint16_t terminal_line_new=0; // new line start
for (uint16_t pos=0; posterminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
}
}
terminal_line = terminal_line_new; // set new line start
terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
}
}
break;
case 'B': // CUD - cursor down
{
uint16_t n = 1; // number of cells to move
if (escape_pos>2) { // number of cells provided
escape_code[escape_pos-1] = '\0'; // terminate string
n = atoi(&escape_code[1]); // get number of cells
}
while (n--) { // go down number of line
if (terminal_last) { // stop if we are already at the last line
break;
}
uint16_t terminal_line_new = terminal_line+strlen(&terminal_buffer[terminal_line])+1; // line start for the next line
if (terminal_pos==terminal_line+strlen(&terminal_buffer[terminal_line])) { // if the position is the end of the current line
terminal_pos=terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set position to end of new line
} else {
terminal_pos += (terminal_line_new-terminal_line); // move position to new line
if (terminal_pos>terminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
}
}
terminal_line = terminal_line_new; // set new line start
terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
}
}
break;
case 'C': // CUF - cursor forward
{
uint16_t n = 1; // number of cells to move
if (escape_pos>2) { // number of cells provided
escape_code[escape_pos-1] = '\0'; // terminate string
n = atoi(&escape_code[1]); // get number of cells
}
while (n--) { // go right number of moves
if (terminal_pos>=terminal_line+strlen(&terminal_buffer[terminal_line])) { // stop if we are already at the end
break;
}
terminal_pos++;
}
}
break;
case 'D': // CUB - cursor back
{
uint16_t n = 1; // number of cells to move
if (escape_pos>2) { // number of cells provided
escape_code[escape_pos-1] = '\0'; // terminate string
n = atoi(&escape_code[1]); // get number of cells
}
while (n--) { // go left number of moves
if (terminal_pos<=terminal_line) { // stop if we are already at the beginning
break;
}
terminal_pos--;
}
}
break;
case '~': // special key
if (3==escape_pos) { // we only expect one parameter bytes
switch (escape_code[1]) {
case '2': // insert
terminal_insert = !terminal_insert; // toggle insert/replace mode
break;
case '3': // delete
if (!terminal_last) { // we are not editing the last line
terminal_copy_line(); // make current line the last line
}
if (terminal_pos0) {
uint16_t terminal_line_new = 0; // set to first line
if (terminal_pos==terminal_line+strlen(&terminal_buffer[terminal_line])) { // if the position is the end of the current line
terminal_pos=terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set position to end of new line
} else {
terminal_pos -= (terminal_line-terminal_line_new); // move position to new line
if (terminal_pos>terminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
}
}
terminal_line = terminal_line_new; // set new line start
terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
}
break;
case '6': // page down
if (!terminal_last) { // stop if we are already at the last line
uint16_t terminal_line_new = terminal_line; // start last line search
for (uint16_t i=terminal_line_new; iterminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
}
}
terminal_line = terminal_line_new; // set new line start
terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
}
break;
}
}
break;
default: // we don't handle other codes
break; // do nothing
}
break;
default: // we don't handle this code
break; // do nothing
}
}
/** print current line and set position */
static void terminal_print_line(void)
{
printf("\r\x1b[K%s%s%s", terminal_prefix ? terminal_prefix : "", ": ", &terminal_buffer[terminal_line]); // erase line and display last one
if (terminal_pos=0x40 && c<=0x5f) { // data is in the right range
escape_code[0] = c; // save C1
escape_pos++; // got to next position
} else { // this is not a C1 code
escape_pos = 0; // stop saving the escape code
terminal_send(c); // process data as non-escape code
}
} else { // after C1 we expect a parameters, intermediate, or final byte
if (c>=0x20 && c<=0x3f) { // received parameter (0x30-0x3f) or intermediate (0x20-0x2f) byte
if (escape_pos<=LENGTH(escape_code)) {
escape_code[escape_pos-1] = c; // save parameter byte
escape_pos++; // go to next position
}
} else if (c>=0x40 && c<=0x7f) { // received final byte
if (escape_pos<=LENGTH(escape_code)) {
escape_code[escape_pos-1] = c; // save final byte
}
terminal_process_escape(); // process escape code since we received the final byte
escape_pos = 0; // stop saving the escape code
terminal_print_line(); // print current line since if might have changed
} else { // this is not a expected byte
escape_pos = 0; // stop saving the escape code
terminal_send(c); // process data as non-escape code
}
}
} else {
if (0==c) { // string end received
terminal_print_line(); // only update line
} else if (0x1b==c) { // receiving new escape code (ESC)
escape_pos = 1; // start filling buffer
} else if (0x03==c) { // received CRTL+C
if (!terminal_last) { // we are not on the last line
uint16_t terminal_line_new = terminal_line; // start last line search
for (uint16_t i=terminal_line_new; i=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
if (terminal_end erase it
terminal_end = 0; // update end
}
terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
}
terminal_pos = terminal_end; // set position to new line
terminal_line = terminal_end; // update line position
printf("\n"); // go to new line
terminal_print_line(); // print current empty line
} else { // all other bytes do some line editing
if (!terminal_last) { // we are not editing the last line
terminal_copy_line(); // make current line the last line
}
if ('\r'== c || '\n'==c) { // line finished
if ('\r'==newline_last && '\n'==c) { // windows newline received
// this newline has already been handled before
} else {
printf("\n"); // print new line
if (terminal_process) {
(*terminal_process)(&terminal_buffer[terminal_line]); // process line
}
if (strlen(&terminal_buffer[terminal_line])>0) { // only store non-empty line
while (terminal_end>=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
if (terminal_end erase it
terminal_end = 0; // update end
}
terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
terminal_pos = terminal_end; // set position to new line
terminal_line = terminal_pos; // update line position
}
}
newline_last = c; // remember last character
} else if (0x7f==c) { // backspace
if (terminal_pos>terminal_line) { // we are not at the beginning of the line
for (uint16_t i=terminal_pos-1; i=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
if (terminal_end=terminal_pos; i--) { // shift buffer
terminal_buffer[i+1] = terminal_buffer[i];
}
terminal_buffer[terminal_pos++] = c; // insert new character
terminal_buffer[++terminal_end] = '\0'; // update end
}
} else { // replace current character
while (terminal_pos==terminal_end && terminal_end>=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
terminal_buffer[terminal_pos] = c; // replace current character
if (terminal_posterminal_end) { // update end if it moved
terminal_end = terminal_pos; // move end
terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
}
}
terminal_print_line(); // print current line
}
}
}