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 ToplevelInit(), Run(), and Shutdown() for elegant, concise codeTResult parameter provides compile-time type safetygraph TB
subgraph ViewTree["View Hierarchy (SuperView/SubView)"]
direction TB
Top[app.Current<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 Toplevel<br/>Waiting]
S3[Base Toplevel<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/>Current: Main
Main->>Dialog: Run(dialog)
activate Dialog
Note over App: SessionStack: [Dialog, Main]<br/>Current: Dialog
Dialog->>App: RequestStop()
deactivate Dialog
Note over App: SessionStack: [Main]<br/>Current: Main
Main->>App: RequestStop()
deactivate Main
Note over App: SessionStack: []<br/>Current: 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();
var top = new Toplevel();
top.Add(myView);
Application.Run(top);
top.Dispose();
Application.Shutdown();
// NEW (v2 recommended - instance-based):
var app = Application.Create();
app.Init();
var top = new Toplevel();
top.Add(myView);
app.Run(top);
top.Dispose();
app.Shutdown();
// NEWEST (v2 with IRunnable and Fluent API):
Color? result = Application.Create()
.Init()
.Run<ColorPickerDialog>()
.Shutdown() as Color?;
Note: The static Application class delegates to ApplicationImpl.Instance (a singleton). Application.Create() creates a new ApplicationImpl 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?.Current?.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.Current?.SetNeedsDraw();
}
}
Terminal.Gui v2 introduces the IRunnable interface pattern that decouples runnable behavior from the Toplevel class hierarchy. Views can implement IRunnable<TResult> to participate in session management without inheritance constraints.
ToplevelTResult parameter provides compile-time type safetyThe fluent API enables elegant method chaining with automatic resource management:
// All-in-one: Create, initialize, run, shutdown, and extract result
Color? result = Application.Create()
.Init()
.Run<ColorPickerDialog>()
.Shutdown() as Color?;
if (result is { })
{
ApplyColor(result);
}
Key Methods:
Init() - Returns IApplication for chainingRun<TRunnable>() - Creates and runs runnable, returns IApplicationShutdown() - Disposes framework-owned runnables, returns object? result"Whoever creates it, owns it":
| Method | Creator | Owner | Disposal |
|---|---|---|---|
Run<TRunnable>() |
Framework | Framework | Automatic in Shutdown() |
Run(IRunnable) |
Caller | Caller | Manual by caller |
// Framework ownership - automatic disposal
var result = app.Run<MyDialog>().Shutdown();
// Caller ownership - manual disposal
var dialog = new MyDialog();
app.Run(dialog);
var 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 TextField { X = 1, Y = 1, Width = Dim.Fill(1) };
var okButton = new Button { 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 RunnableSessionStackIsModal - 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 |
IsModalChanging |
✓ | Before becoming/leaving top | Prevent activation |
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())
{
int 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 RunnableSessionStack manages all running IRunnable sessions:
public interface IApplication
{
/// <summary>
/// Stack of running IRunnable sessions.
/// Each entry is a RunnableSessionToken wrapping an IRunnable.
/// </summary>
ConcurrentStack<RunnableSessionToken>? RunnableSessionStack { get; }
/// <summary>
/// The IRunnable at the top of RunnableSessionStack (currently modal).
/// </summary>
IRunnable? TopRunnable { get; }
}
Stack Behavior:
Begin(IRunnable) adds to top of stackEnd(RunnableSessionToken) removes from stackTopRunnable returns current modal runnableRunnableSessionStack enumerates all running sessionsThe IApplication interface defines the application contract with support for both legacy Toplevel and modern IRunnable patterns:
public interface IApplication
{
// Legacy Toplevel support
Toplevel? Current { get; }
ConcurrentStack<Toplevel> SessionStack { get; }
// IRunnable support
IRunnable? TopRunnable { get; }
ConcurrentStack<RunnableSessionToken>? RunnableSessionStack { get; }
IRunnable? FrameworkOwnedRunnable { get; set; }
// Driver and lifecycle
IDriver? Driver { get; }
IMainLoopCoordinator? MainLoop { get; }
// Fluent API methods
IApplication Init(string? driverName = null);
object? Shutdown();
// Runnable methods
RunnableSessionToken Begin(IRunnable runnable);
void 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(RunnableSessionToken sessionToken);
// Legacy Toplevel methods
SessionToken? Begin(Toplevel toplevel);
void Run(Toplevel view, Func<Exception, bool>? errorHandler = null);
void End(SessionToken sessionToken);
// ... other members
}
Terminal.Gui v2 modernized its terminology for clarity:
The TopRunnable property represents the Toplevel on the top of the session stack (the active runnable session):
// Access the top runnable session
Toplevel? topRunnable = app.TopRunnable;
// From within a view
Toplevel? topRunnable = App?.TopRunnable;
Why "TopRunnable"?
The SessionStack property is the stack of running sessions:
// Access all running sessions
foreach (var toplevel in app.SessionStack)
{
// Process each session
}
// From within a view
int sessionCount = App?.SessionStack.Count ?? 0;
Why "SessionStack" instead of "TopLevels"?
SessionToken terminologyThe static Application class delegates to ApplicationImpl.Instance (a singleton) 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 Toplevel? Current => ApplicationImpl.Instance.Current;
[Obsolete("The legacy static Application object is going away.")]
public static ConcurrentStack<Toplevel> SessionStack => ApplicationImpl.Instance.SessionStack;
// ... other obsolete static members
}
Important: The static Application class uses a singleton (ApplicationImpl.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?.Current?.SetNeedsDraw();
}
Strategy 2: Pass IApplication
// OLD:
void ProcessSessions()
{
foreach (var toplevel in Application.SessionStack)
{
// Process
}
}
// NEW:
void ProcessSessions(IApplication app)
{
foreach (var toplevel 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.Current?.Title = "Processing...";
}
}
Applications manage sessions through Begin() and End():
var app = Application.Create ();
app.Init();
var toplevel = new Toplevel();
// Begin a new session - pushes to SessionStack
SessionToken? token = app.Begin(toplevel);
// Current now points to this toplevel
Debug.Assert(app.Current == toplevel);
// End the session - pops from SessionStack
if (token != null)
{
app.End(token);
}
// Current restored to previous toplevel (if any)
Multiple sessions can run nested:
var app = Application.Create ();
app.Init();
// Session 1
var main = new Toplevel { Title = "Main" };
var token1 = app.Begin(main);
// app.Current == main, SessionStack.Count == 1
// Session 2 (nested)
var dialog = new Dialog { Title = "Dialog" };
var token2 = app.Begin(dialog);
// app.Current == dialog, SessionStack.Count == 2
// End dialog
app.End(token2);
// app.Current == main, SessionStack.Count == 1
// End main
app.End(token1);
// app.Current == 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
var mockApp = new Mock<IApplication>();
mockApp.Setup(a => a.Current).Returns(new Toplevel());
// Create view with mock app
var view = new MyView { App = mockApp.Object };
// Test without Application.Init()!
view.SetNeedsDraw();
Assert.True(view.NeedsDraw);
// No Application.Shutdown() needed!
}
[Fact]
public void MyView_WorksWithRealApplication()
{
var app = Application.Create ();
try
{
app.Init(new FakeDriver());
var view = new MyView();
var top = new Toplevel();
top.Add(view);
app.Begin(top);
// View.App automatically set
Assert.NotNull(view.App);
Assert.Same(app, view.App);
// Test view behavior
view.DoSomething();
}
finally
{
app.Shutdown();
}
}
✅ GOOD:
public void Refresh()
{
App?.Current?.SetNeedsDraw();
}
❌ AVOID:
public void Refresh()
{
Application.TopRunnable?.SetNeedsDraw(); // Obsolete!
}
✅ GOOD:
public class Service
{
public Service(IApplication app) { }
}
❌ AVOID (obsolete pattern):
public void Refresh()
{
Application.TopRunnable?.SetNeedsDraw(); // Obsolete static access
}
✅ PREFERRED:
public void Refresh()
{
App?.Current?.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
var app1 = Application.Create ();
app1.Init(new WindowsDriver());
var top1 = new Toplevel { Title = "App 1" };
// ... configure top1
// Application 2 (different driver!)
var app2 = Application.Create ();
app2.Init(new CursesDriver());
var top2 = new Toplevel { 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
var app = GetApp();
if (app != null)
{
var msg = new MessageBox(message);
app.Begin(msg);
}
}
}