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 中的多线程和并发的区别,并举例说明如何避免常见的并发问题。 答案: 多线程是指程序中有多个线程同时执行,而并发是指程序设计中允许多个操作看起来是同时执行的,即使它们可能不是…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...
【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
Springboot社区养老保险系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,社区养老保险系统小程序被用户普遍使用,为方…...
