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:
parent
8285e148e1
commit
1101c53bf6
2 changed files with 185 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
196
main.c
196
main.c
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue