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.
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└──────────────────────────────────────────────────────────────────┘
Location: Terminal.Gui/App/Application.Run.cs
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.
What it does:
RunState token (handle for Begin/End pairing)TopLevels stackApplication.Top to the new toplevelNotifyNewRunState eventPurpose: Prepares a Toplevel for execution but does NOT start the loop.
Location: ApplicationImpl.Run() and Application.RunLoop()
// 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.
Location: Application.Run.cs
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.
Location: MainLoopCoordinator<T>.RunIteration()
public void RunIteration()
{
_loop.Iteration(); // Delegates to ApplicationMainLoop
}
Purpose: Thin wrapper that delegates to ApplicationMainLoop. Also manages the input thread lifecycle.
Location: ApplicationMainLoop<T>.Iteration() and .IterationImpl()
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.
What it does:
Toplevel.OnUnloaded() eventTopLevels stackToplevel.OnClosed() eventTopPurpose: Balances Begin() - cleans up after execution.
One of the most important aspects is the two-thread architecture:
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 APIsApplicationMainLoop<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.
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
RunState is actually a token or handle for the Begin/End pairing:
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.
Current:
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() methodEnd() gets calledRecommendation: Rename to StopAfterFirstIteration to align with RequestStop.
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)
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
ApplicationMainLoop<T> with coordinator patternApplication.RunLoop() ← Application control loop (for manual control)
└─> Application.RunIteration() ← One iteration
└─> Coordinator.RunIteration()
└─> ApplicationMainLoop.Iteration()
└─> IterationImpl() ← The REAL mainloop work
Based on this deep dive, here are the terminology issues:
RunState should be RunToken
CancellationTokenEndAfterFirstIteration should be StopAfterFirstIteration
End() methodRequestStop()Begin / End - Clear lifecycle pairingRequestStop - "Request" conveys non-blockingRunIteration - Clear it processes one iterationCoordinator - Good separation of concerns patternApplicationMainLoop<T> - Descriptive class nameTerminal.Gui's mainloop is a sophisticated multi-threaded architecture with:
The architecture is now streamlined and modern, with legacy complexity removed. The remaining confusion stems from:
The modern architecture is well-designed and clear. Renaming RunState to RunToken and EndAfterFirstIteration to StopAfterFirstIteration would eliminate the remaining sources of confusion.