mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
459 lines
16 KiB
Markdown
459 lines
16 KiB
Markdown
# Deep Dive: Terminal.Gui MainLoop Architecture Analysis
|
|
|
|
## Executive Summary
|
|
|
|
After independently analyzing the Terminal.Gui codebase, Terminal.Gui uses a **modern, streamlined architecture** with a clear separation between input and UI processing threads. The "MainLoop" is implemented as `ApplicationMainLoop<T>` which coordinates event processing, layout, drawing, and timers in a single cohesive system.
|
|
|
|
**Update (October 2025):** The legacy MainLoop infrastructure has been completely removed, simplifying the architecture significantly.
|
|
|
|
## Architecture Overview
|
|
|
|
### The Modern Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ APPLICATION LAYER │
|
|
│ │
|
|
│ Application.Run(toplevel) │
|
|
│ ├─> Begin(toplevel) → RunState token │
|
|
│ ├─> LOOP: while(toplevel.Running) │
|
|
│ │ └─> Coordinator.RunIteration() │
|
|
│ └─> End(runState) │
|
|
│ │
|
|
└──────────────────────────┬───────────────────────────────────────┘
|
|
│
|
|
┌──────────────────────────▼───────────────────────────────────────┐
|
|
│ MAINLOOP COORDINATOR │
|
|
│ │
|
|
│ Two separate threads: │
|
|
│ │
|
|
│ ┌────────────────────┐ ┌─────────────────────────┐ │
|
|
│ │ INPUT THREAD │ │ MAIN UI THREAD │ │
|
|
│ │ │ │ │ │
|
|
│ │ ConsoleInput.Run()│ │ ApplicationMainLoop │ │
|
|
│ │ (blocking read) │ │ .Iteration() │ │
|
|
│ │ │ │ │ │ │
|
|
│ │ ▼ │ │ 1. ProcessQueue() │ │
|
|
│ │ InputBuffer │───────>│ 2. Check for changes │ │
|
|
│ │ (ConcurrentQueue) │ │ 3. Layout if needed │ │
|
|
│ │ │ │ 4. Draw if needed │ │
|
|
│ │ │ │ 5. RunTimers() │ │
|
|
│ │ │ │ 6. Throttle │ │
|
|
│ └────────────────────┘ └─────────────────────────┘ │
|
|
│ │
|
|
└──────────────────────────┬───────────────────────────────────────┘
|
|
│
|
|
┌──────────────────────────▼───────────────────────────────────────┐
|
|
│ DRIVER LAYER │
|
|
│ │
|
|
│ IConsoleInput<T> IConsoleOutput │
|
|
│ IInputProcessor OutputBuffer │
|
|
│ IComponentFactory<T> IConsoleSizeMonitor │
|
|
│ │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Detailed Component Analysis
|
|
|
|
### 1. Application.Run() - The Complete Lifecycle
|
|
|
|
**Location:** `Terminal.Gui/App/Application.Run.cs`
|
|
|
|
```csharp
|
|
public static void Run(Toplevel view, Func<Exception, bool>? errorHandler = null)
|
|
{
|
|
RunState rs = Application.Begin(view); // Setup phase
|
|
|
|
while (toplevel.Running) // The actual "run loop"
|
|
{
|
|
Coordinator.RunIteration(); // One iteration
|
|
}
|
|
|
|
Application.End(rs); // Cleanup phase
|
|
}
|
|
```
|
|
|
|
**Purpose:** High-level convenience method that orchestrates the complete lifecycle.
|
|
|
|
**Key insight:** `Run()` is a WRAPPER, not the loop itself.
|
|
|
|
### 2. Application.Begin() - Setup Phase
|
|
|
|
**What it does:**
|
|
1. Creates `RunState` token (handle for Begin/End pairing)
|
|
2. Pushes Toplevel onto `TopLevels` stack
|
|
3. Sets `Application.Top` to the new toplevel
|
|
4. Initializes and lays out the toplevel
|
|
5. Draws initial screen
|
|
6. Fires `NotifyNewRunState` event
|
|
|
|
**Purpose:** Prepares a Toplevel for execution but does NOT start the loop.
|
|
|
|
### 3. The Application Control Loop
|
|
|
|
**Location:** `ApplicationImpl.Run()` and `Application.RunLoop()`
|
|
|
|
```csharp
|
|
// ApplicationImpl.Run()
|
|
while (_topLevels.TryPeek(out found) && found == view && view.Running)
|
|
{
|
|
_coordinator.RunIteration(); // Call coordinator
|
|
}
|
|
|
|
// Application.RunLoop() - for manual control
|
|
for (state.Toplevel.Running = true; state.Toplevel?.Running == true;)
|
|
{
|
|
if (EndAfterFirstIteration && !firstIteration)
|
|
return;
|
|
|
|
firstIteration = RunIteration(ref state, firstIteration);
|
|
}
|
|
```
|
|
|
|
**Purpose:** The main control loop at the Application level. Continues until `RequestStop()` is called.
|
|
|
|
**Key insight:** This is the application's execution loop.
|
|
|
|
### 4. Application.RunIteration() - One Cycle
|
|
|
|
**Location:** `Application.Run.cs`
|
|
|
|
```csharp
|
|
public static bool RunIteration(ref RunState state, bool firstIteration = false)
|
|
{
|
|
ApplicationImpl appImpl = (ApplicationImpl)ApplicationImpl.Instance;
|
|
appImpl.Coordinator?.RunIteration();
|
|
|
|
return false;
|
|
}
|
|
```
|
|
|
|
**Purpose:** Process ONE iteration by delegating to the Coordinator.
|
|
|
|
**Simplified:** Direct delegation - no conditional logic.
|
|
|
|
### 5. The Coordinator Layer
|
|
|
|
**Location:** `MainLoopCoordinator<T>.RunIteration()`
|
|
|
|
```csharp
|
|
public void RunIteration()
|
|
{
|
|
_loop.Iteration(); // Delegates to ApplicationMainLoop
|
|
}
|
|
```
|
|
|
|
**Purpose:** Thin wrapper that delegates to ApplicationMainLoop. Also manages the input thread lifecycle.
|
|
|
|
### 6. The ApplicationMainLoop - The Real Work
|
|
|
|
**Location:** `ApplicationMainLoop<T>.Iteration()` and `.IterationImpl()`
|
|
|
|
```csharp
|
|
public void Iteration()
|
|
{
|
|
DateTime dt = Now();
|
|
int timeAllowed = 1000 / MaximumIterationsPerSecond;
|
|
|
|
IterationImpl(); // Do the actual work
|
|
|
|
// Throttle to respect MaximumIterationsPerSecond
|
|
TimeSpan sleepFor = TimeSpan.FromMilliseconds(timeAllowed) - (Now() - dt);
|
|
if (sleepFor.Milliseconds > 0)
|
|
Task.Delay(sleepFor).Wait();
|
|
}
|
|
|
|
internal void IterationImpl()
|
|
{
|
|
InputProcessor.ProcessQueue(); // 1. Process buffered input
|
|
ToplevelTransitionManager.RaiseReadyEventIfNeeded();
|
|
ToplevelTransitionManager.HandleTopMaybeChanging();
|
|
|
|
// 2. Check if any views need layout or drawing
|
|
bool needsDrawOrLayout = AnySubViewsNeedDrawn(...);
|
|
bool sizeChanged = ConsoleSizeMonitor.Poll();
|
|
|
|
if (needsDrawOrLayout || sizeChanged)
|
|
{
|
|
Application.LayoutAndDraw(true); // 3. Layout and draw
|
|
Out.Write(OutputBuffer); // 4. Flush to console
|
|
}
|
|
|
|
SetCursor(); // 5. Update cursor
|
|
TimedEvents.RunTimers(); // 6. Run timeout callbacks
|
|
}
|
|
```
|
|
|
|
**Purpose:** This is the REAL "main loop iteration" that does all the work.
|
|
|
|
### 7. Application.End() - Cleanup Phase
|
|
|
|
**What it does:**
|
|
1. Fires `Toplevel.OnUnloaded()` event
|
|
2. Pops Toplevel from `TopLevels` stack
|
|
3. Fires `Toplevel.OnClosed()` event
|
|
4. Restores previous `Top`
|
|
5. Clears RunState.Toplevel
|
|
6. Disposes RunState token
|
|
|
|
**Purpose:** Balances `Begin()` - cleans up after execution.
|
|
|
|
## The Input Threading Model
|
|
|
|
One of the most important aspects is the **two-thread architecture**:
|
|
|
|
### Input Thread (Background)
|
|
|
|
```
|
|
IConsoleInput<T>.Run(CancellationToken)
|
|
└─> Infinite loop:
|
|
1. Read from console (BLOCKING)
|
|
2. Parse raw input
|
|
3. Push to InputBuffer (thread-safe ConcurrentQueue)
|
|
4. Repeat until cancelled
|
|
```
|
|
|
|
**Purpose:** Dedicated thread for blocking console I/O operations.
|
|
|
|
**Platform-specific implementations:**
|
|
- `NetInput` (DotNet driver) - Uses `Console.ReadKey()`
|
|
- `WindowsInput` - Uses Win32 API `ReadConsoleInput()`
|
|
- `UnixInput` (Unix driver) - Uses Unix terminal APIs
|
|
|
|
### Main UI Thread (Foreground)
|
|
|
|
```
|
|
ApplicationMainLoop<T>.IterationImpl()
|
|
└─> One iteration:
|
|
1. InputProcessor.ProcessQueue()
|
|
└─> Drain InputBuffer
|
|
└─> Translate to Key/Mouse events
|
|
└─> Fire KeyDown/KeyUp/MouseEvent
|
|
2. Check for view changes
|
|
3. Layout if needed
|
|
4. Draw if needed
|
|
5. Update output buffer
|
|
6. Flush to console
|
|
7. Run timeout callbacks
|
|
```
|
|
|
|
**Purpose:** Main thread where all UI operations happen.
|
|
|
|
**Thread safety:** `ConcurrentQueue<T>` provides thread-safe handoff between threads.
|
|
|
|
## The RequestStop Flow
|
|
|
|
```
|
|
User Action (e.g., Ctrl+Q)
|
|
│
|
|
▼
|
|
InputProcessor processes key
|
|
│
|
|
▼
|
|
Fires KeyDown event
|
|
│
|
|
▼
|
|
Application.Keyboard handles QuitKey
|
|
│
|
|
▼
|
|
Application.RequestStop(toplevel)
|
|
│
|
|
▼
|
|
Sets toplevel.Running = false
|
|
│
|
|
▼
|
|
while(toplevel.Running) loop exits
|
|
│
|
|
▼
|
|
Application.End() cleans up
|
|
```
|
|
|
|
## Key Terminology Issues Discovered
|
|
|
|
### Issue 1: "RunState" Doesn't Hold State
|
|
|
|
`RunState` is actually a **token** or **handle** for the Begin/End pairing:
|
|
|
|
```csharp
|
|
public class RunState : IDisposable
|
|
{
|
|
public Toplevel Toplevel { get; internal set; }
|
|
// That's it - no "state" data!
|
|
}
|
|
```
|
|
|
|
**Purpose:** Ensures `End()` is called with the same Toplevel that `Begin()` set up.
|
|
|
|
**Recommendation:** Rename to `RunToken` to clarify it's a token, not state data.
|
|
|
|
### Issue 2: "EndAfterFirstIteration" Confuses "End" with Loop Control
|
|
|
|
**Current:**
|
|
```csharp
|
|
Application.EndAfterFirstIteration = true; // Controls RunLoop, not End()
|
|
RunState rs = Application.Begin(window);
|
|
Application.RunLoop(rs); // Stops after 1 iteration due to flag
|
|
Application.End(rs); // This is actual "End"
|
|
```
|
|
|
|
**Issue:**
|
|
- "End" in the flag name suggests the `End()` method
|
|
- The flag stops the loop, not the lifecycle
|
|
- Creates confusion about when `End()` gets called
|
|
|
|
**Recommendation:** Rename to `StopAfterFirstIteration` to align with `RequestStop`.
|
|
|
|
## The True MainLoop Architecture
|
|
|
|
The **actual mainloop** in Terminal.Gui is:
|
|
|
|
```
|
|
ApplicationMainLoop<T>
|
|
├─ InputBuffer (ConcurrentQueue<T>)
|
|
├─ InputProcessor (processes queue)
|
|
├─ OutputBuffer (buffered drawing)
|
|
├─ ConsoleOutput (writes to terminal)
|
|
├─ TimedEvents (timeout callbacks)
|
|
├─ ConsoleSizeMonitor (detects terminal resizing)
|
|
└─ ToplevelTransitionManager (handles Top changes)
|
|
```
|
|
|
|
**Coordinated by:**
|
|
```
|
|
MainLoopCoordinator<T>
|
|
├─ Input thread: IConsoleInput<T>.Run()
|
|
├─ Main thread: ApplicationMainLoop<T>.Iteration()
|
|
└─ Synchronization via ConcurrentQueue
|
|
```
|
|
|
|
**Exposed via:**
|
|
```
|
|
ApplicationImpl
|
|
└─ IMainLoopCoordinator (hides implementation details)
|
|
```
|
|
|
|
## Complete Execution Flow Diagram
|
|
|
|
```
|
|
Application.Init()
|
|
├─> Create Coordinator
|
|
├─> Start input thread (IConsoleInput.Run)
|
|
├─> Initialize ApplicationMainLoop
|
|
└─> Return to caller
|
|
|
|
Application.Run(toplevel)
|
|
│
|
|
├─> Application.Begin(toplevel)
|
|
│ ├─> Create RunState token
|
|
│ ├─> Push to TopLevels stack
|
|
│ ├─> Initialize & layout toplevel
|
|
│ ├─> Initial draw
|
|
│ └─> Fire NotifyNewRunState
|
|
│
|
|
├─> while (toplevel.Running) ← APPLICATION CONTROL LOOP
|
|
│ │
|
|
│ └─> Coordinator.RunIteration()
|
|
│ │
|
|
│ └─> ApplicationMainLoop.Iteration()
|
|
│ │
|
|
│ ├─> Throttle based on MaxIterationsPerSecond
|
|
│ │
|
|
│ └─> IterationImpl():
|
|
│ │
|
|
│ ├─> 1. InputProcessor.ProcessQueue()
|
|
│ │ └─> Drain InputBuffer
|
|
│ │ └─> Fire Key/Mouse events
|
|
│ │
|
|
│ ├─> 2. ToplevelTransitionManager events
|
|
│ │
|
|
│ ├─> 3. Check if layout/draw needed
|
|
│ │
|
|
│ ├─> 4. If needed:
|
|
│ │ ├─> Application.LayoutAndDraw()
|
|
│ │ └─> Out.Write(OutputBuffer)
|
|
│ │
|
|
│ ├─> 5. SetCursor()
|
|
│ │
|
|
│ └─> 6. TimedEvents.RunTimers()
|
|
│
|
|
└─> Application.End(runState)
|
|
├─> Fire OnUnloaded event
|
|
├─> Pop from TopLevels stack
|
|
├─> Fire OnClosed event
|
|
├─> Restore previous Top
|
|
├─> Clear & dispose RunState
|
|
|
|
Application.Shutdown()
|
|
├─> Coordinator.Stop()
|
|
│ └─> Cancel input thread
|
|
└─> Cleanup all resources
|
|
```
|
|
|
|
## Summary of Discoveries
|
|
|
|
### 1. Streamlined Architecture
|
|
- Modern only: `ApplicationMainLoop<T>` with coordinator pattern
|
|
- Legacy removed: Cleaner, simpler codebase
|
|
|
|
### 2. Two-Thread Model
|
|
- Input thread: Blocking console reads
|
|
- Main UI thread: Event processing, layout, drawing
|
|
|
|
### 3. Clear Iteration Hierarchy
|
|
```
|
|
Application.RunLoop() ← Application control loop (for manual control)
|
|
└─> Application.RunIteration() ← One iteration
|
|
└─> Coordinator.RunIteration()
|
|
└─> ApplicationMainLoop.Iteration()
|
|
└─> IterationImpl() ← The REAL mainloop work
|
|
```
|
|
|
|
### 4. RunState is a Token
|
|
- Not state data
|
|
- Just a handle to pair Begin/End
|
|
- Contains only the Toplevel reference
|
|
|
|
### 5. Simplified Flow
|
|
- No more conditional logic
|
|
- Single, clear execution path
|
|
- Easier to understand and maintain
|
|
|
|
## Terminology Recommendations Based on Analysis
|
|
|
|
Based on this deep dive, here are the terminology issues:
|
|
|
|
### Critical Issues
|
|
|
|
1. **`RunState` should be `RunToken`**
|
|
- It's a token, not state
|
|
- Analogy: `CancellationToken`
|
|
|
|
2. **`EndAfterFirstIteration` should be `StopAfterFirstIteration`**
|
|
- Controls loop stopping, not lifecycle cleanup
|
|
- "End" suggests `End()` method
|
|
- "Stop" aligns with `RequestStop()`
|
|
|
|
### What Works Well
|
|
|
|
- `Begin` / `End` - Clear lifecycle pairing
|
|
- `RequestStop` - "Request" conveys non-blocking
|
|
- `RunIteration` - Clear it processes one iteration
|
|
- `Coordinator` - Good separation of concerns pattern
|
|
- `ApplicationMainLoop<T>` - Descriptive class name
|
|
|
|
## Conclusion
|
|
|
|
Terminal.Gui's mainloop is a sophisticated multi-threaded architecture with:
|
|
- Separate input/UI threads
|
|
- Thread-safe queue for handoff
|
|
- Coordinator pattern for lifecycle management
|
|
- Throttled iterations for performance
|
|
- Clean separation between drivers and application logic
|
|
|
|
The architecture is now streamlined and modern, with legacy complexity removed. The remaining confusion stems from:
|
|
1. Overloaded "Run" terminology
|
|
2. Misleading "RunState" name
|
|
3. The term "EndAfterFirstIteration" conflating loop control with lifecycle cleanup
|
|
|
|
The modern architecture is well-designed and clear. Renaming `RunState` to `RunToken` and `EndAfterFirstIteration` to `StopAfterFirstIteration` would eliminate the remaining sources of confusion.
|