/** terminal prompt interface (code) * @note allows line editing and supports some ANSI escape codes * @file * @author King Kévin * @copyright SPDX-License-Identifier: GPL-3.0-or-later * @date 2018-2019 */ /* 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 && i < LENGTH(terminal_buffer); i++) { // line buffer after the line to start terminal_buffer[i - line_end - 1] = terminal_buffer[i]; } if (terminal_end > line_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_shift 0 ? 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_end < LENGTH(terminal_buffer) - line_len - 1) { // there is enough space to copy the line for (uint16_t i = 0; i < line_len; i++) { // copy line to end of buffer terminal_buffer[terminal_end + i + 1] = terminal_buffer[terminal_line + i]; // copy character } terminal_pos += (terminal_end + 1 - terminal_line); // update current position terminal_line = terminal_end + 1; // update line position terminal_end += line_len + 1; // update buffer end position terminal_buffer[terminal_end] = '\0'; // ensure end is terminated } else if (0 == terminal_line) { // shift (first) line to end of buffer terminal_shift_line(0, line_len, line_len + 1); // shift line terminal_line = terminal_end - line_len; // update line position terminal_pos += terminal_line; // update current position // terminal_end did not change } terminal_last = true; // now we are on the last line } /** process current escape code */ static void terminal_process_escape(void) { if (escape_pos < 2) { // the escape code must have at least 2 bytes (C1 and final) return; } switch (escape_code[0]) { // process escape code according to C1 case '[': // CSI - Control Sequence Introducer switch (escape_code[escape_pos - 1]) { // process CSI code case 'A': // CUU - cursor up { 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 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; pos + 1 < terminal_line && pos < LENGTH(terminal_buffer); pos++) { // find for the last line before the current if ('\0' == terminal_buffer[pos]) { // new line found terminal_line_new = pos + 1; // save new 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 '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_pos 0) { 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; i < terminal_end - 1; i++) { // search for last line if ('\0' == terminal_buffer[i]) { // end of line found terminal_line_new = i + 1; // set new line start } } 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; } } 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\x1b[1m%s:\x1b[0m %s", terminal_prefix ? terminal_prefix : "", &terminal_buffer[terminal_line]); // erase line and display last one if (terminal_pos < terminal_line + strlen(&terminal_buffer[terminal_line])) { // cursor is not at the end of the line printf("\x1b[%uD", terminal_line + strlen(&terminal_buffer[terminal_line]) - terminal_pos); // set position by going back } } void terminal_setup(void) { // reset all buffers terminal_buffer[0] = '\0'; terminal_end = 0; terminal_pos = 0; terminal_line = 0; terminal_last = true; escape_pos = 0; // start prompt terminal_send(0); } void terminal_send(volatile char c) { static char newline_last = 0; // last new linefeed or carrier return character received if (escape_pos) { // currently receiving an escape code if (1 == escape_pos) { // we received C0 and expected C1 if (c >= 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 < terminal_end - 1; i++) { // search for last line if ('\0' == terminal_buffer[i]) { // end of line found terminal_line_new = i + 1; // set new line start } } terminal_line = terminal_line_new; // set new line start terminal_last = true; // remember we are on the last line } if (terminal_line == terminal_end) { // line is empty // nothing to do } else { // do not process current line while (terminal_end >= LENGTH(terminal_buffer) - 1 && terminal_remove_line()); // delete line if not enough space if (terminal_end < LENGTH(terminal_buffer) - 1) { // now there is space terminal_end++; // shift end to new line } else { // the current last line takes all the space -> 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 < LENGTH(terminal_buffer) - 1) { // now there is space terminal_end++; // shift end to new line } else { // the current last line takes all the space -> 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 < terminal_end && i < LENGTH(terminal_buffer) - 1; i++) { // delete character by shifting end of line terminal_buffer[i] = terminal_buffer[i + 1]; // shift character } terminal_end--; // shorten line terminal_pos--; // move position back } } else if (terminal_insert) { while (terminal_end >= LENGTH(terminal_buffer) - 1 && terminal_remove_line()); // delete line if not enough space if (terminal_end < LENGTH(terminal_buffer) - 1) { // there is space to move the end for (int16_t i = terminal_end; i >= 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_pos < LENGTH(terminal_buffer) - 1) { // go to next character (if possible) terminal_pos++; } if (terminal_pos > terminal_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 } } }