Add history and editing feature to line input mode

Use up and down arrow keys to navigate history.

Use left and right arrow keys to move cursor back and forth.

We try mimic the behaviour of GNU readline which we can not use because
we also need to react to key commands.
This commit is contained in:
Martin Lund 2024-06-15 13:37:22 +02:00
parent bed34a0b9a
commit 6ec2ac19d0
3 changed files with 198 additions and 54 deletions

View file

@ -75,3 +75,15 @@ void print_tainted_set()
{ {
print_tainted = true; print_tainted = true;
} }
void print(const char *format, ...)
{
va_list args;
va_start(args, format);
vprintf(format, args);
fflush(stdout);
va_end(args);
print_tainted = false;
}

View file

@ -134,6 +134,7 @@ extern char ansi_format[];
#define tio_debug_printf_raw(format, args...) #define tio_debug_printf_raw(format, args...)
#endif #endif
void print(const char *format, ...);
void print_hex(char c); void print_hex(char c);
void print_normal(char c); void print_normal(char c);
void print_init_ansi_formatting(void); void print_init_ansi_formatting(void);

239
src/tty.c
View file

@ -87,7 +87,8 @@
#define CMSPAR 010000000000 #define CMSPAR 010000000000
#endif #endif
#define LINE_SIZE_MAX 1000 #define MAX_LINE_LEN PATH_MAX
#define MAX_HISTORY 1000
#define KEY_0 0x30 #define KEY_0 0x30
#define KEY_1 0x31 #define KEY_1 0x31
@ -162,7 +163,7 @@ static struct termios tio, tio_old, stdout_new, stdout_old, stdin_new, stdin_old
static unsigned long rx_total = 0, tx_total = 0; static unsigned long rx_total = 0, tx_total = 0;
static bool connected = false; static bool connected = false;
static bool standard_baudrate = true; static bool standard_baudrate = true;
static void (*print)(char c); static void (*printchar)(char c);
static int device_fd; static int device_fd;
static char hex_chars[2]; static char hex_chars[2];
static unsigned char hex_char_index = 0; static unsigned char hex_char_index = 0;
@ -172,7 +173,7 @@ static char *tty_buffer_write_ptr = tty_buffer;
static pthread_t thread; static pthread_t thread;
static int pipefd[2]; static int pipefd[2];
static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER;
static char line[LINE_SIZE_MAX]; static char line[MAX_LINE_LEN];
static void optional_local_echo(char c) static void optional_local_echo(char c)
{ {
@ -181,7 +182,7 @@ static void optional_local_echo(char c)
return; return;
} }
print(c); printchar(c);
if ((option.output_mode == OUTPUT_MODE_NORMAL) && (c == 127)) if ((option.output_mode == OUTPUT_MODE_NORMAL) && (c == 127))
{ {
@ -580,13 +581,13 @@ static int tio_readln(void)
char *p = line; char *p = line;
/* Read line, accept BS and DEL as rubout characters */ /* Read line, accept BS and DEL as rubout characters */
for (p = line ; p < &line[LINE_SIZE_MAX-1]; ) for (p = line ; p < &line[MAX_LINE_LEN-1]; )
{ {
if (read(pipefd[0], p, 1) > 0) if (read(pipefd[0], p, 1) > 0)
{ {
if (*p == 0x08 || *p == 0x7f) if (*p == 0x08 || *p == 0x7f)
{ {
if (p > line ) if (p > line)
{ {
write(STDOUT_FILENO, "\b \b", 3); write(STDOUT_FILENO, "\b \b", 3);
p--; p--;
@ -607,11 +608,11 @@ void tty_output_mode_set(output_mode_t mode)
switch (mode) switch (mode)
{ {
case OUTPUT_MODE_NORMAL: case OUTPUT_MODE_NORMAL:
print = print_normal; printchar = print_normal;
break; break;
case OUTPUT_MODE_HEX: case OUTPUT_MODE_HEX:
print = print_hex; printchar = print_hex;
break; break;
case OUTPUT_MODE_END: case OUTPUT_MODE_END:
@ -1191,7 +1192,7 @@ void stdout_configure(void)
} }
/* At start use normal print function */ /* At start use normal print function */
print = print_normal; printchar = print_normal;
/* Make sure we restore old stdout settings on exit */ /* Make sure we restore old stdout settings on exit */
atexit(&stdout_restore); atexit(&stdout_restore);
@ -2156,7 +2157,11 @@ void forward_to_tty(int fd, char output_char)
else else
{ {
/* Send output to tty device */ /* Send output to tty device */
optional_local_echo(output_char); if (option.input_mode != INPUT_MODE_LINE)
{
optional_local_echo(output_char);
}
if ((output_char == 0) && (option.map_o_nulbrk)) if ((output_char == 0) && (option.map_o_nulbrk))
{ {
status = tcsendbreak(fd, 0); status = tcsendbreak(fd, 0);
@ -2165,6 +2170,7 @@ void forward_to_tty(int fd, char output_char)
{ {
status = tty_write(fd, &output_char, 1); status = tty_write(fd, &output_char, 1);
} }
if (status < 0) if (status < 0)
{ {
tio_warning_printf("Could not write to tty device"); tio_warning_printf("Could not write to tty device");
@ -2195,19 +2201,32 @@ void forward_to_tty(int fd, char output_char)
} }
} }
static void clear_line()
{
print("\r\033[K");
}
static void print_line(const char *string, int cursor_pos)
{
clear_line();
print("%s", string);
print("\r"); // Move the cursor back to the beginning
for (int i = 0; i < cursor_pos; ++i)
{
print("\x1b[C"); // Move the cursor right
}
}
int tty_connect(void) int tty_connect(void)
{ {
fd_set rdfs; /* Read file descriptor set */ fd_set rdfs; /* Read file descriptor set */
int maxfd; /* Maximum file descriptor used */ int maxfd; /* Maximum file descriptor used */
char input_char, output_char; char input_char, output_char;
char input_buffer[BUFSIZ] = {}; char input_buffer[BUFSIZ] = {};
char line_buffer[BUFSIZ] = {};
static bool first = true; static bool first = true;
int status; int status;
bool do_timestamp = false; bool do_timestamp = false;
char* now = NULL; char* now = NULL;
unsigned int line_index = 0;
static char previous_char[2] = {};
struct timeval tval_before = {}, tval_now, tval_result; struct timeval tval_before = {}, tval_now, tval_result;
/* Open tty device */ /* Open tty device */
@ -2351,6 +2370,20 @@ int tty_connect(void)
exit(status); exit(status);
} }
// Initialize readline like history
char *history[MAX_HISTORY];
int history_count = 0;
int history_index = 0;
for (int i = 0; i < MAX_HISTORY; ++i)
{
history[i] = NULL;
}
int line_length = 0;
int cursor_pos = 0;
int escape = 0;
/* Input loop */ /* Input loop */
while (true) while (true)
{ {
@ -2505,8 +2538,8 @@ int tty_connect(void)
/* Map input character */ /* Map input character */
if ((input_char == '\n') && (option.map_i_nl_crnl) && (!option.map_o_msblsb)) if ((input_char == '\n') && (option.map_i_nl_crnl) && (!option.map_o_msblsb))
{ {
print('\r'); printchar('\r');
print('\n'); printchar('\n');
if (option.timestamp) if (option.timestamp)
{ {
do_timestamp = true; do_timestamp = true;
@ -2514,13 +2547,13 @@ int tty_connect(void)
} }
else if ((input_char == '\f') && (option.map_i_ff_escc) && (!option.map_o_msblsb)) else if ((input_char == '\f') && (option.map_i_ff_escc) && (!option.map_o_msblsb))
{ {
print('\e'); printchar('\e');
print('c'); printchar('c');
} }
else else
{ {
/* Print received tty character to stdout */ /* Print received tty character to stdout */
print(input_char); printchar(input_char);
} }
/* Write to log */ /* Write to log */
@ -2593,58 +2626,156 @@ int tty_connect(void)
case INPUT_MODE_LINE: case INPUT_MODE_LINE:
switch (input_char) switch (input_char)
{ {
case '\b':
case 127: // Backspace
if (line_index)
{
printf("\b \b"); // Destructive backspace
line_index--;
}
forward = false;
break;
case '\r': // Carriage return case '\r': // Carriage return
if (line_length > 0)
{
// Save to history
if (history_count < MAX_HISTORY)
{
history[history_count] = strndup(line, line_length);
history_count++;
}
else
{
free(history[0]);
memmove(&history[0], &history[1], (MAX_HISTORY - 1) * sizeof(char*));
history[MAX_HISTORY - 1] = strndup(line, line_length);
}
}
line[line_length] = '\0';
if (option.local_echo == false) if (option.local_echo == false)
{ {
// Delete line clear_line();
int i = line_index;
while (i--)
{
printf("\b \b"); // Destructive backspace
}
} }
else else
{ {
// Preserve line, go to next line print("\r\n");
putchar('\r');
putchar('\n');
} }
// Write buffered line to tty device // Write line to tty device
tty_write(device_fd, line_buffer, line_index); tty_write(device_fd, line, line_length);
tty_sync(device_fd); tty_sync(device_fd);
line_index = 0;
line_length = 0;
cursor_pos = 0;
history_index = history_count;
escape = 0;
break;
case 127: // Backspace
if (cursor_pos > 0)
{
memmove(&line[cursor_pos - 1], &line[cursor_pos], line_length - cursor_pos);
line_length--;
cursor_pos--;
line[line_length] = '\0';
print_line(line, cursor_pos);
}
forward = false;
escape = 0;
break;
case 27: // Escape
escape = 1;
forward = false;
break;
case '[':
if (escape == 1)
{
escape = 2;
}
else
{
escape = 0;
}
break;
case 'A':
if (escape == 2)
{
// Up arrow
if (history_index > 0)
{
history_index--;
strncpy(line, history[history_index], MAX_LINE_LEN-1);
line_length = strlen(line);
cursor_pos = line_length;
print_line(line, cursor_pos);
}
}
forward = false;
escape = 0;
break;
case 'B':
if (escape == 2)
{
// Down arrow
if (history_index < history_count - 1)
{
history_index++;
strncpy(line, history[history_index], MAX_LINE_LEN-1);
line_length = strlen(line);
cursor_pos = line_length;
print_line(line, cursor_pos);
}
else if (history_index == history_count - 1)
{
history_index++;
line_length = 0;
cursor_pos = 0;
line[line_length] = '\0';
print_line(line, cursor_pos);
}
}
forward = false;
escape = 0;
break;
case 'C':
if (escape == 2)
{
// Right arrow
if (cursor_pos < line_length)
{
cursor_pos++;
print("\x1b[C");
}
}
forward = false;
escape = 0;
break;
case 'D':
if (escape == 2)
{
// Left arrow
if (cursor_pos > 0)
{
cursor_pos--;
print("\b");
}
}
forward = false;
escape = 0;
break; break;
default: default:
if (line_index < BUFSIZ) if (line_length < MAX_LINE_LEN - 1)
{ {
putchar(input_char); memmove(&line[cursor_pos + 1], &line[cursor_pos], line_length - cursor_pos);
print_tainted_set(); line[cursor_pos] = input_char;
line_buffer[line_index++] = input_char; line_length++;
cursor_pos++;
line[line_length] = '\0';
print_line(line, cursor_pos);
} }
else escape = 0;
{ break;
tio_error_print("Input exceeds maximum line length. Truncating.");
}
forward = false;
} }
// Save 2 latest stdin input characters
previous_char[1] = previous_char[0];
previous_char[0] = input_char;
break;
default: default:
break; break;
} }