动手写编辑器(一) ~ 文本模式
Table of Contens
写作动机
因为是该项目的第一篇文章,在这里简单介绍实现该项目的动机。作为程序员,动手能力是 第一位的,不管看了多少经典书籍、亦或浏览过多少源码。如果没有自己动手去实现,这些 东西都不是自己的。本人可以说是一个编辑器爱好者吧,前前后后折腾过 N 多编辑器: 如编辑器之神 Vim, 神的编辑器 Emacs, 性感无比 Sublime text 以及宇宙最强 VSCode. 最终本人留下了 emacs 和 VSCode , 前者用于编程和写作,后者用于调试程序。 用了这么多的编辑器,是时候自己实现一个了。当然并不打算参与竞争,单纯为了学习。再 说了,目前我的 emacs+doom 已经能处理各种写作任务,而且我很享受在上面码字,就想 弹钢琴一样,无须鼠标干扰,安心写作。Ok~ 让我们开始一段奇妙的旅行吧~~~
规范模式
我们平常使用的交互式命令终端就是所谓的 canonical mode
。简单来说就是,它每次接收一行文本,
允许通过退格键进行删除输入的内容,以 enter
作为发送触发键发送给程序,然后程序
对其进行响应。这种模式大多用于交互命令处理中。但是用在文本编辑器(如 nano)中
显然是不合适,因为它要处理的是每一个按键(包括 enter)。
文本模式
文本模式(raw mode)可以不处理 enter
触发的发送功能,而是把其作为键值进行处理。这个才
接近文本编辑器的要求。但是呢,终端默认都是规范模式,而且这两个模式之间的差别不仅
仅是这一个,而且要完全切换会涉及很多的标志位的设置。下面我们就一步步来让终端来实
现期望的文本模式。
设置属性
上一章节提到文本模式和规范模式中间隔了很多 flag
,这些 flag
其实就是一些属性,我们通
过修改这些属性的值来实现我们的目标。终端的属性是通过 struct termios
类型来描述
的,我们先看看它的基本组成:
// /usr/include/bits/termios.h
struct termios
{
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
}
从注释中容易看出, termios
是一些 flag
的集合,其中和文本模式相关的主要有
c_lflag
, c_iflag
, c_oflag
。下面我们将分别从这三个标志集合中提取出和文本
模式相关的 flag
。
在终端启动的时候,系统会为其初始化 termios
, 我们需要通过 tcgetattr
获取这些
初始化值,切记不能自己初始化一个新实例,然后设置相关标志,最后用这个新实例去替换
原始的初始值。这可能会引发许多问题,一些标志的可能并不是我们提供的值,因此会修改
很多我们未知的属性。正确的做法是先获取属性值,然后修改我们需要改的属性,再来通过
tcsetattr
替换系统的 termios
。这不是源码分析博客,这两个函数就不介绍了。
本地模式标志 ~~ c_lflag
从名字可以推断这是和本地(local)相关的 flag 集合。 先看看它的定义以及拥有的属性标志吧。
typedef unsigned int tcflag_t;
...
/* c_lflag bits */
#define ISIG 0000001
#define ICANON 0000002
#if defined __USE_MISC || (defined __USE_XOPEN && !defined __USE_XOPEN2K)
# define XCASE 0000004
#endif
#define ECHO 0000010
#define ECHOE 0000020
#define ECHOK 0000040
#define ECHONL 0000100
#define NOFLSH 0000200
#define TOSTOP 0000400
#ifdef __USE_MISC
# define ECHOCTL 0001000
# define ECHOPRT 0002000
# define ECHOKE 0004000
# define FLUSHO 0010000
# define PENDIN 0040000
#endif
#define IEXTEN 0100000
#ifdef __USE_MISC
# define EXTPROC 0200000
#endif
c_lflag 被定义为 unsigned int
类型,也就是 32 位,那么它就可以表示 32 个
flag
。下面就一个个来关闭相关的标志位。
关闭规范模式
规范的英文为 canonical
, 从上面的标志中, ICANON
是规范模式标志,默认
是开启的规范模式, 所以我们只要将该 flag 取反后与 termios
相与就可以将该
属性去掉。即:
termios.c_lflag &= ~ICANON;
关闭回显
我们有时候可能不需要将输入文字打印到显示器,比如密码。这就要求我们把回显关闭:
termios.c_cflag &= ~ECHO;
关闭信号
我们知道再终端要终止正在运行的程序,直接 C+c
(C表示 ctrl 键)就可以退出。
这个过程其实是向运行进程发了一个 SIGINT
信号。类似的还有暂停信号 C+z
,
termios.c_cflag &= ~ISIG;
关闭 C+v
下该快捷键可以实现后面的输入字符逐字符的发送,例如输入处理 C+v
再输入 C+c
会被识别为三个字符,而不会发送信号)。由 IEXTEN 标志。
termios.c_cflag &= ~IEXTEN;
输入模式标志 ~~ c_iflag
和本地模式一样定义为 unsigned int
类型。我们主要看它提供哪些标志。
/* c_iflag bits */
#define IGNBRK 0000001
#define BRKINT 0000002
#define IGNPAR 0000004
#define PARMRK 0000010
#define INPCK 0000020
#define ISTRIP 0000040
#define INLCR 0000100
#define IGNCR 0000200
#define ICRNL 0000400
#define IUCLC 0001000
#define IXON 0002000
#define IXANY 0004000
#define IXOFF 0010000
#define IMAXBEL 0020000
#define IUTF8 0040000
禁用流控制
C+s
和 C+q
是用来控制流的快捷键,比如说,我们运行某个程序一直有输出流,而此
时你不想看这些输出流。那么使用 C+s
就可以关闭输出流,相反 C+q
可以让输出流再次打印
到终端显示器。由 IXON
标志。
termios.c_iflag &= ~IXON;
固定
C-m
组合会发送 ‘\n’ 字符,也就是 enter
键。通过 ICRNL
标志。
termios.c_iflag &= ~ICRNL;
输出模式标志 ~~ c_oflag
同样的,先看看提供哪些标志位。
/* c_oflag bits */
#define OPOST 0000001
#define OLCUC 0000002
#define ONLCR 0000004
#define OCRNL 0000010
#define ONOCR 0000020
#define ONLRET 0000040
#define OFILL 0000100
#define OFDEL 0000200
#if defined __USE_MISC || defined __USE_XOPEN
# define NLDLY 0000400
# define NL0 0000000
# define NL1 0000400
# define CRDLY 0003000
# define CR0 0000000
# define CR1 0001000
# define CR2 0002000
# define CR3 0003000
# define TABDLY 0014000
# define TAB0 0000000
# define TAB1 0004000
# define TAB2 0010000
# define TAB3 0014000
# define BSDLY 0020000
# define BS0 0000000
# define BS1 0020000
# define FFDLY 0100000
# define FF0 0000000
# define FF1 0100000
#endif
#define VTDLY 0040000
#define VT0 0000000
#define VT1 0040000
#ifdef __USE_MISC
# define XTABS 0014000
#endif
再终端的所有的输出中,它都会将 ‘\n’ 转换为 ‘\r\n’ 。因此我们每次输入一行文本,按
下 enter
键后,它总是出现在下一行的最前面,这里其实涉及两个动作:回车和换行。
但在编辑器中我们要避开这种转换。通过关闭 OPOST
标志关闭。
termios.c_oflag &= ~OPOST;
杂项标志
这主要是根据不同的系统以及版本历史的差异导致的不一致,可能有的系统初始话已经关闭 了,为了保险起见,这里统一重新关闭,也为了兼容差异吧。具体细节就不解释了。
termios.c_cflag |= (CS8);
termios.c_iflag &= ~(BRKINT | INPCK | ISTRIP);
开启和关闭文本模式
上面我们提到要开启文本模式,只需要设置相应的标志即可,我们一个一个去设置或者每次 去设置,这是不可取的,因次我们把它放在一个函数里一起处理。
void enable_raw_mode() {
if (tcgetattr(STDIN_FILENO, &_orig_termios) != 0) {
SPDLOG_ERROR("tcgetattr");
}
atexit(disable_raw_mode);
struct termios raw = _orig_termios;
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);
raw.c_oflag &= ~(OPOST);
raw.c_cflag &= ~(CS8);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) {
SPDLOG_ERROR("tcsetattr");
};
}
当然从我们编辑中退出后,那么必须关闭文本模式,使终端回到规范模式。
inline void disable_raw_mode() {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &_orig_termios) != 0) {
SPDLOG_ERROR("tcgetattr");
}
}
总结
这篇博客只是开篇,总结了终端编辑器应该有的模式以及如何设置,同时在写作的过程中可 能会涉及错误的处理,由于本项目重点不是错误处理,因此使用 spdlog 库来处理相关的错 误信息。好了,开篇写完了~~~
参考
https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html