// Copyright (C) 2021, Jakob Wakeling // All rights reserved. #include "../util/log.h" #include "../util/util.h" #include "lineread.h" #include #include #include #include #include #include #include #include #include typedef struct { char *s; uptr sp, sl, sc; } line; typedef struct { line *a; uptr ap, ah, at, al, ac; } hist; static struct termios tco, tcn; static bool rawflag = false, ateflag = false; static hist h = { NULL, 0, 0, 0, 0 }; static const char *const prompt = "$ "; static char *linentty(void); static char *lineedit(void); static void line_esc(line *l, register int c); static inline void tcraw(void); static inline void tcrestore(void); static size_t getcols(void); static void clearscreen(line *l); static void line_refresh(line *l); static void line_reset(line *l); static void line_move_left(line *l); static void line_move_right(line *l); static void line_move_word_home(line *l); static void line_move_word_end(line *l); static void line_move_home(line *l); static void line_move_end(line *l); static void line_push(line *l, char c); static int line_insert(line *l, char c); static void line_backspace(line *l); static void line_delete(line *l); static void line_delete_word_home(line *l); static void line_delete_word_end(line *l); static void line_delete_home(line *l); static void line_delete_end(line *l); static int hist_init(hist *h); static void hist_free(hist *h); static void hist_move_prior(hist *h); static void hist_move_next(hist *h); static void hist_move_home(hist *h); static void hist_move_end(hist *h); static void hist_push(hist *h, line l); /* Read a line from stdin */ char *lineread(void) { if (!isatty(STDIN_FILENO)) { errno = 0; return linentty(); } else { return lineedit(); } } /* Free memory allocated by lineread */ void linefree(void) { hist_free(&h); } /* Read from a non-terminal stdin */ static char *linentty(void) { line l; register char *r; l.sp = 0; l.sl = 0; l.sc = 1024; if (!(l.s = xmalloc(l.sc * sizeof (*l.s)))) { return NULL; } for (register int c; (c = fgetc(stdin));) { if (c == EOF) { if (l.sl) { break; } r = NULL; goto ret; } else { line_push(&l, c); } continue; } end:; r = strndup(l.s, l.sl); ret:; free(l.s); return r; } #define l h.a[h.ap] /* Dynamically read a line from stdin */ static char *lineedit(void) { register char *r; { if (h.a == NULL) { if (hist_init(&h)) { return NULL; } } line m = { NULL, 0, 0, 1024 }; // if (!(m.cols = getcols())) { return NULL; } if (!(m.s = xmalloc(m.sc * sizeof (*m.s)))) { return NULL; } m.s[0] = 0; hist_push(&h, m); } tcraw(); fputs(prompt, stdout); for (register int c; (c = fgetc(stdin));) { if (errno) { log_warn("FIXME: %s", strerror(errno)); errno = 0; } // printf("%02X\n", c); continue; switch (c) { case '\x01': { line_move_home(&l); } continue; // CTRL + A case '\x02': { line_move_left(&l); } continue; // CTRL + B case '\x03': { line_reset(&l); } continue; // CTRL + C case '\x04': { r = NULL; } goto ret; // CTRL + D case '\x05': { line_move_end(&l); } continue; // CTRL + E case '\x06': { line_move_right(&l); } continue; // CTRL + F case '\x07': {} continue; // IGNORE CTRL + G case '\x08': { line_backspace(&l); } continue; // CTRL + H case '\x09': {} continue; // IGNORE CTRL + I case '\x0A': {} goto end; // CTRL + J case '\x0B': { line_delete_end(&l); } continue; // CTRL + K case '\x0C': { clearscreen(&l); } continue; // CTRL + L case '\x0D': {} goto end; // ENTER or CTRL + M case '\x0E': { /* Next history */ } continue; // CTRL + N case '\x0F': {} goto end; // CTRL + O case '\x10': { /* Prior history */ } continue; // CTRL + P case '\x11': { /* Start output */ } continue; // CTRL + Q case '\x12': {} continue; // IGNORE CTRL + R case '\x13': { /* Stop output */ } continue; // CTRL + S case '\x14': { /* Swap with prior */ } continue; // CTRL + T case '\x15': { line_delete_home(&l); } continue; // CTRL + U case '\x16': { /* Insert char code */ } continue; // CTRL + V case '\x17': { line_delete_word_home(&l); } continue; // CTRL + W case '\x18': {} continue; // IGNORE CTRL + X case '\x19': { /* Paste deleted */ } continue; // CTRL + Y case '\x1B': switch ((c = fgetc(stdin))) { case 'b': { line_move_word_home(&l); } continue; // ALT + B case 'd': { line_delete_word_end(&l); } continue; // ALT + D case 'f': { line_move_word_end(&l); } continue; // ALT + F case '[': switch ((c = fgetc(stdin))) { case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { line_esc(&l, c); } continue; case 'A': { hist_move_prior(&h); } continue; // UP case 'B': { hist_move_next(&h); } continue; // DOWN case 'C': { line_move_right(&l); } continue; // RIGHT case 'D': { line_move_left(&l); } continue; // LEFT case 'F': { line_move_end(&l); } continue; // END case 'H': { line_move_home(&l); } continue; // HOME default: {} continue; } default: {} continue; } case '\x7F': { line_backspace(&l); } continue; // BACKSPACE default: { line_insert(&l, c); } continue; } } end:; r = strndup(l.s, l.sl); ret:; tcrestore(); fputc('\n', stdout); return r; } #undef l // h.a[h.ap] /* Handle an extended ^[ sequence */ static void line_esc(line *l, register int c) { switch (c) { case '2': switch ((c = fgetc(stdin))) { case '~': { /* Insert char code */ } return; // INSERT } case '3': switch ((c = fgetc(stdin))) { case '~': { line_delete(l); } return; // DELETE } case '5': switch ((c = fgetc(stdin))) { case '~': { hist_move_home(&h); } return; // PAGE UP } case '6': switch ((c = fgetc(stdin))) { case '~': { hist_move_end(&h); } return; // PAGE DOWN } } } /* Put stdin into raw mode */ static inline void tcraw(void) { if (rawflag) { return; } tcgetattr(STDIN_FILENO, &tco); tcn = tco; if (!ateflag) { atexit(tcrestore); ateflag = true; } /* No break, no CR to NL, no parity check, no strip bit, no flow control */ tcn.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); /* No post process, 8-bit chars */ tcn.c_oflag &= ~(OPOST); tcn.c_cflag |= (CS8); /* Canonical off, echo off, no extensions, no signal characters */ tcn.c_lflag &= ~(ICANON | ECHO | IEXTEN | ISIG); /* Read every byte with no delay */ tcn.c_cc[VMIN] = 1; tcn.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &tcn); rawflag = true; return; } /* Restore stdin to canonical mode */ static inline void tcrestore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &tco); rawflag = false; return; } /* Get the number of columns in the terminal */ static size_t getcols(void) { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { return 0; } return ws.ws_col; } /* Clear the screen */ static void clearscreen(line *l) { fputs("\x1B[H\x1B[2J", stdout); line_refresh(l); return; } /* Refresh line */ static void line_refresh(line *l) { fputs("\r\x1B[0K", stdout); fputs(prompt, stdout); fputs(l->s, stdout); fprintf(stdout, "\r\x1B[%zuC", strlen(prompt) + l->sp); return; } /* Reset line */ static void line_reset(line *l) { l->s[0] = 0; l->sp = 0; l->sl = 0; fputs("^C\n", stdout); line_refresh(l); return; } /* Move cursor left */ static void line_move_left(line *l) { if (l->sp) { --l->sp; fputs("\x1B[D", stdout); } return; } /* Move cursor right */ static void line_move_right(line *l) { if (l->sp != l->sl) { ++l->sp; fputs("\x1B[C", stdout); } return; } /* Move cursor to the word home */ static void line_move_word_home(line *l) { for (; l->sp && l->s[l->sp - 1] == ' '; --l->sp); for (; l->sp && l->s[l->sp - 1] != ' '; --l->sp); line_refresh(l); return; } /* Move cursor to the word end */ static void line_move_word_end(line *l) { for (; l->sp != l->sl && l->s[l->sp] == ' '; ++l->sp); for (; l->sp != l->sl && l->s[l->sp] != ' '; ++l->sp); line_refresh(l); return; } /* Move cursor to the line home */ static void line_move_home(line *l) { l->sp = 0; line_refresh(l); return; } /* Move cursor to the line end */ static void line_move_end(line *l) { l->sp = l->sl; line_refresh(l); return; } /* Push character onto end of line */ static void line_push(line *l, char c) { if (l->sl + 1 > l->sc) { l->sc *= 2; l->s = xrealloc(l->s, l->sc * sizeof (*l->s)); } l->s[l->sl] = c; l->s[++l->sl] = 0; return; } /* Insert character at cursor */ static int line_insert(line *l, char c) { if (l->sl + 1 == l->sc) { /* TODO expand string */ } if (l->sp == l->sl) { // Cursor is at line end l->s[l->sp++] = c; l->s[++l->sl] = 0; return fputc(c, stdout); } else { memmove(l->s + l->sp + 1, l->s + l->sp, l->sl - l->sp); l->s[l->sp++] = c; l->s[++l->sl] = 0; line_refresh(l); } return c; } /* Delete character before cursor */ static void line_backspace(line *l) { if (l->sp) { memmove(l->s + l->sp - 1, l->s + l->sp, l->sl - l->sp); --l->sp; l->s[--l->sl] = 0; line_refresh(l); } return; } /* Delete character at cursor */ static void line_delete(line *l) { if (l->sp != l->sl) { memmove(l->s + l->sp, l->s + l->sp + 1, l->sl - l->sp); l->s[--l->sl] = 0; line_refresh(l); } return; } /* Delete the word preceeding the cursor */ static void line_delete_word_home(line *l) { size_t p = l->sp; for (; l->sp && l->s[l->sp - 1] == ' '; --l->sp); for (; l->sp && l->s[l->sp - 1] != ' '; --l->sp); memmove(l->s + l->sp, l->s + p, l->sl - p + 1); l->sl -= p - l->sp; line_refresh(l); return; } /* Delete the word following the cursor */ static void line_delete_word_end(line *l) { size_t p = l->sp; for (; p != l->sl && l->s[p] == ' '; ++p); for (; p != l->sl && l->s[p] != ' '; ++p); memmove(l->s + l->sp, l->s + p, l->sl - p + 1); l->sl -= p - l->sp; line_refresh(l); return; } /* Delete characters from cursor to home */ static void line_delete_home(line *l) { memmove(l->s, l->s + l->sp, l->sl - l->sp + 1); l->sl -= l->sp; l->sp = 0; line_refresh(l); return; } /* Delete characters from cursor to end */ static void line_delete_end(line *l) { l->s[(l->sl = l->sp)] = 0; line_refresh(l); return; } /* Initialise history */ static int hist_init(hist *h) { /* FIXME do not hardcode history size */ h->ap = 0; h->ah = 0; h->at = 0; h->al = 0; h->ac = 1000 + 1; if (!(h->a = xcalloc(h->ac, sizeof (*h->a)))) { return 1; } /* TODO load history from file */ return 0; } /* Free history */ static void hist_free(hist *h) { for (size_t i = 0; i != h->ac; ++i) { free(h->a[i].s); } free(h->a); } /* Move backwards in history */ static void hist_move_prior(hist *h) { if (h->ap != h->ah) { h->ap == 0 ? h->ap = h->ac - 1 : --h->ap; } line_refresh(&h->a[h->ap]); return; } /* Move forwards in history */ static void hist_move_next(hist *h) { register size_t an = h->ap == h->ac - 1 ? 0 : h->ap + 1; if (an != h->at) { h->ap = an; } line_refresh(&h->a[h->ap]); return; } /* Move to the start of history */ static void hist_move_home(hist *h) { /* TODO */ h->ap = h->ah; line_refresh(&h->a[h->ap]); return; } /* Move to the end of history */ static void hist_move_end(hist *h) { /* TODO */ // h->ap = h->at; lineRefresh(&h->a[h->ap]); return; } /* Push a line onto the end of history */ static void hist_push(hist *h, line l) { if (h->al != h->ac) { ++h->al; h->ap = h->at; if (++h->at == h->ac) { h->at = 0; } } else { h->ap = h->at; if (++h->at == h->ac) { h->at = 0; } h->ah = h->at; } free(h->a[h->ap].s); h->a[h->ap] = l; return; }