笔记1:Raw mode
自己写的代码Gitee链接(持续更新中):
https://gitee.com/lakmiu/text-editor
源地址链接:
https://viewsourcecode.org/snaptoken/kilo/index.html
零,基础功能
#include <unistd.h>
int main() {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q');
return 0;
}
STDIN_FILENO:标准输入的文件描述符,从键盘上获取数据。
一,关闭回显
#include <termios.h>
#include <unistd.h>
void enableRawMode() {
struct termios raw;//存储终端的当前设置
tcgetattr(STDIN_FILENO, &raw);//读取标准输入终端的属性存储到raw中
raw.c_lflag &= ~(ECHO);//修改本地标志字段,关闭回显功能
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);//将修改后的传入标准输入终端
//tcsaflush在更改属性前刷新所有的输入输出
}
int main() {
enableRawMode();//进入原始模式
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q');
return 0;
}
知识:
c_lflag是local flags
c_iflag
(input flags),
c_oflag
(output flags),
c_cflag
(control flags)
ECHO bitflag 00000000000000000000000000001000
&11111111111111111111111111110111
=00000000000000000000000000000000
tip:
1.离开了程序之后还是不显示,输入reset按enter
二,退回原始功能
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
struct termios orig_termios;
void disableRawMode() {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}//将终端设置为一开始保存的设置
void enableRawMode() {
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(disableRawMode);//在退出时执行这个disableRawMode函数
// called automatically
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);//设置
}
int main() {
enableRawMode();
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q');
return 0;
}
知识:
- atexit(disableRawMode) 可以确保在程序退出时,我们将保留终端属性。
三,关闭标准模式
更改逐行读入为逐字节读入,这样程序在读入到q的时候就会立即退出
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
struct termios orig_termios;
void disableRawMode() {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}//将终端设置为一开始保存的设置
void enableRawMode() {
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(disableRawMode);//在退出时执行这个disableRawMode函数
// called automatically
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON);//
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);//设置
}
int main() {
enableRawMode();
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q');
return 0;
}
但是现在不显示什么东西
知识:
- C语言标准输入立即读取(ICANON)
raw.c_lflag &= ~(ECHO | ICANON);
四,展示键盘相应
先来展示一下ASCII码
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
if (iscntrl(c)) {
printf("%d\n", c);
} else {
printf("%d ('%c')\n", c, c);
}
}
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include<stdio.h>
#include<ctype.h>
struct termios orig_termios;
void disableRawMode() {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}//将终端设置为一开始保存的设置
void enableRawMode() {
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(disableRawMode);//在退出时执行这个disableRawMode函数
// called automatically
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON);//
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);//设置
}
int main() {
enableRawMode();
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
if (iscntrl(c)) {
printf("%d\n", c);
} else {
printf("%d ('%c')\n", c, c);
}
}
return 0;
}
知识:
https://www.runoob.com/w3cnote/ascii.html
ASCII控制字符的编号范围是0-31和127(0x00-0x1F和0x7F),共33个字符。
这里iscntrl()测试是不是控制字符
printf()可以打印很多但是
现象:
五,关闭ctrl -z -c -s -q -v 修复-M
关闭c z
raw.c_lflag &= ~(ECHO | ICANON | ISIG);
关闭 s q
raw.c_iflag &= ~(IXON);
有的操作系统键入ctrl -v的时候会wait for you to type another character and then sends that character literally.
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
修复-M
raw.c_iflag &= ~(ICRNL | IXON);
It turns out that the terminal is helpfully translating any carriage returns (13
, '\r'
) inputted by the user into newlines (10
, '\n'
). Let’s turn off this feature.
代码:
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include<stdio.h>
#include<ctype.h>
struct termios orig_termios;
void disableRawMode() {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}
void enableRawMode() {
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(disableRawMode);
struct termios raw = orig_termios;
raw.c_iflag &= ~(ICRNL | IXON);
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
int main() {
enableRawMode();
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
if (iscntrl(c)) {
printf("%d\n", c);
} else {
printf("%d ('%c')\n", c, c);
}
}
return 0;
}
六,关闭所有的输出处理
终端在输出端做了类似的转换。它将打印的每个换行符(“\n”)转换成后跟换行符(“\r\n”)的回车。终端需要这两个字符才能开始新的文本行。
我们将通过关闭post标志来关闭所有的输出处理功能。在实践中,“\n”到“\r\n”的转换可能是默认情况下打开的唯一输出处理功能。
raw.c_oflag &= ~(OPOST);
结果我们只看到光标向下移动该怎么办?在printf()中加入回车
while (read(STDIN_FILENO, &c, 1) == 1 && c != 'q') {
if (iscntrl(c)) {
printf("%d\r\n", c);
} else {
printf("%d ('%c')\r\n", c, c);
}
}
七,Miscellaneous flags
raw.c_cflag |= (CS8);
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
设置目标:
-
When
BRKINT
is turned on, a break condition will cause aSIGINT
signal to be sent to the program, like pressingCtrl-C
. -
INPCK
enables parity checking, which doesn’t seem to apply to modern terminal emulators. -
ISTRIP
causes the 8th bit of each input byte to be stripped, meaning it will set it to0
. This is probably already turned off. -
CS8
is not a flag, it is a bit mask with multiple bits, which we set using the bitwise-OR (|
) operator unlike all the flags we are turning off. It sets the character size (CS) to 8 bits per byte. On my system, it’s already set that way.
八,a timeout for read()
We can set a timeout, so that read()
returns if it doesn’t get any input for a certain amount of time.
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
while (1) {
char c = '\0';
read(STDIN_FILENO, &c, 1);
if (iscntrl(c)) {
printf("%d\r\n", c);
} else {
printf("%d ('%c')\r\n", c, c);
}
if (c == 'q') break;
}
VMIN
值设置了read()可以返回之前所需的最小输入字节数。我们将其设置为0,这样只要有任何输入要读取,read()就会返回。
VTIME
值设置read()返回前等待的最大时间。它的单位是十分之一秒,所以我们把它设为十分之一秒,即100毫秒。如果read()超时,它将返回0,这是有意义的,因为它的通常返回值是读取的字节数。
九,Error handling
现在已经充分进入raw mode,那么我们增加error handling
void die(const char *s) {
perror(s);
exit(1);
}
翻译:
大多数失败的C库函数将设置global errno
变量来指示错误是什么。Perror()查看globa errno
变量,并为它打印一条描述性错误消息。它还在打印错误消息之前打印给定的字符串,这意味着提供有关代码的哪一部分导致错误的上下文。
在打印出错误消息后,我们退出程序,退出状态为1,这表示失败(任何非零值也表示失败)。
让我们检查每个库调用是否失败,并在它们失败时调用die()。
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 | ICRNL | INPCK | ISTRIP | IXON);
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");
}
if (read(STDIN_FILENO, &c, 1) == -1 && errno != EAGAIN) die("read");
验证方法:
tcgetattr()函数用于获取终端的属性。当你的程序以文本文件或管道作为标准输入而不是终端时,tcgetattr()会失败。
1.要以文件作为标准输入运行程序,请运行以下命令:./kilo <kilo.c
。这将把kilo.c文件的内容作为标准输入传递给程序。
2.要以管道作为标准输入运行程序,请运行以下命令:echo test | ./kilo
。这将把"test"字符串通过管道传递给程序。
无论是使用文件还是管道作为标准输入,都会导致tcgetattr()返回类似的错误信息:“Inappropriate ioctl for device”。这意味着你正在尝试对一个不支持的设备执行ioctl操作。
十,Sections
split code into section
在这里呈现完整的代码、
/*** includes ***/
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include<stdio.h>
#include<ctype.h>
#include<errno.h>
/*** 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 | ICRNL | ISTRIP |IXON);
raw.c_lflag &= ~(OPOST);
raw.c_cflag |=CS8;
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 10;
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\n\r", c);
} else {
printf("%d ('%c')\n\r", c, c);
}
if(c=='q') break;
}
return 0;
}