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 中的多线程和并发的区别,并举例说明如何避免常见的并发问题。 答案: 多线程是指程序中有多个线程同时执行,而并发是指程序设计中允许多个操作看起来是同时执行的,即使它们可能不是…...

2024年网络安全威胁
随着2024年的到来,数字世界的版图正在以前所未有的速度扩张,引领我们进入一个技术革新的新时代。然而,这飞速的发展同时也催生了一系列错综复杂的网络安全挑战。在这个数字平台与我们生活日益紧密交织的时代,深入了解这些新兴的威…...

应用层之 HTTP 协议
HTTP 协议 HTTP (全称为 "超文本传输协议") 是一种应用非常广泛的 应用层协议。所谓 "超文本" 的含义, 就是传输的内容不仅仅是文本(比如 html, css 这个就是文本), 还可以是一些 其他的资源, 比如图片, 视频, 音频等二进制的数据。浏览器获取到网页&#…...

解决Word文档中页眉有部分有,有部分没有的问题
问题描述:一个Word文档中,在页眉上添加文档名称和页码,但是有的有,有的没有,选择“链接到前一节”也无法解决该问题。 原因分析:页眉页脚中,勾选了“首页不同”的选项,如下图&#…...

Python爬虫基础知识学习(以爬取某二手房数据、某博数据与某红薯(书)评论数据为例)
一、爬虫基础流程 爬虫的过程模块化,基本上可以归纳为以下几个步骤: 1、分析网页URL:打开你想要爬取数据的网站,然后寻找真实的页面数据URL地址; 2、请求网页数据:模拟请求网页数据,这里我们介…...

JavaScript-输入输出语句
输出语句 document.write( 输出的内容 ) 语法:document.write( 输出的内容) 作用:内容会显示在网页上 如果输出的内容是标签,也会被解析为网页元素 代码: <!DOCTYPE html> <html lang"en"> <head>&…...

peft+llama3训练自定义数据
要微调自己的模型训练 LLaMA 3,则需要准备一个 JSON 格式的数据集,其中每个条目包含输入文本和相应的标签(如果有的话)。以下是一个 JSON 数据集的示例格式: [{"input": "这是一个输入样本。",&q…...

vue+ts+vite+pinia+less+echarts 前端可视化 实战项目
1.初始化前端 输入 npm init vuelatest 命令 然后 选择需要的插件2.构建完成后 在终端切换到vue-project文件夹下 npm install 下载依赖 3.下载 less样式 npm install less less-loader -D 4.下载axios npm install axios 5.下载echarts npm install echarts -S 6.引入中国…...

文心一言指令多样化,你知道的有哪些?
文心一言的指令非常多样化,可以根据用户的需求和场景进行灵活调整。以下是一些常见的文心一言指令示例: 知识问答: 帮我解释一下什么是芯片?中国的历史上有哪些重要的朝代?人工智能在未来会有哪些发展趋势?…...

QT状态机8-使用恢复策略自动恢复属性
当状态分配的属性不再活动时,可能希望将其恢复到初始值,通过设置全局的恢复策略可以使状态机进入一个状态而不用明确制定属性的值。 QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);当设置了恢复策略以后,状态机将自动恢复所有…...

第83天: 代码审计-PHP 项目RCE 安全调试追踪代码执行命令执行
案例一:CNVD拿1day-RCE命令执行-百家CMS 这里用代码审计系统搜索system,可以利用的是第一种 打开看细节 查找函数引用 查找$_file第一次出现的地方 这个时候就明白了,必须上传文件,然后利用文件名,去执行system命令 …...