From ce05175372a9ddca1a225db0765ace1127a39293 Mon Sep 17 00:00:00 2001 From: Nicholas Date: Fri, 12 Nov 2021 09:22:01 -0800 Subject: chore: simplified organizational structure --- src/cmd/rc/input.c | 1679 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1679 insertions(+) create mode 100644 src/cmd/rc/input.c (limited to 'src/cmd/rc/input.c') diff --git a/src/cmd/rc/input.c b/src/cmd/rc/input.c new file mode 100644 index 0000000..cc2383d --- /dev/null +++ b/src/cmd/rc/input.c @@ -0,0 +1,1679 @@ +#include "rc.h" + +#include +#include + +/* don't change order of these without modifying matrix */ +enum +{ + NonPrintable, + Alnum, + Punctuation, + Space +}; + +static int ascii[256] = +{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, + 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, + 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +struct Mode +{ + ushort raw : 1; + ushort multiline : 1; + ushort mask : 1; + ushort defer : 1; + struct { + ushort on : 1; + ushort insert : 1; + } vi ; +}; + +/* + * 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. */ + + struct{ + char *s; /* raw UTF-8 bytes */ + int len; /* number of bytes in prompt */ + int size; /* number of (printed) runes in prompt */ + } prompt; + + struct{ + intptr cap; /* capacity of edit buffer */ + intptr len; /* current number of bytes stored */ + intptr pos; /* position within edit buffer */ + char *buf; + } edit; /* edit buffer */ + + struct{ + intptr cap; /* number of columns in terminal */ + intptr len; /* current edited line length (in runes) */ + intptr pos; /* current cursor position (in runes) */ + intptr old; /* previous refresh cursor position (in runes) */ + } cursor; + + struct{ + intptr cap; + intptr len; + char *buf; + } yank; /* yank buffer */ + + intptr maxrows; /* maximum num of rows used so far (multiline mode) */ + intptr history; /* index of history we are currently editing */ +}; + +/* + * line history (circular buffer) + */ +struct History +{ + char **bot, **top, *entry[1024]; +}; + +/* globals */ +static struct Mode mode; +static struct History history; +static struct termios originalterm; + +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); + +/* vi operations */ +typedef struct +{ + intptr buffer; + intptr cursor; +} Position; + +typedef Position (*Noun)(struct TerminalState*, int); +typedef void (*Verb)(struct TerminalState*, Position); + +static +int +runetype(rune r) +{ + if(r<128) + return ascii[r]; + if(utf8·isspace(r)) + return Space; + if(utf8·isdigit(r) || utf8·isalpha(r)) + return Alnum; + if(utf8·ispunct(r)) + return Punctuation; + + return NonPrintable; +} + +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(!shell.interactive) + 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); +} + +// ----------------------------------------------------------------------- +// command history + +void +inithistory(void) +{ + history.bot = history.top = history.entry; +} + +int +addhistory(char *line) +{ + char *copy; + + copy = strdup(line); + if(!copy) + return 0; + + *history.top++ = copy; + if(history.top == arrend(history.entry)) + history.top = history.entry; + + if(history.top == history.bot){ + efree(history.bot); + history.bot++; + } + + return 1; +} + +static +void +pophistory(void) +{ + if(--history.top < history.entry) + history.top = arrend(history.entry)-1; + efree(*history.top); +} + +static void refreshline(struct TerminalState *); + +static +char ** +currenthistory(struct TerminalState *term, intptr *size) +{ + char **entry; + intptr len, head; + + if(history.top > history.bot){ + len = history.top - history.bot; + entry = history.top - term->history - 1; + }else if(history.top < history.bot){ + len = (arrend(history.entry) - history.bot) + (history.top - history.entry); + if((head=history.top - history.entry) < term->history) + entry = arrend(history.entry) - head; + else + entry = history.top - term->history - 1; + }else + return nil; + + *size = len; + return entry; +} + +static +void +usehistory(struct TerminalState *term, int d) +{ + rune r; + intptr w, len; + char *b, *e, **entry; + + if(!(entry = currenthistory(term, &len))) + return; + + efree(*entry); + *entry = strdup(term->edit.buf); + + term->history += d; + if(term->history < 0){ + term->history = 0; + return; + }else if(term->history >= len){ + term->history = len - 1; + return; + } + entry = currenthistory(term, &len); + + strncpy(term->edit.buf, *entry, term->edit.cap); + term->edit.buf[term->edit.cap-1] = 0; + + /* update cursor/buffer positions */ + term->edit.len = term->edit.pos = strlen(term->edit.buf); + for(w=0, b=term->edit.buf, e=term->edit.buf+term->edit.len; b < e; ){ + b += utf8·decode(b, &r); + w += utf8·runewidth(r); + } + term->cursor.len = term->cursor.pos = w; + + refreshline(term); +} + +// ----------------------------------------------------------------------- +// 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; + + int n, w; + rune r; + int fd = term->ofd; + intptr off = term->prompt.size; + char *buf = term->edit.buf; + intptr len = term->edit.len; + intptr pos = term->cursor.pos; + intptr col = term->cursor.len; + + while((off+pos) >= term->cursor.cap){ + n = utf8·decode(buf, &r); + w = utf8·runewidth(r); + + buf+=n, len-=n; + pos-=w, col-=w; + } + + assert(buf <= term->edit.buf + len); + + while(off+col > term->cursor.cap){ + n = utf8·decodeprev(buf+len-1, &r); + w = utf8·runewidth(r); + + len-=n, col-=w; + } + assert(len >= 0); + + initbuffer(&ab); // TODO: do we need so much malloc pressure? + + /* 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.s, term->prompt.len); + + 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)(off+pos)); // 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) +{ +#if 0 + 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); +#endif +} + +/* 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 rune 'c' at cursor current position. + * on error writing to the terminal -1 is returned, otherwise 0. */ +int +insertrune(struct TerminalState *term, int n, char *c) +{ + int w; + rune r; + + utf8·decode(c, &r); + w = utf8·runewidth(r); + + if(term->edit.len + n <= term->edit.cap){ + if(term->edit.pos == term->edit.len){ + memcpy(term->edit.buf+term->edit.pos, c, n); + + term->edit.pos += n, term->edit.len += n; + term->cursor.pos += w, term->cursor.len += w; + + term->edit.buf[term->edit.len] = '\0'; + + if(!mode.multiline && ((term->prompt.size+term->cursor.pos+n) <= term->cursor.cap)){ + if(mode.mask){ + c = "*"; + n = 1; + } + if(write(term->ofd, c, n) == -1) + return 0; + } + refreshline(term); + }else{ + memmove(term->edit.buf+term->edit.pos+n, term->edit.buf+term->edit.pos, term->edit.len-term->edit.pos); + memcpy(term->edit.buf+term->edit.pos, c, n); + + term->edit.pos += n, term->edit.len += n; + term->cursor.pos += w, term->cursor.len += w; + + term->edit.buf[term->edit.len] = '\0'; + refreshline(term); + } + } + + return 1; +} + +int +insertbytes(struct TerminalState *term, int len, char *buf) +{ + int nr; + if(term->edit.len + len > term->edit.cap){ + len = term->edit.cap - term->edit.len; + buf[len] = 0; + } + nr = utf8·len(buf); + + if(term->edit.pos == term->cursor.len){ + memcpy(term->edit.buf+term->edit.len, buf, len); + + term->edit.pos += len, term->edit.len += len; + term->cursor.pos += nr, term->cursor.len += nr; + + // XXX: transfer the modeline here? + term->edit.buf[term->edit.len] = '\0'; + refreshline(term); + }else{ + memmove(term->edit.buf+term->edit.pos+len,term->edit.buf+term->edit.pos,term->edit.len-term->edit.pos); + memcpy(term->edit.buf+term->edit.pos, buf, len); + + term->edit.pos += len, term->edit.len += len; + term->cursor.pos += nr, term->cursor.len += nr; + + term->edit.buf[term->edit.len] = '\0'; + refreshline(term); + } + + return 1; +} + +// ----------------------------------------------------------------------- +// vi functionality + +/* modes */ + +static +void +normalmode(int fd) +{ + mode.vi.insert = 0; + normalcursor(fd); +} + +static +void +insertmode(int fd) +{ + mode.vi.insert = 1; + insertcursor(fd); +} + +/* actions */ + +static +void +move(struct TerminalState *term, Position to) +{ + if(to.buffer != term->edit.pos){ + term->edit.pos = to.buffer; + term->cursor.pos = to.cursor; + refreshline(term); + } +} + +static +void +yank(struct TerminalState *term, Position to) +{ + intptr len, off; + + if(to.buffer == term->edit.pos) + return; // noop + + if(to.buffer > term->edit.pos){ + len = to.buffer - term->edit.pos; + off = term->edit.pos; + }else{ + len = term->edit.pos - to.buffer; + off = to.buffer; + } + + if(term->yank.cap < len+1){ + efree(term->yank.buf); + term->yank.cap = len+1; + term->yank.buf = emalloc(len+1); + } + term->yank.len = len; + memcpy(term->yank.buf, term->edit.buf+off, len); + term->yank.buf[len] = 0; +} + +static +void +delete(struct TerminalState *term, Position to) +{ + intptr diff; + + // delete characters in front of us (exclusive) + if(to.buffer > term->edit.pos){ + diff = to.buffer - term->edit.pos; + memmove(term->edit.buf+term->edit.pos, term->edit.buf+to.buffer, term->edit.len-to.buffer+1); + term->edit.len -= diff; + + diff = to.cursor - term->cursor.pos; + goto refresh; + } + + // delete characters behind us + if(to.buffer < term->edit.pos){ + diff = term->edit.pos - to.buffer; + memmove(term->edit.buf+to.buffer, term->edit.buf+term->edit.pos, term->edit.len-term->edit.pos+1); + term->edit.pos = to.buffer; + term->edit.len -= diff; + + diff = term->cursor.pos - to.cursor; + term->cursor.pos = to.cursor; + goto refresh; + } + // do nothing + return; + +refresh: + term->cursor.len -= diff; + refreshline(term); +} +/* movements */ + +#define CURRENT(term) (Position){ .buffer=(term)->edit.pos, .cursor=(term)->cursor.pos }; + +// move cursor to the left n boxes +static +Position +left(struct TerminalState *term, int n) +{ + rune r; + int w, d; + Position pos = CURRENT(term); + char *buf = term->edit.buf + term->edit.pos; + + d = 0; + while(n > 0 && buf > term->edit.buf){ + buf -= utf8·decodeprev(buf-1, &r); + + w = utf8·runewidth(r); + n -= w; + d += w; + } + + pos.cursor = MAX(pos.cursor-d, 0); + pos.buffer = MAX(buf-term->edit.buf, 0); + return pos; +} + +// move cursor to the right n boxes +static +Position +right(struct TerminalState *term, int n) +{ + rune r; + int w, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + char *end = term->edit.buf + term->edit.len; + + d = 0; + while(n > 0 && buf < end){ + buf += utf8·decode(buf, &r); + + w = utf8·runewidth(r); + n -= w; + d += w; + } + + pos.cursor = MIN(pos.cursor+d, term->cursor.len); + pos.buffer = MIN(buf-term->edit.buf, term->edit.len); + return pos; +} + +static +Position +prevword(struct TerminalState *term, int n) +{ + rune r; + int c, w, b, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + + d = 0; + while(n-- > 0 && buf > term->edit.buf){ + eatspace: + b = utf8·decodeprev(buf-1, &r); + w = utf8·runewidth(r); + if((c=runetype(r)) == Space){ + buf -= b; + d += w; + + if(buf <= term->edit.buf) + break; + + goto eatspace; + } + + eatword: + if(runetype(r) == c){ + buf -= b; + d += w; + + if(buf <= term->edit.buf) + break; + + b = utf8·decodeprev(buf-1, &r); + w = utf8·runewidth(r); + + goto eatword; + } + } + + pos.cursor = MAX(pos.cursor-d, 0); + pos.buffer = MAX(buf-term->edit.buf, 0); + return pos; +} + +static +Position +nextword(struct TerminalState *term, int n) +{ + rune r; + int c, b, w, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + char *end = term->edit.buf + term->edit.len; + + d = 0; + while(n-- > 0 && buf < end){ + b = utf8·decode(buf, &r); + w = utf8·runewidth(r); + c = runetype(r); + eatword: + if(runetype(r) == c){ + buf += b; + d += w; + + if(buf >= end) + break; + + b = utf8·decode(buf, &r); + w = utf8·runewidth(r); + goto eatword; + } + eatspace: + while((c=runetype(r)) == Space){ + buf += b; + d += w; + + if(buf >= end) + break; + + b = utf8·decode(buf, &r); + w = utf8·runewidth(r); + goto eatspace; + } + } + + pos.cursor = MIN(pos.cursor+d, term->cursor.len); + pos.buffer = MIN(buf-term->edit.buf, term->edit.len); + return pos; +} + + +static +Position +prevWord(struct TerminalState *term, int n) +{ + rune r; + int c, w, b, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + + d = 0; + while(n-- > 0 && buf > term->edit.buf){ + eatspace: + b = utf8·decodeprev(buf-1, &r); + w = utf8·runewidth(r); + if((c=runetype(r)) == Space){ + buf -= b; + d += w; + + if(buf <= term->edit.buf) + break; + + goto eatspace; + } + + eatword: + if((c=runetype(r)) != Space){ + buf -= b; + d += w; + + if(buf <= term->edit.buf) + break; + + b = utf8·decodeprev(buf-1, &r); + w = utf8·runewidth(r); + + goto eatword; + } + } + + pos.cursor = MAX(pos.cursor-d, 0); + pos.buffer = MAX(buf-term->edit.buf, 0); + return pos; +} + +static +Position +nextWord(struct TerminalState *term, int n) +{ + rune r; + int b, w, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + char *end = term->edit.buf + term->edit.len; + + d = 0; + while(n-- > 0 && buf < end){ + eatword: + b = utf8·decode(buf, &r); + w = utf8·runewidth(r); + if(runetype(r) != Space){ + buf += b; + d += w; + + if(buf > end) + break; + + goto eatword; + } + + eatspace: + if(runetype(r) == Space){ + buf += b; + d += w; + + if(buf > end) + break; + + b = utf8·decode(buf, &r); + w = utf8·runewidth(r); + + goto eatspace; + } + } + + pos.cursor = MIN(pos.cursor+d, term->cursor.len); + pos.buffer = MIN(buf-term->edit.buf, term->edit.len); + return pos; +} + +static +Position +nextend(struct TerminalState *term, int n) +{ + rune r; + int c, b, w, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + char *end = term->edit.buf + term->edit.len; + + d = 0; + while(n-- > 0 && buf+1 < end){ + eatspace: + b = utf8·decode(buf+1, &r); + w = utf8·runewidth(r); + while((c=runetype(r)) == Space){ + buf += b; + d += w; + + if(buf+1 >= end) + break; + + goto eatspace; + } + eatword: + if(runetype(r) == c){ + buf += b; + d += w; + + if(buf+1 >= end) + break; + + b = utf8·decode(buf+1, &r); + w = utf8·runewidth(r); + goto eatword; + } + } + + pos.cursor = MIN(pos.cursor+d, term->cursor.len); + pos.buffer = MIN(buf-term->edit.buf, term->edit.len); + return pos; +} + +static +Position +nextEnd(struct TerminalState *term, int n) +{ + rune r; + int b, w, d; + Position pos = CURRENT(term); + + char *buf = term->edit.buf + term->edit.pos; + char *end = term->edit.buf + term->edit.len; + + d = 0; + while(n-- > 0 && buf+1 < end){ + eatspace: + b = utf8·decode(buf+1, &r); + w = utf8·runewidth(r); + if(runetype(r) == Space){ + buf += b; + d += w; + + if(buf+1 > end) + break; + + goto eatspace; + } + + eatword: + if(runetype(r) != Space){ + buf += b; + d += w; + + if(buf+1 > end) + break; + + b = utf8·decode(buf+1, &r); + w = utf8·runewidth(r); + + goto eatword; + } + } + + pos.cursor = MIN(pos.cursor+d, term->cursor.len); + pos.buffer = MIN(buf-term->edit.buf, term->edit.len); + return pos; +} + +#define HOME(term) (Position){0} +#define END(term) (Position){(term)->edit.len, (term)->cursor.len} + +static +int +vi(struct TerminalState *term, char c) +{ + int n = 1; + Verb verb = move; + +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; + + /* composable actions */ + case 'l': verb(term, right(term, n)); break; + case 'h': verb(term, left(term, n)); break; + case '0': verb(term, HOME(term)); break; + case '$': verb(term, END(term)); break; + case 'b': verb(term, prevword(term,n)); break; + case 'B': verb(term, prevWord(term,n)); break; + case 'w': verb(term, nextword(term,n)); break; + case 'W': verb(term, nextWord(term,n)); break; + case 'e': verb(term, nextend(term,n)); break; + case 'E': verb(term, nextEnd(term,n)); break; + + /* verb switches */ + case 'd': // delete + verb = delete; + if(read(term->ifd, &c, 1)<1) + return -1; + /* special cases */ + switch(c){ + case 'd': + move(term, HOME(term)); + delete(term, END(term)); + return 0; + default: + goto action; + } + case 'y': // yank + verb = yank; + if(read(term->ifd, &c, 1)<1) + return -1; + /* special cases */ + switch(c){ + case 'y': + if(term->yank.cap < term->edit.len+1){ + efree(term->yank.buf); + term->yank.len = term->edit.len; + term->yank.cap = term->edit.len+1; + term->yank.buf = emalloc(term->yank.cap); + } + memcpy(term->yank.buf, term->edit.buf, term->edit.len+1); + break; + default: + goto action; + } + break; + + case 'p': // put + insertbytes(term, term->yank.len, term->yank.buf); + refreshline(term); + return 0; + + /* special cases + * sadly I don't know a better way than to have these checks for move + * the vi language doesn't fully compose + */ + case 'i': insertmode: + if(verb != move) goto unrecognized; + insertmode(term->ofd); + break; + + case 'I': + if(verb != move) goto unrecognized; + move(term, HOME(term)); + goto insertmode; + + case 'a': + if(verb != move) goto unrecognized; + if(term->edit.pos < term->edit.len){ + term->edit.pos++; + refreshline(term); + } + goto insertmode; + + case 'A': + if(verb != move) goto unrecognized; + move(term, END(term)); + goto insertmode; + + case 'x': + if(verb != move) goto unrecognized; + delete(term, right(term, 1)); + break; + + case 'X': + if(verb != move) goto unrecognized; + delete(term, left(term, 1)); + break; + + case 'r': + if(verb != move) goto unrecognized; + if(read(term->ifd, &c, 1)<1) + return -1; + if(c < ' ') + break; + term->edit.buf[term->edit.pos] = c; + refreshline(term); + break; + + // TODO: replace mode? + + case 'c': + if(verb != move) goto unrecognized; + insertmode(term->ofd); + verb = delete; + if(read(term->ifd, &c, 1)<1) + return -1; + goto action; + + case 'C': + if(verb != move) goto unrecognized; + insertmode(term->ofd); + goto deleteln; + + case 'D': + if(verb != move) goto unrecognized; + deleteln: + term->edit.len = term->edit.pos; + term->edit.buf[term->edit.pos] = 0; + refreshline(term); + break; + + default: unrecognized: + beep(); + break; + } + + return 0; +} +#undef END + +#define END(term) (Position){(term).edit.len, (term).cursor.len} + +static +int +size(char *s) +{ + rune c; + int n, len = 0;; + while((c=*s)){ + if(c == '\033'){ + n = 1; + esccode: + c = s[n]; + if(!c) // we hit end of string in the middle of parsing an escape code! + return len; + if(c == 'm'){ + s += n + 1; + continue; // outer loop + } + n++; + goto esccode; + } + n = utf8·decode(s, &c); + s += n; + len += utf8·runewidth(c); + } + return len; +} + +/* 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, intptr len, char *prompt) +{ + int n, aux; + char esc[3]; + char c[UTFmax+1] = { 0 }; + rune r; + + struct TerminalState term; + /* + * populate the state that we pass to functions implementing + * specific editing functionalities + */ + term.ifd = ifd; + term.ofd = ofd; + + term.edit.buf = buf; + term.edit.cap = len; + term.edit.len = 0; + term.edit.pos = 0; + + term.prompt.s = prompt; + term.prompt.len = strlen(prompt); + term.prompt.size = size(prompt); + + term.cursor.pos = 0; + term.cursor.len = 0; + term.cursor.cap = columns(ifd, ofd); + + term.maxrows = 0; + term.history = 0; + + term.yank.buf = nil; + term.yank.cap = term.yank.len = 0; + + /* buffer starts empty. */ + term.edit.buf[0] = '\0'; + term.edit.cap--; /* make sure there is always space for the nulterm */ + + /* push current (empty) command onto history stack */ + addhistory(""); + + if(write(term.ofd,prompt,term.prompt.len) == -1) + return -1; + + for(;;){ + n = read(term.ifd,c,1); + if(n <= 0) + goto finish; + + /* partition input by rune */ + if(utf8·onebyte(c[0])){ + r = c[0]; + }else if(utf8·twobyte(c[0])){ + n = read(term.ifd,c+1,1); + if(n < 1 || (n=utf8·decode(c, &r)) != 2) + goto finish; + }else if(utf8·threebyte(c[0])){ + n = read(term.ifd,c+1,2); + if(n < 2 || (n=utf8·decode(c, &r)) != 3) + goto finish; + }else if(utf8·fourbyte(c[0])){ + n = read(term.ifd,c+1,3); + if(n < 3 || (n=utf8·decode(c, &r)) != 4) + goto finish; + }else + goto finish; + + switch(r){ + case KeyEnter: + pophistory(); + if(mode.multiline) + move(&term, END(term)); + goto finish; + + case KeyCtrlC: + errno = EAGAIN; + return -1; + + case KeyBackspace: + case KeyCtrlH: + delete(&term, left(&term, 1)); + break; + + case KeyCtrlD: + if(term.edit.len > 0) + delete(&term, right(&term, 1)); + break; + + case KeyCtrlT: + if(term.edit.pos > 0 && term.edit.pos < term.edit.len){ + aux = buf[term.edit.pos-1]; + + buf[term.edit.pos-1] = buf[term.edit.pos]; + buf[term.edit.pos] = aux; + + if(term.edit.pos != term.edit.len-1) + term.edit.pos++; + + refreshline(&term); + } + break; + + case KeyCtrlB: + move(&term, left(&term, 1)); + break; + + case KeyCtrlF: /* ctrl-f */ + move(&term, right(&term, 1)); + break; + + case KeyCtrlP: /* ctrl-p */ + usehistory(&term, +1); + break; + + case KeyCtrlN: /* ctrl-n */ + usehistory(&term, -1); + 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.edit.pos > 0){ + --term.edit.pos; + refreshline(&term); + } + continue; + } + } + case 1: + if(mode.vi.on){ + if(mode.vi.insert){ + normalmode(term.ofd); + if(vi(&term,esc[0]) < 0){ + term.edit.len = -1; + goto finish; + } + 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, left(&term,1)); + break; + } + } + }else{ + switch(esc[1]) { + case 'A': /* up */ + usehistory(&term, +1); + break; + case 'B': /* down */ + usehistory(&term, -1); + break; + case 'C': /* right */ + move(&term, right(&term, 1)); + break; + case 'D': /* left */ + move(&term, left(&term, 1)); + break; + case 'H': /* home */ + move(&term, HOME(term)); + break; + case 'F': /* end*/ + move(&term, END(term)); + break; + } + } + } + /* ESC O sequences. */ + else if(esc[0] == 'O'){ + switch(esc[1]) { + case 'H': /* home */ + move(&term, HOME(term)); + break; + case 'F': /* end*/ + move(&term, END(term)); + break; + } + } + break; + + default: + if(mode.vi.on && !mode.vi.insert && n == 1){ + if(vi(&term,c[0]) < 0){ + term.edit.len = -1; + goto finish; + } + }else if(!insertrune(&term,n,c)){ + term.edit.len = -1; + goto finish; + } + + break; + + case KeyCtrlU: /* Ctrl+u, delete the whole line. */ + buf[0] = '\0'; + term.edit.pos = term.edit.len = 0; + term.cursor.pos = term.cursor.len = 0; + refreshline(&term); + break; + + case KeyCtrlK: /* Ctrl+k, delete from current to end of line. */ + buf[term.edit.pos] = '\0'; + term.edit.len = term.edit.pos; + term.cursor.len = term.cursor.pos; + refreshline(&term); + break; + + case KeyCtrlA: /* Ctrl+a, go to the start of the line */ + move(&term, HOME(term)); + break; + + case KeyCtrlE: /* ctrl+e, go to the end of the line */ + move(&term, END(term)); + break; + + case KeyCtrlL: /* ctrl+term, clear screen */ + clear(); + refreshline(&term); + break; + + case KeyCtrlW: /* ctrl+w, delete previous word */ + delete(&term, prevword(&term,1)); + break; + } + } +finish: + efree(term.yank.buf); + return term.edit.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, intptr 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(&runner->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 + runner->cmd.io->e = runner->cmd.io->b = runner->cmd.io->buf; + + if(!shell.interactive) + return notty(); + + if((n = raw(runner->cmd.io->e, runner->cmd.io->cap-1, prompt)) == -1) + return 0; + runner->cmd.io->e += n; + + /* insert a newline character at the end */ + put(&runner->cmd.io, '\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