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的字节数组。
[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;
相关文章:
Xed编辑器开发第一期:使用Rust从0到1写一个文本编辑器
这是一个使用Rust实现的轻量化文本编辑器。学过Rust的都知道,Rust 从入门到实践中间还隔着好几个Go语言的难度,因此,如果你也正在学习Rust,那么恭喜你,这个项目被你捡到了。本项目内容较多,大概会分三期左右陆续发布&a…...
农业自动气象监测站:赋能智慧农业的新动力
在信息化、智能化快速发展的今天,农业领域也迎来了前所未有的变革。其中,农业自动气象监测站作为智慧农业的重要组成部分,正在发挥着越来越重要的作用。它们如同农业生产的“眼睛”和“耳朵”,实时感知和记录着大气的微妙变化&…...
2-6 任务 猜数小游戏(单次版)
本任务要求编写一个猜数小游戏(单次版),游戏规则是计算机产生一个0到100之间的随机整数,用户通过输入猜测的数字进行猜测,根据猜测情况给出提示,直到猜对为止。编程思路是利用while循环和多分支结构实现永真…...
springboot 定时任务解决方案
Scheduled (springboot 自带的 注解) 基于注解Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。 EnableScheduling注解: 在配置类上使用,开启计划任务的支持(类上)。…...
谷粒商城实战(024 业务-订单模块-分布式事务1)
Java项目《谷粒商城》架构师级Java项目实战,对标阿里P6-P7,全网最强 总时长 104:45:00 共408P 此文章包含第284p-第p290的内容 简介 模拟积分服务出异常,前方的锁库存事务未回滚,这时候就需要分布式事务 本地事务 事务的隔离…...
.NET使用Microsoft.IdentityModel.Tokens对SAML2.0登录断言校验
如题。使用SAML单点登录对IDP返回的Response断言使用微软提供的Microsoft.IdentityModel.Tokens对断言(Assertion)进行校验。 首先需要安装Muget包,Microsoft.IdentityModel.Tokens和Microsoft.IdentityModel.Tokens.Saml。 简易示例代码如…...
性能测试学习二
瓶颈的精准判断 TPS曲线 tps图 响应时间图 拐点在哪里呢? 这是一个阶梯式增加的场景,拐点在第二个压力阶梯上就出现了,因为响应时间增加了,tps增加的却不多,在第三个阶段时,tps增加的就更少了,响应时间也在不断增加,所以性能瓶颈在加剧,越往后越明显【tps的增长,…...
小丑的身份证和复印件 (BFS + Floyd)
本题链接:登录—专业IT笔试面试备考平台_牛客网 题目: 样例: 输入 2 10 (JOKERjoke #####asdr) 输出 12 思路: 根据题意,要求最短时间,实际上也可以理解为最短距离。 所以应该联想到有关最短距离的算法&…...
C++类与对象(上)
C类与对象 面向过程和面向对象初步认识类的引入类的定义类的两种定义方式: 类的访问限定符及封装访问限定符 封装类的作用域类的实例化类对象模型如何计算类对象的大小结构体内存对齐规则: this指针 面向过程和面向对象初步认识 C语言是面向过程的&…...
Exchanger的 常用场景及使用示例
Exchanger的 常用场景及使用示例 Exchanger是Java并发包中的一个工具类,它用于两个线程之间交换数据。当两个线程都到达同步点并调用exchange()方法时,它们会交换数据然后继续执行。Exchanger特别适用于那些需要两个线程进行协作,交换数据或…...
Spring AI项目Open AI对话接口开发指导
文章目录 创建Spring AI项目配置项目pom、application文件controller接口开发接口测试 创建Spring AI项目 打开IDEA创建一个新的spring boot项目,填写项目名称和位置,类型选择maven,组、工件、软件包名称可以自定义,JDK选择17即可…...
决策规划仿真平台的搭建
以下内容笔记据来自于b站up主忠厚老实的老王,视频;链接如下: 自动驾驶决策规划算法第二章第一节 决策规划仿真平台搭建_哔哩哔哩_bilibili 使用到的软件有matlab、prescan、carsim以及visual stadio。 我电脑上软件的版本是matlab2022a&am…...
RustGUI学习(iced/iced_aw)之扩展小部件(十八):如何使用badge部件来凸显UI元素?
前言 本专栏是学习Rust的GUI库iced的合集,将介绍iced涉及的各个小部件分别介绍,最后会汇总为一个总的程序。 iced是RustGUI中比较强大的一个,目前处于发展中(即版本可能会改变),本专栏基于版本0.12.1. 概述 这是本专栏的第十八篇,主要讲述badge标记部件的使用,会结合实…...
触摸播放视频,并用iframe实现播放外站视频
效果: html: <div:style"{ height: homedivh }"class"rightOne_content_div_div"mouseenter"divSeenter(i)"mouseleave"divLeave(i)"click"ItemClick(i)"><!-- isUser是否是用户上传 --><divv-if…...
接口自动化-requests库
requests库是用来发送请求的库,本篇用来讲解requests库的基本使用。 1.安装requests库 pip install requests 2.requests库底层方法的调用逻辑 (1)get / post / put / delete 四种方法底层调用 request方法 注意:data和json都…...
队列的实现与OJ题目解析
"不是你变优秀了, 那个人就会喜欢你." 文章索引 前言1. 什么是队列2. 队列的实现3. OJ题目解析4. 总结 前言 感情可以培养是个伪命题. 如果有足够多的时间和爱, 就可以让另一个人爱上你的话, 那谁和谁都可以相爱了. 爱情之所以会让人死去活来, 是因为, 答案都写在了…...
中北大学软件学院javaweb实验三JSP+JDBC综合实训(一)__数据库记录的增加、查询
目录 1.实验名称2.实验目的3.实验内容4.实验原理或流程图5.实验过程或源代码(一)编程实现用户的登录与注册功能【步骤1】建立数据库db_news2024和用户表(笔者使用的数据库软件是navicat)【步骤2】实现用户注册登录功能(与上一实验报告不同的是࿰…...
高通QCS6490开发(一): 广翼智联FV01 AI板卡简介
《高通QCS6490开发》是一系列AIoT应用开发文章,我们将会在系列文章中陆续介绍基于QCS6490平台上的AIoT应用开发,在文章中,我们选择了广翼智联(FAIOT)公司的FV01产品作为开发板,介绍如何从底层的硬件板卡接线…...
【知识拓展】大白话说清楚:IP地址、子网掩码、网关、DNS等
前言 工作中常听别人说的本地网络是什么意思?同一网段又是什么意思?它俩有关系吗? 在工作中内经常会遇到相关的网络问题,涉及网络通信中一些常见的词汇,如IP地址、子网掩码、网关和DNS等。具体一点:经常会…...
Java 高级面试问题及答案2
Java 高级面试问题及答案 问题 1: 请解释 Java 中的多线程和并发的区别,并举例说明如何避免常见的并发问题。 答案: 多线程是指程序中有多个线程同时执行,而并发是指程序设计中允许多个操作看起来是同时执行的,即使它们可能不是…...
wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
