- 这是一个使用
Rust实现的轻量化文本编辑器。- 学过
Rust的都知道,Rust从入门到实践中间还隔着好几个Go语言的难度,因此,如果你也正在学习Rust,那么恭喜你,这个项目被你捡到了。- 本项目内容较多,大概会分三期左右陆续发布,欢迎关注!
1. 第一篇
本系列教程默认你已经配置了
Rust开发环境并具有一定的rust基础。所以直接从项目创建开始讲解;
使用下面的命令创建项目
- 项目创建
cargo new xed
- 运行程序
cargo run
如果成功输出Hello World表示项目基本功能正常,本章节完!
2. 第二篇
2.1 读取用户输入
现在修改main.rs,尝试读取用户的输入,你可以随时按下Ctrl + c终止程序;
use std::io;
use std::io::Read;
fn main() {let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 {}
}
- 这里的内容不多,主要涉及到
io的基本操作,所以导包是必要的;- 第4行创建了一个可变的
buf数组,长度为1,初始值为0;io::stdin().read(&mut buf)尝试从标准输入流中读取数据,并将其存储在buf中。read方法返回一个Result类型,其中包含读取的字节数或一个错误。- 所以
expect("Failed to read line")用于处理可能出现的错误情况。如果读取失败,程序将打印出 “Failed to read line” 作为错误信息并终止程序。- 最后的
==1检查读取的字节数是否为1,否则结束循环;
2.2 实现q命令
本小节实现基本功能:用户输入q按下回车执行退出程序的操作。
use std::io;
use std::io::Read;
fn main() {let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf !=[b'q'] {}
}
- 程序会检查
buf中输入的每一个字符,如果与q相同,就会结束程序;在 Rust 中,
[b'q']是一个字节字符串字面量,表示一个包含单个字节q的字节数组。
[b'q']:
b'q'是 Rust 中的字节字面量,表示一个字节,即 ASCII 字符'q'对应的字节值。- 在 Rust 中,使用
b前缀可以将字符转换为对应的字节值。这种表示方式常用于处理字节数据。字节值和字符映射:
- 在 ASCII 编码中,每个字符都有一个对应的字节值。在 ASCII 编码中,字符
'q'对应的字节值是113。- 使用
b'q'可以直接表示这个字节值,而[b'q']则将这个字节值包装在一个长度为 1 的字节数组中。因此,
[b'q']表示一个包含单个字节值为113(即 ASCII 字符'q'对应的字节值)的字节数组。在上下文中,buf != [b'q']的条件判断将检查buf中存储的字节是否不等于'q'对应的字节值,即检查输入的数据是否不是'q'。
- 等价写法:
buf[0] != b'q'
2.3 常规模式与原始模式
上面的情况就是常规模式,也就是程序启动后终端可以正常监听并回显你输入的内容;
而这里说的原始模式的作用和常规模式相反,我们这里可以直接使用crossterm库来实现,添加依赖:
cargo add crossterm
use std::io;
use std::io::Read;
use crossterm::terminal; // 添加依赖
fn main() {terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {}
}
现在如果你运行程序,你的输入在终端并没有任何回显,并且当你输入q的时候也是直接无提示的退出程序,这就是crossterm帮我们实现的原始模式的基本功能;
如果要禁用原始模式,考虑下面的代码,最后一行就是禁用这个模式的逻辑;
use crossterm::terminal; /* add this line */
use std::io;
use std::io::Read;
fn main() {terminal::enable_raw_mode().expect("Could not turn on Raw mode");let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {}terminal::disable_raw_mode().expect("Could not turn off raw mode"); /* add this line */
}
但是这样运行后会出现一个错误:
当在
terminal::enable_raw_mode()之后的函数中发生错误并导致panic时,disable_raw_mode()将不会被调用,导致终端保持在原始模式。这种情况可能会导致程序结束时终端状态不正确,用户体验受到影响。
所以为了解决这个问题,让我们创建 一个 名为 CleanUp的struct;
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}
然后修改原来的代码:
use crossterm::terminal; // 添加依赖
use std::io;
use std::io::Read;
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}fn main() {let _clean_up = CleanUp; // 看这里terminal::enable_raw_modde().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {}// terminal::disable_raw_mode().expect("Could not turn off raw mode"); /* add this line */panic!(""); // 看这里
}
现在我们新增了一个
struct并实现了Drop这个trait;此时drop()函数会在我们的struct实例,也就是_clean_up超出作用域或者该实例出现panic时候执行;一旦上面的情况发生,
drop()被执行,那么将成功禁用原始模式;
但是现在还有问题,此时使用Ctrl +c 无法退出程序;不妨看看当我们按下这些按键的时候输出了什么东西;
fn main() {let _clean_up = CleanUp;terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];while io::stdin().read(&mut buf).expect("Failed to read line") == 1 && buf != [b'q'] {let character = buf[0] as char;if character.is_control() {println!("{}\r", character as u8)} else {println!("{}\r", character)}}
}
is_control()判断按下的是否为控制键位,在正常情况下,控制键位输入的字符我们并不需要;ASCII的0-31都是控制字符,127也是;- 所以
32-126就是可打印的字符,也是我们在编辑文本时需要进行输入回显的;- 另外,请注意我们在打印信息的时候使用的是
\r而不是\n;此时我们在终端输入数据之后,光标会自动调整到屏幕的左侧。
现在请运行程序并尝试按下控制键位,例如方向键、 或 Escape 、 或 Page Up Page Down 、 Home End Backspace Delete 或 Enter 或 。尝试使用 Ctrl 组合键,如 Ctrl-A、Ctrl-B 等。你会发现:
方向键:Page Up、Page Down、Home 和 End 都向终端输入 3 或 4 个字节:
27、、'[',然后是一两个其他字符。这称为转义序列。所有转义序列都以27字节开头。按 Escape 键发送单个27字节作为输入。Backspace 是字节
127。Delete 是一个 4 字节的转义序列。Enter 是 byte
10,这是一个换行符,也称为'\n'或 byte13,这是回车符,也称为\r。另外:
Ctrl-A是1,Ctrl-B是2,Ctrl-C是3…这确实有效的 将Ctrl组合键将字母A-Z映射到代码1-26
通过上面的步骤,我们基本了解了按键是如何转为字节的。
2.4 crossterm提供的事件抽象
crossterm 还提供了对各种关键事件的抽象,因此我们不必记住上面那一堆映射关系;而是使用这个crate带来的实现方法;
下面是使用这些抽象重构之火的main.rs:
use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, terminal}; // 添加依赖
use std::io;
use std::io::Read;
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}fn main() {let _clean_up = CleanUp;terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];// 从这里开始重构loop {if let Event::Key(event) = event::read().expect("Failed to read line") {match event {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::NONE,kind: event::KeyEventKind::Press,state: event::KeyEventState::NONE,} => break,_ => {// todo}}println!("{:?}\r", event);};}
}
Event是一个enum。由于我们目前只对按键感兴趣,因此我们检查返回的Event键是否为Key.然后,我们检查按下的键是否为q。如果用户按下q,我们就会中断loop,程序将终止。- 当然,枚举中其他几个字段也是必须的,参考下文档中枚举的定义如下:
pub struct KeyEvent {pub code: KeyCode,pub modifiers: KeyModifiers,pub kind: KeyEventKind,pub state: KeyEventState, }其中的
kind也是枚举:pub enum KeyEventKind {Press,Repeat,Release, }
sate的定义:pub struct KeyEventState: u8 {/// The key event origins from the keypad.const KEYPAD = 0b0000_0001;/// Caps Lock was enabled for this key event.////// **Note:** this is set for the initial press of Caps Lock itself.const CAPS_LOCK = 0b0000_1000;/// Num Lock was enabled for this key event.////// **Note:** this is set for the initial press of Num Lock itself.const NUM_LOCK = 0b0000_1000;const NONE = 0b0000_0000;}看着有点怕但是不要怕,当下只需要理解代码中按下
q执行程序退出的逻辑就可以。
下面是一个示例输出,它会在你按下按键的时候记录并打印相关的事件信息。你可以测试一下按下q是否正常退出程序。

2.4 超时处理
现在的情况是,read()会无限期的在等待我们的键盘输入后返回。如果我们一直没有输入,那它就已知等待,这是个问题。因此我们需要有一个超时处理的逻辑,比如超过一定时间没用户没有任何操作就执行超时对应的处理逻辑。
use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, terminal}; // 添加依赖
use std::io;
use std::io::Read;
use std::time::Duration; // 新增依赖
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Could not disable raw mode");}
}fn main() {let _clean_up = CleanUp;terminal::enable_raw_mode().expect("Could not run on Raw mode"); // 开启原始模式let mut buf = [0; 1];// 从这里开始重构loop {if event::poll(Duration::from_millis(500)).expect("Program timed out") { // 超时处理if let Event::Key(event) = event::read().expect("Failed to read line") {match event {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::NONE,kind: event::KeyEventKind::Press,state: event::KeyEventState::NONE,} => break,_ => {// todo}}println!("{:?}\r", event);};}}
}
上面的代码中新增的超时处理中用到了crossterm::event::poll这个方法,如果在给定时间内没有 Event 可用, poll 则返回 false ,具体的函数定义信息如下:

2.5 错误处理
一路走来,我们对程序的错误处理都是使用expect()进行简单的捕获,这显然并不是一个很好的选择和习惯,下面通过使用Result来对错误进行进一步的处理,修改main.rs:
use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, terminal};
use std::time::Duration; /* add this line */struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Unable to disable raw mode")}
}fn main() -> std::result::Result<(), std::io::Error> {let _clean_up = CleanUp;terminal::enable_raw_mode()?;loop {if event::poll(Duration::from_millis(500))? {if let Event::Key(event) = event::read()? {match event {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::NONE,kind: _,state: _,} => break,_ => {//todo}}println!("{:?}\r", event);};} else {println!("No input yet\r");}}Ok(())
}
修改部分如下,注意,对于main方法本身也是指定了返回值类型,这在下面的贴图中没有展现。

?算符只能用于返回Result的方法中,因此Option我们必须修改 ourmain()以返回Result.可以crossterm::Result<T>扩展为std::result::Result<T, std::io::Error>因此,对于我们的
main()函数,返回类型可以转换为std::result::Result<(), std::io::Error>。
本期完,下期内容抢先知:
- Ctrl+Q退出
- 键盘输入重构
- 屏幕清理
- 光标定位
- 退出清屏
- 波浪号占位符(类似于vim)
- 追加缓冲区
写在最后:
如果这篇内容跟下来,你还是觉得比较难,那么我推荐你暂时放一下,这里推荐一个我之前写的开源项目untools,这也是一个使用
Rust编写的工具库,可以拿来练手,顺手点个star的同时也欢迎有想法有能力的同学PR;
