读取输入并返回自定义的键值
根据上一章的原始模式的介绍,我们知道终端读取键盘的输入和我们想象的并不一样
普通字符很正常,读到啥就是啥,但对于一些功能键,比如说
原始字符
如果输入的是普通字符,我们直接返回对应的整数值就好了
<Ctrl>组合键
定义一个宏函数#define CTRL_KEY(k) ((k) & 0x1f)
,将字符k的高3位清0,就可以将对应的字符转化成控制序列了
比如说CTRL_KEY(q)
其实就等于Ctrl+Q
,Ctrl+A其实就是1,Ctrl+B其实就是2,以此类推,26个字符,就是1到26
因为控制序列 Ctrl+字母 的组合就只有26个,所以我们将字符k的高3位清0,只保留第5位就已经可以表示0-31了,甚至还超过了
<Esc>组合键
这个也叫作转义序列,一般来说开头两个字节就是 \x1b 和 [,然后跟着字母或者数字就是键盘上对应的一些键了,比如说方向键、导航键等等
然后这些的键值我们是自定义的,向下面这样:
enum editorKey {
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
DEL_KEY,
HOME_KEY,
END_KEY,
PAGE_UP,
PAGE_DOWN
};
只要不与键盘上的这些普通字符、控制序列冲突就没事了,其实就是大于127就好了,因为8位比特最多表示到127。我们这里设的大一点也没事
总结
读取的输入类型,主要就是这三类了,我们需要注意的就是根据终端读取到的输入(1个字节或两个字节或三个字节或以上)进行判断,然后返回对应的键值就好了
(普通字符直接返回对应的整数值,控制序列返回宏函数的值,转义序列返回我们自定义的枚举值)
处理输入,根据键值进行功能映射
首先这是存储编辑器信息的结构体
struct editorConfig { /* 存储编辑器的信息 */
int cx, cy; /* 存储光标位置 */
int screenrows; /* 存储屏幕行数 */
int screencols; /* 存储屏幕列数 */
struct termios orig_termios; /* 存储终端原来的属性 */
};
extern struct editorConfig E;
光标移动
-
光标位置的改变:
在我们的编辑器中,所谓光标的移动其实就是光标的重新定位啦,我们根据 E.cx 和 E.cy 的值在每次刷新屏幕的时候重新定位光标就可以啦所以我们只需根据按下的键来改编cx和cy两个变量的值,在重定位就能实现光标的移动啦
-
光标移动限制
我们的光标的坐标值是不能超过整个屏幕的范围的哦,除非将屏幕进行滑动,不过目前还没到这一步,所以我们要限制光标坐标的范围
然后对于那些导航键而言,其实就是多次的单位移动啦,比如说屏幕有20行,我们按下 Home 键就能将光标定位到最后一行,实现起来就是一个循环,只要光标没到达最后一行,就往下移动一格,相当于按了方向键中的下键一样
屏幕滚动
屏幕的滚动分为垂直和水平,要实现这个功能我们主要分两步走
- 光标定位
这一步是说,当我们进行滚动的时候,本质上是光标移动到屏幕之外,然后我们通过光标重新对屏幕进行校正,所以这个时候就需要两个变量 rowoff和coloff,分别记录行偏移量和列偏移量,简单来说就是记录屏幕顶部和屏幕左部具体是哪一行 - 更新屏幕显示内容
有了rowoff和coloff这两个变量后,我们就能在屏幕上正确的输出信息了,比如屏幕顶部就是 0+rowoff 行的内容,第2行就是 1+rowoff 行的内容等等
屏幕的滚动也是要设置限制的
- 水平滚动的限制就是不超过当前行的最后一个字符的下一个字符,这个位置方便我们进行插入字符操作
- 我们可以通过光标的纵坐标知道我们当前在哪一行,然后限制光标水平移动时不能超过当前行的字符数
- 也可以在此基础上增加相邻行之间的跳转,比如当前行的末尾继续向右移动就跳转到下一行的开头,当前行的开头继续向左移动就跳转到上一行的末尾,注意正确处理边界限制
- 我们可以通过光标的纵坐标知道我们当前在哪一行,然后限制光标水平移动时不能超过当前行的字符数
- 垂直滚动的限制就是不超过最后一个可编辑行的下一行,这个位置方便我们进行插入新行操作
- 我们可以增加一个变量用于 记录编辑器可编辑的行数,然后以这个作为界限垂直滚动就可以了
字符的删除与插入
插入和删除字符其实就是对行缓冲区中的内容进行调整,插入就是一部分内容向后移动,留出一个空位,删除就是一部分内容向前移动
进一步就是可以通过删除进行行的减少,比如在行的开头继续退格能够跳到上一行末尾,这个需要注意的就是要注意字符串的拼接
行的增减
首先我们需要注意的是行缓冲区的分配与释放,这对应着行的增加和删除
其次还需要注意字符串的拼接,因为我们不一定是直接把一行清空,还可能是在一行的中间某部分一直退格从而跳转到上一行
特殊功能
- Ctrl+Q 退出编辑器,也就是退出程序
- Ctrl+S 将编辑内容保存到磁盘,实现的关键是先将行缓冲区中的内容转变为单个字符串,然后通过write函数写到指定文件中
缓冲区
追加缓冲区
其实就是考虑到调用wirte的开销,事实上我们可以将大的内容交给write一次性刷新到屏幕上,而不是多次调用write,一次只写一点内容
然后就创建了一个追加缓冲区的结构体,其实就是动态数组、字符串啦,指针指向分配内存的地址,然后还有一个长度变量,但创建这个的原因还是在于 追加 的特性,因为编辑器总是会时不时地添加或者删去内容,所以这个缓冲区必须是动态的
实现也很简单,就一个添加函数和释放内存的函数,添加函数内就是用realloc函数动态的扩充缓冲区大小来实现追加特性
行缓冲区
其实和追加缓冲区很像,只不过存储的带你为是一行的内容,可以将理解为多个行缓冲区组合成追加缓冲区
刷新屏幕
就是将 追加缓冲区 里的内容 循环 write到屏幕上即可
屏幕渲染问题
首先我们要先区分 文本缓冲区 和 渲染后的文本缓冲区 这两个概念
举个例子,比如说我在文本缓冲区中添加了\t这个制表符,那么它在其中也只是占用1个字节的位置,然后通过终端对其进行渲染,但这样的问题是编辑器对制表符的定义与可能与终端不符合,所以我们要自己进行渲染。那么在渲染后的文本缓冲区里,我们就能自主的将\t渲染成4个或8个空格这样
文本缓冲区是否渲染,对普通字符没什么影响,只不过增加个渲染后的文本缓冲区会更加符合我们的心意,让我们更加自主
除此之外,我们还需要 修复光标在处理制表符(tab)时的显示问题。当前,光标位置计算假设每个字符在屏幕上只占一个列位置,但制表符会占用多个列,也就是说当我们用方向键移动光标若是遇到制表符(未渲染),光标会直接跨过制表符(可能是8列,看终端如何解释),而不是向我们预想中的那样只移动一列
因此,需要引入一个新的水平坐标变量 E.rx 来解决这个问题,也就是说文本缓冲区和渲染后的文本缓冲区要用不同的水平坐标变量来定位,没有制表符时,两者相等,有制表符时,后者比前者大(因为我们将制表符渲染成空格,也就是相当于多了几个字节)
屏幕分区
- 编辑区:显示的是输入或读取的内容
- 状态栏:显示文本编辑过程中的一些状态,比如所在行的行号,文件名等等
- 消息栏:编辑器通知用户的消息,比如文件已保存等等
对屏幕进行分区也很简单,最开始的时候,我们将获取到的窗口大小存储在两个变量中,E.screerows存储的是长,也就是行数,然后后续的一系列操作都是在这个窗口中进行的,当我们要分区的时候直接减少 E.screenrows 的值就行了,这就是减少编辑区的行数,将剩下的行交给状态栏和消息栏
分区后,相应的绘制函数也要定义哦,所以我们这里要定义三个绘制函数(最终都是输送到追加缓冲区中,然后在刷新到屏幕上),并将其放到刷新屏幕函数中执行,这样才能正确显示
屏幕闪烁问题,优化体验
- 不频繁调用 write --->使用追加缓冲区
- 刷新前将光标隐藏,刷新后再显示光标
刷新屏幕思路解析
- 计算当前rowoff和coloff的值,因为输出到屏幕上的内容取决于这两个界定的范围
editorScroll();
- 创建一个追加缓冲区用于存储当前屏幕的输出内容
struct abuf ab = ABUF_INIT; /* 构造一个空缓冲区 */
- 刷新屏幕前隐藏光标,为了流畅性
abAppend(&ab, "\x1b[?25l", 6); /* 刷新屏幕前隐藏光标 */
/* l命令是关闭终端特性 */
/* 参数?25是光标控制码 */
- 将要输出的文本内容写入缓冲区中
editorDrawRows(&ab); /* 行 */
editorDrawStatusBar(&ab); /* 状态栏 */
editorDrawMessageBar(&ab); /* 消息栏 */
- 重新定位移动后的光标
char buf[32]; /* 存储写入终端的指令 */
snprintf(buf, sizeof(buf), "\x1b[%d;%dH", (E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1);
abAppend(&ab, buf, strlen(buf)); /* 定位光标 */
- 最后要添加到缓冲区的是,显示光标
abAppend(&ab, "\x1b[?25h", 6); /* 刷新屏幕后显示光标 */
/* h命令是关闭终端特性 */
/* 参数?25是光标控制码 */
- 将缓冲区中的内容写到屏幕上
write(STDOUT_FILENO, ab.b, ab.len); /* 这里才是真正的把波浪号画在屏幕上 */
- 释放缓冲区
abFree(&ab);
刷新屏幕就是不断的删除之前内容,在显示当前内容,就一直不断地重复,不论你有没有进行操作
补充
向终端发送指令
我们可以将特定的转移序列写入标准输出,这可以向终端发送指令
获取窗口大小
- 调用 带有 TIOCGWINSZ 请求的 ioctl()
- 通过将光标移动到最右下角,然后询问终端,返回的值就是窗口的长和宽
最后一行的波浪线的绘制
我们若是循环 画一个波浪线然后换行 ,这会导致画到最后一行后还是换行,使得屏幕上滑,新的空行成为了最后一行,看起来就像是最后一行没画一样,所以我们要特殊处理最后一行,就是不添加换行
清屏转变为循环清除一行
刷新屏幕其实就是先清屏,然后在把内容重新绘制上去,而我们用循环地清除每行和清屏的效果是一样的,只是这样我们会更有选择性
标签:字符,简易,编辑器,终端,缓冲区,屏幕,就是,我们,光标 From: https://www.cnblogs.com/winter-z/p/18354008