lettershell项目链接 https://github.com/NevermindZZT/letter-shell
更多精彩内容欢迎关注微信公众号:码农练功房
往期精彩内容:
Lettershell之移植篇
Lettershell之命令注册
Lettershell之按键识别
输入缓冲区在Lettershell中扮演着十分关键的角色,它有如下重要作用:
- 暂存输入:在用户完全键入一条命令之前,输入缓冲区会存储用户的输入,直到用户按下回车键提交命令。
- 编辑能力:输入缓冲区通常支持基本的编辑功能,如向前或向后移动光标、删除字符等。
- 历史记录:用户可以通过向上箭头键访问先前输入的命令,这是通过维护一个输入历史记录列表实现的,而输入缓冲区是这个过程的一部分。
- 自动补全:现代的命令行界面通常支持命令或文件名补全功能,这需要访问当前输入的内容,因此输入缓冲区是非常重要的。
数据结构
输入缓冲区在Lettershell中的定义如下(由于历史记录和输入缓冲区有着紧密关系,所以一并列出):
typedef struct shell_def
{
// ......
struct
{
unsigned short length; /**< 输入数据长度 */
unsigned short cursor; /**< 当前光标位置 */
char *buffer; /**< 输入缓冲 */
char *param[SHELL_PARAMETER_MAX_NUMBER]; /**< 参数 */
unsigned short bufferSize; /**< 输入缓冲大小 */
unsigned short paramCount; /**< 参数数量 */
int keyValue; /**< 输入按键键值 */
} parser;
#if SHELL_HISTORY_MAX_NUMBER > 0
struct
{
char *item[SHELL_HISTORY_MAX_NUMBER]; /**< 历史记录 */
unsigned short number; /**< 历史记录数 */
unsigned short record; /**< 当前记录位置 */
signed short offset; /**< 当前历史记录偏移 */
} history;
#endif /** SHELL_HISTORY_MAX_NUMBER > 0 */
// ......
} Shell;
初始化
在shellInit
函数中进行输入缓冲区的初始化:
#define SHELL_HISTORY_MAX_NUMBER 5
char shellBuffer[512];
//......
void shellInit(Shell *shell, char *buffer, unsigned short size)
{
// ......
shell->parser.length = 0;
shell->parser.cursor = 0;
shell->parser.buffer = buffer;
shell->parser.bufferSize = size / (SHELL_HISTORY_MAX_NUMBER + 1);
#if SHELL_HISTORY_MAX_NUMBER > 0
shell->history.offset = 0;
shell->history.number = 0;
shell->history.record = 0;
for (short i = 0; i < SHELL_HISTORY_MAX_NUMBER; i++)
{
shell->history.item[i] = buffer + shell->parser.bufferSize * (i + 1);
}
#endif
// ......
}
// ......
shellInit(&shell, shellBuffer, 512);
根据代码,我们不难得出下图:
在当前配置下,输入缓冲区shellBuffer被划分成了6个区域,每个区域占用85字节。
其中第一个85字节的空间用来存储用户当前输入内容,其余空间用来储存历史记录,目前最多可以保存5条历史记录。
对输入缓冲区的维护,其实就是对输入缓冲区数据的增删改查,而为了实现增删改查,则需要管理好如下索引(变量):
shell->parser.length;
shell->parser.cursor;
shell->history.offset;
shell->history.number;
shell->history.record;
数据增加
数据增加分成两种情况,一种是尾部插入(这里维护了parser.length和parser.cursor这两个计数器):
void shellInsertByte(Shell *shell, char data)
{
// ......
if (shell->parser.cursor == shell->parser.length)
{
// printf("---\n");
shell->parser.buffer[shell->parser.length++] = data;
shell->parser.buffer[shell->parser.length] = 0;
shell->parser.cursor++;
shellWriteByte(shell, shell->status.isChecked ? data : '*');
}
// ......
}
还有一种情况是通过使用光标在指定位置插入。这种情况的处理由两个步骤组成:
- 识别到左右方向键后,回调对应按键处理函数。
- 指定位置插入字符。
先看第一个步骤,这里回调了一开始注册的按键处理函数:
/**
* @brief shell右方向键输入
*/
void shellRight(Shell *shell)
{
if (shell->parser.cursor < shell->parser.length)
{
shellWriteByte(shell, shell->parser.buffer[shell->parser.cursor++]);
}
}
/**
* @brief shell左方向键输入
*/
void shellLeft(Shell *shell)
{
if (shell->parser.cursor > 0)
{
// '\b'为退格(BS),将当前位置移到前一列
shellWriteByte(shell, '\b');
shell->parser.cursor--;
}
}
主要就是在维护parser.cursor
计数器,同时向终端工具发送字符,控制显示。
第二个步骤对应我们输入动作,处理如下:
void shellInsertByte(Shell *shell, char data)
{
// ......
else if (shell->parser.cursor < shell->parser.length)
{
// 插入位置之后的所有元素向后移动一位
for (short i = shell->parser.length - shell->parser.cursor; i > 0; i--)
{
shell->parser.buffer[shell->parser.cursor + i] =
shell->parser.buffer[shell->parser.cursor + i - 1];
}
shell->parser.buffer[shell->parser.cursor++] = data;
shell->parser.buffer[++shell->parser.length] = 0;
// 控制终端工具回显
for (short i = shell->parser.cursor - 1; i < shell->parser.length; i++)
{
shellWriteByte(shell,
shell->status.isChecked ? shell->parser.buffer[i] : '*');
}
for (short i = shell->parser.length - shell->parser.cursor; i > 0; i--)
{
shellWriteByte(shell, '\b');
}
}
// ......
}
由于不是在数组尾部插入数据,所以在插入位置之后的所有元素需要向后移动一位,然后再控制终端工具正确回显。
数据删除
数据删除也分成两种情况,一种是从尾部删除,一旦我们按下backspace,便回调之前注册的函数:
/**
* @brief shell 退格
*
* @param shell shell对象
*/
void shellBackspace(Shell *shell)
{
shellDeleteByte(shell, 1);
}
// 删除方向 {@code 1}删除光标前字符 {@code -1}删除光标处字符
void shellDeleteByte(Shell *shell, signed char direction)
{
// ......
if (shell->parser.cursor == shell->parser.length && direction == 1)
{
shell->parser.cursor--;
shell->parser.length--;
shell->parser.buffer[shell->parser.length] = 0;
shellDeleteCommandLine(shell, 1);
}
// ......
}
void shellDeleteCommandLine(Shell *shell, unsigned char length)
{
while (length--)
{
shellWriteString(shell, "\b \b");
}
}
还有一种情况是通过使用光标删除指定位置数据,这里光标控制在前一节已经有介绍了,不再赘述:
void shellDeleteByte(Shell *shell, signed char direction)
{
// ......
else
{
for (short i = offset; i < shell->parser.length - shell->parser.cursor; i++)
{
shell->parser.buffer[shell->parser.cursor + i - 1] =
shell->parser.buffer[shell->parser.cursor + i];
}
shell->parser.length--;
if (!offset)
{
shell->parser.cursor--;
shellWriteByte(shell, '\b');
}
shell->parser.buffer[shell->parser.length] = 0;
for (short i = shell->parser.cursor; i < shell->parser.length; i++)
{
shellWriteByte(shell, shell->parser.buffer[i]);
}
shellWriteByte(shell, ' ');
for (short i = shell->parser.length - shell->parser.cursor + 1; i > 0; i--)
{
shellWriteByte(shell, '\b');
}
}
}
由于不是在数组尾部删除数据,所以在删除位置之后的所有元素需要向前移动一位,然后再控制终端工具正确回显。
数据修改
数据修改其实就是增加、删除的复合操作,这里不重复介绍了。
数据查询
命令自动补全,还有历史记录这两个功能涉及到输入缓冲区数据查询。限于篇幅,此处不详细展开,后面文章我们会有介绍。
总结
- 从代码上看,缓冲区的管理逻辑被分散在多个函数中,阅读代码时,我们需要在多个函数间跳转才能看到全貌。
- 除了需要维护好缓冲区外,还需要在增删改查数据的同时维护好回显。