Files
Terminal.Gui/docfx/docs/application.md
Tig d303943809 Fixes #4004 & #4445 - Merge of Application.ForceDriver and Driver.Force16Colors and windows" broken in conhost and cmd (#4448)
* Fixes #4004. Driver "windows" broken in conhost and cmd

* Fix unit tests

* Remove IsVirtualTerminal from IApplication. Add IDriverInternal and IOutputInternal interfaces

* Fix result.IsSupported

* Remove internal interfaces and add them in the implementations classes

* Move Sixel from IApplication to IDriver interface it's a characteristic of the driver

* Only if IOutput is OutputBase then set the internal properties

* Prevents driver windows error on Unix system

* Fix scenario sixel error

* Comment some tests because is keyboard layout dependent and shifted key is needed to produce them (Pt)

* Add 🇵🇹 regional indicators test proving they ca be joined as only one grapheme

* SetConsoleActiveScreenBuffer is already called by the constructor and is only needed once

* Finally fixed non virtual terminal in windows driver

* Add more Sixel unit tests

* Add unit tests for OutputBase class

* Avoid emit escape sequence

* Fix assertion failure in UICatalog

* Let each driver to deal with the Sixel write

* When Shutdown is called by the static Application then the ApplicationImpl.ResetStateStatic should be also called

* Add more OutputBase with Sixel unit tests

* Fix some issues with IsVirtualTerminal and Force16Colors with unit tests improvement

* Add Sixel Detect method unit test

* Make Sixel IsSupported and SupportsTransparency consistent with more unit tests

* Fix namespaces and unit test

* Covering more ApplicationImpl Sixel unit test

* Remove DriverImplProxy because sometimes fails in parallel unit tests

* Fix Init_KeyBindings_Are_Not_Reset unit test failing

* Revert "Fix Init_KeyBindings_Are_Not_Reset unit test failing"

This reverts commit 0ab298bc56.

* Fix Force16Colors but still use Application.Force16Colors because of CM

* Enforce conditional

* Revert change

* Moving to a new file

* Add the same workaround as the All_Scenarios_Benchmark unit test

* Fixes #4440. TextView with ReadOnly as true, MoveRight doesn't select text up to the end of the line

* Fixes #4442. TextField PositionCursor doesn't treat zero width as one column

* Each character must return at least one column, with the exception of Tab.

* Add unit test for the ScrollOffset

* Each character must return at least one column, with the exception of Tab.

* Add unit test for the LeftColumn

* WIP

* Refactor DriverImpl and OutputBase for maintainability

Refactored `DriverImpl` to remove `IDisposable` and streamline event
handling, including replacing `OnSizeMonitorOnSizeChanged` with an
inline lambda. Reintroduced `SizeChanged` and updated `SetScreenSize`
to invoke it. Moved `SupportsTrueColor` from `OutputBase` to
`DriverImpl` and reintroduced `Force16Colors` with updated logic.

Reintroduced and updated several `OutputBuffer`-related properties
and methods in `DriverImpl`, including `Screen`, `Clip`, `Cols`, and
`Contents`. Moved `Clipboard` from `OutputBase` to `DriverImpl` and
initialized it with `FakeClipboard`. Simplified `Refresh` and `ToAnsi`
methods in `DriverImpl`.

Removed `Force16Colors` from `OutputBase` and simplified method
signatures, including `ToAnsi` and `BuildAnsiForRegion`. Fixed a
parameter name typo in `AppendOrWriteAttribute`. Made minor code
formatting adjustments.

These changes improve code maintainability, reduce redundancy, and
align the implementation with updated design requirements.

* Refactor Force16Colors handling and improve UICatalog

Refactored the `Force16Colors` property:
- Moved it from `DriverImpl` to `IOutput` and `OutputBase`.
- Simplified its management by removing redundant logic.
- Added `OnDriverOnForce16ColorsChanged` to handle updates.

Updated `UICatalogRunnable`:
- Replaced `Driver.Force16Colors` with `Application.Driver.Force16Colors`.
- Added an `F7` shortcut to toggle `Force16Colors`.
- Removed redundant event handlers and improved formatting.

Updated `config.json`:
- Replaced `Application.Force16Colors` with `Driver.Force16Colors`.
- Improved theme configuration formatting for readability.

Other changes:
- Removed the `force16Colors` parameter from `IOutput.ToAnsi`.
- Improved diagnostics handling in `UICatalogRunnable`.
- General code cleanup for readability and maintainability.

* Refactor `Force16Colors` access and improve null safety

Refactored `Force16Colors` property access to use `Application.Driver!`
for null safety and consistency. Updated event handlers to align with
this pattern. Replaced nullable `DrawContext?` parameters with
non-nullable `DrawContext` in `OnDrawingContent` overrides across
multiple classes to enforce stricter nullability checks.

Removed unused `_cachedCursorVisibility` field in `OutputBase.cs` and
cleaned up commented-out legacy code in `UICatalogRunnable.cs`. Updated
XML documentation to reflect method signature changes and property
references. Refactored `Shortcut` example in documentation for
consistency.

Replaced `Application.LayoutAndDraw` with `SetNeedsDraw` for marking
views as needing redraw. Performed general code cleanup to remove
redundant code and improve consistency.

* Refactor ForceDriver and Force16Colors properties

Removed `[Obsolete]` from `Application.ForceDriver`, making it a stable API. Added comments to clarify its role as a configuration property and its synchronization with `IApplication.ForceDriver`. Introduced `_forceDriver` as a private backing field.

Removed `Force16Colors` from `ApplicationImpl` and eliminated reset logic for `ForceDriver` and `Force16Colors` during shutdown, shifting state management responsibility to the library user.

Updated comments in `Driver.cs` to document `Force16Colors` as a configuration property and its synchronization with `IDriver.Force16Colors`. Retained `_force16Colors` as a private backing field for configuration overrides.

* Updated docs

* There is no way to detect Sixel transparency and so relying in VTS or Xterm with transparency

* Fix detect Sixel unit tests with the adjusting code

* Refactored Output.

* MErging

* - Added `OnDriverOnForce16ColorsChanged` method to handle `Driver.Force16ColorsChanged` events and update the `Force16Colors` property.

- Implemented `IDisposable` to ensure proper cleanup of resources, including unsubscribing from `SizeMonitor.SizeChanged` and `Driver.Force16ColorsChanged` events, and disposing of `_output`.
- Replaced inline `SizeMonitor.SizeChanged` event handler with a dedicated method, `OnSizeMonitorOnSizeChanged`, for better readability and maintainability.

- Simplified the `Screen` property by removing commented-out code and directly returning a `Rectangle` based on `OutputBuffer` dimensions.
- Updated the `Force16Colors` property to use `_output` for both getting and setting its value.
- Performed general cleanup, including removing unused code and improving code structure.

* merged

* Refactor Sixel handling with ConcurrentQueue

Replaced `List<SixelToRender>` with `ConcurrentQueue<SixelToRender>`
to improve thread safety and performance in sixel management.
Updated the `Images` class to avoid unnecessary removal and
re-creation of sixel objects by updating existing ones in place.

Refactored `Application.Sixel` to return a `ConcurrentQueue` and
introduced `GetSixels` in `IDriver` and `IOutput` for consistent
access. Updated `OutputBase` to use a private `ConcurrentQueue`
and adjusted rendering logic accordingly.

Removed legacy and redundant code, including `Application.Driver?.Sixel.Clear()`
and unused properties in `DriverImpl` and `ApplicationImpl`. Updated
tests in `OutputBaseTests` to align with the new implementation.

Added `using System.Collections.Concurrent` where necessary and
improved documentation to reflect the changes. These updates
enhance thread safety, simplify the codebase, and align with
modern concurrent programming practices.

* Tweak

* Refactor DriverImpl to use Dispose and improve modularity

Replaced `Driver.End()` with `Driver.Dispose()` across the codebase, aligning with the `IDisposable` pattern for proper resource cleanup. Updated `DriverImpl` to implement `Dispose`, ensuring event unsubscriptions and resource disposal.

Enhanced `DriverImpl` structure by organizing code into logical regions, improving modularity and readability. Refactored and reintroduced methods and properties like `Clipboard`, `Screen`, `SetScreenSize`, `Cols`, `Rows`, and others for better encapsulation.

Updated the `IDriver` interface to include `IDisposable` and reorganized it into regions. Added new methods and properties such as `Init`, `Refresh`, `Suspend`, `QueueAnsiRequest`, and `ToAnsi`.

Refactored unit tests to replace `driver.End()` with `driver.Dispose()` and ensured proper resource cleanup. Improved code comments and documentation for better clarity.

Aligned with modern C# practices, adopting features like null-coalescing operators and pattern matching. Removed redundant code, addressed some TODOs, and modularized the codebase for maintainability and extensibility.

* Refactor driver docs and update View.Driver usage

Updated `application.md` to clarify the purpose of the `View.Driver` property, replacing the obsolete `Application.Driver`. Added a reference to the "Drivers Deep Dive" documentation for further details.

Refactored the `OnDrawContent` method to use the `Driver` property, ensuring compatibility with the new driver architecture.

Added a new section, "Testing with the New Architecture," to `application.md`, highlighting the improved testability of the instance-based architecture.

Expanded and reorganized `drivers.md` to provide a detailed breakdown of the `IDriver` interface, including lifecycle, components, screen and display, color support, content buffer, drawing, cursor, input events, and ANSI escape sequences. Introduced new subsections for clarity and emphasized the modular design for maintainability.

Added a note in `drivers.md` discouraging direct access to the `Driver` and recommending higher-level abstractions like `Terminal.Gui.App.Application.Screen` and `Terminal.Gui.ViewBase.View` methods for positioning and drawing.

* Refactor IsVirtualTerminal to IsLegacyConsole

Replaced the `IsVirtualTerminal` property with `IsLegacyConsole` across the codebase to better represent legacy versus modern terminal environments. Updated logic in `SixelSupportDetector`, `DriverImpl`, and `OutputBase` to use the new property.

Refactored tests to align with the updated property, including renaming test methods, adjusting mock setups, and replacing `VirtualTerminalTests` with `LegacyConsoleTests`.

Simplified `WindowsOutput` implementation to handle console modes and sixel rendering based on `IsLegacyConsole`. Removed redundant code related to `IsVirtualTerminal`.

Improved code readability and maintainability by using more descriptive property names and ensuring consistency across the codebase. Updated `.DotSettings` with new entries.

* Update Examples/UICatalog/Scenarios/LineDrawing.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Examples/UICatalog/Scenarios/Images.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Examples/UICatalog/Scenarios/Images.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/App/IApplication.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Examples/UICatalog/Scenarios/ColorPicker.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Examples/UICatalog/Scenarios/ColorPicker.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix formatting and typo in code and documentation

Improved code readability in `LineDrawing.cs` by fixing spacing
around the ternary operator in `Width` and `Y` property assignments.
Corrected a typo in `drivers.md` by changing "Configuraiton Manager"
to "Configuration Manager" for accurate documentation.

* Test failure casued by assert left in by accident.

* Added a workaround in `OutputBase.cs` to address dirty cell handling in legacy console mode by marking all buffer cells as dirty.

Refactored `_disableMouseCb` event handling in `UICatalogRunnable.cs` to use the `Selecting` event for toggling `Application.IsMouseDisabled`. Simplified `MouseImpl.cs` by converting `App` to an auto-implemented property and removing redundant namespace usage.

Streamlined logging in `WindowsOutput.cs` by replacing verbose `Logging.Logger` calls with shorter alternatives (`Logging.Information`, `Logging.Error`, etc.).

* Update theme and remove unused ListView component

The application's default theme configuration was updated from "Light" to "Amber Phosphor" by modifying the `ConfigurationManager.RuntimeConfig` value.

Additionally, the `ListView` component in the `ExampleWindow` class was removed. This included its initialization, layout properties (`Y`, `Height`, `Width`), and its data source (["One", "Two", "Three", "Four"]).

* Increase safety timeout in NestedRunTimeoutTests to 10s

The timeout duration for the safety mechanism in the
`NestedRunTimeoutTests` class was increased from 5000ms (5s)
to 10000ms (10s). This change allows the app more time to
complete before triggering the safety timeout, reducing the
likelihood of premature termination during long-running tests.

Refactor and enhance test coverage

Refactored `Load_WithInvalidJson_AddsJsonError` test in `SourcesManagerTests.cs` to improve organization and added a note about its impact on parallel execution. Increased the safety timeout in `NestedRunTimeoutTests.cs` from 5 seconds to 10 seconds to address potential premature test timeouts.

* Handle null Driver gracefully in event subscription

Replaced `ArgumentNullException.ThrowIfNull(Driver)` with a null-check conditional in `SubscribeDriverEvents` and `UnsubscribeDriverEvents`. If `Driver` is `null`, the methods now log an error using `Logging.Error` and return early. This prevents potential exceptions and improves error handling.

---------

Co-authored-by: BDisp <bd.bdisp@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 17:40:48 -07:00

24 KiB

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 an IApplication instance instead of static methods
  • IRunnable Interface: Views implement IRunnable<TResult> to participate in session management without inheriting from Runnable
  • Fluent API: Chain Init() and Run() for elegant, concise code
  • IDisposable Pattern: Proper resource cleanup with Dispose() or using statements
  • Automatic Disposal: Framework-created runnables are automatically disposed
  • Type-Safe Results: Generic TResult parameter 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:

// 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> ();
}

// ALTERNATIVE (manual disposal):
IApplication app = Application.Create ().Init ();
app.Run<ColorPickerDialog> ();
Color? result = app.GetResult<Color> ();
app.Dispose ();

// OLD (v1 / early v2 - obsolete, avoid in new code):
Application.Init ();
Window top = new ();
top.Add (myView);
Application.Run (top);
top.Dispose ();
Application.Shutdown (); // Obsolete - use Dispose() instead

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 obsolete static Application
        IApplication? app = App;
        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;
        // Completely decoupled from obsolete 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 TResult parameter 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() - Returns IApplication for chaining
  • Run<TRunnable>() - Creates and runs runnable, returns IApplication
  • GetResult() / GetResult<T>() - Extract typed result after run
  • Dispose() - Release all resources (called automatically with using)

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;
            App?.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 on SessionStack
  • IsModal - 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 ())
        {
            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);
}

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: TopRunnable returns current modal runnable
  • All: SessionStack enumerates 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 just Runnable

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 SessionToken terminology
  • 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
// 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 completes
  • Shutdown() - 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 ();

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

Driver Management

ForceDriver Configuration Property

The ForceDriver property is a configuration property that allows you to specify which driver to use. It can be set via code or through the configuration system (e.g., config.json):

// RECOMMENDED: Set on instance
using (IApplication app = Application.Create ())
{
    app.ForceDriver = "fake";
    app.Init ();
}

// ALTERNATIVE: Set on legacy static Application (obsolete)
Application.ForceDriver = "dotnet";
Application.Init ();

Valid driver names: "dotnet", "windows", "unix", "fake"

ForceDriverChanged Event

The static Application.ForceDriverChanged event is raised when the ForceDriver property changes:

// ForceDriverChanged event (on legacy static Application)
Application.ForceDriverChanged += (sender, e) =>
{
    Debug.WriteLine ($"Driver changed from '{e.OldValue}' to '{e.NewValue}'");
};

Application.ForceDriver = "fake";

Getting Available Drivers

You can query which driver types are available using GetDriverTypes():

// Get available driver types and names
(List<Type?> types, List<string?> names) = Application.GetDriverTypes();

foreach (string? name in names)
{
    Debug.WriteLine($"Available driver: {name}");
}
// Output:
// Available driver: dotnet
// Available driver: windows
// Available driver: unix
// Available driver: fake

Note: This method uses reflection and is marked with [RequiresUnreferencedCode] for AOT compatibility considerations.

View.Driver Property

Similar to View.App, views now have a Driver property for accessing driver functionality.

public override void OnDrawContent (Rectangle viewport)
{
    // Use view's driver instead of obsolete Application.Driver
    Driver?.Move (0, 0);
    Driver?.AddStr ("Hello");
}

Note: See Drivers Deep Dive for complete driver architecture details, including the organized interface structure with lifecycle, components, display, rendering, cursor, and input regions.

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 ();
    Runnable runnable = new ();
    mockApp.Setup (a => a.TopRunnable).Returns (runnable);
    
    // Create view with mock app
    MyView view = new () { App = mockApp.Object };
    
    // Test without Application.Init()!
    view.SetNeedsDraw ();
    Assert.True (view.NeedsDraw);
    
    // No disposal needed for mock!
}

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);
        
        SessionToken? token = app.Begin (top);
        
        // View.App automatically set
        Assert.NotNull (view.App);
        Assert.Same (app, view.App);
        
        // Test view behavior
        view.DoSomething ();
        
        if (token is { })
        {
            app.End (token);
        }
    }
}

Best Practices

DO: Use View.App

// ✅ GOOD - Use View.App (modern instance-based pattern):
public void Refresh ()
{
    App?.TopRunnableView?.SetNeedsDraw ();
}

DON'T: Use Static Application

// ❌ AVOID - Obsolete static Application:
public void Refresh ()
{
    Application.TopRunnableView?.SetNeedsDraw (); // Obsolete!
}

DO: Pass IApplication as Dependency

// ✅ GOOD - Dependency injection:
public class Service
{
    private readonly IApplication _app;
    
    public Service (IApplication app)
    {
        _app = app;
    }
}

DON'T: Use Static Application in New Code

// ❌ AVOID - Obsolete static Application in new code:
public void Refresh ()
{
    Application.TopRunnableView?.SetNeedsDraw (); // Obsolete!
}

// ✅ PREFERRED - Use View.App property:
public void Refresh ()
{
    App?.TopRunnableView?.SetNeedsDraw ();
}

DO: Override GetApp() for Custom Resolution

// ✅ GOOD - Custom application resolution:
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 ("fake");
    Window top1 = new () { Title = "App 1" };
    // ... configure and run top1
}

// Application 2 (different driver!)
using (IApplication app2 = Application.Create ())
{
    app2.Init ("fake");
    Window top2 = new () { Title = "App 2" };
    // ... configure and run top2
}

// Views in top1 use app1
// Views in top2 use app2

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