Application Architecture
Terminal.Gui v2 uses an instance-based application architecture with the IRunnable interface pattern that decouples views from the global application state, improving testability, enabling multiple application contexts, and providing type-safe result handling.
Key Features
- Instance-Based: Use
Application.Create()to get anIApplicationinstance instead of static methods - IRunnable Interface: Views implement
IRunnable<TResult>to participate in session management without inheriting fromRunnable - Fluent API: Chain
Init()andRun()for elegant, concise code - IDisposable Pattern: Proper resource cleanup with
Dispose()orusingstatements - Automatic Disposal: Framework-created runnables are automatically disposed
- Type-Safe Results: Generic
TResultparameter provides compile-time type safety - CWP Compliance: All lifecycle events follow the Cancellable Work Pattern
View Hierarchy and Run Stack
graph TB
subgraph ViewTree["View Hierarchy (SuperView/SubView)"]
direction TB
Top[app.TopRunnable<br/>Window]
Menu[MenuBar]
Status[StatusBar]
Content[Content View]
Button1[Button]
Button2[Button]
Top --> Menu
Top --> Status
Top --> Content
Content --> Button1
Content --> Button2
end
subgraph Stack["app.SessionStack"]
direction TB
S1[Window<br/>Currently Active]
S2[Previous Runnable<br/>Waiting]
S3[Base Runnable<br/>Waiting]
S1 -.-> S2 -.-> S3
end
Top -.->|"same instance"| S1
style Top fill:#ccffcc,stroke:#339933,stroke-width:3px
style S1 fill:#ccffcc,stroke:#339933,stroke-width:3px
Usage Example Flow
sequenceDiagram
participant App as IApplication
participant Main as Main Window
participant Dialog as Dialog
Note over App: Initially empty SessionStack
App->>Main: Run(mainWindow)
activate Main
Note over App: SessionStack: [Main]<br/>TopRunnable: Main
Main->>Dialog: Run(dialog)
activate Dialog
Note over App: SessionStack: [Dialog, Main]<br/>TopRunnable: Dialog
Dialog->>App: RequestStop()
deactivate Dialog
Note over App: SessionStack: [Main]<br/>TopRunnable: Main
Main->>App: RequestStop()
deactivate Main
Note over App: SessionStack: []<br/>TopRunnable: null
Key Concepts
Instance-Based vs Static
Terminal.Gui v2 supports both static and instance-based patterns. The static Application class is marked obsolete but still functional for backward compatibility. The recommended pattern is to use Application.Create() to get an IApplication instance:
// OLD (v1 / early v2 - still works but obsolete):
Application.Init ();
Window top = new ();
top.Add (myView);
Application.Run (top);
top.Dispose ();
Application.Shutdown (); // Obsolete - use Dispose() instead
// RECOMMENDED (v2 - instance-based with using statement):
using (IApplication app = Application.Create ().Init ())
{
Window top = new ();
top.Add (myView);
app.Run (top);
top.Dispose ();
} // app.Dispose() called automatically
// WITH IRunnable (fluent API with automatic disposal):
using (IApplication app = Application.Create ().Init ())
{
app.Run<ColorPickerDialog> ();
Color? result = app.GetResult<Color> ();
}
// SIMPLEST (manual disposal):
IApplication app = Application.Create ().Init ();
app.Run<ColorPickerDialog> ();
Color? result = app.GetResult<Color> ();
app.Dispose ();
Note: The static Application class delegates to a singleton instance accessible via Application.Instance. Application.Create() creates a new application instance, enabling multiple application contexts and better testability.
View.App Property
Every view now has an App property that references its application context:
public class View
{
/// <summary>
/// Gets the application context for this view.
/// </summary>
public IApplication? App { get; internal set; }
/// <summary>
/// Gets the application context, checking parent hierarchy if needed.
/// Override to customize application resolution.
/// </summary>
public virtual IApplication? GetApp () => App ?? SuperView?.GetApp ();
}
Benefits:
- Views can be tested without
Application.Init() - Multiple applications can coexist
- Clear ownership: views know their context
- Reduced global state dependencies
Accessing Application from Views
Recommended pattern:
public class MyView : View
{
public override void OnEnter (View view)
{
// Use View.App instead of static Application
App?.TopRunnable?.SetNeedsDraw ();
// Access SessionStack
if (App?.SessionStack.Count > 0)
{
// Work with sessions
}
}
}
Alternative - dependency injection:
public class MyView : View
{
private readonly IApplication _app;
public MyView (IApplication app)
{
_app = app;
// Now completely decoupled from static Application
}
public void DoWork ()
{
_app.TopRunnable?.SetNeedsDraw ();
}
}
IRunnable Architecture
Terminal.Gui v2 introduces the IRunnable interface pattern that decouples runnable behavior from the Runnable class hierarchy. Views can implement IRunnable<TResult> to participate in session management without inheritance constraints.
Key Benefits
- Interface-Based: No forced inheritance from
Runnable - Type-Safe Results: Generic
TResultparameter provides compile-time type safety - Fluent API: Method chaining for elegant, concise code
- Automatic Disposal: Framework manages lifecycle of created runnables
- CWP Compliance: All lifecycle events follow the Cancellable Work Pattern
Fluent API Pattern
The fluent API enables elegant method chaining with automatic resource management:
// Recommended: using statement with GetResult
using (IApplication app = Application.Create ().Init ())
{
app.Run<ColorPickerDialog> ();
Color? result = app.GetResult<Color> ();
if (result is { })
{
ApplyColor (result);
}
}
// Alternative: Manual disposal
IApplication app = Application.Create ().Init ();
app.Run<ColorPickerDialog> ();
Color? result = app.GetResult<Color> ();
app.Dispose ();
if (result is { })
{
ApplyColor (result);
}
Key Methods:
Init()- ReturnsIApplicationfor chainingRun<TRunnable>()- Creates and runs runnable, returnsIApplicationGetResult()/GetResult<T>()- Extract typed result after runDispose()- Release all resources (called automatically withusing)
Disposal Semantics
"Whoever creates it, owns it":
| Method | Creator | Owner | Disposal |
|---|---|---|---|
Run<TRunnable>() |
Framework | Framework | Automatic when Run<T>() returns |
Run(IRunnable) |
Caller | Caller | Manual by caller |
// Framework ownership - automatic disposal
using (IApplication app = Application.Create ().Init ())
{
app.Run<MyDialog> (); // Dialog disposed automatically when Run returns
MyResultType? result = app.GetResult<MyResultType> ();
}
// Caller ownership - manual disposal
using (IApplication app = Application.Create ().Init ())
{
MyDialog dialog = new ();
app.Run (dialog);
MyResultType? result = dialog.Result;
dialog.Dispose (); // Caller must dispose
}
Creating Runnable Views
Derive from Runnable<TResult> or implement IRunnable<TResult>:
public class FileDialog : Runnable<string?>
{
private TextField _pathField;
public FileDialog ()
{
Title = "Select File";
_pathField = new () { X = 1, Y = 1, Width = Dim.Fill (1) };
Button okButton = new () { Text = "OK", IsDefault = true };
okButton.Accepting += (s, e) =>
{
Result = _pathField.Text;
Application.RequestStop ();
};
Add (_pathField, okButton);
}
protected override bool OnIsRunningChanging (bool oldValue, bool newValue)
{
if (!newValue) // Stopping - extract result before disposal
{
Result = _pathField?.Text;
}
return base.OnIsRunningChanging (oldValue, newValue);
}
}
Lifecycle Properties
IsRunning- True when runnable is onSessionStackIsModal- True when runnable is at top of stack (capturing all input)Result- Typed result value set before stopping
Lifecycle Events (CWP-Compliant)
All events follow Terminal.Gui's Cancellable Work Pattern:
| Event | Cancellable | When | Use Case |
|---|---|---|---|
IsRunningChanging |
✓ | Before add/remove from stack | Extract result, prevent close |
IsRunningChanged |
✗ | After stack change | Post-start/stop cleanup |
IsModalChanged |
✗ | After modal state change | Update UI after focus change |
Example - Result Extraction:
protected override bool OnIsRunningChanging (bool oldValue, bool newValue)
{
if (!newValue) // Stopping
{
// Extract result before views are disposed
Result = _colorPicker.SelectedColor;
// Optionally cancel stop (e.g., unsaved changes)
if (HasUnsavedChanges ())
{
var response = MessageBox.Query ("Save?", "Save changes?", "Yes", "No", "Cancel");
if (response == 2)
{
return true; // Cancel stop
}
if (response == 0)
{
Save ();
}
}
}
return base.OnIsRunningChanging (oldValue, newValue);
}
SessionStack
The SessionStack manages all running IRunnable sessions:
public interface IApplication
{
/// <summary>
/// Stack of running IRunnable sessions.
/// Each entry is a SessionToken wrapping an IRunnable.
/// </summary>
ConcurrentStack<SessionToken>? SessionStack { get; }
/// <summary>
/// The IRunnable at the top of SessionStack (currently modal).
/// </summary>
IRunnable? TopRunnable { get; }
}
Stack Behavior:
- Push:
Begin(IRunnable)adds to top of stack - Pop:
End(SessionToken)removes from stack - Peek:
TopRunnablereturns current modal runnable - All:
SessionStackenumerates all running sessions
IApplication Interface
The IApplication interface defines the application contract with support for both legacy Runnable and modern IRunnable patterns:
public interface IApplication
{
// IRunnable support (primary)
IRunnable? TopRunnable { get; }
View? TopRunnableView { get; }
ConcurrentStack<SessionToken>? SessionStack { get; }
// Driver and lifecycle
IDriver? Driver { get; }
IMainLoopCoordinator? Coordinator { get; }
// Fluent API methods
IApplication Init (string? driverName = null);
void Dispose (); // IDisposable
// Runnable methods
SessionToken? Begin (IRunnable runnable);
object? Run (IRunnable runnable, Func<Exception, bool>? errorHandler = null);
IApplication Run<TRunnable> (Func<Exception, bool>? errorHandler = null) where TRunnable : IRunnable, new();
void RequestStop (IRunnable? runnable);
void End (SessionToken sessionToken);
// Result extraction
object? GetResult ();
T? GetResult<T> () where T : class;
// ... other members
}
Terminology Changes
Terminal.Gui v2 modernized its terminology for clarity:
Application.TopRunnable (formerly "Current", and before that "Top")
The TopRunnable property represents the IRunnable on the top of the session stack (the active runnable session):
// Access the top runnable session
IRunnable? topRunnable = app.TopRunnable;
// From within a view
IRunnable? topRunnable = App?.TopRunnable;
// Cast to View if needed
View? topView = app.TopRunnableView;
Why "TopRunnable"?
- Clearly indicates it's the top of the runnable session stack
- Aligns with the IRunnable architecture
- Distinguishes from other concepts like "Current" which could be ambiguous
- Works with any view that implements
IRunnable, not justRunnable
Application.SessionStack (formerly "Runnables")
The SessionStack property is the stack of running sessions:
// Access all running sessions
foreach (SessionToken runnable in app.SessionStack)
{
// Process each session
}
// From within a view
var sessionCount = App?.SessionStack.Count ?? 0;
Why "SessionStack" instead of "Runnables"?
- Describes both content (sessions) and structure (stack)
- Aligns with
SessionTokenterminology - Follows .NET naming patterns (descriptive + collection type)
Migration from Static Application
The static Application class delegates to a singleton instance and is marked obsolete. All static methods and properties are marked with [Obsolete] but remain functional for backward compatibility:
public static partial class Application
{
[Obsolete ("The legacy static Application object is going away.")]
public static View? TopRunnableView => Instance.TopRunnableView;
[Obsolete ("The legacy static Application object is going away.")]
public static IRunnable? TopRunnable => Instance.TopRunnable;
[Obsolete ("The legacy static Application object is going away.")]
public static ConcurrentStack<SessionToken>? SessionStack => Instance.SessionStack;
// ... other obsolete static members
}
Important: The static Application class uses a singleton (Application.Instance), while Application.Create() creates new instances. For new code, prefer the instance-based pattern using Application.Create().
Migration Strategies
Strategy 1: Use View.App
// OLD:
void MyMethod ()
{
Application.TopRunnable?.SetNeedsDraw ();
}
// NEW:
void MyMethod (View view)
{
view.App?.TopRunnableView?.SetNeedsDraw ();
}
Strategy 2: Pass IApplication
// OLD:
void ProcessSessions ()
{
foreach (SessionToken runnable in Application.SessionStack)
{
// Process
}
}
// NEW:
void ProcessSessions (IApplication app)
{
foreach (SessionToken runnable in app.SessionStack)
{
// Process
}
}
Strategy 3: Store IApplication Reference
public class MyService
{
private readonly IApplication _app;
public MyService (IApplication app)
{
_app = app;
}
public void DoWork ()
{
_app.TopRunnable?.Title = "Processing...";
}
}
Resource Management and Disposal
Terminal.Gui v2 implements the IDisposable pattern for proper resource cleanup. Applications must be disposed after use to:
- Stop the input thread cleanly
- Release driver resources
- Prevent thread leaks in tests
- Free unmanaged resources
Using the using Statement (Recommended)
// Automatic disposal with using statement
using (IApplication app = Application.Create ().Init ())
{
app.Run<MyDialog> ();
// app.Dispose() automatically called when scope exits
}
Manual Disposal
// Manual disposal
IApplication app = Application.Create ();
try
{
app.Init ();
app.Run<MyDialog> ();
}
finally
{
app.Dispose (); // Ensure cleanup even if exception occurs
}
Dispose() and Result Retrieval
Dispose()- Standard IDisposable pattern for resource cleanup (required)GetResult()/GetResult<T>()- Retrieve results after run completesShutdown()- Obsolete (useDispose()instead)
// RECOMMENDED (using statement):
using (IApplication app = Application.Create ().Init ())
{
app.Run<MyDialog> ();
MyResult? result = app.GetResult<MyResult> ();
// app.Dispose() called automatically here
}
// ALTERNATIVE (manual disposal):
IApplication app = Application.Create ().Init ();
app.Run<MyDialog> ();
MyResult? result = app.GetResult<MyResult> ();
app.Dispose (); // Must call explicitly
// OLD (obsolete - do not use):
object? result = app.Run<MyDialog> ().Shutdown ();
Input Thread Lifecycle
When calling Init(), Terminal.Gui starts a dedicated input thread that continuously polls for console input. This thread must be stopped properly:
IApplication app = Application.Create ();
app.Init ("fake"); // Input thread starts here
// Input thread runs in background at ~50 polls/second (20ms throttle)
app.Dispose (); // Cancels input thread and waits for it to exit
Important for Tests: Always dispose applications in tests to prevent thread leaks:
[Fact]
public void My_Test ()
{
using IApplication app = Application.Create ();
app.Init ("fake");
// Test code here
// app.Dispose() called automatically
}
Singleton Re-initialization
The legacy static Application singleton can be re-initialized after disposal (for backward compatibility with old tests):
// Test 1
Application.Init ();
Application.Shutdown (); // Obsolete but still works for legacy singleton
// Test 2 - singleton resets and can be re-initialized
Application.Init (); // ✅ Works!
Application.Shutdown (); // Obsolete but still works for legacy singleton
However, instance-based applications follow standard IDisposable semantics and cannot be reused after disposal:
IApplication app = Application.Create ();
app.Init ();
app.Dispose ();
app.Init (); // ❌ Throws ObjectDisposedException
Session Management
Begin and End
Applications manage sessions through Begin() and End():
using IApplication app = Application.Create ();
app.Init ();
Window window = new ();
// Begin a new session - pushes to SessionStack
SessionToken? token = app.Begin (window);
// TopRunnable now points to this window
Debug.Assert (app.TopRunnable == window);
// End the session - pops from SessionStack
if (token != null)
{
app.End (token);
}
// TopRunnable restored to previous runnable (if any)
Nested Sessions
Multiple sessions can run nested:
using IApplication app = Application.Create ();
app.Init ();
// Session 1
Window main = new () { Title = "Main" };
SessionToken? token1 = app.Begin (main);
// app.TopRunnable == main, SessionStack.Count == 1
// Session 2 (nested)
Dialog dialog = new () { Title = "Dialog" };
SessionToken? token2 = app.Begin (dialog);
// app.TopRunnable == dialog, SessionStack.Count == 2
// End dialog
app.End (token2);
// app.TopRunnable == main, SessionStack.Count == 1
// End main
app.End (token1);
// app.TopRunnable == null, SessionStack.Count == 0
View.Driver Property
Similar to View.App, views now have a Driver property:
public class View
{
/// <summary>
/// Gets the driver for this view.
/// </summary>
public IDriver? Driver => GetDriver ();
/// <summary>
/// Gets the driver, checking application context if needed.
/// Override to customize driver resolution.
/// </summary>
public virtual IDriver? GetDriver () => App?.Driver;
}
Usage:
public override void OnDrawContent (Rectangle viewport)
{
// Use view's driver instead of Application.Driver
Driver?.Move (0, 0);
Driver?.AddStr ("Hello");
}
Testing with the New Architecture
The instance-based architecture dramatically improves testability:
Testing Views in Isolation
[Fact]
public void MyView_DisplaysCorrectly ()
{
// Create mock application
Mock<IApplication> mockApp = new ();
mockApp.Setup (a => a.TopRunnable).Returns (new Runnable ());
// Create view with mock app
MyView view = new () { App = mockApp.Object };
// Test without Application.Init()!
view.SetNeedsDraw ();
Assert.True (view.NeedsDraw);
// No Application.Shutdown() needed!
}
Testing with Real Application
[Fact]
public void MyView_WorksWithRealApplication ()
{
using IApplication app = Application.Create ();
app.Init ("fake");
MyView view = new ();
Window top = new ();
top.Add (view);
app.Begin (top);
// View.App automatically set
Assert.NotNull (view.App);
Assert.Same (app, view.App);
// Test view behavior
view.DoSomething ();
}
Best Practices
DO: Use View.App
✅ GOOD:
public void Refresh ()
{
App?.TopRunnableView?.SetNeedsDraw ();
}
DON'T: Use Static Application
❌ AVOID:
public void Refresh ()
{
Application.TopRunnableView?.SetNeedsDraw (); // Obsolete!
}
DO: Pass IApplication as Dependency
✅ GOOD:
public class Service
{
public Service (IApplication app) { }
}
DON'T: Use Static Application in New Code
❌ AVOID (obsolete pattern):
public void Refresh ()
{
Application.TopRunnableView?.SetNeedsDraw (); // Obsolete static access
}
✅ PREFERRED:
public void Refresh ()
{
App?.TopRunnableView?.SetNeedsDraw (); // Use View.App property
}
DO: Override GetApp() for Custom Resolution
✅ GOOD:
public class SpecialView : View
{
private IApplication? _customApp;
public override IApplication? GetApp ()
{
return _customApp ?? base.GetApp ();
}
}
Advanced Scenarios
Multiple Applications
The instance-based architecture enables multiple applications:
// Application 1
using IApplication app1 = Application.Create ();
app1.Init ("windows");
Window top1 = new () { Title = "App 1" };
// ... configure top1
// Application 2 (different driver!)
using IApplication app2 = Application.Create ();
app2.Init ("unix");
Window top2 = new () { Title = "App 2" };
// ... configure top2
// Views in top1 use app1
// Views in top2 use app2
Application-Agnostic Views
Create views that work with any application:
public class UniversalView : View
{
public void ShowMessage (string message)
{
// Works regardless of which application context
IApplication? app = GetApp ();
if (app != null)
{
MessageBox msg = new (message);
app.Begin (msg);
}
}
}
See Also
- Navigation - Navigation with the instance-based architecture
- Keyboard - Keyboard handling through View.App
- Mouse - Mouse handling through View.App
- Drivers - Driver access through View.Driver
- Multitasking - Session management with SessionStack