From bf03074e346b004659196b6c17eee04dbffd3ac2 Mon Sep 17 00:00:00 2001 From: Nicholas Noll Date: Fri, 15 Oct 2021 16:18:02 -0700 Subject: feat(rc): working prototype of input->compile->print loop --- sys/cmd/rc/input.c | 1045 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1045 insertions(+) create mode 100644 sys/cmd/rc/input.c (limited to 'sys/cmd/rc/input.c') diff --git a/sys/cmd/rc/input.c b/sys/cmd/rc/input.c new file mode 100644 index 0000000..cf05382 --- /dev/null +++ b/sys/cmd/rc/input.c @@ -0,0 +1,1045 @@ +#include "rc.h" +#include +#include + +struct Mode { + ushort raw : 1; + ushort multiline : 1; + ushort mask : 1; + ushort defer : 1; + struct { + ushort on : 1; + ushort insert : 1; + } vi ; +}; + +static struct Mode mode; +static struct termios originalterm; + +/* + * The structure represents the state during line editing. + * We pass this state to functions implementing specific editing + * functionalities + */ +struct TerminalState +{ + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + uintptr buflen; /* Edited line buffer size. */ + char *prompt; /* Prompt to display. */ + uintptr plen; /* Prompt length. */ + uintptr pos; /* Current cursor position. */ + uintptr oldpos; /* Previous refresh cursor position. */ + uintptr len; /* Current edited line length. */ + uintptr cols; /* Number of columns in terminal. */ + uintptr maxrows; /* Maximum num of rows used so far (multiline mode) */ + int history_index; /* The history index we are currently editing. */ +}; + +enum +{ + KeyNil = 0, /* nil */ + KeyCtrlA = 1, /* Ctrl+a */ + KeyCtrlB = 2, /* Ctrl-b */ + KeyCtrlC = 3, /* Ctrl-c */ + KeyCtrlD = 4, /* Ctrl-d */ + KeyCtrlE = 5, /* Ctrl-e */ + KeyCtrlF = 6, /* Ctrl-f */ + KeyCtrlH = 8, /* Ctrl-h */ + KeyTab = 9, /* Tab */ + KeyCtrlK = 11, /* Ctrl+k */ + KeyCtrlL = 12, /* Ctrl+l */ + KeyEnter = 13, /* Enter */ + KeyCtrlN = 14, /* Ctrl-n */ + KeyCtrlP = 16, /* Ctrl-p */ + KeyCtrlT = 20, /* Ctrl-t */ + KeyCtrlU = 21, /* Ctrl+u */ + KeyCtrlW = 23, /* Ctrl+w */ + KeyEsc = 27, /* Escape */ + KeyBackspace = 127 /* Backspace */ +}; + + + +static void doatexit(void); + +static +void +normalcursor(int fd) +{ + write(fd,"\e[2 q",5); +} + +static +void +insertcursor(int fd) +{ + write(fd,"\e[6 q",5); +} + +/* Raw mode: 1960 magic shit. */ +static +int +enterraw(int fd) +{ + struct termios raw; + + if(!isatty(0)) + goto fatal; + + if(!mode.defer) { + atexit(doatexit); + mode.defer = 1; + } + if(tcgetattr(fd,&originalterm) == -1) + goto fatal; + + raw = originalterm; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + mode.raw = 1; + return 1; + +fatal: + errno = ENOTTY; + return 0; +} + +static +void +exitraw(int fd) +{ + /* Don't even check the return value as it's too late. */ + if(mode.raw && tcsetattr(fd,TCSAFLUSH,&originalterm) != -1) + mode.raw = 0; +} + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor. */ +static +int +cursorposition(int ifd, int ofd) +{ + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if(write(ofd, "\x1b[6n", 4) != 4) + return -1; + + /* Read the response: ESC [ rows ; cols R */ + while(i < sizeof(buf)-1) { + if(read(ifd,buf+i,1) != 1) + break; + if(buf[i] == 'R') + break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if(buf[0] != KeyEsc || buf[1] != '[') + return -1; + if(sscanf(buf+2,"%d;%d",&rows,&cols) != 2) + return -1; + + return cols; +} + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ +static +int +columns(int ifd, int ofd) +{ + struct winsize ws; + + if(ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0){ + /* ioctl() failed. Try to query the terminal itself. */ + int start, cols; + + /* Get the initial position so we can restore it later. */ + start = cursorposition(ifd,ofd); + if(start == -1) + goto failed; + + /* Go to right margin and get position. */ + if(write(ofd,"\x1b[999C",6) != 6) + goto failed; + cols = cursorposition(ifd,ofd); + if(cols == -1) + goto failed; + + /* Restore position. */ + if(cols > start){ + char esc[32]; + snprintf(esc,32,"\x1b[%dD",cols-start); + if(write(ofd,esc,strlen(esc)) == -1) + ; + } + return cols; + }else + return ws.ws_col; + +failed: + return 80; +} + +static +void +clear(void) +{ + if(write(1,"\x1b[H\x1b[2J",7) <= 0) + ; +} + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ +static +void +beep(void) +{ + fprintf(stderr, "\x7"); + fflush(stderr); +} + +/* =========================== Line editing ================================= */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +struct Buffer { + int len; + char *b; +}; + +static +void +initbuffer(struct Buffer *ab) +{ + ab->b = nil; + ab->len = 0; +} + +static +void +append(struct Buffer *ab, const char *s, int len) +{ + char *new = realloc(ab->b,ab->len+len); + + if (new == nil) return; + memcpy(new+ab->len,s,len); + ab->b = new; + ab->len += len; +} + +static +void +freebuffer(struct Buffer *ab) +{ + free(ab->b); +} + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ +static +void +refreshsingleline(struct TerminalState *term) +{ + char esc[64]; + struct Buffer ab; + + uintptr plen = term->plen; + int fd = term->ofd; + char *buf = term->buf; + uintptr len = term->len; + uintptr pos = term->pos; + + while((plen+pos) >= term->cols) { + buf++; + len--; + pos--; + } + while(plen+len > term->cols) + len--; + + // TODO: do we need so much malloc pressure? + initbuffer(&ab); + + /* move cursor to left edge */ + snprintf(esc,64,"\r"); + append(&ab,"\r",1); + + /* write the prompt and the current buffer content */ + append(&ab,term->prompt,strlen(term->prompt)); +#if 0 + if(mode.vi.on){ + if(mode.vi.insert) + append(&ab,"[I]",3); + else + append(&ab,"[N]",3); + } + append(&ab,">",1); +#endif + if(mode.mask == 1) + while(len--) + append(&ab,"*",1); + else + append(&ab,buf,len); + + + snprintf(esc,64,"\x1b[0K"); // erase to right + append(&ab,esc,strlen(esc)); + + snprintf(esc,64,"\r\x1b[%dC", (int)(pos+plen)); // move cursor to original position + append(&ab,esc,strlen(esc)); + + if(write(fd,ab.b,ab.len) == -1) /* Can't recover from write error. */ + ; + + freebuffer(&ab); +} + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ +static +void +refreshmultilines(struct TerminalState *term) +{ + char esc[64]; + int plen = term->plen; + int rows = (plen+term->len+term->cols-1)/term->cols; /* rows used by current buf. */ + int rpos = (plen+term->oldpos+term->cols)/term->cols; /* cursor relative row. */ + int rpos2; /* rpos after refresh. */ + int col; /* colum position, zero-based. */ + int i; + int old_rows = term->maxrows; + int fd = term->ofd, j; + struct Buffer ab; + + /* Update maxrows if needed. */ + if(rows > (int)term->maxrows) + term->maxrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + initbuffer(&ab); + if(old_rows-rpos > 0){ + snprintf(esc,64,"\x1b[%dB", old_rows-rpos); + append(&ab,esc,strlen(esc)); + } + + /* Now for every row clear it, go up. */ + for(j = 0; j < old_rows-1; j++){ + snprintf(esc,64,"\r\x1b[0K\x1b[1A"); + append(&ab,esc,strlen(esc)); + } + + /* clean the top line. */ + snprintf(esc,64,"\r\x1b[0K"); + append(&ab,esc,strlen(esc)); + + /* Write the prompt and the current buffer content */ + append(&ab,term->prompt,strlen(term->prompt)); + if(mode.mask == 1){ + for(i = 0; i < term->len; i++) append(&ab,"*",1); + }else + append(&ab,term->buf,term->len); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if(term->pos && term->pos == term->len && (term->pos+plen) % term->cols == 0) { + append(&ab,"\n",1); + snprintf(esc,64,"\r"); + append(&ab,esc,strlen(esc)); + rows++; + if(rows > (int)term->maxrows) + term->maxrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (plen+term->pos+term->cols)/term->cols; /* current cursor relative row. */ + + /* Go up till we reach the expected positon. */ + if(rows-rpos2 > 0){ + snprintf(esc,64,"\x1b[%dA", rows-rpos2); + append(&ab,esc,strlen(esc)); + } + + /* Set column. */ + col = (plen+(int)term->pos) % (int)term->cols; + if(col) + snprintf(esc,64,"\r\x1b[%dC", col); + else + snprintf(esc,64,"\r"); + append(&ab,esc,strlen(esc)); + + term->oldpos = term->pos; + + if(write(fd,ab.b,ab.len) == -1) /* Can't recover from write error. */ + ; + + freebuffer(&ab); +} + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ +static +void +refreshline(struct TerminalState *term) +{ + if(mode.multiline) + refreshmultilines(term); + else + refreshsingleline(term); +} + +/* insert the character 'c' at cursor current position. + * on error writing to the terminal -1 is returned, otherwise 0. */ +int +insert(struct TerminalState *term, char c) +{ + char d; + if(term->len < term->buflen){ + if(term->len == term->pos){ + term->buf[term->pos] = c; + term->pos++; + term->len++; + term->buf[term->len] = '\0'; + if((!mode.multiline && term->plen+term->len < term->cols)){ + d = (mode.mask==1) ? '*' : c; + if(write(term->ofd,&d,1) == -1) + return 0; + } else + refreshline(term); + }else{ + memmove(term->buf+term->pos+1,term->buf+term->pos,term->len-term->pos); + term->buf[term->pos] = c; + term->len++; + term->pos++; + term->buf[term->len] = '\0'; + refreshline(term); + } + } + return 1; +} + +/* move cursor to the left n boxes */ +static +void +moveleft(struct TerminalState *term, int n) +{ + if(term->pos > n){ + term->pos -= n; + refreshline(term); + }else if(term->pos){ + term->pos = 0; + refreshline(term); + } +} + +/* move cursor to the right n boxes */ +static +void +moveright(struct TerminalState *term, int n) +{ + if(term->pos < term->len-n){ + term->pos += n; + refreshline(term); + }else if(term->pos != term->len){ + term->pos = term->len; + refreshline(term); + } +} + +/* Move cursor to the start of the line. */ +static +void +movehome(struct TerminalState *term) { + if(term->pos != 0){ + term->pos = 0; + refreshline(term); + } +} + +/* move cursor to the end of the line. */ +static +void +moveend(struct TerminalState *term) +{ + if(term->pos != term->len){ + term->pos = term->len; + refreshline(term); + } +} + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +void +movehistory(struct TerminalState *term, int dir) +{ +} + +/* delete the character at the right of the cursor without altering the cursor + * position. basically this is what happens with the "Delete" keyboard key. */ +void +delete(struct TerminalState *term) +{ + if(term->len > 0 && term->pos < term->len){ + memmove(term->buf+term->pos,term->buf+term->pos+1,term->len-term->pos-1); + term->len--; + term->buf[term->len] = '\0'; + refreshline(term); + } +} + +/* backspace implementation. */ +static +void +backspace(struct TerminalState *term) +{ + if(term->pos > 0 && term->len > 0){ + memmove(term->buf+term->pos-1,term->buf+term->pos,term->len-term->pos); + term->pos--; + term->len--; + term->buf[term->len] = '\0'; + refreshline(term); + } +} + +#define ITERATE_BACK_UNTIL(CONDITION) \ + uintptr d, x = term->pos; \ + char *it = term->buf + x; \ + \ + while(n-- > 0 && it > term->buf){ \ + while(it > term->buf && CONDITION(it[-1])) \ + --it; \ + } \ + \ + return it; \ + +static +int +prevword(struct TerminalState *term, int n) +{ + char *it = term->buf + term->pos; + + while(n-- > 0 && it > term->buf){ + /* consume any leading space chars */ + while(isspace(it[-1]) && --it >= term->buf) + ; + + /* consume word chars */ + while(it > term->buf && (isalnum(it[-1]) || it[-1] == '_')) + --it; + } + + return it-term->buf; +} + +static +int +nextword(struct TerminalState *term, int n) +{ + char *it = term->buf + term->pos; + char *end = term->buf + term->len; + + while(n-- > 0 && it < end){ + /* consume any leading word chars */ + while(it < end && (isalnum(*it) || *it == '_')) + ++it; + /* consume any space chars */ + while(isspace(*it) && it < end) + ++it; + } + + return it-term->buf; +} + +static +void +deleteprevword(struct TerminalState *term) +{ + uintptr old_pos = term->pos; + uintptr diff; + + while(term->pos > 0 && term->buf[term->pos-1] == ' ') + term->pos--; + while(term->pos > 0 && term->buf[term->pos-1] != ' ') + term->pos--; + + diff = old_pos - term->pos; + memmove(term->buf+term->pos,term->buf+old_pos,term->len-old_pos+1); + + term->len -= diff; + + refreshline(term); +} + +static +void +normalmode(int fd) +{ + mode.vi.insert = 0; + normalcursor(fd); +} + +static +void +insertmode(int fd) +{ + mode.vi.insert = 1; + insertcursor(fd); +} + +static +int +vi(struct TerminalState *term, char c) +{ + int n = 1; + +action: + switch(c){ + /* # of repeats */ + case '1': case '2': case '3': + case '4': case '5': case '6': + case '7': case '8': case '9': + n = 0; + while('0' <= c && c <= '9'){ + n = 10*n + (c-'0'); + if(read(term->ifd, &c, 1)<1) + return -1; + } + goto action; + + /* movements */ + case 'l': + moveright(term, n); + break; + + case 'h': + moveleft(term, n); + break; + + case '0': + movehome(term); + break; + + case '$': + moveend(term); + break; + + case 'b': + term->pos = prevword(term,n); + refreshline(term); + break; + + case 'w': + term->pos = nextword(term,n); + refreshline(term); + break; + + case 'a': + if(term->pos < term->len){ + term->pos++; + refreshline(term); + } + goto insertmode; + + case 'A': + moveend(term); + goto insertmode; + + case 'I': + movehome(term); + goto insertmode; + + case 'i': insertmode: + insertmode(term->ofd); + break; + + case 'C': + insertmode(term->ofd); + /* fallthough */ + case 'D': + term->len = term->pos; + term->buf[term->pos] = 0; + refreshline(term); + break; + + case 'd': + if(read(term->ifd,&c,1)) + break; + switch(c){ + default: + beep(); + normalmode(term->ofd); + break; + } + + default: + beep(); + break; + } + + return 0; +} + + +/* This function is the core of the line editing capability of linenoise. + * It expects 'fd' to be already in "raw mode" so that every key pressed + * will be returned ASAP to read(). + * + * The resulting string is put into 'buf' when the user type enter, or + * when ctrl+d is typed. + * + * The function returns the length of the current buffer. */ +static +int +interact(int ifd, int ofd, char *buf, uintptr len, char *prompt) +{ + char c; + int n; + char esc[3]; + + struct TerminalState term; + /* + * populate the state that we pass to functions implementing + * specific editing functionalities + */ + term.ifd = ifd; + term.ofd = ofd; + term.buf = buf; + term.buflen = len; + term.prompt = prompt; + term.plen = strlen(prompt); + term.oldpos = term.pos = 0; + term.len = 0; + term.cols = columns(ifd, ofd); + term.maxrows = 0; + term.history_index = 0; + + /* Buffer starts empty. */ + term.buf[0] = '\0'; + term.buflen--; /* Make sure there is always space for the nulterm */ + + if(write(term.ofd,prompt,term.plen) == -1) + return -1; + + for(;;){ + n = read(term.ifd,&c,1); + if(n <= 0) + return term.len; + + switch(c){ + case KeyEnter: + if(mode.multiline) + moveend(&term); + return (int)term.len; + + case KeyCtrlC: + errno = EAGAIN; + return -1; + + case KeyBackspace: + case KeyCtrlH: + backspace(&term); + break; + + case KeyCtrlD: + if(term.len > 0) + delete(&term); + break; + + case KeyCtrlT: + if(term.pos > 0 && term.pos < term.len){ + int aux = buf[term.pos-1]; + buf[term.pos-1] = buf[term.pos]; + buf[term.pos] = aux; + if (term.pos != term.len-1) term.pos++; + refreshline(&term); + } + break; + + case KeyCtrlB: + moveleft(&term, 1); + break; + + case KeyCtrlF: /* ctrl-f */ + moveright(&term, 1); + break; + + case KeyCtrlP: /* ctrl-p */ + /* TODO next history */ + break; + + case KeyCtrlN: /* ctrl-n */ + /* TODO prev history */ + break; + + case KeyEsc: /* escape sequence */ + /* + * try to read two bytes representing the escape sequence. + * if we read less than 2 and we are in vi mode, interpret as command + * NOTE: we could do a timed read here + */ + switch(read(term.ifd,esc,2)){ + case 0: + if(mode.vi.on){ + if(mode.vi.insert){ + normalmode(term.ofd); + if(term.pos > 0){ + --term.pos; + refreshline(&term); + } + continue; + } + } + case 1: + if(mode.vi.on){ + if(mode.vi.insert){ + normalmode(term.ofd); + if(vi(&term,esc[0]) < 0) + return -1; + continue; + } + } + default: // 2 + ; + } + + /* ESC [ sequences. */ + if(esc[0] == '['){ + if(0 <= esc[1] && esc[1] <= '9'){ + /* extended escape, read additional byte. */ + if(read(term.ifd,esc+2,1) == -1) + break; + + if(esc[2] == '~'){ + switch(esc[1]){ + case '3': /* delete key. */ + delete(&term); + break; + } + } + }else{ + switch(esc[1]) { + case 'A': /* up */ + movehistory(&term, 1); + break; + case 'B': /* down */ + movehistory(&term, 0); + break; + case 'C': /* right */ + moveright(&term, 1); + break; + case 'D': /* left */ + moveleft(&term, 1); + break; + case 'H': /* home */ + movehome(&term); + break; + case 'F': /* end*/ + moveend(&term); + break; + } + } + } + /* ESC O sequences. */ + else if(esc[0] == 'O'){ + switch(esc[1]) { + case 'H': /* home */ + movehome(&term); + break; + case 'F': /* end*/ + moveend(&term); + break; + } + } + + break; + + default: + if(mode.vi.on && !mode.vi.insert){ + if(vi(&term,c) < 0) + return -1; + }else if(!insert(&term,c)) + return -1; + break; + + case KeyCtrlU: /* Ctrl+u, delete the whole line. */ + buf[0] = '\0'; + term.pos = term.len = 0; + refreshline(&term); + break; + + case KeyCtrlK: /* Ctrl+k, delete from current to end of line. */ + buf[term.pos] = '\0'; + term.len = term.pos; + refreshline(&term); + break; + + case KeyCtrlA: /* Ctrl+a, go to the start of the line */ + movehome(&term); + break; + + case KeyCtrlE: /* ctrl+e, go to the end of the line */ + moveend(&term); + break; + + case KeyCtrlL: /* ctrl+term, clear screen */ + clear(); + refreshline(&term); + break; + + case KeyCtrlW: /* ctrl+w, delete previous word */ + deleteprevword(&term); + break; + } + } + return term.len; +} + +/* + * this special mode is used by linenoise in order to print scan codes + * on screen for debugging / development purposes. It is implemented + * by the linenoise_example program using the --keycodes option. + */ +void +printkeycode(void) +{ + int n; + char c, quit[4]; + + printf("entering debugging mode. printing key codes.\n" + "press keys to see scan codes. type 'quit' at any time to exit.\n"); + + if(!enterraw(0)) + return; + + memset(quit,' ',4); + + for(;;){ + n = read(0,&c,1); + if(n <= 0) + continue; + memmove(quit,quit+1,sizeof(quit)-1); // shift string to left + quit[arrlen(quit)-1] = c; /* Insert current char on the right. */ + + if(memcmp(quit,"quit",sizeof(quit)) == 0) + break; + + printf("'%c' %02x (%d) (type quit to exit)\n", isprint(c) ? c : '?', (int)c, (int)c); + printf("\r"); /* go to left edge manually, we are in raw mode. */ + fflush(stdout); + } + exitraw(0); +} + +/* + * This function calls the line editing function edit() using + * the stdin set in raw mode + */ +static +int +raw(char *buf, uintptr len, char *prompt) +{ + int n; + + if(!len){ + errno = EINVAL; + return -1; + } + + // XXX: should we not hardcode stdin and stdout fd? + if(!enterraw(0)) return -1; + n = interact(0, 1, buf, len, prompt); + exitraw(0); + + return n; +} + +/* + * called when readline() is called with the standard + * input file descriptor not attached to a TTY. For example when the + * program is called in pipe or with a file redirected to its standard input + * in this case, we want to be able to return the line regardless of its length + */ +static +int +notty(void) +{ + int c; + + for(;;){ + c = fgetc(stdin); + put(&shell->cmd.io, c); + } +} + +void +enablevi(void) +{ + mode.vi.on = 1; + insertmode(1); +} + +/* + * The high level function that is the main API. + * This function checks if the terminal has basic capabilities and later + * either calls the line editing function or uses dummy fgets() so that + * you will be able to type something even in the most desperate of the + * conditions. + */ +int +readline(char *prompt) +{ + int n; + + // reset the command buffer + shell->cmd.io->e = shell->cmd.io->b = shell->cmd.io->buf; + + if(!isatty(0)) + return notty(); + + if((n = raw(shell->cmd.io->e, shell->cmd.io->cap-1, prompt)) == -1) + return 0; + shell->cmd.io->e += n; + + /* insert a newline character at the end */ + put(&shell->cmd.io, '\n'); + printf("\n"); + + return 1; +} + +/* At exit we'll try to fix the terminal to the initial conditions. */ +static +void +doatexit(void) +{ + exitraw(0); + normalcursor(1); +} -- cgit v1.2.1