解鎖!玩轉 HelloGitHub 的新姿勢
本文不會涉及太多技術細節和源碼,請放心食用
大家好,我是 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() {
...
}
});
}
其他剩下的就是業務邏輯,完整的程式碼可以直接看倉庫 //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 愛你們喲~