Skip to content

コンポーネントライブラリ:Box/Input/Markdown/SelectList

源码版本v0.73.1

@mariozechner/pi-tuicomponents/ ディレクトリは再利用可能なコンポーネントを置き、どれも Component インターフェース(render(width): string[] + invalidate())を実装する。コンポーネントは三種類に分かれる:レイアウト系(BoxTextTruncatedTextSpacer)、インタラクション系(InputSelectListSettingsListEditor)、表示系(MarkdownImageLoader)。TUI 自身は具体的なコンポーネントを実装せず、コンポーネントツリーのスケジュールだけを管する。

責務

components/ は三つの仕事をする:

  1. 統一インターフェース:すべてのコンポーネントは Component インターフェースを実装する。render(width) は ANSI エスケープを含む行配列を返し、invalidate() はテーマ変更時にキャッシュをクリアする。packages/tui/src/tui.ts:39-63 参照。
  2. キャッシュレンダリング:TextBoxMarkdown はいずれも cachedLines を持ち、テキストも幅も変わらなければ前回の結果をそのまま返し、毎フレームの word-wrap 再計算を避ける。packages/tui/src/components/text.ts:45-58 参照。
  3. Focusable プロトコル:InputEditorFocusable(focused フィールド)を実装する。TUIsetFocus で設定し、コンポーネントは render 時にカーソル位置で CURSOR_MARKER を発する。packages/tui/src/tui.ts:74-82 参照。

設計動機

なぜすべてのコンポーネントが string[] を返すのか。直接 write しないのか?TUI が差分を取るためには、各行の最終内容(ANSI 含む)を手に入れないと前フレームと比較できないからだ。コンポーネントは行を生成するだけで、ターミナルにどう書くかは関知しない——これでコンポーネントはテスト可能(純関数 render(width) → string[])になり、組み立て可能(Container が children の行を繋ぐ)になる。BoxbgFn で全体に背景色を塗るとき、applyBackgroundToLine は既存の ANSI と ANSI の間に背景色を差し込む。包むのではなく。これで visibleWidth が正確に計算できる。

主要ファイル

ディレクトリ packages/tui/src/components/ 配下:

Text.render はキャッシュにヒットしたらそのまま返し、さもなくば word-wrap をやり直す:

typescript
// 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 spaces

SelectList.renderstartIndex でウィンドウスクロールし、maxVisible 件だけ描く。選択項目にはプレフィックスでハイライトを付ける:

typescript
// 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 を付ける:

typescript
// 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 を呼び、再帰的にすべての子キャッシュをクリアする。サブクラスは invalidate を上書きして自分の cachedLines をクリアしないと、古いテーマの行を使い回す。packages/tui/src/components/markdown.ts:110-114 参照。
  • visibleWidth は正確に:Box は padding 後に行幅が width を超えることがあり、TUI が検出すると crash する。Box.matchCachebgSamplebgFn 出力の変化を検出しキャッシュヒットを決める。packages/tui/src/components/box.ts:56-65 参照。
  • SelectList 空リストのフォールバック:filteredItems.length === 0 のときは theme.noMatch("No matching commands") を描くだけでエラーを投げず、呼び出し側が続行できる。
  • Markdown 画像行は折り返さない:isImageLine(line) の行は wrapTextWithAnsi を通さずそのまま push する。kitty graphics シーケンスが折り返されると壊れるからだ。
  • Input は単行で改行なし:InputEditor の簡略版で、\n を含むペーストをどう扱うかは handlePaste 次第。主に SelectList の検索ボックス、SettingsList のサブメニューで使われ、複数行編集には向かない。

まとめ

components/Component インターフェースの実装集合:レイアウトコンポーネント(Box/Text)が組版を、インタラクションコンポーネント(Input/SelectList/Editor)が入力を、表示コンポーネント(Markdown/Loader)がレンダリングを分担する。どれも TUI クラス の diff ループで変化をターミナルに書き出す。Editor は中でも最も複雑で、エディタコンポーネント 参照。キーボードのバイトストリームがコンポーネントの handleInput に至るまでの解析は キーボード解析:kitty プロトコル 参照。