- 这是一个使用
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
;