Eine TUI ist mehr als farbiges Write-Host
Terminalanwendungen erleben seit einigen Jahren eine kleine Renaissance.
Viele moderne Entwicklerwerkzeuge laufen wieder dort, wo Softwareentwicklung ohnehin häufig stattfindet: im Terminal. Paketmanager, Git-Werkzeuge, Cloud-CLIs und zunehmend auch KI-Assistenten zeigen, dass eine textbasierte Oberfläche weder altmodisch noch zwangsläufig primitiv sein muss.
Moderne Terminalanwendungen verwenden Farben, Panels, Tabellen, Spinner, Fortschrittsanzeigen und dynamische Statusinformationen. Manche fühlen sich eher wie kleine Anwendungen als wie klassische Kommandozeilenprogramme an.
Das verleitet zu einer einfachen Schlussfolgerung:
Ein paar ANSI-Farben, ein Spinner und etwas ASCII-Art – fertig ist die TUI.
Genau das ist der Fehler.
Eine gute Terminal User Interface ist keine Sammlung hübscher Write-Host-Aufrufe.
Sie ist eine Benutzeroberfläche.
Und damit gelten erstaunlich viele der gleichen Architekturprinzipien wie bei einer GUI.
CLI und TUI sind nicht dasselbe
Zunächst lohnt sich eine begriffliche Trennung.
Eine klassische CLI kann beispielsweise so aussehen:
> tool process
Connecting...
Found 184 items.
Processing...
184 items processed.
Done.
Das Programm schreibt Informationen nacheinander auf stdout.
Jede neue Ausgabe erweitert das bisherige Terminalprotokoll.
Eine TUI funktioniert anders.
Sie behandelt das Terminal zumindest teilweise als Darstellungsfläche.
Beispielsweise:
┌──────────────────────────────────────────────────────┐
│ PROCESSING │
├──────────────────────────────────────────────────────┤
│ Current folder Archive / 2025 │
│ │
│ ███████████████████████░░░░░░░░░░░░░ 63% │
│ │
│ Processed 116 │
│ Remaining 68 │
│ Failed 0 │
└──────────────────────────────────────────────────────┘
Beim nächsten Update soll nicht zwangsläufig ein weiterer solcher Block darunter erscheinen.
Stattdessen verändert sich der bestehende Screen.
Damit entsteht ein völlig anderes Problem.
Es geht nicht mehr nur darum, was ausgegeben wird.
Es geht darum:
- wo etwas dargestellt wird,
- wann es aktualisiert wird,
- was stehen bleibt,
- was gelöscht werden muss,
- welche Informationen zusammengehören,
- wie Zustandswechsel aussehen,
- was passiert, wenn sich die Terminalgröße ändert.
Damit betreten wir UI-Territorium.
Write-Host ist kein UI-Framework
PowerShell macht den Einstieg besonders einfach:
Write-Host "Connecting..." -ForegroundColor Cyan
Write-Host "Success" -ForegroundColor Green
Das funktioniert hervorragend für normale Konsolenausgaben.
Dann kommt vielleicht ein Rahmen dazu:
Write-Host "┌────────────────────┐"
Write-Host "│ PROCESSING │"
Write-Host "└────────────────────┘"
Dann ein Spinner:
$frames = @('⠋','⠙','⠹','⠸','⠼')
Dann ANSI-Farben.
Dann Clear-Host.
Dann Cursorsteuerung.
Und plötzlich enthält der Code überall Dinge wie:
Write-Host ...
Write-Raw ...
SetCursorPosition(...)
Clear-Host
Start-Sleep ...
Das Ergebnis kann optisch beeindruckend sein.
Architektonisch ist es trotzdem häufig problematisch.
Denn jetzt entscheidet die Geschäftslogik gleichzeitig:
Was passiert?
+
Was soll angezeigt werden?
+
Wo soll es angezeigt werden?
+
Wie soll es aussehen?
Diese Verantwortlichkeiten sollten getrennt werden.
Der wichtigste Perspektivwechsel
Eine TUI sollte nicht primär aus Ausgabebefehlen bestehen.
Sie sollte einen Anwendungszustand darstellen.
Statt:
Write-Host "Processed: $processed"
Write-Host "Remaining: $remaining"
sollte man gedanklich zuerst diesen Zustand haben:
$state = [pscustomobject]@{
Phase = 'Processing'
CurrentItem = 'Archive / 2025'
Processed = 116
Total = 184
Failed = 0
}
Die TUI bekommt diesen Zustand und entscheidet, wie er aussieht.
Konzeptionell:
Application
│
│ State
▼
Renderer
│
▼
Terminal
Die Geschäftslogik kennt damit nicht mehr:
Cyan
Green
Cursor Position
Box Drawing
Spinner Frame
Terminal Width
Sie kennt nur ihren eigenen Zustand.
Das ist der zentrale Architekturgedanke hinter einer stabilen TUI.
Eine TUI ist ein Renderer für eine Zustandsmaschine
Viele Werkzeuge haben ohnehin natürliche Zustände.
Ein API-basiertes Verarbeitungstool könnte beispielsweise folgende Phasen besitzen:
INITIALIZING
↓
CONNECTING
↓
DISCOVERING
↓
AUDITING
↓
CONFIRMING
↓
PROCESSING
↓
VERIFYING
↓
COMPLETED
Zusätzlich können Zustände wie diese auftreten:
RETRYING
CANCELLED
FAILED
Diese Zustände existieren unabhängig davon, ob wir sie modellieren.
Wenn wir sie nicht explizit modellieren, verstecken sie sich irgendwann in Variablen:
$isConnected = $true
$isProcessing = $false
$isVerifying = $true
$hasFailed = $false
Oder noch schlimmer: Der Zustand ergibt sich nur daraus, welche UI-Funktion gerade aufgerufen wird.
Besser ist:
Application State
↓
Screen Selection
↓
Rendering
Zum Beispiel:
switch ($State.Phase) {
'Connecting' {
Show-ConnectingScreen $State
}
'Processing' {
Show-ProcessingScreen $State
}
'Verifying' {
Show-VerificationScreen $State
}
'Completed' {
Show-CompletedScreen $State
}
}
Jetzt ist klar:
Der Screen ist eine Darstellung eines Zustands.
Nicht der Zustand selbst.
Screens statt verstreuter Ausgaben
Eine hilfreiche Abstraktion ist das Konzept eines Screens.
Beispielsweise:
Boot Screen
Connection Screen
Audit Screen
Confirmation Screen
Processing Screen
Verification Screen
Completion Screen
Error Screen
Jeder Screen beantwortet eine bestimmte Frage.
Der Connection Screen:
Mit welchem System verbindet sich die Anwendung gerade?
Der Audit Screen:
Was wurde gefunden?
Der Confirmation Screen:
Was wird verändert, wenn ich fortfahre?
Der Processing Screen:
Wie weit ist die Operation?
Der Completion Screen:
Was ist tatsächlich passiert?
Das klingt nach UX-Theorie, hat aber einen sehr praktischen Effekt:
Man vermeidet Informationschaos.
Eine gute TUI braucht Informationshierarchie
Terminalplatz ist begrenzt.
Deshalb sollte nicht alles gleich wichtig aussehen.
Ein Processing Screen könnte beispielsweise so strukturiert sein:
FILE PROCESSOR
PROCESSING
Current
Archive / Customers / 2025
████████████████████████░░░░░░░░░░░ 68%
125 processed
59 remaining
0 failed
Elapsed 00:01:42
Die Hierarchie ist offensichtlich:
- Zustand
- aktuelles Objekt
- Fortschritt
- Kennzahlen
- sekundäre Information
Schlechter wäre:
Status: Processing
Account: example@example.com
Mode: Write
Folder: Archive / Customers / 2025
Processed: 125
Total: 184
Remaining: 59
Failed: 0
Elapsed: 00:01:42
API: Microsoft Graph
Batch size: 20
Retries: 1
Die Informationen können alle korrekt sein.
Aber die Oberfläche zwingt den Benutzer, selbst herauszufinden, was gerade wichtig ist.
Eine gute TUI reduziert diese kognitive Arbeit.
Layout sollte aus Primitives entstehen
Ein häufiger Fehler ist, Screens direkt mit Leerzeichen zusammenzubauen.
Zum Beispiel:
Write-Host "Processed: $processed"
Write-Host "Remaining: $remaining"
Das funktioniert so lange, bis:
- Zahlen größer werden,
- Beschriftungen länger werden,
- Unicode verwendet wird,
- das Terminal schmaler wird,
- unterschiedliche Inhalte dargestellt werden.
Stattdessen lohnt sich eine kleine Menge wiederverwendbarer Layout-Primitives.
Zum Beispiel:
Header
Panel
KeyValue
ProgressBar
StatusChip
Table
Footer
Rule
Ein Screen wird daraus zusammengesetzt:
Screen
├── Header
├── StatusChip
├── Panel
│ ├── Current Item
│ └── ProgressBar
├── KeyValue Statistics
└── Footer
Die einzelnen Komponenten wissen, wie breit sie sein dürfen.
Der Screen entscheidet nur, welche Komponenten vorkommen.
Terminalbreite ist Teil des Layouts
Eine feste Breite funktioniert auf dem eigenen Rechner erstaunlich lange.
Bis jemand das Terminal verkleinert.
Dann passiert:
┌──────────────────────────────────────┐
│ Current folder: Archive / Customers / Europe / Switzerland / ...
└──────────────────────────────────────┘
Der Text läuft aus dem Rahmen.
Deshalb sollte die Terminalbreite einmal zentral bestimmt werden.
Zum Beispiel:
$width = [Console]::WindowWidth
Darauf basierend kann eine sinnvolle Content-Breite berechnet werden.
Beispielsweise:
Terminal: 120 Zeichen
UI: 90 Zeichen
Terminal: 80 Zeichen
UI: 76 Zeichen
Terminal: 55 Zeichen
UI: 51 Zeichen
Wichtig ist nicht die konkrete Formel.
Wichtig ist:
Komponenten sollten ihre verfügbare Breite kennen.
Dann können sie Inhalte:
- abschneiden,
- umbrechen,
- verkürzen,
- anders anordnen.
Sichtbare Länge ist nicht immer String-Länge
Sobald ANSI-Sequenzen verwendet werden, entsteht ein weiteres Problem.
Dieser String:
ESC[32mSUCCESS ESC[0m
enthält mehr Zeichen als das Wort:
SUCCESS
Auf dem Bildschirm sind aber nur sieben Zeichen sichtbar.
Ein simples:
$text.Length
liefert deshalb für Layoutberechnungen die falsche Information.
Eine TUI braucht gegebenenfalls zwei Vorstellungen von Länge:
String Length
Visible Length
ANSI-Sequenzen müssen bei der sichtbaren Länge ignoriert werden.
Sonst entstehen typische Fehler:
┌────────────────────────────┐
│ SUCCESS │
└─────────────────────────┘
Die Berechnung glaubt, der Text sei länger als er tatsächlich dargestellt wird.
Unicode ist eine Pipeline
Moderne Terminal-UIs profitieren stark von Unicode.
Zum Beispiel:
✓
◆
◇
─
│
┌
┐
└
┘
⠋
⠙
⠹
Wenn solche Zeichen nicht korrekt dargestellt werden, wird schnell der Font verdächtigt.
Das kann stimmen.
Es gibt aber mehrere mögliche Ebenen:
Quelldatei
↓
PowerShell
↓
Encoding
↓
Console API
↓
Terminal
↓
Font
Deshalb sollte man Unicode-Probleme isolieren.
Ein einfacher Test:
Write-Host "⠋ ⠙ ⠹ ⠸ ⠼ ◇ ◆ ✓"
Wenn dieser funktioniert, kann das Terminal die Zeichen grundsätzlich darstellen.
Dann liegt das Problem wahrscheinlich nicht beim Font.
Diese Art von Minimaltest ist oft wertvoller als sofort den Renderer umzubauen.
Codepoints können robuster sein
Gerade bei Skripten, die durch unterschiedliche Editoren oder Encoding-Pipelines laufen, kann es sinnvoll sein, kritische UI-Zeichen zur Laufzeit zu erzeugen.
Statt:
$spinner = @('⠋','⠙','⠹')
beispielsweise:
$spinner = @(
[char]0x280B
[char]0x2819
[char]0x2839
)
Das löst nicht jedes Unicode-Problem.
Es verhindert aber, dass bestimmte Glyphen bereits beim Speichern oder Kopieren des Skripts beschädigt werden.
Der schwierigste Teil: Redraw
Sobald sich ein Screen verändert, muss entschieden werden, wie neu gerendert wird.
Die einfachste Variante:
Clear Screen
Draw Screen
Wait
Clear Screen
Draw Screen
Wait
Sie ist leicht zu implementieren.
Und häufig hässlich.
Das Terminal flackert.
Besonders bei kurzen Refresh-Intervallen wirkt die Anwendung nervös.
Warum permanentes Clear-Host flackert
Ein kompletter Clear erzeugt vereinfacht:
alter Frame
↓
leerer Bildschirm
↓
neuer Frame
Das menschliche Auge kann den leeren Zwischenzustand wahrnehmen.
Bei mehreren Updates pro Sekunde entsteht Flackern.
Die intuitive Reaktion lautet dann:
Nicht mehr löschen.
Damit entsteht das nächste Problem.
Wenn der neue Frame kürzer ist als der alte:
OLD:
Processing folder with a very long name
NEW:
Done
kann zurückbleiben:
Doneessing folder with a very long name
Jetzt haben wir Ghosting statt Flicker.
Zwei verschiedene Arten von Redraw
Die Lösung besteht darin, zwei Fälle zu unterscheiden.
1. Zustandswechsel
Beispielsweise:
AUDIT
↓
CONFIRMATION
Hier verändert sich der gesamte Informationskontext.
Ein kompletter Screenwechsel ist sinnvoll:
Clear
↓
Draw Confirmation Screen
Eine sehr kurze Pause kann den Wechsel sogar verständlicher machen.
2. Live-Update innerhalb desselben Zustands
Beispielsweise:
Processing 41%
↓
Processing 42%
Hier sollte der Bildschirm nicht komplett gelöscht werden.
Stattdessen:
Cursor Home
↓
Frame neu zeichnen
↓
übrig gebliebenen Rest löschen
Das ergibt ein wesentlich ruhigeres Bild.
Das grundlegende Rendering-Muster
Damit entsteht ein sehr brauchbares Modell:
Programmstart
↓
Full Clear
Zustandswechsel
↓
Full Clear
↓
neuen Screen zeichnen
Live Update
↓
Cursor Home
↓
Screen neu zeichnen
↓
Rest unterhalb löschen
Programmende
↓
statischen Final Screen anzeigen
Allein diese Trennung beseitigt einen großen Teil typischer TUI-Probleme.
Nicht jedes Event braucht einen Frame
Nehmen wir an, eine API verarbeitet 1’000 Elemente.
Wenn nach jedem Element die komplette TUI neu gezeichnet wird, entstehen 1’000 Renderings.
Das ist meistens unnötig.
Die Verarbeitung kann deutlich schneller sein als die Wahrnehmung des Benutzers.
Deshalb sollte Rendering gedrosselt werden.
Zum Beispiel:
Engine Events:
1
2
3
4
5
6
7
8
9
...
Renderer:
Frame
Frame
Frame
Eine Aktualisierung ungefähr alle 100 bis 200 Millisekunden reicht für viele Terminal-UIs bereits aus.
Die Engine darf schneller arbeiten.
Die UI muss nicht jeden einzelnen Zwischenzustand darstellen.
Progress ist nicht Animation
Ein Spinner und eine Progressbar beantworten unterschiedliche Fragen.
Ein Spinner:
⠹ Connecting...
sagt:
Das Programm arbeitet noch.
Er sagt nicht:
37 Prozent sind erledigt.
Eine Progressbar:
██████████████░░░░░░░░░░ 58%
sagt:
Wir kennen Gesamtmenge und Fortschritt.
Deshalb:
unbekannte Dauer
→ Spinner
bekannte Gesamtmenge
→ Progressbar
Beides gleichzeitig ist nur selten notwendig.
Animation braucht eine Bedeutung
Animation sollte einen Zustand kommunizieren.
Sinnvolle Beispiele:
Spinner
→ Arbeit läuft
Progressbar
→ messbarer Fortschritt
kurzer Übergang
→ neuer Programmzustand
Pulse
→ temporäre Aktivität
Completion Effect
→ Operation abgeschlossen
Problematisch wird Animation, wenn sie nur existiert, weil sie technisch möglich ist.
Dann bekommt man:
Boot Animation
↓
Loading Animation
↓
Transition Animation
↓
Spinner
↓
Progress Animation
↓
Completion Animation
Das Tool beginnt, sich selbst zu inszenieren.
Bei einem Werkzeug, das regelmäßig benutzt wird, wird das schnell lästig.
Eine gute TUI fühlt sich schnell an.
Nicht unbedingt, weil jede Operation schnell ist.
Sondern weil sie unnötige Wartezeit nicht künstlich verlängert.
-NoAnimation ist kein unnötiges Feature
Sobald Animation vorhanden ist, sollte sie deaktivierbar sein.
Zum Beispiel:
.\tool.ps1 -NoAnimation
Das ist aus mehreren Gründen sinnvoll:
- Remote-Terminals
- Screenreader
- CI
- Logging
- langsamere Terminalumgebungen
- persönliche Präferenz
Auch das ist ein Hinweis darauf, dass Animation eine Darstellungseigenschaft sein sollte.
Nicht Teil der Geschäftslogik.
Sound gehört ebenfalls zur Darstellung
Das Gleiche gilt für akustisches Feedback.
Ein Abschluss-Beep kann charmant sein.
Er sollte aber nicht tief in der Verarbeitung stecken:
Invoke-ApiOperation
[Console]::Beep()
Besser:
Operation Completed
↓
UI entscheidet über
↓
visual completion
sound
nothing
Damit kann beispielsweise ein -NoSound-Modus problemlos existieren.
Die TUI darf die Engine nicht besitzen
Das ist wahrscheinlich die wichtigste technische Grenze.
Schlecht:
function Show-ProcessingScreen {
$response = Invoke-MgGraphRequest ...
...
}
Jetzt führt der Screen die Geschäftsoperation aus.
Das erzeugt eine starke Kopplung.
Besser:
$result = Invoke-Processing
Show-ProcessingScreen -State $result
Noch besser für Live-Updates ist ein Event- oder Callback-Modell:
Engine
│
├── ProgressChanged
├── ItemCompleted
├── RetryStarted
└── ProcessingCompleted
│
▼
UI
Die Engine meldet, was passiert.
Die TUI entscheidet, was davon sichtbar wird.
Dasselbe Programm sollte ohne TUI funktionieren
Das ist ein hervorragender Architekturtest.
Angenommen:
.\tool.ps1
zeigt eine interaktive TUI.
Dann sollte beispielsweise:
.\tool.ps1 -Quiet
dieselbe Operation ausführen können und stattdessen etwas wie dieses liefern:
{
"status": "completed",
"processed": 184,
"failed": 0,
"remaining": 0
}
Wenn das problemlos möglich ist, sind Engine und UI wahrscheinlich ausreichend getrennt.
Wenn -Quiet bedeutet, dass an 47 Stellen:
if (-not $Quiet) {
Write-Host ...
}
eingebaut werden muss, ist das ein Warnsignal.
Human Interface und Machine Interface
Ein gutes Kommandozeilenwerkzeug kann zwei Zielgruppen haben.
Den Menschen:
┌────────────────────────────────────────────┐
│ CASE CLOSED │
│ │
│ 184 items processed │
│ 0 failures │
│ 0 remaining │
└────────────────────────────────────────────┘
Und andere Software:
{
"processed": 184,
"failed": 0,
"remaining": 0
}
Beide Oberflächen verwenden dieselbe Engine.
Damit entsteht:
Core
│
┌────────┴────────┐
│ │
TUI JSON
│ │
Human Automation
Das ist ein überraschend guter Maßstab für die Qualität einer TUI-Architektur.
stdout ist nicht einfach eine Leinwand
Gerade bei CLI-Tools sollte außerdem bewusst entschieden werden, wohin welche Ausgabe gehört.
Maschinenlesbare Ausgabe auf stdout verliert ihren Wert, wenn dazwischen plötzlich steht:
Connecting...
⠹ Processing...
Success!
{"processed":184}
Ein JSON-Modus sollte wirklich JSON liefern.
Keine Spinner.
Keine Banner.
Keine ANSI-Sequenzen.
Keine Statusmeldungen davor.
Das ist besonders relevant für Pipelines:
$result = .\tool.ps1 -Quiet | ConvertFrom-Json
Eine saubere TUI-Architektur schützt damit gleichzeitig die Automatisierbarkeit des Werkzeugs.
Farbe sollte Bedeutung haben
Eine weitere Versuchung ist, möglichst viele Farben zu verwenden.
Besser ist eine kleine semantische Palette.
Zum Beispiel:
neutral
→ normale Information
accent
→ aktiver Zustand
success
→ erfolgreich
warning
→ Aufmerksamkeit
error
→ Fehler
muted
→ sekundäre Information
Dann bedeutet Grün immer ungefähr dasselbe.
Rot ebenfalls.
Wenn jede Überschrift eine andere Farbe bekommt, wird Farbe zur Dekoration statt zur Information.
Farbe darf nicht die einzige Information sein
Ein Fehler sollte nicht nur rot sein.
Also nicht nur:
Connection failed
in roter Schrift.
Sondern beispielsweise:
[ERROR] Connection failed
Damit bleibt die Bedeutung erhalten:
- ohne ANSI
- bei Farbsehschwäche
- in Logs
- bei Copy & Paste
- in Terminals mit eingeschränkter Farbdarstellung
Das Gleiche gilt für Erfolg:
[OK] Connected
statt nur Grün.
ANSI ist mächtig – und eine Abhängigkeit
ANSI-Escape-Sequenzen ermöglichen unter anderem:
- Farben
- Cursorbewegung
- Screen Clearing
- Line Clearing
- Cursor Visibility
- Terminal Title
Damit lassen sich erstaunlich komplexe Oberflächen bauen.
Aber ANSI-Unterstützung sollte nicht blind vorausgesetzt werden.
Ein robustes Tool sollte sich fragen:
Ist die Ausgabe interaktiv?
Unterstützt das Terminal ANSI?
Ist Output redirected?
Ist Quiet Mode aktiv?
Daraus kann ein Rendering-Modus entstehen:
Interactive
Plain
Machine
Das ist häufig sinnvoller als ein einzelnes:
UI = true/false
Der Cursor gehört zum Ressourcenmanagement
Eine kleine, aber wichtige Lektion:
Wenn eine TUI den Cursor versteckt:
[Console]::CursorVisible = $false
muss sie ihn garantiert wiederherstellen.
Auch bei Fehlern.
Also:
try {
...
}
finally {
[Console]::CursorVisible = $true
}
Dasselbe gilt für:
- Terminaltitel
- Farben
- Buffer-Zustand
- alternative Screens
- andere veränderte Terminaleigenschaften
Eine TUI sollte das Terminal so sauber verlassen, wie sie es vorgefunden hat.
Fehler-Screens sind Teil der TUI
Fehlerbehandlung ist nicht nur:
catch {
Write-Host $_ -ForegroundColor Red
}
Ein Fehler ist ebenfalls ein Anwendungszustand.
Also kann er einen eigenen Screen besitzen:
PROCESSING FAILED
Operation
Mark items as processed
Reason
HTTP 403 Forbidden
Completed
142 / 184
Checkpoint
Available
The operation can be resumed.
Das ist wesentlich hilfreicher als ein roter Stacktrace.
Für Entwickler kann der Stacktrace trotzdem verfügbar bleiben:
-Verbose
-Debug
oder in einem Log.
Die Benutzeroberfläche sollte aber zuerst erklären, was der Fehler für den aktuellen Vorgang bedeutet.
Completion ist ebenfalls ein Zustand
Dasselbe gilt für Erfolg.
Ein Tool sollte nicht einfach nach langer dynamischer Ausgabe mit:
Done.
enden.
Der finale Screen ist die Quittung des Vorgangs.
Er sollte beantworten:
Was wurde getan?
Wie viel?
Gab es Fehler?
Wurde das Ergebnis verifiziert?
Beispielsweise:
CASE CLOSED
184 items processed
0 failed
0 remaining
Post-run verification successful.
Gerade bei mutierenden Operationen ist das wertvoll.
Der Renderer sollte keine Wahrheit erfinden
Eine besonders wichtige Regel:
Die TUI darf nur Zustände darstellen, die die Engine tatsächlich kennt.
Wenn die API 184 erfolgreiche Requests meldet, darf die TUI sagen:
184 operations completed
Sie darf daraus aber nicht automatisch machen:
Everything verified successfully
Dafür braucht es eine tatsächliche Verifikation.
UI-Texte sind Teil der Semantik des Systems.
Ein grüner:
SUCCESS
ist technisch gesehen eine Behauptung.
Sie sollte stimmen.
Eine Progressbar braucht ebenfalls ehrliche Daten
Dasselbe gilt für:
████████████████████████████████ 100%
100 Prozent können unterschiedliche Dinge bedeuten:
alle Requests gesendet
oder:
alle Requests beantwortet
oder:
alle Elemente erfolgreich verarbeitet
oder:
gewünschter Endzustand verifiziert
Diese Dinge sind nicht identisch.
Die UI sollte klar definieren, was ihr Fortschritt misst.
TUI-Entwicklung ist Software Engineering
An diesem Punkt wird deutlich, warum eine gute TUI überraschend viel Architektur benötigt.
Nicht weil Terminalausgabe kompliziert sein muss.
Sondern weil eine dynamische Benutzeroberfläche zwangsläufig Fragen erzeugt zu:
State
Rendering
Lifecycle
Layout
Events
Error Handling
Accessibility
Output Contracts
Resource Cleanup
Das sind klassische Software-Engineering-Themen.
Nur eben im Terminal.
Eine sinnvolle Architektur
Für ein mittelgroßes PowerShell-Werkzeug könnte die Struktur beispielsweise so aussehen:
Tool.ps1
Tool.psm1
Public/
Invoke-Tool.ps1
Private/
Api.ps1
State.ps1
UI.Terminal.ps1
UI.Screens.ps1
Tests/
Api.ps1 weiß nichts über die Darstellung.
State.ps1 beschreibt den Zustand.
UI.Terminal.ps1 enthält technische Rendering-Primitives:
Clear Screen
Cursor Home
Visible Length
Panel
Progressbar
Color
Terminal Width
UI.Screens.ps1 baut daraus semantische Screens:
Audit
Confirmation
Processing
Verification
Completion
Error
Und Invoke-Tool.ps1 orchestriert den Lebenszyklus.
Damit entsteht ungefähr:
Orchestrator
│
┌─────────┴─────────┐
│ │
Engine State
│ │
└─────────┬─────────┘
│
Renderer
│
┌────┴────┐
│ │
TUI JSON
Das ist für viele Tools bereits vollkommen ausreichend.
Man braucht dafür kein großes UI-Framework.
Was ich heute anders bauen würde
Bei einer neuen PowerShell-TUI würde ich nicht mit Farben beginnen.
Auch nicht mit dem Banner.
Und definitiv nicht mit dem Spinner.
Die Reihenfolge wäre:
1. Operation
2. State
3. Zustandsübergänge
4. Output Contract
5. Screens
6. Layout-Primitives
7. Rendering
8. Progress
9. Animation
10. Farben und Effekte
Das wirkt zunächst weniger befriedigend.
Die ersten Versionen sehen langweilig aus.
Aber sobald die Grundlage steht, lassen sich visuelle Ideen wesentlich leichter hinzufügen.
Vor allem, ohne dabei die Geschäftslogik zu beschädigen.
Der Spinner kommt zuletzt
Das ist vielleicht die einfachste Zusammenfassung.
Bei einer TUI ist der Spinner das Sichtbarste.
Deshalb ist er verführerisch.
Architektonisch ist er aber fast das Unwichtigste.
Unter ihm liegen:
State
Lifecycle
Events
Rendering
Layout
Terminal Capabilities
Wenn diese Dinge sauber sind, ist ein Spinner trivial.
Wenn sie nicht sauber sind, wird selbst ein Spinner plötzlich zum Problem.
Fazit
Eine moderne Terminaloberfläche kann erstaunlich viel.
Sie kann interaktiv, informativ und visuell hochwertig sein, ohne das Terminal zu verlassen.
Aber eine gute TUI entsteht nicht dadurch, dass normale Konsolenausgabe dekoriert wird.
Der entscheidende Schritt ist ein anderer:
Nicht mehr darüber nachdenken, welche Zeile als Nächstes ausgegeben werden soll, sondern welchen Zustand die Anwendung gerade hat und wie dieser Zustand dargestellt werden soll.
Aus:
Print
Print
Clear
Print
Sleep
Print
wird:
Application
↓
State
↓
Renderer
↓
Screen
Danach bekommen auch Cursorbewegungen, ANSI, Unicode, Progressbars und Animationen einen klaren Platz.
Die wichtigste Erkenntnis ist deshalb:
Eine gute TUI ist ein Renderer für eine Zustandsmaschine – keine Sammlung farbiger
Write-Host-Statements.
Und vielleicht ist genau das der Punkt, an dem aus einem hübschen Terminalskript eine echte Benutzeroberfläche wird.