这两天跟着gemini的推荐读了一个1k行左右的项目kilo,是redis的作者在16年使用纯c写的一个简单的tui文本编辑器,没有使用额外的终端库(例如<ncurses.h>),而是直接使用控制字符来控制终端行为。
在ai时代读这个,感觉就是在考古,有考古的乐趣哈哈。
代码还算易读,错误其实蛮多,毕竟作者说自己只花了几个小时在这个项目(单文件)上。
一些收获
- 核心数据结构是一个全局结构体变量,最重要的字段是光标的坐标,行和列的偏移量以及一个rows数组,数组的每个元素包含指向该行实际内容的
char *,指向渲染内容(将tab渲染成八个空格)的char *和指向渲染内容对应的高亮类型的char *。 - 当某一行的多行注释状态变化时(比如从在行首添加了多行注释的起始符),需要将此变化传播给后面的行。
- 通过向stdin写字符流来控制终端的行为,其中包含控制字符和可打印字符。
realloc:尽可能原地满足缩扩容,如果不行则寻找全新的内存块,将旧数据搬运过去并且释放旧内存。若找不到新的内存块,返回null,旧内存不会被释放。- 可变长参数:本身不包含数量的信息,需要额外的计数,只能使用
cdecl,由调用者清理栈帧。实现原理是去栈上参数的位置进行指针运算,在使用寄存器传参的时候则更复杂。 atexit:在调用exit()的时候触发这个钩子。SIGWINCH:当终端的大小改变时,内核会给运行在终端内的程序发送该信号。
一些改动
- 一些简单的逻辑错误就懒得说了,毕竟不重要。
- 当kilo退出的时候,终端上留有kilo的残影,可以通过交替屏幕解决,即在进入raw mode的时候发送
\x1b[?1049h来进入一个临时的屏幕,在退出时发送\x1b[?1049l退出临时屏幕。仓库里面的todo文件也提到了这一点。 - 当我改变终端大小时整个程序会直接退出,经过测试发现是当我们不输入的时候,程序会卡在while等待输入 此时遇到了
1
2while ((nread = read(fd,&c,1)) == 0);
if (nread == -1) exit(1);SIGWINCH信号,read会被强制打断返回-1,修复方法是判断全局的errno是否等于EINTR,若是则重新尝试read。修复之后发现还是有错位错行的问题,懒得修了QAQ。