05.09.2026 · Fabricio Ruch

Erst die Zustandsmaschine, dann der Spinner

AutomationProgramming PrinciplesSoftware ArchitectureSoftware EngineeringSystem Engineering

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:

ZustandSinnvolle Darstellung
ConnectingSpinner
AuditingSpinner oder Counter
WaitingForConfirmationkeine Animation
ProcessingProgressbar
RetryingCountdown / Spinner
VerifyingSpinner
Completedstatischer Screen
Failedstatischer 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.