diff --git a/README.org b/README.org index 4515796..7284e71 100644 --- a/README.org +++ b/README.org @@ -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~) - ~exit~ builtin 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 commands with ~arsh -c 'echo $PATH'~ to launch something quickly with ~arsh~. * Build diff --git a/main.c b/main.c index c24db11..59433e9 100644 --- a/main.c +++ b/main.c @@ -8,6 +8,7 @@ #include #include #include +#include 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);