解鎖!玩轉 HelloGitHub 的新姿勢

削微寒發表於2022-06-01

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

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

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

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

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

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

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

一、起因

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

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

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

官網:https://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() {
            ...
        }
    });
}

其他剩下的就是業務邏輯,完整的程式碼可以直接看倉庫 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✨

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

五、最後

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

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

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

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

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

相關文章