#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(!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); } /* =========================== 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(&proc->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 proc->cmd.io->e = proc->cmd.io->b = proc->cmd.io->buf; if(!shell.interactive) return notty(); if((n = raw(proc->cmd.io->e, proc->cmd.io->cap-1, prompt)) == -1) return 0; proc->cmd.io->e += n; /* insert a newline character at the end */ put(&proc->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); }