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.
Application.Create() to get an IApplication instance instead of static methodsIRunnable<TResult> to participate in session management without inheriting from RunnableInit() and Run() for elegant, concise codeDispose() or using statementsTResult parameter provides compile-time type safetygraph 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
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
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.
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:
Application.Init()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 ();
}
}
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.
RunnableTResult parameter provides compile-time type safetyThe 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() - Returns IApplication for chainingRun<TRunnable>() - Creates and runs runnable, returns IApplicationGetResult() / GetResult<T>() - Extract typed result after runDispose() - Release all resources (called automatically with using)"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
}
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);
}
}
IsRunning - True when runnable is on SessionStackIsModal - True when runnable is at top of stack (capturing all input)Result - Typed result value set before stoppingAll 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);
}
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:
Begin(IRunnable) adds to top of stackEnd(SessionToken) removes from stackTopRunnable returns current modal runnableSessionStack enumerates all running sessionsThe 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
}
Terminal.Gui v2 modernized its terminology for clarity:
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"?
IRunnable, not just RunnableThe 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"?
SessionToken terminologyThe 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().
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...";
}
}
Terminal.Gui v2 implements the IDisposable pattern for proper resource cleanup. Applications must be disposed after use to:
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
IApplication app = Application.Create ();
try
{
app.Init ();
app.Run<MyDialog> ();
}
finally
{
app.Dispose (); // Ensure cleanup even if exception occurs
}
Dispose() - Standard IDisposable pattern for resource cleanup (required)GetResult() / GetResult<T>() - Retrieve results after run completesShutdown() - Obsolete (use Dispose() 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 ();
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
}
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
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)
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
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");
}
The instance-based architecture dramatically improves testability:
[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!
}
[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 ();
}
✅ GOOD:
public void Refresh ()
{
App?.TopRunnableView?.SetNeedsDraw ();
}
❌ AVOID:
public void Refresh ()
{
Application.TopRunnableView?.SetNeedsDraw (); // Obsolete!
}
✅ GOOD:
public class Service
{
public Service (IApplication app) { }
}
❌ AVOID (obsolete pattern):
public void Refresh ()
{
Application.TopRunnableView?.SetNeedsDraw (); // Obsolete static access
}
✅ PREFERRED:
public void Refresh ()
{
App?.TopRunnableView?.SetNeedsDraw (); // Use View.App property
}
✅ GOOD:
public class SpecialView : View
{
private IApplication? _customApp;
public override IApplication? GetApp ()
{
return _customApp ?? base.GetApp ();
}
}
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
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);
}
}
}