Improved readline function to support <- and ->

- readline function now uses raw terminal input mode (if in
interactive session)
- readline function supports cursor movement to the left and to the
right and inline editing (also unicode support)
This commit is contained in:
Aleksandr Lebedev 2026-04-21 14:44:05 +02:00
parent 8285e148e1
commit 1101c53bf6
2 changed files with 185 additions and 13 deletions

196
main.c
View file

@ -8,6 +8,7 @@
#include <setjmp.h>
#include <ctype.h>
#include <fcntl.h>
#include <termios.h>
constexpr size_t max_length = 1024;
constexpr size_t max_env_var_length = 256;
@ -48,18 +49,10 @@ void* ccalloc(size_t size, size_t n)
static constexpr size_t max_line = 1024;
static char string_buffer[max_line];
char* readline(char* print, FILE* stream)
char* __readline_file(FILE* stream)
{
if(print)
{
printf("%s", print);
}
if(fgets(string_buffer, max_line, stream) == NULL)
{
if (feof(stdin))
{
return NULL; // Ctrl+D
}
if(ferror(stream))
{
perror("fgets error");
@ -78,6 +71,185 @@ char* readline(char* print, FILE* stream)
return string_buffer;
}
static struct termios orig_termios;
void disable_raw_mode()
{
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}
void enable_raw_mode()
{
if (tcgetattr(STDIN_FILENO, &orig_termios) == -1)
exit(1);
atexit(disable_raw_mode);
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON);
raw.c_iflag &= ~(IXON | ICRNL);
raw.c_oflag &= ~(OPOST);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
size_t utf8_char_len(unsigned char c) {
if ((c & 0x80) == 0) return 1;
if ((c & 0xE0) == 0xC0) return 2;
if ((c & 0xF0) == 0xE0) return 3;
if ((c & 0xF8) == 0xF0) return 4;
return 1;
}
size_t prev_char_start(char* buf, size_t pos) {
if (pos == 0) return 0;
pos--;
while (pos > 0 && ((buf[pos] & 0xC0) == 0x80)) {
pos--;
}
return pos;
}
size_t next_char_start(char* buf, size_t len, size_t pos) {
if (pos >= len) return len;
return pos + utf8_char_len((unsigned char)buf[pos]);
}
ssize_t read_utf8_char(int fd, char* out) {
unsigned char c;
if (read(fd, &c, 1) != 1) return -1;
size_t len = utf8_char_len(c);
out[0] = c;
for (size_t i = 1; i < len; i++) {
if (read(fd, &out[i], 1) != 1)
return -1;
}
return len;
}
size_t utf8_display_width(const char *buf, size_t len)
{
size_t i = 0;
size_t width = 0;
while (i < len) {
unsigned char c = buf[i];
if ((c & 0x80) == 0) {
i += 1;
width += 1;
}
else if ((c & 0xE0) == 0xC0) {
i += 2;
width += 1;
}
else if ((c & 0xF0) == 0xE0) {
i += 3;
width += 1;
}
else {
i += 4;
width += 1;
}
}
return width;
}
char* __readline_interactive(char* prompt)
{
enable_raw_mode();
static char buffer[max_length];
size_t cursor = 0;
size_t len = 0;
ssize_t swrite(int fd, const char* str)
{
return write(fd, str, strlen(str));
}
void render_line(char *prompt, char *buf, size_t len, size_t cursor)
{
swrite(STDOUT_FILENO, "\r");
swrite(STDOUT_FILENO, prompt);
write(STDOUT_FILENO, buf, len);
swrite(STDOUT_FILENO, "\x1b[K");
size_t prompt_width = utf8_display_width(prompt, strlen(prompt));
size_t cell_cursor = utf8_display_width(buf, cursor);
char seq[64];
snprintf(seq, sizeof(seq), "\r\x1b[%zuC", prompt_width + cell_cursor);
swrite(STDOUT_FILENO, seq);
}
render_line(prompt, buffer, len, cursor);
while (1) {
char utf8[4];
ssize_t clen = read_utf8_char(STDIN_FILENO, utf8);
if (clen <= 0) continue;
if (utf8[0] == '\x1b') {
char seq[2];
if (read(STDIN_FILENO, &seq[0], 1) != 1) continue;
if (read(STDIN_FILENO, &seq[1], 1) != 1) continue;
if (seq[0] == '[') {
switch (seq[1])
{
case 'A': break; // Arrow up
case 'B': break; // Arrow down
case 'C': // Arrow right
cursor = next_char_start(buffer, len, cursor);
break;
case 'D': // Arrow left
cursor = prev_char_start(buffer, cursor);
break;
}
}
} else if (utf8[0] == '\r') { // enter
buffer[len] = '\0';
swrite(STDOUT_FILENO, "\r\n");
break;
} else if (utf8[0] == 127) { // backspace
if (len > 0)
{
if (cursor > 0) {
size_t prev = prev_char_start(buffer, cursor);
memmove(buffer + prev, buffer + cursor, len - cursor);
len -= (cursor - prev);
cursor = prev;
}
}
}
else {
if(len < sizeof(buffer))
{
memmove(buffer + cursor + clen, buffer + cursor, len - cursor);
memcpy(buffer + cursor, utf8, clen);
cursor += clen;
len += clen;
}
}
render_line(prompt, buffer, len, cursor);
}
disable_raw_mode();
return buffer;
}
char* readline(char* prompt, FILE* stream)
{
if (isatty(fileno(stream)))
{
return __readline_interactive(prompt);
}
return __readline_file(stream);
}
/**
* Struct to represent a command and its arguments.
*/
@ -737,7 +909,7 @@ char* prettify_pwd(char* pwd)
{
if (!pwd)
{
fprintf(stderr, "Internal Error: pwd can't be null");
fprintf(stderr, "\nInternal Error: pwd can't be null\n");
exit(1);
}
char* home = get_home();
@ -817,7 +989,7 @@ char* generate_ps1_prompt()
size_t len = strlen(data);
if(j + i - 1 - start + len >= sizeof(prompt_buf))
{
fprintf(stderr, "Out of memory(1) for prompt: %d >= %d", j + i - start + len, sizeof(prompt_buf));
fprintf(stderr, "\nOut of memory(1) for prompt: %d >= %d\n", j + i - start + len, sizeof(prompt_buf));
break;
}
size_t start_len = min_size_t(sizeof(prompt_buf) - j, i - start - 1);
@ -825,7 +997,7 @@ char* generate_ps1_prompt()
j += start_len;
if(j + len >= sizeof(prompt_buf))
{
fprintf(stderr, "Out of memory(2) for prompt: %d >= %d", j + len, sizeof(prompt_buf));
fprintf(stderr, "\nOut of memory(2) for prompt: %d >= %d\n", j + len, sizeof(prompt_buf));
break;
}
memcpy(prompt_buf + j, data, len);