本文不會涉及太多技術細節和原始碼,請放心食用
大家好,我是 HelloGitHub 的老荀,好久不見啊!
我在完成 HelloZooKeeper 系列之後,就很少“露面了”。但是我對開源和 HelloGitHub 的熱情並沒有絲毫的減少。這不,逮著個機會就來輸出一波,防止被大家遺忘?。
這次帶來的是我寫的一款在終端瀏覽 HelloGitHub 的工具:hg-tui,讓你雙手不離開鍵盤就能暢遊在 HG 的開源世界。功能如下:
- 色彩豐富、平鋪展示
- 關鍵字搜尋月刊往期的專案
- 類 Vim 的快捷鍵操作方式
- 一鍵直達開源專案首頁
- 支援 Linux、macOS、Windows
下面我將分享自己發起這個開源專案的緣起、構思、再到開發的全部過程,最後分享一下,我通過做這個專案對開源的一些感悟。
一、起因
我本職是做 Java 開發,但架不住 Rust 太有意思了!所以最近在學 Rust 恰好前段時間看到 HG 講解 tui.rs 的文章。
看完後手癢得厲害,就寫了一篇 tui.rs 入門文章,但感覺還不過癮就想寫一個專案練手。
因為我平時經常上 HelloGitHub 找開源專案,所以就決定用 tui.rs
做一個終端瀏覽 HelloGitHub 官網的工具。
二、構思
首先我希望這個應用能有以下功能:
- 有搜尋框,可以按關鍵詞搜尋 HelloGitHub 中的任意專案
- 通過表格按列展示搜尋結果
- 既然是終端應用,那操作方式肯定是使用鍵盤方式,快捷鍵我採用了一些大家熟知的 Vim 快捷鍵
- 瀏覽專案的途中,可以隨時在瀏覽器中開啟當前瀏覽的專案
有了這些主要功能點的思路,下面就要想想怎麼設計一個介面了,我本職工作後端一碰到畫介面就頭疼,幾經周折大概把介面設計成了這樣:
又因為是 TUI 介面層級不能太深,所以再多弄個詳情頁面(用來瀏覽文字明細)或者彈窗頁面(提示訊息)就差不多了。
我又想到了 GitHub 為每一種程式語言都設計了一種顏色,我也可以把這些顏色應用在我的專案裡,讓整個終端介面看起來沒那麼單調,色彩更豐富。效果如下:
主介面:
詳情頁:
彈窗提示:
最後為了向 TUI 妥協,按期數或類別搜尋,我是通過使用搜尋字首來和普通關鍵詞搜尋作出區別。
上面展示的這些差不多已經是這個專案的全部了
三、開發
3.1 技術選型
要實現上述的那些功能,就要從 Rust 的生態中選擇合適的庫了
下面這些是我在這個專案中使用到的:
- 基礎設施:
anyhow
、thiserror
、lazy_static
、better-panic
- 繪製 UI:
tui
、crossterm
- HTTP client:
reqwest
- 快取:
cached
- HTML 解析:
nipper
- 工具:
regex
、crossbeam-channel
- 命令列:
clap
雖然 Rust 還是程式設計界的小學生(2011 年啟動),但是經過了這些年的發展,生態已經逐漸完善,工具庫已經很豐富了。再加上 Rust 是系統級的語言,值得投入時間學習!
3.2 專案結構
專案結構規劃(非全部)
src
├── app.rs // 統一管理整個應用的狀態
├── cli.rs // 命令列解析
├── draw.rs // 繪製 UI
├── events.rs // UI 事件、輸入事件、通知
├── fetch.rs // HTTP 請求
├── main.rs // 入口
├── parse.rs // HTML 解析
├── utils.rs // 工具
└── widget // 自定義元件
├── ...
合理的分檔案(目錄)開發,可以讓每個功能模組 高內聚、低耦合,並且可以很容易地分開進行單元測試。
當然這些檔案也不是在專案之初就已經一股腦地建立好的,都是在完善功能的路上一點點新增進來的~
3.3 主要程式碼
因為是基於 tui.rs
開發的應用,所以主流程肯定是遵循該庫的設計的,首先需要定義一個 App
用來儲存整個專案的狀態資訊。
pub struct App {
/// 使用者輸入框
pub input: InputState,
/// 內容展示
pub content: ContentState,
/// 彈窗提示
pub popup: PopupState,
/// 狀態列
pub statusline: StatusLineState,
/// 模式
pub mode: AppMode,
/// 專案明細子頁面
pub project_detail: ProjectDetailState,
...
}
每一個狀態欄位,其實就是對應一個自定義元件.要在 tui.rs
中實現自定義元件(實現方式也是我自己的理解)也很簡單隻要三步,我以 Input
元件為例。
/// 使用者輸入框元件,元件本身沒有欄位,是一個無狀態的物件
/// 無狀態物件只關心 UI 怎麼繪製,不儲存資料
pub struct Input {}
/// 元件的狀態,每一個欄位就是元件需要儲存的資料
#[derive(Debug)]
pub struct InputState {
input: String,
active: bool,
pub mode: SearchMode,
}
/// 最後為 Input 元件實現 StatefulWidget trait
impl StatefulWidget for Input {
type State = InputState; // 指定關聯型別為 InputState
/// area 繪製的區域
/// buf 緩衝區(可以直接寫入字串,如果要高度定製的話,可以理解為畫筆)
/// state 從這個變數中直接取繪製過程中需要的資料
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// 具體繪製的邏輯
...
}
}
只要是面向使用者的應用,都會處理各種各樣的使用者輸入(事件)。Rust 中一般都使用 channel 來解耦處理各種各樣的事件,再利用 Rust 強大的列舉支援,定義各種各樣的事件(使用者輸入和非使用者輸入)即可。
/// 定義事件列舉
#[derive(Debug, Clone)]
pub enum HGEvent {
/// 使用者事件(鍵盤事件)
UserEvent(KeyEvent),
/// 應用內部元件的通知事件
NotifyEvent(Notify),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Notify {
/// 重繪介面
Redraw,
/// 退出應用
Quit,
/// 彈出視窗展示訊息
Message(Message),
/// tick,比如一些資料需要每隔一段時間自動更新的(比如:顯示的時間)
Tick,
}
/// 彈窗的訊息,分為 錯誤、警告、提示
#[derive(Debug, Clone, PartialEq)]
pub enum Message {
Error(String),
Warn(String),
Tips(String),
}
為了區分使用者事件和通知,我使用了兩個不同的 channel 分別處理這兩類:
lazy_static! {
/// 因為通知佇列希望被應用內部共享,所以使用了 lazy_static 方便使用
pub static ref NOTIFY: (Sender<HGEvent>, Receiver<HGEvent>) = bounded(1024);
}
又因為不同的事件處理,並不應該互相阻塞,所以整個應用採用了最基礎的多執行緒模型來提高效能,這裡使用的也是標準庫的多執行緒。
pub fn handle_key_event(event_app: Arc<Mutex<App>>) {
let (sender, receiver) = unbounded();
...
std::thread::spawn(move || loop {
// 單獨一個執行緒接收使用者事件
if let Ok(Event::Key(event)) = crossterm::event::read() {
sender.send(HGEvent::UserEvent(event)).unwrap();
}
});
std::thread::spawn(move || loop {
// 單獨一個執行緒處理使用者事件
if let Ok(HGEvent::UserEvent(key_event)) = receiver.recv() {
...
}
});
}
其他剩下的就是業務邏輯,完整的程式碼可以直接看倉庫 https://github.com/kaixinbaba/hg-tui
四、心路歷程
一開始我做 hg-tui
專案的時候,僅僅是為了做個實際的專案把玩一下 tui.rs
這個框架,做好之後問題層出不窮,但我深知沒有與生俱來的完美,只有不斷的迭代才能讓它越來越好,經過 100 多次的提交後,現在用著感覺順手多了。畢竟作者是專案的第一個使用者,自己用著不舒服其他人就更不喜歡了!
我想著既然要讓別人用,一定要容易安裝。接著我做了基於 GitHub Action 自動編譯和釋出,支援 Windows、Linux、macOS 直接下載就能用。
我還做了對 homebrew 安裝的支援,但因為 Star 數不夠沒有收錄到 homecore 要求:30 forks、30 watchers、75 stars
希望大家看到這裡的話能給個 star✨
五、最後
hg-tui
它從出生那一刻起,體內流淌的就是開源的血。
它很小甚至是微不足道,我本不想開源,但蛋蛋的一段話讓我改變了主意:開源不是完結,僅僅只是開始。
一個開源專案可能只是作者的一個靈光乍現,也可能只是為了解決自己實際工作生活中的小小痛點,沒準用完就丟到角落裡了。但開源出來或許就能找到有相同需求的人,從而延續這個專案的生命,或許這就是開源的本意吧。
以上就是我做這個專案的全部心得和收穫,如果你們對 hg-tui
有什麼建議和問題,歡迎給我提 issue
最後,如果你喜歡本文和專案的話,歡迎點贊和 Star 愛你們喲~