我建構終端應用程式(TUIs),例如用於 k8s 日誌的 kl、jeeves 和用於 Nomad 的 wander。TUIs 的核心功能包括與大量文字進行互動,例如應用程式資訊清單和日誌。我在 Go 中創建了一個可重複使用的視圖組件,用於我專案中的文字導航。
終端分頁器是允許您互動式導航多頁文字的程式。我使用我的視圖組件來製作 lore,我現在每天都將其作為我的終端分頁器使用。
在這篇文章中,我將詳細介紹我希望在視圖組件中支援的功能,以及在實現這些功能過程中獲得的一些學習和設計決策。
除了執行命令之外,終端也經常是檢視和導航文字的地方。
終端具有類似網格的特性,並使用等寬字體。其大小以列和行定義,文字會相應地填滿這個網格。
您可以使用 ANSI escape codes 在終端中為文字加上樣式。
這種樣式是我們在終端中從 chessboard.txt 獲得灰色棋盤格圖案的方式:chessboard.txt 包含帶有 ANSI escape codes 的文字,用於在終端中進行樣式設定。
開發人員經常在終端中掃描大量文字:
當文字輸出少於一個終端檢視的高度時,輸出通常會直接列印到您的終端,而無需特殊的分頁器。然後您可以使用滑鼠捲動,並使用終端模擬器的內建功能進行搜尋(例如 iTerm2 中的 cmd+f)。或者,您可能在終端模擬器中使用 tmux 這樣的終端多工處理器,它有自己的搜尋和捲動綁定。
但對於跨越多個終端頁面的文字,程式通常會使用分頁器進行互動式文字導航。程式會檢查 PAGER 環境變數,如果已設定,則使用該程式來顯示文字,而不是將其傾印到 stdout。PAGER 環境變數指向一個預期接收管道輸入 `<command> | mypager` 或檔案參數 `mypager myfile.txt` 的程式。
像 git、man 等程式具有一些內部邏輯來檢查 PAGER 是否已設定並有條件地使用它。它們通常會直接將內容管道傳輸到 PAGER,如果 stdout 不是 TTY(互動式終端會話),則會跳過。因此,在非 TTY stdout(例如 `git diff | grep ...`)中,PAGER 並不相關。
如果 stdout 是 TTY,程式可能會選擇使用 PAGER。它們會將設定為 PAGER 的程式作為子程序啟動,然後使用管道將分頁器程序的 stdin 與程式的 stdout 連接起來。
我喜歡 Andrew Healey 最近關於建構 Shell 的文章,其中更詳細地介紹了管道和 Shell 用於實現這一切的系統呼叫。
開發人員機器上的大多數程式在未設定 PAGER 時,會使用 less 作為後備分頁器。如果您希望所有內容都直接傾印到您的終端,請將您的 PAGER 環境變數設定為 cat。其他選項包括 bat、most 和 delta。也有專門的分頁環境變數,例如,設定 GIT_PAGER 專門用於 git 輸出,或 BAT_PAGER 用於在 bat 執行語法高亮後進行分頁。
最常見的分頁器 less,透過有效利用選項和設定,功能非常強大。例如,預設情況下,文字在您退出 less 後就會遺失,這通常是合理的,但您可以使用 `--no-init/-X` 選項,讓您在退出前分頁過的文字保留在終端輸出中。並使用 `--ignore-case/-i` 選項來進行不區分大小寫的搜尋。我推薦這些關於 less 選項和 less設定的文章,以了解更多資訊。
終端應用程式,或稱 TUIs,有點像原生桌面應用程式或網站,只是您在終端中執行它們。
TUIs 通常使用 alt screen 來暫時接管整個終端畫面,顯示應用程式元件,如標題、側邊欄、說明文字和其中的文字視圖。文字視圖元件類似於終端分頁器,但只佔用螢幕的一部分。TUIs 中其他常見的使用者工作流程包括:
在 TUI 中,最小的可編輯單位是終端網格單元,而不是像素。這提供了一個很好的限制,迫使終端應用程式移除所有不必要的資訊,並提供一個由鍵盤驅動的資料層級檢視,而不是像網站或桌面應用程式那樣提供一個大型的可捲動畫面和充滿按鈕的工具列。
以 kl 為例,這是一個我為與多個叢集和命名空間的 Kubernetes 日誌互動而建構的 TUI。啟動時,您會看到兩個文字視圖:左側是 Kubernetes 實體層級結構,顯示您設定的叢集、命名空間、Pod 和容器;右側是最初為空的日誌檢視。
您從選取檢視開始,在那裡您可以選取一個或多個容器來追蹤日誌。
按下 L 會以全螢幕顯示這些日誌,隱藏選取樹。從那裡,您可以使用 / 搜尋精確匹配。
您可以按下 x 來查看帶有周圍(不匹配)上下文的匹配項,或僅查看匹配的項目。按下 p 會將 JSON 日誌進行格式化,增加間距和縮排。按下 Enter 會縮放到單一日誌檢視。按下 ? 會顯示所有可能的命令,按下 Ctrl+C 會退出。
這讓您對 TUI 如何成為一個由鍵盤驅動的、由元件組成的應用程式有一個概念,而 TUI 中最重要的元件通常只是迷你終端分頁器。在 kl 中,選取樹和日誌檢視都是迷你終端分頁器。我將這個共用功能提取出來,形成了一個視圖組件。
視圖組件是一個彈性大小的方塊,包含任意數量的文字。這個文字方塊可調整大小、可捲動,提供您在文字中目前位置的百分比指示器,當文字未換行時支援水平平移來換行或取消換行,啟用帶有匹配導航的搜尋,允許項目選取,支援 ANSI escape codes 來為文字加上樣式,處理 Unicode,並且即使處理大量文字也能保持高效能。
這個視圖組件是用 Go 編寫的,可以使用 Bubble Tea TUI 架構輕鬆整合到應用程式中。
為了實現這些功能,實作包含三個模組:
如果您已安裝 Go,最快的嘗試 filterableviewport(這些元素的組合)的方法是執行:
或者,如果您沒有 Go,可以在 Docker 中執行:
大多數 Unicode 字元在終端中佔用 1 個單元格寬度,但特殊字元則不然。
這是一個使用 wcwidth 函式庫來檢查字串在終端欄位中的寬度的 shell 函式:
簡單字串的單元格寬度符合您的預期:
火花表情符號,以其 Unicode 編碼點 ✨ U+2728 SPARKLES 表示,在終端中的寬度佔用兩個單元格。
在終端環境下的 Unicode 文字中,有:
僅從視覺上看,很難猜測給定的字元會佔用多少終端單元格寬度:
您可以用多種不同的方式在 Unicode 中表示單寬字元,例如 é:可以是一個單獨的 é 編碼點,或者是一個 e 編碼點後面跟著一個組合重音編碼點。
為了在我的視圖組件中支援 Unicode,我必須考慮字串位元組(在 Go 中通常是 UTF-8)、編碼點、字形及其對應終端寬度之間的映射。
Item 介面處理了實作,簡化如下:
不可變字串,例如 Kubernetes 日誌,可以一次性預先實例化其 Item 物件。實例化會建立一個稀疏的內部映射,將編碼點對應到位元組偏移量和終端單元格寬度。
MultiItem 實作滿足相同的介面,但處理多個單獨的 Item,允許高效的動態項目前綴(例如行號、時間戳記、容器名稱等),而無需重建整個 SingleItem 即可更改前綴。
對於多行內容,例如格式化的 JSON 日誌,MultiLineItem 實作支援跨越多行斷點的項目。
Item 抽象很好地支援了換行符——連續呼叫 Take 直到項目的底層內容被用完——以及在項目未換行時高效地向左和向右平移。
我發現為精確匹配搜尋(/)、正規表達式搜尋(r)和不區分大小寫搜尋(i)設定不同的鍵盤快捷鍵很有價值,我將其實現為僅正規表達式搜尋,並自動加上不區分大小寫標誌 (?i)。為這些常見搜尋動作設定單獨的單鍵快捷鍵,讓應用程式感覺更敏捷。
套用的搜尋會儲存在記憶體緩衝區中,可以使用上下箭頭鍵進行導航。
filterableviewport 維護一個所有 Item 的序列,它根據過濾器從中過濾出目前可見的項目集。x 鍵盤快捷鍵在僅顯示匹配項或包含匹配項的周圍上下文之間切換。每個項目可以有多個匹配項,n/N 用於在匹配項之間導航,將焦點匹配項帶入螢幕檢視。
對於某些文字檢視,逐頁瀏覽內容就足夠了。對於其他檢視,則需要從可見集中選取一個項目。視圖組件透過啟用或禁用選取來處理這兩種情況。例如,在 kl 中,按下 Enter 鍵選擇一個日誌,會將您帶到該日誌的全頁檢視,格式精美,您可以從中捲動、平移、搜尋或逐一翻閱周圍的日誌。
為了支援項目選取,視圖組件對物件類型是通用的:
呼叫者可以監聽 Enter 等鍵盤事件,在焦點視圖組件上觸發 GetSelectedItem(),並相應地回應返回的物件。
就像之前的 less 和 more 一樣,lore 是我簡單的終端分頁器,它是一個 alt-screen Bubble Tea TUI 應用程式,依賴視圖組件來處理所有功能。只要我在 kl 和 wander 等應用程式中建構符合我對檢視和導航文字偏好的迷你終端分頁器,我就可以在終端中擁有相同的分頁功能!
lore 只支援 less 的一部分功能,但對我日常活動來說更直觀、更有用。我也發現從頭開始理解它很有價值,從位元組到終端檢視,並在我對終端分頁器真正想要和需要的東西有更多了解時繼續改進它。
lore 的安裝說明在這裡。我經常執行 `lore <path-to-some-file>`,或 `<command> | lore`。我也在我的 `~/.zshrc` 中匯出 `PAGER=lore`,每次有新程式在我的日常終端互動中使用 lore 而不是 less 時,我都感到很高興。
這個領域很深入。我對 libghostty 等套件的進展感到興奮,它們涵蓋了我用 Go 視圖組件實現的大部分功能,但具有 Zig 和 C 綁定。我將繼續改進核心視圖組件功能、kl 和我的終端分頁器 lore,以及未來將出現的其他 TUI。