Skip to content

Commit 0f337ae

Browse files
junjie.xun521xueweihan
junjie.xun
authored andcommitted
初稿提交
1 parent 9e7f7d5 commit 0f337ae

File tree

7 files changed

+229
-0
lines changed

7 files changed

+229
-0
lines changed

contents/Rust/hg-tui/content.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# 摸鱼神器 —— 在终端浏览 HelloGitHub
2+
3+
> 本文不会涉及太多技术细节和源码,请放心食用
4+
5+
<img src="./images/cover.jpg" style="zoom:80%;" />
6+
7+
自从对 [tui-rs](https://github.com/fdehau/tui-rs) 产生兴趣后,就一直在琢磨如何能基于她写出一个终端应用呢?写一个什么应用呢?
8+
9+
[hellogithub.com](https://hellogithub.com/) 本就是我平时没事喜欢闲逛的网站,一个想法很自然的就产生了,我能不能在终端浏览呢?
10+
11+
## 一、初步构思
12+
13+
首先我希望这个应用能有以下功能:
14+
15+
- 有搜索框,可以按关键词搜索 hellogithub 中的任意项目
16+
- 通过表格按列展示搜索结果
17+
- 既然是终端应用,那操作方式肯定是使用键盘方式,快捷键我采用了一些大家熟知的 Vim 快捷键
18+
- 浏览项目的途中,可以随时在浏览器中打开当前浏览的项目
19+
20+
有了这些主要功能点的思路,下面就要想想怎么设计一个界面了,我本职工作是一条后端 🐶,一碰到画界面就头疼,几经周折,大概把界面设计成了这样:
21+
22+
![](./images/1.png)
23+
24+
又因为是 TUI 界面层级不能太深,所以再多弄个详情页面(用来浏览文字明细)或者弹窗页面(提示消息)就差不多了,我又想到了 GitHub 为每一种编程语言都设计了一种颜色,我可以把这些颜色应用在我的项目里,让整个终端界面看起来没那么单调,色彩更丰富。我这里展示下目前的成品效果图
25+
26+
主界面:
27+
28+
![](./images/2.png)
29+
30+
详情页:
31+
32+
![](./images/3.png)
33+
34+
弹窗提示:
35+
36+
![](./images/4.png)
37+
38+
最后为了向 TUI 妥协,按期数或类别搜索,我是通过使用搜索前缀来和普通关键词搜索作出区别
39+
40+
上面展示的这些差不多已经是这个项目的全部了
41+
42+
## 二、技术选型
43+
44+
要实现上述的那些功能,就要从 Rust 的生态中选择合适的库了
45+
46+
下面这些是我这个项目中使用到的:
47+
48+
- 基础设施: `anyhow``thiserror``lazy_static``better-panic`
49+
- 绘制 UI:`tui``crossterm`
50+
- HTTP client:`reqwest`
51+
- 缓存:`cached`
52+
- HTML 解析:`nipper`
53+
- 工具:`regex``crossbeam-channel`
54+
- 命令行:`clap`
55+
56+
Rust 虽然还是编程界的小学生(2011 年启动),但是经过了这些年的发展,生态已经逐渐完善(和几位大哥还是差很多),加上 Rust 是系统级的语言,所以我相信未来 Rust 一定能成为多面手。
57+
58+
项目目录规划(非全部)
59+
60+
```rust
61+
src
62+
├── app.rs // 统一管理整个应用的状态
63+
├── cli.rs // 命令行解析
64+
├── draw.rs // 绘制 UI
65+
├── events.rs // UI 事件、输入事件、通知
66+
├── fetch.rs // HTTP 请求
67+
├── main.rs // 入口
68+
├── parse.rs // HTML 解析
69+
├── utils.rs // 工具
70+
└── widget // 自定义组件
71+
├── ...
72+
```
73+
74+
合理的分文件(目录)开发,可以让每个功能模块 高内聚、低耦合,并且可以很容易的分开进行单元测试。
75+
76+
当然这些文件也不是在项目之初就已经一股脑的建立好的,都是在完善功能的路上一点点添加进来的~
77+
78+
## 三、实现代码片段
79+
80+
因为是基于 `tui-rs` 开发的应用,所以主流程肯定是遵循该库的设计的,首先需要定义一个 `App` 用来保存整个项目的状态信息
81+
82+
```rust
83+
pub struct App {
84+
/// 用户输入框
85+
pub input: InputState,
86+
87+
/// 内容展示
88+
pub content: ContentState,
89+
90+
/// 弹窗提示
91+
pub popup: PopupState,
92+
93+
/// 状态栏
94+
pub statusline: StatusLineState,
95+
96+
/// 模式
97+
pub mode: AppMode,
98+
99+
/// 项目明细子页面
100+
pub project_detail: ProjectDetailState,
101+
102+
...
103+
}
104+
105+
```
106+
107+
每一个状态字段其实就是对应一个自定义组件,要在 `tui-rs` 中实现自定义组件(实现方式也是我自己的理解)也很简单,只要三步,我以 `Input` 组件为例
108+
109+
```rust
110+
/// 用户输入框组件,组件本身没有字段,是一个无状态的对象
111+
/// 无状态对象只关心 UI 怎么绘制,不存储数据
112+
pub struct Input {}
113+
114+
/// 组件的状态,每一个字段就是组件需要存储的数据
115+
#[derive(Debug)]
116+
pub struct InputState {
117+
input: String,
118+
active: bool,
119+
pub mode: SearchMode,
120+
}
121+
122+
/// 最后为 Input 组件实现 StatefulWidget trait
123+
impl StatefulWidget for Input {
124+
type State = InputState; // 指定关联类型为 InputState
125+
126+
/// area 绘制的区域
127+
/// buf 缓冲区(可以直接写入字符串,如果要高度定制的话,可以理解为画笔)
128+
/// state 从这个变量中直接取绘制过程中需要的数据
129+
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
130+
// 具体绘制的逻辑
131+
...
132+
}
133+
}
134+
```
135+
136+
只要是面向用户的应用都会处理各种各样的用户输入(事件),Rust 中一般都使用 channel 来解耦处理各种各样的事件,再利用 Rust 强大的枚举支持,定义各种各样的事件(用户输入和非用户输入)
137+
138+
```rust
139+
/// 定义事件枚举
140+
#[derive(Debug, Clone)]
141+
pub enum HGEvent {
142+
/// 用户事件(键盘事件)
143+
UserEvent(KeyEvent),
144+
145+
/// 应用内部组件的通知事件
146+
NotifyEvent(Notify),
147+
}
148+
149+
#[derive(Debug, Clone, PartialEq)]
150+
pub enum Notify {
151+
/// 重绘界面
152+
Redraw,
153+
154+
/// 退出应用
155+
Quit,
156+
157+
/// 弹出窗口展示消息
158+
Message(Message),
159+
160+
/// tick,比如一些数据需要每隔一段时间自动更新的(比如:显示的时间)
161+
Tick,
162+
}
163+
164+
/// 弹窗的消息,分为 错误、警告、提示
165+
#[derive(Debug, Clone, PartialEq)]
166+
pub enum Message {
167+
Error(String),
168+
169+
Warn(String),
170+
171+
Tips(String),
172+
}
173+
```
174+
175+
为了区分用户事件和通知,我使用了两个不同的 channel 分别处理这两类
176+
177+
```rust
178+
lazy_static! {
179+
/// 因为通知队列希望被应用内部共享,所以使用了 lazy_static 方便使用
180+
pub static ref NOTIFY: (Sender<HGEvent>, Receiver<HGEvent>) = bounded(1024);
181+
}
182+
```
183+
184+
又因为不同的事件处理,并不应该互相阻塞,所以整个应用采用了最基础的多线程模型来提高性能,这里使用的也是标准库的多线程
185+
186+
```rust
187+
pub fn handle_key_event(event_app: Arc<Mutex<App>>) {
188+
let (sender, receiver) = unbounded();
189+
...
190+
191+
std::thread::spawn(move || loop {
192+
// 单独一个线程接收用户事件
193+
if let Ok(Event::Key(event)) = crossterm::event::read() {
194+
sender.send(HGEvent::UserEvent(event)).unwrap();
195+
}
196+
});
197+
std::thread::spawn(move || loop {
198+
// 单独一个线程处理用户事件
199+
if let Ok(HGEvent::UserEvent(key_event)) = receiver.recv() {
200+
...
201+
}
202+
});
203+
}
204+
```
205+
206+
其他剩下的就是本应用的业务逻辑,具体的代码可以直接看仓库 [https://github.com/kaixinbaba/hg-tui](https://github.com/kaixinbaba/hg-tui)
207+
208+
## 四、开发心路
209+
210+
仔细想想这可能是我写的第一个拥有完整功能的 Rust TUI 项目,从有想法到完成开发前后差不多用了三周不到的时间,期间碰到了各种各样的问题,我整理了一下:
211+
212+
- tui-rs 如何使用,为了看懂她的模板流程,我基本看完了 tui-rs 本身的所有源码(源码很少说实话,并不是一件难事)
213+
- 查看其他使用的 tui-rs 的项目,学习她们是如何使用 tui-rs 的(看了不下数十个项目,如果你有兴趣的话,这里是[地址](https://github.com/fdehau/tui-rs#apps-using-tui)
214+
- 在生态中寻找合适的 Rust crate 来处理我当前的场景并学会使用她
215+
- 和 Rust 编译器斗智斗勇(Rust 新手的第一座大山,Orz)
216+
- 尽量编写符合 Rust 的代码风格项目
217+
218+
其实一开始我也只是打算把这个项目完成基本功能,然后开源就完事了,但在[蛋蛋](https://github.com/521xueweihan)的建议下我又完成一些功能的完善,但我还是觉得我这个项目太小,没什么可说的,但他说了一句话我影响很深刻:**任何一个开源项目都是从小项目开始的,完成一个开源项目不难,十年如一日的维护才是最难的**
219+
220+
---
221+
222+
如果你们有什么好的建议,欢迎给我提 [issue](https://github.com/kaixinbaba/hg-tui/issues)
223+
224+
最后如果你喜欢本文章和本项目的话,欢迎点赞,star~爱你们哟~
225+
226+
![](./images/5.jpeg)
227+
228+
229+

contents/Rust/hg-tui/images/1.png

175 KB
Loading

contents/Rust/hg-tui/images/2.png

593 KB
Loading

contents/Rust/hg-tui/images/3.png

197 KB
Loading

contents/Rust/hg-tui/images/4.png

557 KB
Loading

contents/Rust/hg-tui/images/5.jpeg

24.2 KB
Loading

contents/Rust/hg-tui/images/cover.jpg

148 KB
Loading

0 commit comments

Comments
 (0)