Skip to content

Komponentenbibliothek: Box/Input/Markdown/SelectList

源码版本v0.73.1

Das Verzeichnis components/ aus @mariozechner/pi-tui hält wiederverwendbare Komponenten, die alle das Component-Interface implementieren (render(width): string[] + invalidate()). Sie fallen in drei Gruppen: Layout (Box, Text, TruncatedText, Spacer), interaktiv (Input, SelectList, SettingsList, Editor) und darstellend (Markdown, Image, Loader). TUI selbst implementiert keine konkreten Komponenten, sondern schedult nur den Komponentenbaum.

Zuständigkeiten

components/ macht drei Dinge:

  1. Einheitliches Interface: Alle Komponenten implementieren das Component-Interface; render(width) liefert ein Zeilen-Array mit ANSI-Escapes, invalidate() leert den Cache bei Themenwechsel. Siehe packages/tui/src/tui.ts:39-63.
  2. Caching beim Rendering: Text, Box und Markdown cachen cachedLines; wenn Text und Breite unverändert sind, wird das letzte Ergebnis zurückgegeben, damit nicht jeder Frame neu word-wrappt. Siehe packages/tui/src/components/text.ts:45-58.
  3. Focusable-Protokoll: Input und Editor implementieren Focusable (Feld focused); TUI setzt es beim setFocus, und die Komponente sendet beim Render am Cursor den CURSOR_MARKER. Siehe packages/tui/src/tui.ts:74-82.

Designmotivation

Warum geben alle Komponenten string[] zurück, anstatt selbst zu schreiben? Weil TUI difft: Sie braucht den finalen Inhalt jeder Zeile (inklusive ANSI), um ihn mit dem vorigen Frame zu vergleichen. Komponenten erzeugen nur Zeilen und kümmern sich nicht darum, wie sie ins Terminal kommen — das macht sie testbar (reine Funktion render(width) → string[]) und komponierbar (Container verkettet die Zeilen der Children). Wenn Box mit bgFn einer ganzen Region eine Hintergrundfarbe gibt, fügt applyBackgroundToLine die Farbe in die bestehenden ANSI-Sequenzen ein, anstatt sie nur außen herumzuwickeln; so bleibt visibleWidth korrekt.

Wichtige Dateien

Unter packages/tui/src/components/:

Text.render liefert bei Cache-Treffer direkt zurück, sonst wird neu word-wrappt:

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.render scrollt über startIndex als Fenster und zeichnet nur maxVisible Einträge; der ausgewählte Eintrag bekommt einen Highlight-Präfix:

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 lexrt zuerst mit marked die Tokens, wandelt jeden Token in styled Zeilen um, bricht sie mit wrapTextWithAnsi nach Spaltenbreite um und setzt am Ende 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);
  }

Datenfluss

Wie die Ausgabe einer Komponente in den TUI-Diff gelangt:

Randbedingungen und Fehler

  • Cache-Gültigkeit über invalidate: Beim Themenwechsel ruft TUI Container.invalidate auf und leert rekursiv die Caches aller Children. Subklassen müssen invalidate überschreiben und ihr cachedLines leeren, sonst bleiben Zeilen aus dem alten Thema stehen. Siehe packages/tui/src/components/markdown.ts:110-114.
  • visibleWidth muss stimmen: Wenn Box nach padding die Zeilenbreite width überschreitet, crasht TUI. Box.matchCache prüft über bgSample, ob sich die bgFn-Ausgabe geändert hat, um Cache-Treffer zu entscheiden. Siehe packages/tui/src/components/box.ts:56-65.
  • SelectList leerer Fallback: Wenn filteredItems.length === 0, zeichnet die Liste theme.noMatch("No matching commands") und wirft nicht; der Aufrufer kann weiterlaufen.
  • Markdown Bildzeilen werden nicht umgebrochen: Zeilen, auf die isImageLine(line) zutrifft, werden direkt gepusht und nicht an wrapTextWithAnsi übergeben, weil kitty-graphics-Sequenzen beim Umbrechen kaputtgehen.
  • Input einzeilig ohne Umbruch: Input ist eine vereinfachte Editor-Version; wie Paste mit \n behandelt wird, hängt von handlePaste ab. Input wird hauptsächlich in der SelectList-Suchbox und den SettingsList-Untermenüs eingesetzt und ist nicht für mehrzeilige Bearbeitung gedacht.

Zusammenfassung

components/ ist eine Sammlung von Implementierungen des Component-Interface: Layout-Komponenten (Box/Text) kümmern sich um die Anordnung, interaktive Komponenten (Input/SelectList/Editor) um Eingabe, darstellende Komponenten (Markdown/Loader) um das Rendering. Alle verlassen sich auf die Diff-Schleife der TUI-Klasse, um Änderungen ins Terminal zu schreiben; Editor ist die komplexeste davon, siehe Editor-Komponente; wie der Byte-Stream zum handleInput der Komponenten geparst wird, steht in Tastatur-Parsing: kitty-Protokoll.