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

View file

@ -11,7 +11,7 @@ Simple shell for Unix-like systems written in C, that has a funny name (for germ
- ~cd~ builtin command (~cd~ without arguments moves you to ~$HOME~) - ~cd~ builtin command (~cd~ without arguments moves you to ~$HOME~)
- ~exit~ builtin command - ~exit~ builtin command
- ~CTRL+C~ stops running command - ~CTRL+C~ stops running command
- Custom ~readline~ function - Custom ~readline~ function with support for cursor moving (<-, ->) and inline editing
- Run scripts with ~arsh /path/to/script~ or by putting ~#!/usr/bin/env arsh~ at the first line of a script and making it executable - Run scripts with ~arsh /path/to/script~ or by putting ~#!/usr/bin/env arsh~ at the first line of a script and making it executable
- Run commands with ~arsh -c 'echo $PATH'~ to launch something quickly with ~arsh~. - Run commands with ~arsh -c 'echo $PATH'~ to launch something quickly with ~arsh~.
* Build * Build

196
main.c
View file

@ -8,6 +8,7 @@
#include <setjmp.h> #include <setjmp.h>
#include <ctype.h> #include <ctype.h>
#include <fcntl.h> #include <fcntl.h>
#include <termios.h>
constexpr size_t max_length = 1024; constexpr size_t max_length = 1024;
constexpr size_t max_env_var_length = 256; 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 constexpr size_t max_line = 1024;
static char string_buffer[max_line]; 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(fgets(string_buffer, max_line, stream) == NULL)
{ {
if (feof(stdin))
{
return NULL; // Ctrl+D
}
if(ferror(stream)) if(ferror(stream))
{ {
perror("fgets error"); perror("fgets error");
@ -78,6 +71,185 @@ char* readline(char* print, FILE* stream)
return string_buffer; 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. * Struct to represent a command and its arguments.
*/ */
@ -737,7 +909,7 @@ char* prettify_pwd(char* pwd)
{ {
if (!pwd) if (!pwd)
{ {
fprintf(stderr, "Internal Error: pwd can't be null"); fprintf(stderr, "\nInternal Error: pwd can't be null\n");
exit(1); exit(1);
} }
char* home = get_home(); char* home = get_home();
@ -817,7 +989,7 @@ char* generate_ps1_prompt()
size_t len = strlen(data); size_t len = strlen(data);
if(j + i - 1 - start + len >= sizeof(prompt_buf)) 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; break;
} }
size_t start_len = min_size_t(sizeof(prompt_buf) - j, i - start - 1); 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; j += start_len;
if(j + len >= sizeof(prompt_buf)) 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; break;
} }
memcpy(prompt_buf + j, data, len); memcpy(prompt_buf + j, data, len);