Rust学习-制作一个文本编辑器: Reading User Input

本文最后更新于:2024年1月10日 下午

读取用户的输入

现在来尝试读取用户的输入操作,main.rs如下:

src/main.rs
1
2
3
4
5
6
7
8
use std::io::{self, Read};

fn main() {
for b in io::stdin().bytes(){
let c = b.unwrap() as char;
println!("{}", c);
}
}

其中use类似于importinclude,用来引入我们要使用的库。在这里我们使用io用来进行输入输出操作use std::io::{self, Read};,这一步相当于:

src/main.rs
1
2
use std::io;
use std::io::Read;

main函数中主要包含一个循环,其中io::stdin().bytes()将返回一个迭代器,在每一轮循环前赋值给b后进入循环,直到没有可以读的内容了。我们可以通过Ctrl+D来发出eof,或者直接Ctrl+C来终止程序。

在实际运行的时候,这个程序会将用户输入的字符串打印出来。然而,在默认情况下,仅当用户按下Enter时输入才会发送给程序,这也允许用户通过·来编辑修复内容(canonical mode / cooked mode),但是对于一个文本编辑器来说,我们希望在按下键盘时程序能立刻响应(raw mode)。

输入Q来退出

当程序收到用户按下Q时,退出程序:

src/main.rs
1
2
3
4
5
6
7
8
9
fn main() {
for b in io::stdin().bytes(){
let c = b.unwrap() as char;
println!("{}", c);
if c == 'q' {
break;
}
}
}

需要注意的是,在Rust中,字符需要用单引号''包裹,字符串是双引号"",这和C++是类似的。

现在当我们在程序中输入q,循环将停止,程序就会退出了,而在q后面的字符不会被打印,并在程序退出时一起被销毁。

Raw Mode

为了切换到Raw Mode,我们需要使用termion,可以在Cargo.toml中引入:

1
2
[dependencies]
termion = "1"

我们再次运行cargo buildrun,就会下载并编译termion

1
2
3
4
5
6
7
8
9
10
  Updating crates.io index
Downloaded libc v0.2.150
Downloaded numtoa v0.1.0
Downloaded termion v1.5.6
Downloaded 3 crates (751.0 KB) in 6.06s
Compiling libc v0.2.150
Compiling numtoa v0.1.0
Compiling termion v1.5.6
Compiling iTEditor v0.1.0 (/root/iTEditor)
Finished dev [unoptimized + debuginfo] target(s) in 11.53s

现在让我们把main.rs中的代码改写成支持raw mode的:

src/main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::io::{self, stdout, Read};
use termion::raw::IntoRawMode;

fn main() {
let _stdout = stdout().into_raw_mode().unwrap();

for b in io::stdin().bytes(){
let c = b.unwrap() as char;
println!("{}", c);
if c == 'q' {
break;
}
}
}

我们使用termion提供stdout()并调用into_raw_mode()函数。为什么要在stdout而不是stdin呢?因为终端的状态是由写入者控制而不是读取者。writer用于在屏幕上绘图或移动光标,同时也用于修改模式。

除此之外,我们还新建了_stdout变量,用于储存into_raw_mode()的返回,但是从未在之后的程序中使用过它。这涉及到Rust中所有权的概念,简单来说,终端可以拥有一些属性,不属于自己的东西将会被移除,而into_raw_mode()修改了终端并返回了一个值,我们需要将它储存起来,通过绑定到_stdout变量,一旦删除_stdout,终端就又会变为canonical mode_stdout的变量名中添加了前缀_,这会告诉编译器我们希望保留这个变量即使没有被使用过,否则在编译时回返回一个warning

运行一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   Compiling iTEditor v0.1.0 (/root/iTEditor)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/iTEditor`
d
s
j
f
h
s
r
i
1
q
#

监视按键

为了更好的探索Raw Mode的工作,我们让程序输出读到的所有byte

src/main.rs
1
2
3
4
5
6
7
8
9
10
11
12
for b in io::stdin().bytes(){
let b = b.unwrap();
let c = b as char;
if c.is_control(){
println!("{:?} \r", b);
} else {
println!("{:?} ({})\r", b, c);
}
if c == 'q' {
break;
}
}

这段代码中对变量b进行了二次声明,这是叫做变量遮蔽的特性:在新变量的作用域中,原先的变量会被屏蔽,这样就不必费心思想新的变量名了。函数is_control()可以用来判断是否是控制字符。控制字符是不可被打印出来的,我们可以在ASCII表中找到他们。

ASCII
Non-printing ASCII control codes.

通过测试我们可以发现,Ctrl+A...Z对应的就是ASCII表中1-26的控制符。

Ctrl+Q退出

现在我们可以将Ctrl+Q映射到退出操作:

src/main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn to_ctrl_byte(c: char) -> u8{
let byte = c as u8;
byte & 0b0001_1111
}

fn main() {
let _stdout = stdout().into_raw_mode().unwrap();

/*codes*/
if b == to_ctrl_byte('q') {
break;
}
}
}

函数to_ctrl_byte()将返回字符和00011111相与的二进制结果。

异常处理

现在是时候该旅一下如何处理错误了。我们给程序增加一个die()函数打印错误并退出:

src/main.rs
1
2
3
fn die(e: std::io::Error){
panic!(e);
}

panic!会导致程序关闭并输出错误信息。和其他语言不一样,Rust没有类似于try...catch的结构来捕获可能发生的错误,取而代之的是,Rust采用一种将错误作为函数返回值的方式进行传递,这样我们就可以在程序的顶层统一处理这些错误。

Rust中,当函数可能发生错误时,它会返回一个名为Result的类型。Result是一个包装器,内含期望的结果或一个错误。在这种情况下,每一个值b本质上都是一个Result,它要么包含一个表示已读取字节的Ok,要么包含一个Err,后者包装了一个错误对象,表明在读取字节时出现了问题。为了得到我们需要的值,我们可以调用unwrap方法:如果我们得到的是Ok,就返回它包含的值;如果是Err,则触发panic

我们希望在错误发生时自行控制程序崩溃,而不是让Rust直接触发panic,因为后续我们想在程序崩溃前清除屏幕,避免给用户留下未完成绘制的输入信息。目前,我们只需检查错误然后调用die函数,这个函数会替我们执行panic

我们通过枚举(match)来实现这个流程,match可以当成一个增强版的if-then-else。由它处理变量b,这个变量要么包含我们想要的值,这个值被Ok包装,要么包含一个错误,这个错误被Err包装:

src/main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for b in io::stdin().bytes(){
match b{
Ok(b) => {
let c = b as char;
if c.is_control() {
println!("{:?} \r", b);
} else {
println!("{:?} ({})\r", b, c);
}
if b == to_ctrl_byte('q'){
break;
}
}
Err(err) => die(err),
}
}

本文链接: https://zone.ivanz.cc/p/8e0787d5.html


Rust学习-制作一个文本编辑器: Reading User Input
https://zone.ivanz.cc/p/8e0787d5
作者
Ivan Zhang
发布于
2023年11月10日
更新于
2024年1月10日
许可协议