---
tags: 研讀筆記, trace, C
---
# 研讀筆記 linenoise
[antirez/linenoise](https://github.com/antirez/linenoise)
#### `linenoise`
這個 function 是整個程式主要的 API,一開始會檢查是否是 `tty`,然後在看終端機是否有支援,有支援了話就會呼叫 `linenoiseRaw`。
```cpp=
char *linenoise(const char *prompt) {
char buf[LINENOISE_MAX_LINE];
int count;
if (!isatty(STDIN_FILENO)) {
/* Not a tty: read from file / pipe. In this mode we don't want any
* limit to the line size, so we call a function to handle that. */
return linenoiseNoTTY();
} else if (isUnsupportedTerm()) {
size_t len;
printf("%s",prompt);
fflush(stdout);
if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL;
len = strlen(buf);
while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
len--;
buf[len] = '\0';
}
return strdup(buf);
} else {
count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt);
if (count == -1) return NULL;
return strdup(buf);
}
}
```
#### `linenoiseRaw`
這個函式會先呼叫 `enableRawMode`,然後呼叫 `linenoiseEdit` 進入編輯模式,最後會呼叫 `disableRawMode`。
```cpp=
/* This function calls the line editing function linenoiseEdit() using
* the STDIN file descriptor set in raw mode. */
static int linenoiseRaw(char *buf, size_t buflen, const char *prompt) {
int count;
if (buflen == 0) {
errno = EINVAL;
return -1;
}
if (enableRawMode(STDIN_FILENO) == -1) return -1;
count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt);
disableRawMode(STDIN_FILENO);
printf("\n");
return count;
}
```
#### `enableRawMode`
這邊要先了解到 [Terminal mode](https://en.wikipedia.org/wiki/Terminal_mode),不同的模式下面會決定終端機要如何轉換(interpreted)字元。
- cooked mode 又稱 canonical mode,一般 linux terminal 的模式,在 data 傳入程式之前會先被預處理。
- raw mode 又稱 non-canonical mode,會直接把 data 傳到執行程式,不會對任何特殊字元預先做轉換。
一般的終端機是 line-based system,輸入的字元會被放到 buffer 直到收到 carriage return (Enter or Return),這個叫做 cooked,會允許特殊字元被先處理,可以看 stty(1) 像是 Ctrl+D, Ctrl+S, Backspace 這些都是,終端機的驅動會先 "cooks" 處理這些字元。
再來看看 `termios` 這個結構,是用來設定終端機的屬性,這邊會先用 `tcgetattr` 拿到原本終端機的屬性並且保存到 `orig_termios`,最後在 `disableRawMode` 的時候透過 `tcsetattr` 把 `orig_termios` 設定回去。
```cpp=
static int enableRawMode(int fd) {
struct termios raw;
if (!isatty(STDIN_FILENO)) goto fatal;
if (!atexit_registered) {
atexit(linenoiseAtExit);
atexit_registered = 1;
}
if (tcgetattr(fd,&orig_termios) == -1) goto fatal;
raw = orig_termios; /* 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;
rawmode = 1;
return 0;
fatal:
errno = ENOTTY;
return -1;
}
```
#### `linenoiseEdit`
這邊會用到一個結構 `linenoiseState` 用來記錄下一些在命令列編輯時的特定資訊,一開始會先對這些結構初始化。
```cpp=
struct linenoiseState {
int ifd; /* Terminal stdin file descriptor. */
int ofd; /* Terminal stdout file descriptor. */
char *buf; /* Edited line buffer. */
size_t buflen; /* Edited line buffer size. */
const char *prompt; /* Prompt to display. */
size_t plen; /* Prompt length. */
size_t pos; /* Current cursor position. */
size_t oldpos; /* Previous refresh cursor position. */
size_t len; /* Current edited line length. */
size_t cols; /* Number of columns in terminal. */
size_t maxrows; /* Maximum num of rows used so far (multiline mode) */
int history_index; /* The history index we are currently editing. */
};
```
這個主要就是用在編輯命令列上的文字,可以看到第 5 行這邊是先把 prompt 的文字寫到終端機,也就是 `hello>`。接著進入迴圈,每次迴圈第 10 行都會去 `read` 1 個字元放到變數 `c`,下面就會對這個字元做對應的動作。
第 12 行先判斷 `c == 9` 在 ASCII 當中 9 代表 `tab`,所以這邊如果是使用者輸入 tab,如果有註冊 autocomplete 的 callback function 就會執行 auto complete,呼叫 `completeLine`。
`completeLine` 做完後會回傳最後一個收到的字元,再往下看到 `switch`,會對應不同的輸入進行不同的行為,如果是一般的輸入字元的話會呼叫 `linenoiseEditInsert`,插入一個字元到游標目前位置,其他對應的動作如下。
- `linenoiseEditBackspace`
- `linenoiseEditDelete`
- `linenoiseEditMoveLeft`
- `linenoiseEditMoveRight`
- `linenoiseEditMoveHome`
- `linenoiseEditMoveEnd`
- `linenoiseEditHistoryNext`
- `linenoiseClearScreen`
```cpp=
static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt)
{
struct linenoiseState l;
...
if (write(l.ofd,prompt,l.plen) == -1) return -1;
while(1) {
char c;
int nread;
char seq[3];
nread = read(l.ifd,&c,1);
if (nread <= 0) return l.len;
if (c == 9 && completionCallback != NULL) {
c = completeLine(&l);
/* Return on errors */
if (c < 0) return l.len;
/* Read next character when 0 */
if (c == 0) continue;
}
switch(c) {
...
}
}
return l.len;
```
#### `completeLine`
第 6 行會呼叫 `completionCallback` 也就是一開始註冊的 callback function,可以先看到 `example.c` 第 45 行,有先透過 `linenoiseSetCompletionCallback` 先註冊好 callback function `completion`,所以在這邊呼叫 `completionCallback` 就是呼叫 `completion`。
第 7 行開始判斷 `lc` 是否為 0,如果是 0 代表現在輸入的這個 buffer 沒有 match 到任何 completion 的規則,就會呼叫 `linenoiseBeep`。
如果有的話就會進入到 `while` 迴圈當中,這邊就會根據輸入的字元做對應的動作,按一次 `tab` 就會去 `lc.cvec` 裡面的字串做 auto complete,如果還有就會繼續補下去,`lc.cvec` 裡面的全部做完會回到 `i` 會回到 0,就會是原本的輸入。
如果輸入是 `escape` 或是其他字元,`while` 迴圈就會停止,並且把最後收到的字元 `return`。
```cpp=
void completion(const char *buf, linenoiseCompletions *lc) {
if (buf[0] == 'h') {
linenoiseAddCompletion(lc,"hello");
linenoiseAddCompletion(lc,"hello there");
}
}
```
```cpp=
static int completeLine(struct linenoiseState *ls) {
linenoiseCompletions lc = { 0, NULL };
int nread, nwritten;
char c = 0;
completionCallback(ls->buf,&lc);
if (lc.len == 0) {
linenoiseBeep();
} else {
size_t stop = 0, i = 0;
while(!stop) {
/* Show completion or original buffer */
if (i < lc.len) {
struct linenoiseState saved = *ls;
ls->len = ls->pos = strlen(lc.cvec[i]);
ls->buf = lc.cvec[i];
refreshLine(ls);
ls->len = saved.len;
ls->pos = saved.pos;
ls->buf = saved.buf;
} else {
refreshLine(ls);
}
nread = read(ls->ifd,&c,1);
if (nread <= 0) {
freeCompletions(&lc);
return -1;
}
switch(c) {
case 9: /* tab */
i = (i+1) % (lc.len+1);
if (i == lc.len) linenoiseBeep();
break;
case 27: /* escape */
/* Re-show original buffer */
if (i < lc.len) refreshLine(ls);
stop = 1;
break;
default:
/* Update buffer and return */
if (i < lc.len) {
nwritten = snprintf(ls->buf,ls->buflen,"%s",lc.cvec[i]);
ls->len = ls->pos = nwritten;
}
stop = 1;
break;
}
}
}
freeCompletions(&lc);
return c; /* Return last read character */
}
```
#### `linenoiseAddCompletion`
這個函式式用來建立 auto complete 的內容,會透過剛剛在 `completion` 當中呼叫給定的字串來建立。這邊用到一個結構 `linenoiseCompletions`,變數 `cvec` 是指標的指標,用來把拿到的字串存起來,所以這邊開頭為 `h` 的輸入就會有 "hello"、"hello there" 兩個 auto complete。
```cpp=
typedef struct linenoiseCompletions {
size_t len;
char **cvec;
} linenoiseCompletions;
```
```cpp=
void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) {
size_t len = strlen(str);
char *copy, **cvec;
copy = malloc(len+1);
if (copy == NULL) return;
memcpy(copy,str,len+1);
cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1));
if (cvec == NULL) {
free(copy);
return;
}
lc->cvec = cvec;
lc->cvec[lc->len++] = copy;
}
```
#### `refreshLine`
這個是用來更新目前的 buffer 內容、游標位置到正確的位置。
有兩種模式 `MultiLine`、`SingleLine`,可以在一開始的時候指定,如果沒有指定了話會是 SingleLine mode,所以這邊就會進到 `refreshSingleLine`。
```cpp=
struct abuf {
char *b;
int len;
};
static void refreshSingleLine(struct linenoiseState *l) {
char seq[64];
size_t plen = strlen(l->prompt);
int fd = l->ofd;
char *buf = l->buf;
size_t len = l->len;
size_t pos = l->pos;
struct abuf ab;
while((plen+pos) >= l->cols) {
buf++;
len--;
pos--;
}
while (plen+len > l->cols) {
len--;
}
abInit(&ab);
/* Cursor to left edge */
snprintf(seq,64,"\r");
abAppend(&ab,seq,strlen(seq));
/* Write the prompt and the current buffer content */
abAppend(&ab,l->prompt,strlen(l->prompt));
if (maskmode == 1) {
while (len--) abAppend(&ab,"*",1);
} else {
abAppend(&ab,buf,len);
}
/* Show hits if any. */
refreshShowHints(&ab,l,plen);
/* Erase to right */
snprintf(seq,64,"\x1b[0K");
abAppend(&ab,seq,strlen(seq));
/* Move cursor to original position. */
snprintf(seq,64,"\r\x1b[%dC", (int)(pos+plen));
abAppend(&ab,seq,strlen(seq));
if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */
abFree(&ab);
}
```
這邊有用到一個結構 `abuf` 是用來 append buffer,目的是要一次更新完到終端機上,避免文字在螢幕上閃爍。
這邊還要先了解到 [ANSI escape code](https://en.wikipedia.org/wiki/ANSI_escape_code) 是一種制定的標準,還可以搭配 [VT100、ANSI 碼說明](https://www.csie.ntu.edu.tw/~r88009/Java/html/Network/vt100.htm)。用來控制終端機的游標位置、顏色等,大多數已 `ESC` 跳脫字元和 `[` 字元開始,後面跟著參數,可以看 wiki 的說明如下。
> The general format for an ANSI-compliant escape sequence is defined by ANSI X3.41 (equivalent to ECMA-35 or ISO/IEC 2022). The ESC (27 / hex 0x1B / oct 033) is followed by zero or more intermediate "I" bytes between hex 0x20 and 0x2F inclusive, followed by a final "F" byte between 0x30 and 0x7E inclusive
一般的格式是由 `ESC`(二進位 27 十六進為 0x1B) 開頭,第二個位元則是 0x40–0x5F (ASCII `@A–Z[\]^_`) 範圍內的字元,最後一個則是範圍 0x30-0x7E。
```c
snprintf(seq,64,"\r");
abAppend(&ab,seq,strlen(seq));
```
這邊一開始先用 `\r` carriage return 把游標位置回到最左邊。
```c
abAppend(&ab,l->prompt,strlen(l->prompt));
abAppend(&ab,buf,len);
```
這邊是把 prompt 和原本的 buffer 內容寫回去。
```c
snprintf(seq,64,"\x1b[0K");
abAppend(&ab,seq,strlen(seq));
```
這邊的 `0K` 對應到的動作是 EL (Erase in Line),n 為 0 的時候會從游標的位置開始清除到最後行尾。
```c
snprintf(seq,64,"\r\x1b[%dC", (int)(pos+plen));
abAppend(&ab,seq,strlen(seq));
```
這邊則是先把游標先移到最左,這邊的動作是 CUF (Cursor Forward),是把游標向右移動 n 個,然後 `%d` 這邊是吃 `pos+plen`,也就是從最左邊移動到原本的游標位置。
```c
if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */
```
最後一併 write 到終端機輸出。
#### `refreshShowHints`
這邊會呼叫到 `hintsCallback`,這個 callback function 也是在一開始就指定的,所以對應 `example.c` 的 function 就是 `hints`,會設定好顏色、粗細,回傳 hint 的字串。
```cpp=
char *hints(const char *buf, int *color, int *bold) {
if (!strcasecmp(buf,"hello")) {
*color = 35;
*bold = 0;
return " World";
}
return NULL;
}
```
```cpp=
void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) {
char seq[64];
if (hintsCallback && plen+l->len < l->cols) {
int color = -1, bold = 0;
char *hint = hintsCallback(l->buf,&color,&bold);
if (hint) {
int hintlen = strlen(hint);
int hintmaxlen = l->cols-(plen+l->len);
if (hintlen > hintmaxlen) hintlen = hintmaxlen;
if (bold == 1 && color == -1) color = 37;
if (color != -1 || bold != 0)
snprintf(seq,64,"\033[%d;%d;49m",bold,color);
else
seq[0] = '\0';
abAppend(ab,seq,strlen(seq));
abAppend(ab,hint,hintlen);
if (color != -1 || bold != 0)
abAppend(ab,"\033[0m",4);
/* Call the function to free the hint returned. */
if (freeHintsCallback) freeHintsCallback(hint);
}
}
}
```
#### `linenoiseHistory` 相關
這是為了記錄下每次出入的指令,作法是用指標的指標 `history` 記錄下來每次輸入的字串。
```cpp=
static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN;
static int history_len = 0;
static char **history = NULL;
```
最一開始啟動的時候會呼叫 `linenoiseHistoryLoad`,去從實體檔案當中將內容讀出來到記憶體,
```cpp=
linenoiseHistoryLoad("history.txt"); /* Load the history at startup */
```
之後在每次輸入完指令都會呼叫 `linenoiseHistoryAdd` 新增一筆,然後用 `linenoiseHistorySave` 存到實體檔案中。
```cpp=
while((line = linenoise("hello> ")) != NULL) {
/* Do something with the string. */
if (line[0] != '\0' && line[0] != '/') {
printf("echo: '%s'\n", line);
linenoiseHistoryAdd(line); /* Add to the history. */
linenoiseHistorySave("history.txt"); /* Save the history on disk. */
} else if (!strncmp(line,"/historylen",11)) {
/* The "/historylen" command will change the history len. */
int len = atoi(line+11);
linenoiseHistorySetMaxLen(len);
} else if (!strncmp(line, "/mask", 5)) {
linenoiseMaskModeEnable();
} else if (!strncmp(line, "/unmask", 7)) {
linenoiseMaskModeDisable();
} else if (line[0] == '/') {
printf("Unreconized command: %s\n", line);
}
free(line);
}
```
然後透過 `linenoiseEditHistoryNext` 把當前的輸入改成上一個或下一個 `history` 的內容。