元件庫:Box/Input/Markdown/SelectList
@mariozechner/pi-tui 的 components/ 資料夾放可複用元件,都實作 Component 介面(render(width): string[] + invalidate())。元件分三類:佈局類別(Box、Text、TruncatedText、Spacer)、互動類別(Input、SelectList、SettingsList、Editor)、展示類別(Markdown、Image、Loader)。TUI 自己不實作具體元件,只管排程元件樹。
職責
components/ 做三件事:
- 統一介面:所有元件實作
Component介面,render(width)回傳帶 ANSI 轉義的行陣列,invalidate()在主題變更時清快取。見packages/tui/src/tui.ts:39-63。 - 快取渲染:
Text、Box、Markdown都快取cachedLines,文字和寬度都沒變就直接回傳上次結果,避免每幀重新 word-wrap。見packages/tui/src/components/text.ts:45-58。 - Focusable 協定:
Input、Editor實作Focusable(focused欄位),TUI呼叫setFocus時設定,元件 render 時在游標位置發CURSOR_MARKER。見packages/tui/src/tui.ts:74-82。
設計動機
為什麼所有元件都回傳 string[] 而不是直接 write?因為 TUI 要做差分:它需要拿到每一行的最終內容(含 ANSI)才能和上一幀比對。元件只負責生成行,不關心怎麼寫到終端機——這讓元件可測試(純函式 render(width) → string[])、可組合(Container 拼接 children 行)。Box 用 bgFn 給整塊上背景色時,透過 applyBackgroundToLine 把背景色插到已有 ANSI 之間,而不是包一層,這樣 visibleWidth 仍能算對。
關鍵檔案
資料夾 packages/tui/src/components/ 下:
packages/tui/src/components/box.ts:14-60—Box容器,paddingX/Y+bgFn給 children 整塊上背景,帶 render cache。packages/tui/src/components/text.ts:7-58—Text多行文字 + 自動換行 + 自訂背景函式。packages/tui/src/components/truncated-text.ts:7-50—TruncatedText只取首行按寬度截斷,用於狀態列。packages/tui/src/components/input.ts:18-50—Input單行輸入,橫向捲動,自己管 paste/kill-ring/undo(和Editor同一套)。packages/tui/src/components/select-list.ts:40-100—SelectList上下選擇清單,帶 filter、雙列(主列+描述)、scroll indicator。packages/tui/src/components/settings-list.ts:34-83—SettingsList設定面板,內嵌Input搜尋 + 子選單遞迴。packages/tui/src/components/markdown.ts:78-159—Markdown用 marked 詞法 + 自訂 inline style 轉帶 ANSI 的行。packages/tui/src/components/loader.ts/packages/tui/src/components/cancellable-loader.ts— spinner,每幀只動一行,正好走 diff 快路徑。
Text.render 命中快取直接回傳,否則重新 word-wrap:
// packages/tui/src/components/text.ts:45-58
render(width: number): string[] {
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
return this.cachedLines;
}
if (!this.text || this.text.trim() === "") {
const result: string[] = [];
this.cachedText = this.text;
this.cachedWidth = width;
this.cachedLines = result;
return result;
}
// Replace tabs with 3 spacesSelectList.render 用 startIndex 做視窗捲動,只繪 maxVisible 條,選中項加前綴高亮:
// packages/tui/src/components/select-list.ts:74-100
render(width: number): string[] {
const lines: string[] = [];
if (this.filteredItems.length === 0) {
lines.push(this.theme.noMatch(" No matching commands"));
return lines;
}
const primaryColumnWidth = this.getPrimaryColumnWidth();
const startIndex = Math.max(
0,
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),
);
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);Markdown.render 先 marked lexer 出 token,每個 token 轉 styled 行,再 wrapTextWithAnsi 按列寬重折行,最後 padding/background:
// packages/tui/src/components/markdown.ts:116-159
render(width: number): string[] {
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
return this.cachedLines;
}
const contentWidth = Math.max(1, width - this.paddingX * 2);
// ...
const tokens = markdownParser.lexer(normalizedText);
const renderedLines: string[] = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
renderedLines.push(...tokenLines);
}資料流
元件 render 的輸出怎麼進 TUI diff:
邊界與失敗
- 快取失效靠
invalidate:TUI主題切換時呼叫Container.invalidate遞迴清所有 child 快取。子類別必須重寫invalidate清自己的cachedLines,否則會用舊主題的行。見packages/tui/src/components/markdown.ts:110-114。 visibleWidth必須算對:Box在 padding 後行寬可能超width,TUI偵測到會 crash。Box.matchCache透過bgSample偵測bgFn輸出變化以決定快取命中。見packages/tui/src/components/box.ts:56-65。SelectList空清單 fallback:filteredItems.length === 0時繪theme.noMatch("No matching commands"),不拋例外,讓呼叫方繼續執行。Markdown圖像行不折行:isImageLine(line)的行直接 push 不走wrapTextWithAnsi,因為 kitty graphics 序列被折行會破壞。Input單行無換行:Input是Editor的簡化版,貼上含\n會怎樣取決於handlePaste——Input主要用在SelectList搜尋框、SettingsList子選單,不適合編輯多行。
小結
components/ 是 Component 介面的實作集合:佈局元件(Box/Text)管排版,互動元件(Input/SelectList/Editor)管輸入,展示元件(Markdown/Loader)管渲染。它們都靠 TUI 類別 的 diff 迴圈把變化寫到終端機;Editor 是其中最複雜的,見 編輯器元件;鍵盤位元組流到元件 handleInput 的解析見 鍵盤解析:kitty 協定。