当前位置: 首页 > news >正文

Xed编辑器开发第一期:使用Rust从0到1写一个文本编辑器

  • 这是一个使用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 的字节数组。

  1. [b'q']

    • b'q' 是 Rust 中的字节字面量,表示一个字节,即 ASCII 字符 'q' 对应的字节值。
    • 在 Rust 中,使用 b 前缀可以将字符转换为对应的字节值。这种表示方式常用于处理字节数据。
  2. 字节值和字符映射:

    • 在 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() 将不会被调用,导致终端保持在原始模式。这种情况可能会导致程序结束时终端状态不正确,用户体验受到影响。

所以为了解决这个问题,让我们创建 一个 名为 CleanUpstruct;

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 DownHome End Backspace DeleteEnter 或 。尝试使用 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' 或 byte 13 ,这是回车符,也称为 \r

  • 另外:Ctrl-A 1 Ctrl-B2 Ctrl-C3…这确实有效的 将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是否正常退出程序。

image-20240514221935305


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 ,具体的函数定义信息如下:

image-20240515084930391


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方法本身也是指定了返回值类型,这在下面的贴图中没有展现。

image-20240515090210813

  • ? 算符只能用于返回 Result 的方法中,因此 Option 我们必须修改 our main() 以返回 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;
在这里插入图片描述

http://www.lryc.cn/news/349811.html

相关文章:

  • 农业自动气象监测站:赋能智慧农业的新动力
  • 2-6 任务 猜数小游戏(单次版)
  • springboot 定时任务解决方案
  • 谷粒商城实战(024 业务-订单模块-分布式事务1)
  • .NET使用Microsoft.IdentityModel.Tokens对SAML2.0登录断言校验
  • 性能测试学习二
  • 小丑的身份证和复印件 (BFS + Floyd)
  • C++类与对象(上)
  • Exchanger的 常用场景及使用示例
  • Spring AI项目Open AI对话接口开发指导
  • 决策规划仿真平台的搭建
  • RustGUI学习(iced/iced_aw)之扩展小部件(十八):如何使用badge部件来凸显UI元素?
  • 触摸播放视频,并用iframe实现播放外站视频
  • 接口自动化-requests库
  • 队列的实现与OJ题目解析
  • 中北大学软件学院javaweb实验三JSP+JDBC综合实训(一)__数据库记录的增加、查询
  • 高通QCS6490开发(一): 广翼智联FV01 AI板卡简介
  • 【知识拓展】大白话说清楚:IP地址、子网掩码、网关、DNS等
  • Java 高级面试问题及答案2
  • 2024年网络安全威胁
  • 应用层之 HTTP 协议
  • 解决Word文档中页眉有部分有,有部分没有的问题
  • Python爬虫基础知识学习(以爬取某二手房数据、某博数据与某红薯(书)评论数据为例)
  • JavaScript-输入输出语句
  • peft+llama3训练自定义数据
  • vue+ts+vite+pinia+less+echarts 前端可视化 实战项目
  • 文心一言指令多样化,你知道的有哪些?
  • QT状态机8-使用恢复策略自动恢复属性
  • 第83天: 代码审计-PHP 项目RCE 安全调试追踪代码执行命令执行
  • 学习Uni-app开发小程序Day8