Komponentenbibliothek: Box/Input/Markdown/SelectList
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:
- Einheitliches Interface: Alle Komponenten implementieren das
Component-Interface;render(width)liefert ein Zeilen-Array mit ANSI-Escapes,invalidate()leert den Cache bei Themenwechsel. Siehepackages/tui/src/tui.ts:39-63. - Caching beim Rendering:
Text,BoxundMarkdowncachencachedLines; wenn Text und Breite unverändert sind, wird das letzte Ergebnis zurückgegeben, damit nicht jeder Frame neu word-wrappt. Siehepackages/tui/src/components/text.ts:45-58. - Focusable-Protokoll:
InputundEditorimplementierenFocusable(Feldfocused);TUIsetzt es beimsetFocus, und die Komponente sendet beim Render am Cursor denCURSOR_MARKER. Siehepackages/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/:
packages/tui/src/components/box.ts:14-60—Box-Container,paddingX/Y+bgFngibt Children eine gemeinsame Hintergrundfarbe, mit render cache.packages/tui/src/components/text.ts:7-58—Textmehrzeiliger Text + automatischer Umbruch + eigene Hintergrundfunktion.packages/tui/src/components/truncated-text.ts:7-50—TruncatedTextnimmt nur die erste Zeile und schneidet auf eine Breite ab, für die Statusleiste.packages/tui/src/components/input.ts:18-50—Inputeinzeilige Eingabe mit horizontalem Scroll, verwaltet selbst paste/kill-ring/undo (gleiches Set wieEditor).packages/tui/src/components/select-list.ts:40-100—SelectListAuf-/Abwahlliste mit filter, zwei Spalten (Hauptspalte + Beschreibung) und scroll indicator.packages/tui/src/components/settings-list.ts:34-83—SettingsListEinstellungs-Panel, integriertInput-Suche + rekursive Untermenüs.packages/tui/src/components/markdown.ts:78-159—Markdownnutzt marked-Lexer + eigene Inline-Styles, um Zeilen mit ANSI zu erzeugen.packages/tui/src/components/loader.ts/packages/tui/src/components/cancellable-loader.ts— spinner; pro Frame bewegt sich nur eine Zeile, was genau den schnellen Diff-Pfad trifft.
Text.render liefert bei Cache-Treffer direkt zurück, sonst wird neu word-wrappt:
// 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 scrollt über startIndex als Fenster und zeichnet nur maxVisible Einträge; der ausgewählte Eintrag bekommt einen Highlight-Präfix:
// 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:
// 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 ruftTUIContainer.invalidateauf und leert rekursiv die Caches aller Children. Subklassen müsseninvalidateüberschreiben und ihrcachedLinesleeren, sonst bleiben Zeilen aus dem alten Thema stehen. Siehepackages/tui/src/components/markdown.ts:110-114. visibleWidthmuss stimmen: WennBoxnach padding die Zeilenbreitewidthüberschreitet, crashtTUI.Box.matchCacheprüft überbgSample, ob sich diebgFn-Ausgabe geändert hat, um Cache-Treffer zu entscheiden. Siehepackages/tui/src/components/box.ts:56-65.SelectListleerer Fallback: WennfilteredItems.length === 0, zeichnet die Listetheme.noMatch("No matching commands")und wirft nicht; der Aufrufer kann weiterlaufen.MarkdownBildzeilen werden nicht umgebrochen: Zeilen, auf dieisImageLine(line)zutrifft, werden direkt gepusht und nicht anwrapTextWithAnsiübergeben, weil kitty-graphics-Sequenzen beim Umbrechen kaputtgehen.Inputeinzeilig ohne Umbruch:Inputist eine vereinfachteEditor-Version; wie Paste mit\nbehandelt wird, hängt vonhandlePasteab.Inputwird hauptsächlich in derSelectList-Suchbox und denSettingsList-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.