blob: f7b56de4cbaba2cf8ddf3177e2468b20598052e4 [file] [log] [blame]
/*
* Copyright (C) 2013 Intel Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <termios.h>
#include <stdlib.h>
#include "terminal.h"
#include "history.h"
/*
* Character sequences recognized by code in this file
* Leading ESC 0x1B is not included
*/
#define SEQ_INSERT "[2~"
#define SEQ_DELETE "[3~"
#define SEQ_HOME "OH"
#define SEQ_END "OF"
#define SEQ_PGUP "[5~"
#define SEQ_PGDOWN "[6~"
#define SEQ_LEFT "[D"
#define SEQ_RIGHT "[C"
#define SEQ_UP "[A"
#define SEQ_DOWN "[B"
#define SEQ_STAB "[Z"
#define SEQ_M_n "n"
#define SEQ_M_p "p"
#define SEQ_CLEFT "[1;5D"
#define SEQ_CRIGHT "[1;5C"
#define SEQ_CUP "[1;5A"
#define SEQ_CDOWN "[1;5B"
#define SEQ_SLEFT "[1;2D"
#define SEQ_SRIGHT "[1;2C"
#define SEQ_SUP "[1;2A"
#define SEQ_SDOWN "[1;2B"
#define SEQ_MLEFT "[1;3D"
#define SEQ_MRIGHT "[1;3C"
#define SEQ_MUP "[1;3A"
#define SEQ_MDOWN "[1;3B"
#define KEY_SEQUENCE(k) { KEY_##k, SEQ_##k }
struct ansii_sequence {
int code;
const char *sequence;
};
/* Table connects single int key codes with character sequences */
static const struct ansii_sequence ansii_sequnces[] = {
KEY_SEQUENCE(INSERT),
KEY_SEQUENCE(DELETE),
KEY_SEQUENCE(HOME),
KEY_SEQUENCE(END),
KEY_SEQUENCE(PGUP),
KEY_SEQUENCE(PGDOWN),
KEY_SEQUENCE(LEFT),
KEY_SEQUENCE(RIGHT),
KEY_SEQUENCE(UP),
KEY_SEQUENCE(DOWN),
KEY_SEQUENCE(CLEFT),
KEY_SEQUENCE(CRIGHT),
KEY_SEQUENCE(CUP),
KEY_SEQUENCE(CDOWN),
KEY_SEQUENCE(SLEFT),
KEY_SEQUENCE(SRIGHT),
KEY_SEQUENCE(SUP),
KEY_SEQUENCE(SDOWN),
KEY_SEQUENCE(MLEFT),
KEY_SEQUENCE(MRIGHT),
KEY_SEQUENCE(MUP),
KEY_SEQUENCE(MDOWN),
KEY_SEQUENCE(STAB),
KEY_SEQUENCE(M_p),
KEY_SEQUENCE(M_n),
{ 0, NULL }
};
#define KEY_SEQUNCE_NOT_FINISHED -1
#define KEY_C_C 3
#define KEY_C_D 4
#define KEY_C_L 12
#define isseqence(c) ((c) == 0x1B)
/*
* Number of characters that consist of ANSI sequence
* Should not be less then longest string in ansi_sequences
*/
#define MAX_ASCII_SEQUENCE 10
static char current_sequence[MAX_ASCII_SEQUENCE];
static int current_sequence_len = -1;
/* single line typed by user goes here */
static char line_buf[LINE_BUF_MAX];
/* index of cursor in input line */
static int line_buf_ix = 0;
/* current length of input line */
static int line_len = 0;
/* line index used for fetching lines from history */
static int line_index = 0;
static char prompt_buf[10] = "> ";
static const char *const noprompt = "";
static const char *current_prompt = prompt_buf;
static const char *prompt = prompt_buf;
/*
* Moves cursor to right or left
*
* n - positive - moves cursor right
* n - negative - moves cursor left
*/
static void terminal_move_cursor(int n)
{
if (n < 0) {
for (; n < 0; n++)
putchar('\b');
} else if (n > 0) {
printf("%*s", n, line_buf + line_buf_ix);
}
}
/* Draw command line */
void terminal_draw_command_line(void)
{
/*
* this needs to be checked here since line_buf is not cleared
* before parsing event though line_len and line_buf_ix are
*/
if (line_len > 0)
printf("%s%s", prompt, line_buf);
else
printf("%s", prompt);
/* move cursor to it's place */
terminal_move_cursor(line_buf_ix - line_len);
}
/* inserts string into command line at cursor position */
void terminal_insert_into_command_line(const char *p)
{
int len = strlen(p);
if (line_len == line_buf_ix) {
strcat(line_buf, p);
printf("%s", p);
line_len = line_len + len;
line_buf_ix = line_len;
} else {
memmove(line_buf + line_buf_ix + len,
line_buf + line_buf_ix, line_len - line_buf_ix + 1);
memmove(line_buf + line_buf_ix, p, len);
printf("%s", line_buf + line_buf_ix);
line_buf_ix += len;
line_len += len;
terminal_move_cursor(line_buf_ix - line_len);
}
}
/* Prints string and redraws command line */
int terminal_print(const char *format, ...)
{
va_list args;
int ret;
va_start(args, format);
ret = terminal_vprint(format, args);
va_end(args);
return ret;
}
/* Prints string and redraws command line */
int terminal_vprint(const char *format, va_list args)
{
int ret;
printf("\r%*s\r", (int) line_len + 1, " ");
ret = vprintf(format, args);
terminal_draw_command_line();
fflush(stdout);
return ret;
}
/*
* Call this when text in line_buf was changed
* and line needs to be redrawn
*/
static void terminal_line_replaced(void)
{
int len = strlen(line_buf);
/* line is shorter that previous */
if (len < line_len) {
/* if new line is shorter move cursor to end of new end */
while (line_buf_ix > len) {
putchar('\b');
line_buf_ix--;
}
/* If cursor was not at the end, move it to the end */
if (line_buf_ix < line_len)
printf("%.*s", line_len - line_buf_ix,
line_buf + line_buf_ix);
/* over write end of previous line */
while (line_len >= len++)
putchar(' ');
}
/* draw new line */
printf("\r%s%s", prompt, line_buf);
/* set up indexes to new line */
line_len = strlen(line_buf);
line_buf_ix = line_len;
fflush(stdout);
}
static void terminal_clear_line(void)
{
line_buf[0] = '\0';
terminal_line_replaced();
}
static void terminal_clear_screen(void)
{
line_buf[0] = '\0';
line_buf_ix = 0;
line_len = 0;
printf("\x1b[2J\x1b[1;1H%s", prompt);
}
static void terminal_delete_char(void)
{
/* delete character under cursor if not at the very end */
if (line_buf_ix >= line_len)
return;
/*
* Prepare buffer with one character missing
* trailing 0 is moved
*/
line_len--;
memmove(line_buf + line_buf_ix, line_buf + line_buf_ix + 1,
line_len - line_buf_ix + 1);
/* print rest of line from current cursor position */
printf("%s \b", line_buf + line_buf_ix);
/* move back cursor */
terminal_move_cursor(line_buf_ix - line_len);
}
/*
* Function tries to replace current line with specified line in history
* new_line_index - new line to show, -1 to show oldest
*/
static void terminal_get_line_from_history(int new_line_index)
{
new_line_index = history_get_line(new_line_index,
line_buf, LINE_BUF_MAX);
if (new_line_index >= 0) {
terminal_line_replaced();
line_index = new_line_index;
}
}
/*
* Function searches history back or forward for command line that starts
* with characters up to cursor position
*
* back - true - searches backward
* back - false - searches forward (more recent commands)
*/
static void terminal_match_hitory(bool back)
{
char buf[line_buf_ix + 1];
int line;
int matching_line = -1;
int dir = back ? 1 : -1;
line = line_index + dir;
while (matching_line == -1 && line >= 0) {
int new_line_index;
new_line_index = history_get_line(line, buf, line_buf_ix + 1);
if (new_line_index < 0)
break;
if (0 == strncmp(line_buf, buf, line_buf_ix))
matching_line = line;
line += dir;
}
if (matching_line >= 0) {
int pos = line_buf_ix;
terminal_get_line_from_history(matching_line);
/* move back to cursor position to original place */
line_buf_ix = pos;
terminal_move_cursor(pos - line_len);
}
}
/*
* Converts terminal character sequences to single value representing
* keyboard keys
*/
static int terminal_convert_sequence(int c)
{
int i;
/* Not in sequence yet? */
if (current_sequence_len == -1) {
/* Is ansi sequence detected by 0x1B ? */
if (isseqence(c)) {
current_sequence_len++;
return KEY_SEQUNCE_NOT_FINISHED;
}
return c;
}
/* Inside sequence */
current_sequence[current_sequence_len++] = c;
current_sequence[current_sequence_len] = '\0';
for (i = 0; ansii_sequnces[i].code; ++i) {
/* Matches so far? */
if (0 != strncmp(current_sequence, ansii_sequnces[i].sequence,
current_sequence_len))
continue;
/* Matches as a whole? */
if (ansii_sequnces[i].sequence[current_sequence_len] == 0) {
current_sequence_len = -1;
return ansii_sequnces[i].code;
}
/* partial match (not whole sequence yet) */
return KEY_SEQUNCE_NOT_FINISHED;
}
terminal_print("ansi char 0x%X %c\n", c);
/*
* Sequence does not match
* mark that no in sequence any more, return char
*/
current_sequence_len = -1;
return c;
}
typedef void (*terminal_action)(int c, line_callback process_line);
#define TERMINAL_ACTION(n) \
static void n(int c, void (*process_line)(char *line))
TERMINAL_ACTION(terminal_action_null)
{
}
/* Mapping between keys and function */
typedef struct {
int key;
terminal_action func;
} KeyAction;
int action_keys[] = {
KEY_SEQUNCE_NOT_FINISHED,
KEY_LEFT,
KEY_RIGHT,
KEY_HOME,
KEY_END,
KEY_DELETE,
KEY_CLEFT,
KEY_CRIGHT,
KEY_SUP,
KEY_SDOWN,
KEY_UP,
KEY_DOWN,
KEY_BACKSPACE,
KEY_INSERT,
KEY_PGUP,
KEY_PGDOWN,
KEY_CUP,
KEY_CDOWN,
KEY_SLEFT,
KEY_SRIGHT,
KEY_MLEFT,
KEY_MRIGHT,
KEY_MUP,
KEY_MDOWN,
KEY_STAB,
KEY_M_n,
KEY_M_p,
KEY_C_C,
KEY_C_D,
KEY_C_L,
'\t',
'\r',
'\n',
};
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
/*
* current_actions holds all recognizable kes and actions for them
* additional element (index 0) is used for default action
*/
static KeyAction current_actions[NELEM(action_keys) + 1];
/* KeyAction comparator by key, for qsort and bsearch */
static int KeyActionKeyCompare(const void *a, const void *b)
{
return ((const KeyAction *) a)->key - ((const KeyAction *) b)->key;
}
/* Find action by key, NULL if no action for this key */
static KeyAction *terminal_get_action(int key)
{
KeyAction a = { .key = key };
return bsearch(&a, current_actions + 1, NELEM(action_keys), sizeof(a),
KeyActionKeyCompare);
}
/* Sets new set of actions to use */
static void terminal_set_actions(const KeyAction *actions)
{
int i;
/* Make map with empty function for every key */
for (i = 0; i < NELEM(action_keys); ++i) {
/*
* + 1 due to 0 index reserved for default action that is
* called for non mapped key
*/
current_actions[i + 1].key = action_keys[i];
current_actions[i + 1].func = terminal_action_null;
}
/* Sort action from 1 (index 0 - default action) */
qsort(current_actions + 1, NELEM(action_keys), sizeof(KeyAction),
KeyActionKeyCompare);
/* Set default action (first in array) */
current_actions[0] = *actions++;
/* Copy rest of actions into their places */
for (; actions->key; ++actions) {
KeyAction *place = terminal_get_action(actions->key);
if (place)
place->func = actions->func;
}
}
TERMINAL_ACTION(terminal_action_left)
{
/* if not at the beginning move to previous character */
if (line_buf_ix <= 0)
return;
line_buf_ix--;
terminal_move_cursor(-1);
}
TERMINAL_ACTION(terminal_action_right)
{
/*
* If not at the end, just print current character
* and modify position
*/
if (line_buf_ix < line_len)
putchar(line_buf[line_buf_ix++]);
}
TERMINAL_ACTION(terminal_action_home)
{
/* move to beginning of line and update position */
printf("\r%s", prompt);
line_buf_ix = 0;
}
TERMINAL_ACTION(terminal_action_end)
{
/* if not at the end of line */
if (line_buf_ix < line_len) {
/* print everything from cursor */
printf("%s", line_buf + line_buf_ix);
/* just modify current position */
line_buf_ix = line_len;
}
}
TERMINAL_ACTION(terminal_action_del)
{
terminal_delete_char();
}
TERMINAL_ACTION(terminal_action_word_left)
{
int old_pos;
/*
* Move by word left
*
* Are we at the beginning of line?
*/
if (line_buf_ix <= 0)
return;
old_pos = line_buf_ix;
line_buf_ix--;
/* skip spaces left */
while (line_buf_ix && isspace(line_buf[line_buf_ix]))
line_buf_ix--;
/* skip all non spaces to the left */
while (line_buf_ix > 0 &&
!isspace(line_buf[line_buf_ix - 1]))
line_buf_ix--;
/* move cursor to new position */
terminal_move_cursor(line_buf_ix - old_pos);
}
TERMINAL_ACTION(terminal_action_word_right)
{
int old_pos;
/*
* Move by word right
*
* are we at the end of line?
*/
if (line_buf_ix >= line_len)
return;
old_pos = line_buf_ix;
/* skip all spaces */
while (line_buf_ix < line_len && isspace(line_buf[line_buf_ix]))
line_buf_ix++;
/* skip all non spaces */
while (line_buf_ix < line_len && !isspace(line_buf[line_buf_ix]))
line_buf_ix++;
/*
* Move cursor to right by printing text
* between old cursor and new
*/
if (line_buf_ix > old_pos)
printf("%.*s", (int) (line_buf_ix - old_pos),
line_buf + old_pos);
}
TERMINAL_ACTION(terminal_action_history_begin)
{
terminal_get_line_from_history(-1);
}
TERMINAL_ACTION(terminal_action_history_end)
{
if (line_index > 0)
terminal_get_line_from_history(0);
}
TERMINAL_ACTION(terminal_action_history_up)
{
terminal_get_line_from_history(line_index + 1);
}
TERMINAL_ACTION(terminal_action_history_down)
{
if (line_index > 0)
terminal_get_line_from_history(line_index - 1);
}
TERMINAL_ACTION(terminal_action_tab)
{
/* tab processing */
process_tab(line_buf, line_buf_ix);
}
TERMINAL_ACTION(terminal_action_backspace)
{
if (line_buf_ix <= 0)
return;
if (line_buf_ix == line_len) {
printf("\b \b");
line_len = --line_buf_ix;
line_buf[line_len] = 0;
} else {
putchar('\b');
line_buf_ix--;
line_len--;
memmove(line_buf + line_buf_ix,
line_buf + line_buf_ix + 1,
line_len - line_buf_ix + 1);
printf("%s \b", line_buf + line_buf_ix);
terminal_move_cursor(line_buf_ix - line_len);
}
}
TERMINAL_ACTION(terminal_action_find_history_forward)
{
/* Search history forward */
terminal_match_hitory(false);
}
TERMINAL_ACTION(terminal_action_find_history_backward)
{
/* Search history forward */
terminal_match_hitory(true);
}
TERMINAL_ACTION(terminal_action_ctrl_c)
{
terminal_clear_line();
}
TERMINAL_ACTION(terminal_action_ctrl_d)
{
if (line_len > 0) {
terminal_delete_char();
} else {
puts("");
exit(0);
}
}
TERMINAL_ACTION(terminal_action_clear_screen)
{
terminal_clear_screen();
}
TERMINAL_ACTION(terminal_action_enter)
{
/*
* On new line add line to history
* forget history position
*/
history_add_line(line_buf);
line_len = 0;
line_buf_ix = 0;
line_index = -1;
/* print new line */
putchar(c);
prompt = noprompt;
process_line(line_buf);
/* clear current line */
line_buf[0] = '\0';
prompt = current_prompt;
printf("%s", prompt);
}
TERMINAL_ACTION(terminal_action_default)
{
char str[2] = { c, 0 };
if (!isprint(c))
/*
* TODO: remove this print once all meaningful sequences
* are identified
*/
printf("char-0x%02x\n", c);
else if (line_buf_ix < LINE_BUF_MAX - 1)
terminal_insert_into_command_line(str);
}
/* Callback to call when user hit enter during prompt for */
static line_callback prompt_callback;
static KeyAction normal_actions[] = {
{ 0, terminal_action_default },
{ KEY_LEFT, terminal_action_left },
{ KEY_RIGHT, terminal_action_right },
{ KEY_HOME, terminal_action_home },
{ KEY_END, terminal_action_end },
{ KEY_DELETE, terminal_action_del },
{ KEY_CLEFT, terminal_action_word_left },
{ KEY_CRIGHT, terminal_action_word_right },
{ KEY_SUP, terminal_action_history_begin },
{ KEY_SDOWN, terminal_action_history_end },
{ KEY_UP, terminal_action_history_up },
{ KEY_DOWN, terminal_action_history_down },
{ '\t', terminal_action_tab },
{ KEY_BACKSPACE, terminal_action_backspace },
{ KEY_M_n, terminal_action_find_history_forward },
{ KEY_M_p, terminal_action_find_history_backward },
{ KEY_C_C, terminal_action_ctrl_c },
{ KEY_C_D, terminal_action_ctrl_d },
{ KEY_C_L, terminal_action_clear_screen },
{ '\r', terminal_action_enter },
{ '\n', terminal_action_enter },
{ 0, NULL },
};
TERMINAL_ACTION(terminal_action_answer)
{
putchar(c);
terminal_set_actions(normal_actions);
/* Restore default prompt */
current_prompt = prompt_buf;
/* No prompt for prints */
prompt = noprompt;
line_buf_ix = 0;
line_len = 0;
/* Call user function with what was typed */
prompt_callback(line_buf);
line_buf[0] = 0;
/* promot_callback could change current_prompt */
prompt = current_prompt;
printf("%s", prompt);
}
TERMINAL_ACTION(terminal_action_prompt_ctrl_c)
{
printf("^C\n");
line_buf_ix = 0;
line_len = 0;
line_buf[0] = 0;
current_prompt = prompt_buf;
prompt = current_prompt;
terminal_set_actions(normal_actions);
printf("%s", prompt);
}
static KeyAction prompt_actions[] = {
{ 0, terminal_action_default },
{ KEY_LEFT, terminal_action_left },
{ KEY_RIGHT, terminal_action_right },
{ KEY_HOME, terminal_action_home },
{ KEY_END, terminal_action_end },
{ KEY_DELETE, terminal_action_del },
{ KEY_CLEFT, terminal_action_word_left },
{ KEY_CRIGHT, terminal_action_word_right },
{ KEY_BACKSPACE, terminal_action_backspace },
{ KEY_C_C, terminal_action_prompt_ctrl_c },
{ KEY_C_D, terminal_action_ctrl_d },
{ '\r', terminal_action_answer },
{ '\n', terminal_action_answer },
{ 0, NULL },
};
void terminal_process_char(int c, line_callback process_line)
{
KeyAction *a;
c = terminal_convert_sequence(c);
/* Get action for this key */
a = terminal_get_action(c);
/* No action found, get default one */
if (a == NULL)
a = &current_actions[0];
a->func(c, process_line);
fflush(stdout);
}
void terminal_prompt_for(const char *s, line_callback process_line)
{
current_prompt = s;
if (prompt != noprompt) {
prompt = s;
terminal_clear_line();
}
prompt_callback = process_line;
terminal_set_actions(prompt_actions);
}
static struct termios origianl_tios;
static void terminal_cleanup(void)
{
tcsetattr(0, TCSANOW, &origianl_tios);
}
void terminal_setup(void)
{
struct termios tios;
terminal_set_actions(normal_actions);
tcgetattr(0, &origianl_tios);
tios = origianl_tios;
/*
* Turn off echo since all editing is done by hand,
* Ctrl-c handled internally
*/
tios.c_lflag &= ~(ICANON | ECHO | BRKINT | IGNBRK);
tcsetattr(0, TCSANOW, &tios);
/* Restore terminal at exit */
atexit(terminal_cleanup);
printf("%s", prompt);
fflush(stdout);
}