Erst die Zustandsmaschine, dann der Spinner
Bei Terminalanwendungen beginnt die Arbeit oft an der falschen Stelle.
Man hat eine Idee für ein Tool, baut die eigentliche Funktion und möchte dann schnell etwas Feedback im Terminal anzeigen.
Also kommt zuerst:
Connecting...
Dann:
Processing...
Dann ein Spinner.
Dann Farben.
Dann eine Progressbar.
Dann ein Banner.
Dann Übergänge.
Dann ein finaler Erfolgs-Screen.
Das Ergebnis sieht irgendwann gut aus.
Und trotzdem fühlt sich der Code merkwürdig fragil an.
Der Grund liegt häufig darin, dass die Darstellung gebaut wurde, bevor überhaupt klar war, welche Zustände die Anwendung besitzt.
Das ist die falsche Reihenfolge.
Eine gute TUI beginnt nicht mit dem Spinner.
Sie beginnt mit der Zustandsmaschine.
Ein Spinner ist nur ein Symptom eines Zustands
Ein Spinner zeigt nicht einfach „Animation“.
Er transportiert eine Information:
Die Anwendung arbeitet gerade, aber der Fortschritt ist nicht exakt messbar.
Das ist bereits ein Zustand.
Wenn dort steht:
⠹ Connecting...
dann steckt dahinter semantisch:
State = Connecting
Wenn dort steht:
██████████████████░░░░ 72%
dann steckt dahinter eher:
State = Processing
Progress = 72%
Und wenn später erscheint:
VERIFICATION SUCCESSFUL
dann ist auch das kein dekorativer Screen.
Es ist die Darstellung eines anderen Zustands.
Die TUI ist deshalb nicht die Quelle der Wahrheit.
Sie ist nur die Visualisierung eines bereits existierenden Zustandsmodells.
Was ist überhaupt eine Zustandsmaschine?
Eine Zustandsmaschine beschreibt, in welchem Zustand sich ein System befindet und welche Übergänge zwischen diesen Zuständen erlaubt sind.
Für ein einfaches Werkzeug könnte das so aussehen:
START
↓
CONNECTING
↓
PROCESSING
↓
COMPLETED
Das klingt zunächst fast trivial.
Sobald reale Fehlerfälle dazukommen, wird es interessanter:
START
↓
CONNECTING
├──→ FAILED
↓
READY
↓
PROCESSING
├──→ RETRYING
│ ↓
│ PROCESSING
│
├──→ FAILED
↓
VERIFYING
├──→ FAILED
↓
COMPLETED
Plötzlich wird sichtbar:
Das Programm besteht nicht nur aus einer Reihe von Funktionen.
Es bewegt sich durch definierte Zustände.
Implizite Zustände entstehen sowieso
Viele Skripte besitzen bereits Zustände, obwohl sie nie so genannt werden.
Zum Beispiel:
$connected = $false
$processing = $false
$completed = $false
$failed = $false
Dann später:
$connected = $true
$processing = $true
und irgendwann:
$processing = $false
$completed = $true
Damit wurde bereits eine Zustandsmaschine gebaut.
Nur eine schlechte.
Denn theoretisch wären jetzt auch Kombinationen möglich wie:
connected = false
processing = true
completed = true
failed = true
Was soll das bedeuten?
Mit mehreren Booleans wächst die Zahl möglicher Kombinationen extrem schnell.
Vier Boolean-Werte haben bereits:
2⁴ = 16
mögliche Kombinationen.
Aber vermutlich sind nur wenige davon tatsächlich gültig.
Ein expliziter State ist deshalb besser:
$state = 'Processing'
Statt:
$isConnected = $true
$isProcessing = $true
$isCompleted = $false
$isFailed = $false
Ein Zustand sollte eindeutig sein
Eine Anwendung sollte zu einem Zeitpunkt möglichst klar beantworten können:
Was mache ich gerade?
Zum Beispiel:
Initializing
Connecting
Auditing
WaitingForConfirmation
Processing
Retrying
Verifying
Completed
Failed
Cancelled
Diese Zustände schließen sich weitgehend gegenseitig aus.
Das reduziert Ambiguität.
Die TUI kann darauf direkt reagieren.
switch ($State.Phase) {
'Connecting' {
Show-ConnectingScreen
}
'Processing' {
Show-ProcessingScreen
}
'Verifying' {
Show-VerificationScreen
}
'Completed' {
Show-CompletedScreen
}
}
Damit wird die Oberfläche deterministisch.
Der Zustand ist mehr als ein Name
Nur:
$State = 'Processing'
reicht meist nicht.
Ein Zustand braucht Kontext.
Beispielsweise:
$state = [pscustomobject]@{
Phase = 'Processing'
CurrentFolder = 'Archive / 2025'
Processed = 116
Total = 184
Failed = 0
StartedAt = $startedAt
}
Die TUI kann daraus alles ableiten, was sie braucht.
Zum Beispiel:
PROCESSING
Archive / 2025
████████████████████████░░░░░░░░ 63%
116 processed
68 remaining
0 failed
Der Renderer muss nichts über die Geschäftslogik wissen.
Er bekommt nur State.
State und Event sind nicht dasselbe
Das ist eine wichtige Unterscheidung.
Ein Event beschreibt:
Etwas ist gerade passiert.
Ein State beschreibt:
So sieht die Situation jetzt aus.
Beispiel:
Event:
MessageProcessed
Danach kann der Zustand sein:
Processed = 117
Remaining = 67
Oder:
Event:
RetryScheduled
Danach:
Phase = Retrying
RetryCount = 2
RetryAfter = 5
Events sind punktuell.
State ist persistent.
Das ist besonders für TUIs wichtig, weil ein Screen jederzeit aus dem aktuellen Zustand neu aufgebaut werden können sollte.
Gute TUIs rendern State, nicht Event-Historie
Schlecht wäre:
Connected
Folder found
Message processed
Message processed
Message processed
Retry
Message processed
...
Das ist Log-Ausgabe.
Eine TUI sollte eher den aktuellen Zustand verdichten:
PROCESSING
Current folder
Archive / 2025
Processed 117
Remaining 67
Retries 1
Failed 0
Logs beantworten:
Was ist alles passiert?
Eine TUI beantwortet:
Wo stehen wir jetzt?
Diese Trennung ist zentral.
Ein Screen ist eine Projektion des Zustands
Ein Screen sollte nicht selbst entscheiden, wie die Anwendung weiterläuft.
Er stellt nur State dar.
Zum Beispiel:
State
──────────────
Phase = Processing
Processed = 117
Total = 184
Failed = 0
wird projiziert auf:
PROCESSING
█████████████████████████░░░░░ 64%
117 / 184
0 failed
Man könnte denselben State aber auch als JSON darstellen:
{
"phase": "processing",
"processed": 117,
"total": 184,
"failed": 0
}
Oder als Log:
2026-09-05T14:32:10Z processed=117 total=184 failed=0
Das zeigt den Vorteil:
State
├── TUI
├── JSON
└── Log
Die Engine bleibt dieselbe.
Zustandsübergänge müssen bewusst definiert sein
Wenn Zustände explizit werden, sollte auch klar sein, welche Übergänge erlaubt sind.
Zum Beispiel:
Initializing
↓
Connecting
↓
Auditing
↓
WaitingForConfirmation
↓
Processing
↓
Verifying
↓
Completed
Ein Übergang:
Completed → Processing
wäre dagegen wahrscheinlich ungültig.
Oder:
Connecting → Verifying
ergibt keinen Sinn.
Diese Regeln müssen nicht zwingend in einem großen State-Machine-Framework landen.
Oft reicht schon eine klare Orchestrierung.
Aber man sollte sie bewusst besitzen.
Fehler sind Zustände, nicht nur Exceptions
Eine Exception ist ein technisches Ereignis.
Ein Fehlerzustand ist eine fachliche Situation.
Beispiel:
catch {
$state.Phase = 'Failed'
$state.Error = $_.Exception.Message
}
Der Screen kann dann darstellen:
PROCESSING FAILED
Completed
117 / 184
Reason
HTTP 503 Service Unavailable
Resume possible
Das ist wesentlich besser als:
Exception calling Invoke-MgGraphRequest...
Die Exception bleibt intern wichtig.
Die TUI zeigt aber den Zustand, den sie verursacht hat.
Retry ist ein eigener Zustand
Retry wird oft nur als Schleife implementiert:
catch {
Start-Sleep -Seconds 5
retry
}
Für die Anwendung ist aber etwas Relevantes passiert.
Sie wartet bewusst.
Also kann das ein Zustand sein:
RETRYING
Server throttled the request.
Retry 2 / 5
Continuing in 4 seconds
Das ist für den Benutzer wesentlich verständlicher als ein eingefrorener Spinner.
Auch hier gilt:
Der Renderer braucht keine Kenntnis über HTTP 429.
Er bekommt:
$state = [pscustomobject]@{
Phase = 'Retrying'
RetryNumber = 2
RetryMax = 5
RetryAfter = 4
Reason = 'Rate limited'
}
Warten ist ebenfalls ein Zustand
Ein besonders wichtiger Zustand ist:
WaitingForConfirmation
Denn die Anwendung arbeitet hier nicht.
Sie wartet auf den Benutzer.
Das sollte sich visuell unterscheiden von:
Processing
Ein Spinner wäre hier sogar falsch.
Denn:
⠹ Waiting for confirmation...
suggeriert Aktivität.
Tatsächlich ist die Anwendung blockiert und braucht eine Entscheidung.
Besser:
CONFIRMATION REQUIRED
184 items will be modified.
Type FILE to continue.
Auch kleine UI-Details werden klarer, sobald der Zustand sauber modelliert ist.
Ein Spinner gehört nur zu bestimmten Zuständen
Wenn die Zustände definiert sind, lässt sich auch Animation sinnvoll zuordnen.
Zum Beispiel:
| Zustand | Sinnvolle Darstellung |
|---|---|
| Connecting | Spinner |
| Auditing | Spinner oder Counter |
| WaitingForConfirmation | keine Animation |
| Processing | Progressbar |
| Retrying | Countdown / Spinner |
| Verifying | Spinner |
| Completed | statischer Screen |
| Failed | statischer Screen |
Jetzt bekommt Animation eine Semantik.
Sie ist nicht mehr Dekoration.
Fortschritt braucht eine Definition
Sobald ein State Progress enthält, muss klar sein, was er bedeutet.
Beispiel:
$state.Processed = 117
$state.Total = 184
Dann:
117 / 184 = 63.6%
Aber was bedeutet Processed?
Mögliche Interpretationen:
Requests gesendet
Responses erhalten
erfolgreiche Operationen
Elemente verifiziert
Diese Dinge sind nicht gleich.
Der Zustand sollte deshalb möglichst ehrlich modelliert werden.
Zum Beispiel:
$state = [pscustomobject]@{
Attempted = 120
Succeeded = 117
Failed = 1
Retrying = 2
Total = 184
}
Dann kann die TUI klar sagen:
117 successful
1 failed
2 retrying
64 remaining
Das ist wesentlich informativer als eine vage Zahl.
Der Zustand sollte die Wahrheit des Systems abbilden
Das ist ein wichtiger Grundsatz:
Die TUI darf nicht mehr wissen als der State.
Wenn der State nur sagt:
184 requests completed
darf der Screen nicht behaupten:
ALL ITEMS VERIFIED
Dafür braucht es einen separaten Verifikationszustand.
Zum Beispiel:
Phase = Verifying
und danach:
Phase = Completed
Remaining = 0
Verified = true
Dann ist ein grünes:
VERIFIED
auch tatsächlich gerechtfertigt.
Zustandsmaschinen reduzieren UI-Bugs
Viele klassische TUI-Probleme entstehen dadurch, dass Screens zu viel Eigenlogik besitzen.
Beispielsweise:
Wenn $processing und nicht $verifying und $failed -eq 0,
dann grüne Progressbar,
außer $retrying...
Mit expliziten Zuständen wird daraus:
Processing
Retrying
Verifying
Failed
Das vereinfacht Rendering massiv.
Statt dutzender Bedingungen:
if (...) {
if (...) {
if (...) {
gibt es:
switch ($state.Phase) {
Das ist einfacher zu verstehen und zu testen.
State Explosion vermeiden
Natürlich kann man es auch übertreiben.
Man könnte für jedes Detail einen Zustand erfinden:
ConnectingToApi
ConnectingToMailbox
LoadingRootFolders
LoadingHiddenFolders
LoadingChildFolders
BuildingQueue
...
Das wird schnell unübersichtlich.
Eine gute Faustregel:
Ein Zustand sollte relevant sein, wenn er mindestens eine dieser Eigenschaften besitzt:
- andere Benutzerinformation
- anderes erlaubtes Verhalten
- anderer Fehlerpfad
- andere Interaktion
- andere Renderinglogik
- relevante Wiederaufnahmegrenze
Nicht jeder interne Funktionsaufruf braucht einen State.
Hauptzustand und Unterzustand
Bei komplexeren Tools kann eine Hierarchie helfen.
Zum Beispiel:
Processing
├── LoadingBatch
├── SendingBatch
├── WaitingForRetry
└── UpdatingCheckpoint
Die TUI muss vielleicht nur:
Processing
darstellen.
Intern kann trotzdem ein detaillierterer Substate existieren.
Das verhindert, dass das UI-Modell mit Implementierungsdetails überladen wird.
State sollte möglichst zentral verwaltet werden
Ein häufiger Fehler ist, State über das ganze Programm zu verteilen.
Zum Beispiel:
$Script:Processed
$Script:Failed
$Script:CurrentFolder
$Script:RetryCount
$Script:IsVerifying
$Script:StartedAt
Das funktioniert.
Aber irgendwann ist schwer nachvollziehbar, welche Funktion welche Variable verändern darf.
Besser ist ein zusammenhängendes Objekt:
$state = [pscustomobject]@{
Phase = 'Processing'
CurrentFolder = $null
Processed = 0
Failed = 0
Retried = 0
Total = 0
StartedAt = Get-Date
}
Dann wird klar:
Application State
ist eine eigene Einheit.
Noch besser: kontrollierte Zustandsänderungen
Bei größeren Tools kann man Änderungen über Funktionen laufen lassen:
Set-FBState -Phase Processing
Update-FBProgress -Processed 117 -Failed 0
oder über Events:
Publish-FBEvent -Type ItemProcessed -Data $item
Der Vorteil:
Zustandsänderungen sind zentral kontrollierbar.
Man kann dabei:
- validieren
- loggen
- rendern
- testen
Das ist besonders wertvoll, wenn mehrere UI-Modi existieren.
TUI und Orchestrator sind unterschiedliche Dinge
Die Zustandsmaschine gehört konzeptionell eher zum Orchestrator als zum Renderer.
Der Orchestrator entscheidet:
Connecting
↓
Auditing
↓
Confirmation
↓
Processing
Der Renderer sagt nur:
Wenn Connecting:
zeige Connecting Screen
Also:
Orchestrator
│
│ changes State
▼
State
│
▼
Renderer
Nicht:
Renderer
↓
entscheidet, was als Nächstes passiert
Das ist eine wichtige Grenze.
Der Renderer sollte möglichst deterministisch sein
Idealerweise gilt:
gleicher State
=
gleicher Screen
Das macht Rendering testbar.
Wenn:
$state = @{
Phase = 'Completed'
Processed = 184
Failed = 0
Remaining = 0
}
dann sollte immer derselbe Completion Screen entstehen.
Der Renderer braucht dafür keine API.
Keine Dateien.
Keine Business Rules.
Nur State.
Screenshots werden dadurch reproduzierbar
Das hat einen angenehmen Nebeneffekt.
Man kann UI-Zustände isoliert entwickeln.
Zum Beispiel:
$testState = [pscustomobject]@{
Phase = 'Processing'
CurrentFolder = 'Inbox / Archive / 2026'
Processed = 125
Total = 184
Failed = 2
}
Dann:
Show-ProcessingScreen -State $testState
Keine echte API-Verbindung nötig.
Keine 184 echten Nachrichten.
Keine Wartezeit.
Damit wird TUI-Design plötzlich viel angenehmer.
Fehlerzustände lassen sich ebenfalls simulieren
Zum Beispiel:
$testState = [pscustomobject]@{
Phase = 'Failed'
Processed = 125
Total = 184
Error = 'HTTP 503 Service Unavailable'
CanResume = $true
}
Damit lässt sich der Fehler-Screen jederzeit prüfen.
Das ist ein großer Unterschied zu einer UI, die nur sichtbar wird, wenn zufällig ein echter Fehler auftritt.
Zustandsmaschinen verbessern Tests
Auch funktionale Tests werden einfacher.
Man kann beispielsweise prüfen:
Initial State
= Initializing
Nach erfolgreicher Authentifizierung:
Connecting
→ Auditing
Nach Bestätigung:
WaitingForConfirmation
→ Processing
Nach erfolgreicher Verarbeitung:
Processing
→ Verifying
Nach Verifikation:
Verifying
→ Completed
Bei Fehler:
Processing
→ Failed
Das ist wesentlich präziser als zu testen, ob irgendwo:
"Success"
ausgegeben wurde.
Resume braucht Zustand
Spätestens bei Wiederaufnahme wird State unvermeidlich.
Angenommen, eine Operation verarbeitet:
184 Elemente
Nach 120 Elementen bricht das Programm ab.
Ein Checkpoint könnte enthalten:
{
"phase": "processing",
"processed": 120,
"succeededIds": [
"..."
],
"startedAt": "2026-09-05T14:00:00Z"
}
Beim nächsten Start entsteht:
RESUMING
als eigener sinnvoller Zustand.
Die TUI könnte anzeigen:
RESUMING PREVIOUS RUN
120 items already completed
64 items remaining
Ohne explizites Zustandsmodell wirkt Resume schnell wie ein angeklebtes Sonderfeature.
Persistierter Zustand ist nicht automatisch UI-State
Dabei sollte man zwei Dinge unterscheiden.
Persistenter Arbeitszustand:
welche IDs sind bereits erfolgreich?
welcher Run war aktiv?
UI-State:
welcher Screen wird angezeigt?
welcher Ordner ist aktuell?
welche Animation läuft?
Nicht alles muss gespeichert werden.
Nach einem Neustart ist beispielsweise egal, welches Spinner-Frame zuletzt sichtbar war.
Relevant ist nur der fachliche Zustand.
Das ist ein gutes Beispiel für die Trennung von:
Domain State
UI State
Domain State und UI State
Ein sauberes Modell könnte so aussehen:
Domain State
────────────
Processed
Failed
Remaining
CurrentItem
VerificationResult
UI State
────────
CurrentScreen
LastRenderTime
SpinnerFrame
TerminalWidth
AnimationEnabled
Der Renderer verwendet beide.
Die Geschäftslogik sollte aber nur Domain State kennen.
Warum das gegen Flackern hilft
Interessanterweise verbessert ein sauberes State-Modell auch das Rendering.
Wenn die UI weiß:
State vorher = Processing
State jetzt = Processing
dann braucht sie keinen Full Clear.
Nur ein Live Update.
Wenn dagegen:
State vorher = Processing
State jetzt = Verifying
kann sie bewusst einen Screen Transition durchführen.
Damit ergibt sich automatisch:
same state
→ in-place redraw
state transition
→ full redraw
Das ist wesentlich sauberer als überall manuell zu entscheiden, wann Clear-Host ausgeführt wird.
Rendering wird vom State Change getrieben
Damit kann man ein sehr einfaches Modell bauen:
if ($PreviousState.Phase -ne $CurrentState.Phase) {
Invoke-ScreenTransition
}
Render-State $CurrentState
Zusätzlich können Live-Updates gedrosselt werden:
if ((Get-Date) - $LastRender -gt 150ms) {
Render-State $CurrentState
}
Die Zustandsmaschine liefert also nicht nur semantische Klarheit.
Sie liefert auch eine Grundlage für effizientes Rendering.
Animation ist ein Detail des Renderers
Jetzt kann der Spinner endlich an die richtige Stelle.
Zum Beispiel:
if ($State.Phase -eq 'Connecting') {
$frame = Get-SpinnerFrame
}
Die Engine weiß nicht, dass ein Spinner existiert.
Das ist genau richtig.
Wenn Animation deaktiviert wird:
-NoAnimation
ändert sich:
Connecting
nicht.
Nur seine Darstellung.
Vielleicht:
⠹ Connecting...
wird zu:
Connecting...
Der Anwendungszustand bleibt identisch.
Dasselbe gilt für Farben
Wenn:
Phase = Failed
dann kann der Renderer entscheiden:
Rot
[ERROR]
Ein Plain Renderer könnte dagegen nur schreiben:
ERROR: Processing failed
Und JSON:
{
"phase": "failed"
}
Wieder:
State bleibt gleich
Darstellung ändert sich
Genau so sollte es sein.
Machine Output profitiert davon besonders
Ein guter State lässt sich fast direkt serialisieren.
Zum Beispiel:
$state | ConvertTo-Json
Natürlich sollte ein öffentliches JSON-Schema bewusst definiert werden.
Aber konzeptionell ist die Nähe hilfreich.
Das verhindert, dass TUI und -Quiet unterschiedliche Vorstellungen vom Ergebnis entwickeln.
Beide basieren auf derselben Wahrheit.
Die Zustandsmaschine sollte nicht zum Selbstzweck werden
Natürlich braucht ein kleines Skript kein Framework mit:
StateFactory
StateTransitionManager
StateRepository
StateObserverProvider
Ein einfacher switch und ein State-Objekt können völlig ausreichen.
Zum Beispiel:
$state.Phase = 'Connecting'
Connect-Service
$state.Phase = 'Processing'
Invoke-Processing
$state.Phase = 'Verifying'
Invoke-Verification
$state.Phase = 'Completed'
Das ist bereits viel besser, als State nur implizit über Ausgaben zu definieren.
Architektur ist nicht die Zahl der Abstraktionen.
Architektur ist Klarheit über Verantwortlichkeiten.
Eine praktische Reihenfolge
Wenn ich heute eine neue TUI-Anwendung bauen würde, wäre die Reihenfolge:
1. Fachlichen Ablauf definieren
Connect
Discover
Confirm
Process
Verify
2. Zustände daraus ableiten
Connecting
Auditing
WaitingForConfirmation
Processing
Verifying
Completed
3. Fehler- und Sonderzustände ergänzen
Retrying
Failed
Cancelled
Resuming
4. Daten pro Zustand definieren
Beispiel:
Processing:
CurrentItem
Processed
Total
Failed
Retrying:
RetryNumber
RetryAfter
Reason
Completed:
Processed
Failed
Remaining
Verified
5. Erst danach Screens bauen
Show-ProcessingScreen
Show-RetryScreen
Show-CompletedScreen
6. Danach Rendering optimieren
Cursor Home
Clear Below
Throttle
Responsive Width
7. Ganz am Ende Animation
Spinner
Pulse
Transitions
Completion Effect
Das ist fast die umgekehrte Reihenfolge, in der solche Tools oft spontan entstehen.
Warum der Spinner zuletzt kommt
Der Spinner ist visuell auffällig.
Architektonisch ist er aber trivial.
Er beantwortet nur:
Wie visualisiere ich einen laufenden, nicht exakt messbaren Zustand?
Wenn nicht einmal klar ist, was:
laufend
bedeutet, ist der Spinner zu früh.
Wenn State und Renderer dagegen sauber getrennt sind, ist seine Implementierung fast nebensächlich.
Das ist die eigentliche Pointe.
Ein schlechtes Beispiel
Angenommen:
Write-Host "Connecting"
Connect-Service
Clear-Host
Write-Host "Processing"
foreach ($item in $items) {
Write-Host "Processing $item"
}
Clear-Host
Write-Host "Done"
Die Zustände existieren zwar.
Aber nur implizit in der Reihenfolge der Ausgabe.
Wenn jetzt Retry hinzukommt:
catch {
Write-Host "Retrying"
}
und später Verification:
Write-Host "Verifying"
beginnt die UI den Ablauf zu definieren.
Das wird zunehmend schwer zu kontrollieren.
Dasselbe mit explizitem State
Stattdessen:
$state.Phase = 'Connecting'
Render-State $state
Connect-Service
$state.Phase = 'Processing'
Render-State $state
foreach ($item in $items) {
$state.CurrentItem = $item
$state.Processed++
Render-State $state
}
$state.Phase = 'Verifying'
Render-State $state
$verification = Invoke-Verification
$state.Phase = 'Completed'
$state.Verified = $verification.Success
Render-State $state
Jetzt ist die Darstellung austauschbar.
Und der Ablauf bleibt verständlich.
Der entscheidende Test
Eine einfache Frage zeigt schnell, ob die Architektur stimmt:
Kann ich die Anwendung komplett ohne Terminaloberfläche ausführen?
Wenn ja, ist die Zustandsmaschine wahrscheinlich von der TUI getrennt.
Zum Beispiel:
Engine
→ State
→ JSON
statt:
Engine
→ TUI
→ irgendwie Ergebnis
Ein zweiter Test:
Kann ich jeden Screen mit künstlichem State anzeigen?
Wenn ja, ist der Renderer wahrscheinlich sauber getrennt.
Beide Tests zusammen sind sehr stark.
Die TUI ist nicht das Programm
Das ist vielleicht die wichtigste Erkenntnis.
Eine gute TUI kann sich wie das Programm anfühlen.
Sie ist es aber nicht.
Das Programm besteht aus:
Business Logic
State
Lifecycle
Error Handling
Die TUI ist nur eine mögliche Oberfläche darauf.
Application
│
State
│
┌───────────┼───────────┐
│ │ │
TUI JSON Log
Wenn diese Trennung gelingt, wird fast alles einfacher:
- Tests
- Quiet Mode
- Automation
- Fehlerbehandlung
- Resume
- Rendering
- Animation
Fazit
Bei einer Terminalanwendung ist die Versuchung groß, mit dem sichtbaren Teil zu beginnen.
Banner.
Farben.
Spinner.
Progressbar.
Transitions.
Aber diese Dinge sind nur Darstellungen.
Unter ihnen liegt etwas Wichtigeres:
Was macht die Anwendung gerade?
Diese Frage sollte jederzeit eindeutig beantwortbar sein.
Deshalb ist die richtige Reihenfolge:
Operation
↓
State
↓
State Transition
↓
Renderer
↓
Screen
↓
Animation
Nicht:
Spinner
↓
noch ein Spinner
↓
if
↓
Clear-Host
↓
mehr State irgendwie nachbauen
Die wichtigste Regel lautet daher:
Erst die Zustandsmaschine, dann der Spinner.
Wenn der Zustand sauber modelliert ist, wird die TUI fast automatisch einfacher.
Wenn der Zustand nicht sauber modelliert ist, kann selbst ein Spinner zum Architekturproblem werden.