解鎖!玩轉 HelloGitHub 的新姿勢

本文不會涉及太多技術細節和源碼,請放心食用

大家好,我是 HelloGitHub 的老荀,好久不見啊!

我在完成 HelloZooKeeper 系列之後,就很少「露面了」。但是我對開源和 HelloGitHub 的熱情並沒有絲毫的減少。這不,逮著個機會就來輸出一波,防止被大家遺忘😂。

這次帶來的是我寫的一款在終端瀏覽 HelloGitHub 的工具:hg-tui,讓你雙手不離開鍵盤就能暢遊在 HG 的開源世界。功能如下:

  • 色彩豐富、平鋪展示
  • 關鍵字搜索月刊往期的項目
  • 類 Vim 的快捷鍵操作方式
  • 一鍵直達開源項目首頁
  • 支援 Linux、macOS、Windows

地址://github.com/kaixinbaba/hg-tui

下面我將分享自己發起這個開源項目的緣起、構思、再到開發的全部過程,最後分享一下,我通過做這個項目對開源的一些感悟。

一、起因

我本職是做 Java 開發,但架不住 Rust 太有意思了!所以最近在學 Rust 恰好前段時間看到 HG 講解 tui.rs 的文章

看完後手癢得厲害,就寫了一篇 tui.rs 入門文章,但感覺還不過癮就想寫一個項目練手。

因為我平時經常上 HelloGitHub 找開源項目,所以就決定用 tui.rs 做一個終端瀏覽 HelloGitHub 官網的工具。

官網://hellogithub.com/

二、構思

首先我希望這個應用能有以下功能:

  • 有搜索框,可以按關鍵詞搜索 HelloGitHub 中的任意項目
  • 通過表格按列展示搜索結果
  • 既然是終端應用,那操作方式肯定是使用鍵盤方式,快捷鍵我採用了一些大家熟知的 Vim 快捷鍵
  • 瀏覽項目的途中,可以隨時在瀏覽器中打開當前瀏覽的項目

有了這些主要功能點的思路,下面就要想想怎麼設計一個介面了,我本職工作後端一碰到畫介面就頭疼,幾經周折大概把介面設計成了這樣:

又因為是 TUI 介面層級不能太深,所以再多弄個詳情頁面(用來瀏覽文字明細)或者彈窗頁面(提示消息)就差不多了。

我又想到了 GitHub 為每一種程式語言都設計了一種顏色,我也可以把這些顏色應用在我的項目里,讓整個終端介面看起來沒那麼單調,色彩更豐富。效果如下:

主介面:

詳情頁:

彈窗提示:

最後為了向 TUI 妥協,按期數或類別搜索,我是通過使用搜索前綴來和普通關鍵詞搜索作出區別。

上面展示的這些差不多已經是這個項目的全部了

三、開發

3.1 技術選型

要實現上述的那些功能,就要從 Rust 的生態中選擇合適的庫了

下面這些是我在這個項目中使用到的:

  • 基礎設施:anyhowthiserrorlazy_staticbetter-panic
  • 繪製 UI:tuicrossterm
  • HTTP client:reqwest
  • 快取:cached
  • HTML 解析:nipper
  • 工具:regexcrossbeam-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✨

地址://github.com/kaixinbaba/hg-tui

五、最後

hg-tui 它從出生那一刻起,體內流淌的就是開源的血。

它很小甚至是微不足道,我本不想開源,但蛋蛋的一段話讓我改變了主意:開源不是完結,僅僅只是開始

一個開源項目可能只是作者的一個靈光乍現,也可能只是為了解決自己實際工作生活中的小小痛點,沒準用完就丟到角落裡了。但開源出來或許就能找到有相同需求的人,從而延續這個項目的生命,或許這就是開源的本意吧。

以上就是我做這個項目的全部心得和收穫,如果你們對 hg-tui 有什麼建議和問題,歡迎給我提 issue

最後,如果你喜歡本文和項目的話,歡迎點贊和 Star 愛你們喲~