feat: line editor

This commit is contained in:
2026-10-09 15:04:13 +02:00
parent 3bc5d25e1f
commit 9a82b590e7
2 changed files with 215 additions and 21 deletions

View File

@@ -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:

216
src/msh.c
View File

@@ -7,6 +7,7 @@
#include <sys/wait.h>
#include <sys/utsname.h>
#include <errno.h>
#include <termios.h>
#define ESC \x1B
@@ -35,13 +36,112 @@ 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) {
int retval = 0;
char* tokens[LINESIZE] = {};
char** args;
char line[LINESIZE] = {};
char prompt[LINESIZE] = {};
int linelen = 0;
char* tokenwork;
char* token;
char* newline;
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) {
// 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");
return 0;
retval = 0;
goto cleanup;
}
newline = strrchr(line, '\n');
if (newline != NULL) {
newline[0] = 0;
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);
}
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;
}