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+sC+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