From 9a82b590e758997a57573f983c743d30a0aa7e84 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 9 Oct 2026 15:04:13 +0200 Subject: [PATCH] feat: line editor --- Makefile | 4 +- src/msh.c | 232 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 215 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 17a0f76..263464f 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,11 @@ ifeq ($(RELEASE),true) CFLAGS += -O3 endif -.PHONY: clean +.PHONY: clean release-$(RELEASE) all: dist/msh -dist/msh: $(SOURCES) dist +dist/msh: $(SOURCES) dist release-$(RELEASE) $(CC) -o $@ $(SOURCES) $(CFLAGS) dist: diff --git a/src/msh.c b/src/msh.c index 2db3f47..3dcfbae 100644 --- a/src/msh.c +++ b/src/msh.c @@ -7,6 +7,7 @@ #include #include #include +#include #define ESC \x1B @@ -35,14 +36,113 @@ void pretty_path_home(char** path) { } +#define ANSI_UNKNOWN -1 +#define ANSI_CURSOR_LEFT 0 +#define ANSI_CURSOR_RIGHT 1 +#define ANSI_CURSOR_UP 2 +#define ANSI_CURSOR_DOWN 3 +#define ANSI_DELETE_WORD 4 +#define ANSI_CURSOR_LEFT_WORD 5 +#define ANSI_CURSOR_RIGHT_WORD 6 +#define ANSI_CURSOR_UP_WORD 7 +#define ANSI_CURSOR_DOWN_WORD 8 + + +int read_ansi() { + switch (getchar()) { + case '[': + switch(getchar()) { + case 'A': + return ANSI_CURSOR_UP; + case 'B': + return ANSI_CURSOR_DOWN; + case 'C': + return ANSI_CURSOR_RIGHT; + case 'D': + return ANSI_CURSOR_LEFT; + case '1': + switch(getchar()) { + case ';': + switch(getchar()) { + // CTRL + case '5': + // ALT + case '3': + switch (getchar()) { + case 'C': + return ANSI_CURSOR_RIGHT_WORD; + case 'D': + return ANSI_CURSOR_LEFT_WORD; + } + break; + } + break; + } + break; + } + break; + case 127: + case 8: + return ANSI_DELETE_WORD; + default: + return ANSI_UNKNOWN; + } + + return ANSI_UNKNOWN; +} + + +// Find the length of the prefix until the last occurrence of a rejected character +size_t strnrcspn(char* str, size_t n, char* reject) { + size_t i; + int seenaccept = 0; + + for (i = n - 1; i > 0; i--) { + // If this is NOT a reject character + if(strchr(reject, str[i]) == NULL) { + seenaccept = 1; + continue; + } else { + if (seenaccept == 1) { + break; + } + } + } + + return i; +} + +// Find the length of the prefix until the first occurrence of a rejected character after an offset +size_t strnocspn(char* str, size_t n, size_t o, char* reject) { + size_t i; + int seenaccept = 0; + + for (i = o; i < n; i++) { + // If this is NOT a reject character + if(strchr(reject, str[i]) == NULL) { + seenaccept = 1; + continue; + } else { + if (seenaccept == 1) { + break; + } + } + } + + return i; +} + + int main(int argc, char** argv) { - char* tokens[LINESIZE] = {}; - char** args; - char line[LINESIZE] = {}; - char* tokenwork; - char* token; - char* newline; - int ntokens = 0; + int retval = 0; + char* tokens[LINESIZE] = {}; + char** args; + char line[LINESIZE] = {}; + char prompt[LINESIZE] = {}; + int linelen = 0; + char* tokenwork; + char* token; + int ntokens = 0; int i; @@ -63,15 +163,27 @@ int main(int argc, char** argv) { int result = uname(&uts); if (result != 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); - return errno; + retval = errno; + goto cleanup; } host = uts.nodename; + + struct termios term, orig_term; + tcgetattr(fileno(stdin), &term); + orig_term = term; + + // disable echo and icanon (don't wait for newline) + term.c_lflag &= ~(ECHO | ICANON); + + + // Main loop while(1) { char* result = getcwd(pwd, LINESIZE - 1); if (result == NULL) { - return 1; + retval = 1; + goto cleanup; } pwd_disp = pwd; @@ -81,19 +193,95 @@ int main(int argc, char** argv) { if (laststatus != 0) { fprintf(stdout, "❌"); } - fprintf(stdout, "%s@%s %s$ ", user, host, pwd_disp); - char* fgets_res = fgets(line, LINESIZE, stdin); + snprintf(prompt, LINESIZE, "%s@%s %s$ ", user, host, pwd_disp); + printf("%s", prompt); - if (fgets_res == NULL) { - printf("^D\n"); - return 0; + + // prepare for input + linelen = 0; + int readc; + int cursor = 0; + + tcsetattr(fileno(stdin), 0, &term); + + memset(line, 0, LINESIZE); + + while ((readc = getchar()) != '\n') { + if (readc == EOF) { + printf("^D\n"); + retval = 0; + goto cleanup; + } + + if (readc == '\x1b') { + int ansi = read_ansi(); + if (ansi == ANSI_CURSOR_LEFT) { + if (cursor > 0) { + cursor--; + } + } else if (ansi == ANSI_CURSOR_RIGHT) { + if (cursor < linelen) { + cursor++; + } + } else if (ansi == ANSI_DELETE_WORD) { + if (cursor > 0) { + size_t index = strnrcspn(line, cursor, " \t"); + memmove(line + index, line + cursor, LINESIZE - cursor); + linelen -= cursor - index; + cursor = index; + } + } else if (ansi == ANSI_CURSOR_LEFT_WORD) { + size_t index = strnrcspn(line, cursor, " \t"); + cursor = index; + } else if (ansi == ANSI_CURSOR_RIGHT_WORD) { + size_t index = strnocspn(line, linelen, cursor, " \t"); + cursor = index; + } + + goto print_prompt; + } + + // backspace + if (readc == 127) { + if (cursor > 0) { + cursor--; + linelen--; + } + + if (cursor == linelen) { + line[cursor] = 0; + } else { + memmove(line + cursor, line + cursor + 1, LINESIZE - (cursor)); + } + + // We don't want to change the line + goto print_prompt; + } + + if (cursor != linelen) { + memmove(line + cursor + 1, line + cursor, LINESIZE - cursor - 1); + } + + line[cursor] = readc; + cursor++; + linelen++; + +print_prompt: + // Manual echo + printf("\x1b[2K\x1b[0G%s%s", prompt, line); + for (int j = 0; j < linelen - cursor; j++) { + printf("\x1b[D"); + } + fflush(stdout); } - newline = strrchr(line, '\n'); - if (newline != NULL) { - newline[0] = 0; - } + putc('\n', stdout); + tcsetattr(fileno(stdin), 0, &orig_term); + + line[linelen] = 0; + + // Split into tokens for (tokenwork = line, i = 0; ; tokenwork = NULL, i++) { token = strtok(tokenwork, " \t"); if (token == NULL) { @@ -114,7 +302,8 @@ int main(int argc, char** argv) { // Shell builtins if (strcmp("exit", tokens[0]) == 0) { printf("Goodbye!\n"); - return 0; + retval = 0; + goto cleanup; } if (strcmp("cd", tokens[0]) == 0) { @@ -134,5 +323,10 @@ int main(int argc, char** argv) { } } + + +cleanup: + tcsetattr(fileno(stdin), 0, &orig_term); + return retval; }