按 Ctrl-Q 退出

在上一章中,我们看到 Ctrl 键和字母键组合起来似乎映射到 1–26 字节。 我们可以使用它来检测 Ctrl 键组合并将其映射到编辑器中的不同操作。 首先,将 Ctrl-Q 映射到退出操作

附一张 ascii 码表

可以看到,ctrl+A-Z 都是有对应的值的,而且跟字母在字母表里的顺序一样

image.png
image.png

我们可以定义一个这样的宏来表示
#define CTRL_KEY(k) ((k)&0x1f)

这个 k & 0x1f 实际上是与 0001 1111 做了一个与运算(一共 26 个字母,最少要有五位才行啊)

image.png
image.png

那接下来只要 CTRL_KEY(q) == c 就可以退出了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*** includes ***/
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>

/*** defines ***/
#define CTRL_KEY(k)((k)&0x1f)

/*** data ***/
struct termios orig_termios;

/*** terminal ***/
void die(const char *s){
perror(s);
exit(1);
}

void disableRawMode(){
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1)
die("tcsetattr");
}

void enableRawMode(){
if(tcgetattr(STDIN_FILENO,&orig_termios) == -1) die("tcgetattr");
atexit(disableRawMode);
struct termios raw = orig_termios;
raw.c_iflag &= ~(BRKINT | INPCK | ISTRIP | IXON | ICRNL);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr");
}

/*** init ***/
int main(){
enableRawMode();
while(1){
char c = '\0';
if(read(STDIN_FILENO, &c, 1) == -1 && errno != EAGAIN) die("read");
if(iscntrl(c)){
printf("%d\r\n",c);
} else {
printf("%d ('%c')\r\n",c,c);
}
if(c == CTRL_KEY('q')) break;
}
return 0;
}

重构键盘输入

我们创建一个用于低级按键读取的功能,以及另一个将按键映射到编辑器操作的功能。 此时,我们还将停止打印按键

editorReadKey() 的工作是等待一次按键,然后将其返回。 稍后,我们将扩展该功能以处理转义序列,这涉及读取代表单个按键的多个字节,如箭头键一样
editorProcessKeypress() 等待按键,然后对其进行处理。 稍后,它将各种 Ctrl 键组合和其他特殊键映射到不同的编辑器功能,并将任何字母数字和其他可打印键的字符插入正在编辑的文本中
请注意,editorReadKey() 属于/ 终端 /部分,因为它处理底层终端输入,而 editorProcessKeypress() 属于新的/ 输入 /部分,因为它处理 将键映射到更高级别的编辑器功能
现在我们大大简化了 main(),我们将尝试保持这种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/*** includes ***/
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>

/*** defines ***/

#define CTRL_KEY(k)((k)&0x1f)

/*** data ***/

struct termios orig_termios;

/*** terminal ***/

void die(const char *s){
perror(s);
exit(1);
}

void disableRawMode(){
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1)
die("tcsetattr");
}

void enableRawMode(){
if(tcgetattr(STDIN_FILENO,&orig_termios) == -1) die("tcgetattr");
atexit(disableRawMode);
struct termios raw = orig_termios;
raw.c_iflag &= ~(BRKINT | INPCK | ISTRIP | IXON | ICRNL);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;

if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr");
}

char editorRedayKey(){
int nread;
char c;
while((nread = read(STDIN_FILENO, &c ,1))!=1){
if(nread == -1 && errno != EAGAIN) die("read");
}
return c;
}

/*** input ***/

void editorProcessKeypress(){
char c = editorRedayKey();
switch(c){
case CTRL_KEY('q'):
exit(0);
break;
}
}

/*** init ***/

int main(){
enableRawMode();

while(1){
editorProcessKeypress();
}
return 0;
}

清屏

向用户展示程序界面,从清空屏幕开始
加一个函数 editorRefreshScreen()

1
2
3
void editorRefreshScreen() {
write(STDOUT_FILENO, "\x1b[2J", 4);
}

然后再主函数里面调用它

write() 和 STDOUT_FILENO 来自 <unistd.h>
write() 调用中的 4 表示我们正在向终端写入 4 个字节。第一个字节是 \x1b,它是转义字符,或十进制的 27。 (尝试并记住\ x1b,我们将大量使用它。)其他三个字节为[2J
我们正在向终端写一个转义序列。转义序列始终以转义字符(27)开头,后跟[字符。转义序列指示终端执行各种文本格式化任务,例如为文本着色,四处移动光标以及清除屏幕的一部分。

我们正在使用 J 命令(在显示中擦除)来清除屏幕。转义序列命令带有参数,该参数位于命令之前。在这种情况下,参数为 2,表示要清除整个屏幕。 [1J 将清除屏幕直到光标所在的位置,而 [0J 将清除屏幕从光标直到屏幕结尾的位置。另外,0 是 J 的默认参数,因此仅 [J 本身也可以将屏幕从光标移到末尾。

重定位光标

现在清屏之后光标还是在原来的位置,把它重定位到 左上角
write(STDOUT_FILENO, "\x1b[H", 3);

此转义序列只有 3 个字节长,并使用 H 命令(光标位置)定位光标。 H 命令实际上接受两个参数:将光标定位在的行号和列号。 因此,如果您有一个 80×24 尺寸的终端,并且希望光标位于屏幕中央,则可以使用命令 [12; 40H(多个参数用 ; 字符分隔)H 的默认参数都碰巧为 1,因此我们可以将两个参数都省略掉,它将把光标定位在第一行和第一列,就像我们发送了< esc> [1;1H 命令(行和列的编号从 1 开始,而不是 0)

退出时清除屏幕

退出后直接清屏,然后定位光标到左上角(再 CTRL_KEY(‘q’) 这里加上这两句就行)
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);

另外打印错误信息的地方也要加上,报错的时候清一下屏幕

绘制波浪线

在屏幕左侧绘制一排波浪线就像 vim 那样?

1
2
3
4
5
6
void editorDrawRows() {
int y;
for (y = 0; y < 24; y++) {
write(STDOUT_FILENO, "~\r\n", 3);
}
}

加上个这个函数,然后再 刷新屏幕的函数里面调用一下,因为不知道窗口大小,所以先输出 24 个波浪线
然后再把光标定位到左上角

全局状态变量设置窗口大小

orig_termios 之前的这个结构体中可以找到当前窗口的大小,通过他来判断应该输入多少个波浪线

1
2
3
4
struct editorConfig {
struct termios orig_termios;
};
struct editorConfig E;

注意之前的 orig_termios 都要改成 E.orig_termios 了
我们可以通过 ioctl() (要引入这个 #include <sys/ioctl.h>)来获取窗口大小,首先定义一个获取窗口大小的函数

1
2
3
4
5
6
7
8
9
10
int getWindowSize(int *rows, int *cols) {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
return -1;
} else {
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
}

ioctl(),TIOCGWINSZ 和 struct winsize 来自 <sys/ioctl.h>
成功后,ioctl() 会将终端的列数宽和行数高处放入给定的 winsize 结构中。失败时,ioctl() 返回 -1。 我们还检查以确保返回的值不为 0,因为显然这可能是错误的结果。 如果 ioctl() 以任何一种方式失败,我们将通过返回 -1 来使 getWindowSize() 报告失败
如果成功,则通过设置传递给函数的 int 引用将值传递回去(这是使函数在 C 中返回多个值的常用方法。它还允许您使用返回值指示成功或失败)
现在,我们将 screenrows 和 screencols 添加到我们的全局编辑器状态,然后调用 getWindowSize() 来填充这些值

再上面定义的结构体中增加

1
2
int screenrows;
int screencols;

然后定义一个初始化的函数,如果 getWindowSize 返回 -1 的话就打印 getWindowSize,注意这里传参的时候用了取地址符,这样就能修改变量 screenrows 和 screencols 的值

1
2
3
void initEditor() {
if (getWindowSize(&E.screenrows, &E.screencols) == -1) die("getWindowSize");
}

当然还要在主函数中调用它一下,然后就可以把之前的绘制波浪线的 y 改成 E.screenrows 了

现在整个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/*** includes ***/
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>

/*** defines ***/

#define CTRL_KEY(k)((k)&0x1f)

/*** data ***/

struct editorConfig{
int screenrows;
int screencols;
struct termios orig_termios;
};
struct editorConfig E;//起一个别名?
/*** terminal ***/

void die(const char *s){
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
perror(s);
exit(1);
}

void disableRawMode(){
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.orig_termios) == -1)
die("tcsetattr");
}

void enableRawMode(){
if(tcgetattr(STDIN_FILENO,&E.orig_termios) == -1) die("tcgetattr");
atexit(disableRawMode);
struct termios raw = E.orig_termios;
raw.c_iflag &= ~(BRKINT | INPCK | ISTRIP | IXON | ICRNL);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;

if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr");
}

char editorRedayKey(){
int nread;
char c;
while((nread = read(STDIN_FILENO, &c ,1))!=1){
if(nread == -1 && errno != EAGAIN) die("read");
}
return c;
}

int getWindowSize(int *rows, int *cols){
struct winsize ws;
if(ioctl(STDOUT_FILENO,TIOCGWINSZ,&ws) == -1 || ws.ws_col == 0){
return -1;
} else {
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}

}

/*** output ***/

void editorDrawRows(){
int y;
for(y=0;y<E.screenrows;y++){
write(STDOUT_FILENO,"~\r\n",3);
}
}


void editorRefreshScreen(){
write(STDOUT_FILENO,"\x1b[2J",4);
write(STDOUT_FILENO,"\x1b[H",3);
editorDrawRows();
write(STDOUT_FILENO,"\x1b[H",3);
}


/*** input ***/

void editorProcessKeypress(){
char c = editorRedayKey();
switch(c){
case CTRL_KEY('q'):
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);

exit(0);
break;
}
}

/*** init ***/

void initEditor(){
if(getWindowSize(&E.screenrows,&E.screencols) == -1) die("getWindowSize");
}

int main(){

//initEditor();
enableRawMode();
initEditor();

while(1){
editorRefreshScreen();
editorProcessKeypress();

}
return 0;
}

获取窗口大小备用方法

因为没法保证上面那个方法一直能够使用,所以需要加一个备用方法,比较麻烦但是原理比较简单
首先把光标移动到右下角,然后输出来,因为没有方法能直接把光标移动到右下角,所以直接粗暴的往右下角移动

1
2
3
if (1 || ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) return -1;
editorReadKey();

我们依次发送两个转义序列。 C 命令(向前光标)将光标向右移动,而 B 命令(向下光标)将光标向下移动。该论点说明了将其向右或向下移动多少。我们使用一个非常大的值 999,该值应确保光标到达屏幕的右边缘和下边缘。
专门记录了 C 和 B 命令,以阻止光标越过屏幕边缘。我们之所以不使用 [999; 999H] 命令,是因为该文档没有指定尝试将光标移到屏幕外时会发生的情况
请注意,我们设置了一个 1 || 暂时置于 if 条件的最前面,以便我们可以测试我们正在开发的此后备分支
因为此时我们总是从 getWindowSize() 返回 -1(表示发生错误),所以我们对 editorReadKey() 进行了调用,因此我们可以在程序调用 die() 之前观察转义序列的结果,并清除屏幕。运行该程序时,您应该看到光标位于屏幕的右下角,然后当您按下某个键时,它会在清除屏幕后看到 die() 打印的错误消息

接下来,我们需要获取光标位置。 n 命令(设备状态报告)可用于向终端查询状态信息。我们想给它一个参数 6 来要求光标位置。然后,我们可以从标准输入中读取答复。让我们从标准输入中打印出每个字符,以查看回复的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int getCursorPosition(int *rows, int *cols) {
if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) return -1;
printf("\r\n");
char c;
while (read(STDIN_FILENO, &c, 1) == 1) {
if (iscntrl(c)) {
printf("%d\r\n", c);
} else {
printf("%d ('%c')\r\n", c, c);
}
}
editorReadKey();
return -1;
}

emmm 弄完就这样,补充上面的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*** includes ***/
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>

/*** defines ***/

#define CTRL_KEY(k)((k)&0x1f)

/*** data ***/

struct editorConfig{
int screenrows;
int screencols;
struct termios orig_termios;
};
struct editorConfig E;//起一个别名?
/*** terminal ***/

void die(const char *s){
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
perror(s);
exit(1);
}

void disableRawMode(){
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.orig_termios) == -1)
die("tcsetattr");
}

void enableRawMode(){
if(tcgetattr(STDIN_FILENO,&E.orig_termios) == -1) die("tcgetattr");
atexit(disableRawMode);
struct termios raw = E.orig_termios;
raw.c_iflag &= ~(BRKINT | INPCK | ISTRIP | IXON | ICRNL);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;

if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr");
}

char editorReadKey(){
int nread;
char c;
while((nread = read(STDIN_FILENO, &c ,1))!=1){
if(nread == -1 && errno != EAGAIN) die("read");
}
return c;
}

int getCursorPosition(int *rows,int *cols){
char buf[32];
unsigned int i = 0;
if(write(STDOUT_FILENO,"\x1b[6n",4) != 4) return -1;
while(i<sizeof(buf)-1){
if(read(STDIN_FILENO,&buf[i],1)!=1) break;
if(buf[i] =='R') break;
i++;
}
buf[i]='\0';

if(buf[0]!='\x1b' || buf[1]!='[')return -1;
if(sscanf(&buf[2],"%d%d",rows,cols)!=2)return -1;
return 0;

}

int getWindowSize(int *rows, int *cols){
struct winsize ws;
if(ioctl(STDOUT_FILENO,TIOCGWINSZ,&ws) == -1 || ws.ws_col == 0){
if(write(STDOUT_FILENO,"\x1b[999C\x1b[999B",12) != 12) return -1;

return getCursorPosition(rows,cols);
} else {
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}

}

/*** output ***/

void editorDrawRows(){
int y;
for(y=0;y<E.screenrows;y++){
write(STDOUT_FILENO,"~\r\n",3);
}
}


void editorRefreshScreen(){
write(STDOUT_FILENO,"\x1b[2J",4);
write(STDOUT_FILENO,"\x1b[H",3);
editorDrawRows();
write(STDOUT_FILENO,"\x1b[H",3);
}


/*** input ***/

void editorProcessKeypress(){
char c = editorReadKey();
switch(c){
case CTRL_KEY('q'):
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);

exit(0);
break;
}
}

/*** init ***/

void initEditor(){
if(getWindowSize(&E.screenrows,&E.screencols) == -1) die("getWindowSize");
}

int main(){

//initEditor();
enableRawMode();
initEditor();

while(1){
editorRefreshScreen();
editorProcessKeypress();

}
return 0;
}