Files
Terminal.Gui/docfx/docs/application.md
Tig a84b2c4896 Fixes #4419, #4148, #4408 - Toplevel is GONE - Replaced by Runnable (#4422)
* WIP: Broken

* Got working. Mostly.

* Parllel tests pass

* More progres

* Fixed app tests.

* Mouse

* more progress.

* working on shortcut

* Shortcut accept on ENTER is broken.

* One left...

* More test progress.

* All unit tests pass. Still some issues though.

* tweak

* Fixed Integration Tests

* Fixed UI Catalog

* Tweaking CP to try to find race condition

* Refactor StandardColors and improve ColorPicker logic

Refactored `StandardColors` to use lazy initialization for static fields, improving performance and avoiding static constructor convoy effects. Introduced `NamesValueFactory` and `MapValueFactory` methods for encapsulated initialization logic.

Simplified `GetColorNames` to directly return `_names.Value`. Improved `TryParseColor` by clarifying default value usage and adopting object initializer syntax. Updated `TryNameColor` to use `_argbNameMap.Value`.

Refactored `GetArgb` for better readability. Replaced `MultiStandardColorNameResolver` with `StandardColorsNameResolver` in `ColorPicker`. Commented out `app.Init("Fake")` in `ColorPickerTests` for testing purposes.

Made minor formatting improvements, including updated comments and XML documentation for consistency.

* revert

* Throttle input loop to prevent CPU spinning

Introduce a 20ms delay in the input loop of `InputImpl<TInputRecord>`
to prevent excessive CPU usage when no input is available. Removed
the `DateTime dt = Now();` line and the `while (Peek())` block, which
previously enqueued input records.

This change improves resource management, especially in scenarios
where multiple `ApplicationImpl` instances are created in parallel
tests without calling `Shutdown()`. It prevents thread pool
exhaustion and ensures better performance in such cases.

* Refactor ApplicationImpl to use IDisposable pattern

Implemented the IDisposable pattern in ApplicationImpl to improve resource management. Added `Dispose` and `DisposeCore` methods, and marked the `Shutdown` method as obsolete, encouraging the use of `Dispose` or `using` statements instead. Updated the `IApplication` interface to inherit from IDisposable and added `GetResult` methods for retrieving run session results.

Refactored unit tests to adopt the new lifecycle management approach, replacing legacy `Shutdown` calls with `Dispose` or `using`. Removed fragile and obsolete tests, and re-enabled previously skipped tests after addressing underlying issues.

Updated `FakeApplicationLifecycle` and `SetupFakeApplicationAttribute` to align with the new disposal pattern. Improved documentation and examples to guide users toward modern usage patterns. Maintained backward compatibility for legacy singleton usage.

* Add IDisposable pattern with input loop throttling

- Add IDisposable to IApplication for proper resource cleanup
- Add 20ms throttle to input loop (prevents CPU spinning)
- Add Lazy<T> to StandardColors (eliminates convoy effect)
- Add MainLoopCoordinatorTests suite (5 new tests)
- Add Dispose() calls to all 16 ColorPickerTests
- Mark Application.Shutdown() as [Obsolete]

IApplication now requires Dispose() for cleanup

Performance: 100x CPU reduction, 15x faster disposal, tests complete in <5s

Fixes: Thread leaks, CPU saturation, test hangs in parallel execution
Docs: Updated application.md and newinv2.md with disposal patterns

* Refactor test for input loop throttling clarity

Updated `InputLoop_Throttle_Limits_Poll_Rate` test to improve clarity, reliability, and efficiency:
- Rewrote summary comment to clarify purpose and emphasize the 20ms throttle's role in preventing CPU spinning.
- Replaced `var` with explicit types for better readability.
- Reduced test duration from 1s to 500ms to improve test speed.
- Revised assertions:
  - Replaced range-based assertion with upper-bound check to ensure poll count is below 500, avoiding timing sensitivity issues.
  - Added assertion to verify the thread ran and was not immediately canceled.
- Added a 2-second timeout to `inputTask.Wait` and verified task completion.
- Improved comments to explain test behavior and reasoning behind changes.

* tweaks

* Fix nullabiltiy stuff.

* runnable fixes

* more nullabe

* More nullability

* warnings gone

* Fixed fluent test failure.

* Refactor ApplicationImpl and update Runnable layout logic

Refactored `ApplicationImpl.Run.cs` for improved readability and
atomicity:
- Combined `if (wasModal)` with `SessionStack?.TryPop` to streamline
  logic.
- Simplified restoration of `previousRunnable` by reducing nesting.
- Updated comments for clarity and retained `SetIsModal` call.

Simplified focus-setting logic in `ApplicationImpl.Run.cs` using
pattern matching for `TopRunnableView`.

In `Runnable<TResult>`, added `SetNeedsLayout` after `IsModalChanged`
to ensure layout updates. Removed an unused empty line for cleanup.

Corrected namespace in `GetViewsUnderLocationForRootTests.cs` to
align with test structure.

* Update layout on modal state change

A call to `SetNeedsLayout()` was added to the `OnIsModalChanged`
method in the `Runnable` class. This ensures that the layout
is updated whenever the modal state changes.

* Increase test timeout for inputTask.Wait to 10 seconds

Extended the timeout duration for the `inputTask.Wait` method
from 4 seconds to 10 seconds in `MainLoopCoordinatorTests`.
This change ensures the test has a longer window to complete
under conditions of increased load or slower execution
environments, reducing the likelihood of false test failures.

* Refactor project files and simplify test logic

Removed `<LangVersion>` and `<ImplicitUsings>` properties from
`UnitTests.csproj` and `UnitTests.Parallelizable.csproj` to rely
on default SDK settings and disable implicit global usings.

Simplified the `SizeChanged_Event_Still_Fires_For_Compatibility`
test in `FakeDriverTests` by removing the `screenChangedFired`
variable, its associated event handler, and related assertions.
Also removed obsolete warning suppression directives as they
are no longer needed.

* Reduce UnitTestsParallelizable iterations from 10 to 3

Reduced the number of iterations for the UnitTestsParallelizable
test suite from 10 to 3 to save time and resources while still
exposing concurrency issues. Updated the loop and log messages
to reflect the new iteration count.

* disabled InputLoop_Throttle_Limits_Poll_Rate

* Refactor app lifecycle and improve Runnable API

Refactored `Program.cs` to simplify application lifecycle:
- Modularized app creation, initialization, and disposal.
- Improved result handling and ensured proper resource cleanup.

Re-implemented `Runnable<TResult>` with a cleaner design:
- Retained functionality while improving readability and structure.
- Added XML documentation and followed the Cancellable Work Pattern.

Re-implemented `RunnableWrapper<TView, TResult>`:
- Enabled wrapping any `View` to make it runnable with typed results.
- Added examples and remarks for better developer guidance.

Re-implemented `ViewRunnableExtensions`:
- Provided fluent API for making views runnable with or without results.
- Enhanced documentation with examples for common use cases.

General improvements:
- Enhanced code readability, maintainability, and error handling.
- Replaced redundant code with cleaner, more maintainable versions.

* Modernize codebase for Terminal.Gui and MVVM updates

Refactored `LoginView` to remove redundant `Application.LayoutAndDraw()`
call. Enhanced `LoginViewModel` with new observable properties for
automatic property change notifications. Updated `Message` class to use
nullable generics for improved type safety.

Replaced legacy `Application.Init()` and `Application.Run()` calls with
the modern `IApplication` API across `Program.cs`, `Example.cs`, and
`ReactiveExample`. Ensured proper disposal of `IApplication` instances
to prevent resource leaks.

Updated `TerminalScheduler` to integrate with `IApplication` for
invoking actions and managing timeouts. Added null checks and improved
timeout disposal logic for robustness.

Refactored `ExampleWindow` for better readability and alignment with
modern `Terminal.Gui` conventions. Cleaned up unused imports and
improved code clarity across the codebase.

Updated README.md to reflect the latest `Terminal.Gui` practices,
including examples of the `IApplication` API and automatic UI refresh
handling. Renamed `LoginAction` to `LoginActions` for consistency.

* Refactor: Transition to IRunnable-based architecture

Replaced `Toplevel` with `Window` as the primary top-level UI element. Introduced the `IRunnable` interface to modernize the architecture, enabling greater flexibility and testability. Deprecated the static `Application` class in favor of the instance-based `IApplication` model, which supports multiple application contexts.

Updated methods like `Application.Run()` and `Application.RequestStop()` to use `IRunnable`. Removed or replaced legacy `Modal` properties with `IsModal`. Enhanced the `IApplication` interface with a fluent API, including methods like `Run<TRunnable>()` and `GetResult<T>()`.

Refactored tests and examples to align with the new architecture. Updated documentation to reflect the instance-based model. Deprecated obsolete members and methods, including `Application.Current` and `Application.TopRunnable`.

Improved event handling by replacing the `Accept` event with `Accepting` and using `e.Handled` for event processing. Updated threading examples to use `App?.Invoke()` or `app.Invoke()` for UI updates. Cleaned up redundant code and redefined modal behavior for better consistency.

These changes modernize the `Terminal.Gui` library, improving clarity, usability, and maintainability while ensuring backward compatibility where possible.

* Refactor: Replace Toplevel with Runnable class

This commit introduces a major architectural update to the `Terminal.Gui` library, replacing the legacy `Toplevel` class with the new `Runnable` class. The changes span the entire codebase, including core functionality, tests, documentation, and configuration files.

- **Core Class Replacement**:
  - Replaced `Toplevel` with `Runnable` as the base class for modal views and session management.
  - Updated all references to `Toplevel` in the codebase, including constructors, methods, and properties.

- **Configuration Updates**:
  - Updated `tui-config-schema.json` to reflect the new `Runnable` scheme.

- **New Classes**:
  - Added `UICatalogRunnable` for managing the UI Catalog application.
  - Introduced `Runnable<TResult>` as a generic base class for blocking sessions with result handling.

- **Documentation and Tests**:
  - Updated documentation to emphasize `Runnable` and mark `Toplevel` as obsolete.
  - Refactored test cases to use `Runnable` and ensure compatibility.

- **Behavioral Improvements**:
  - Enhanced lifecycle management and alignment with the `IRunnable` interface.
  - Improved clarity and consistency in naming conventions.

These changes modernize the library, improve flexibility, and provide a clearer architecture for developers.

* Refactor: Consolidate Runnable classes and decouple View from ApplicationImpl

- Made Runnable<TResult> inherit from Runnable (eliminating ~180 LOC duplication)
- Moved View init/layout/cursor logic from ApplicationImpl to Runnable lifecycle events
- ApplicationImpl.Begin now operates purely on IRunnable interface

Related to #4419

* Simplified the disposal logic in `ApplicationImpl.Run.cs` by replacing
the type-specific check for `View` with a more general check for
`IDisposable`. This ensures proper disposal of any `IDisposable`
object, improving robustness.

Removed the `FrameworkOwnedRunnable` property from the `ApplicationImpl`
class in `ApplicationImpl.cs` and the `IApplication` interface in
`IApplication.cs`. This eliminates the need to manage this property,
reducing complexity and improving maintainability.

Updated `application.md` to reflect the removal of the
`FrameworkOwnedRunnable` property, ensuring the documentation aligns
with the updated codebase.

* Replaces the legacy `Shutdown()` method with `Dispose()` to align
with the `IDisposable` pattern, ensuring proper resource cleanup
and simplifying the API. The `Dispose()` method is now the
recommended way to release resources, with `using` statements
encouraged for automatic disposal.

Key changes:
- Marked `Shutdown()` as obsolete; it now internally calls `Dispose()`.
- Updated the fluent API to remove `Shutdown()` from chaining.
- Enhanced session lifecycle management for thread safety.
- Updated tests to validate proper disposal and state reset.
- Improved `IRunnable` integration with automatic disposal for
  framework-created runnables.
- Maintained backward compatibility for the legacy static
  `Application` singleton.
- Refactored documentation and examples to reflect modern practices
  and emphasize `Dispose()` usage.

These changes modernize the `Terminal.Gui` lifecycle, improve
testability, and encourage alignment with .NET conventions.

* Refactor runnable app context handling in ApplicationImpl

Refactor how the application context is set for `runnable` objects
by introducing a new `SetApp` method in the `IRunnable` interface.
This replaces the previous logic of directly setting the `App`
property for `View` objects, making the process more generic and
encapsulated within `IRunnable` implementations.

Simplify `Mouse.UngrabMouse()` by removing the conditional check
and calling it unconditionally.

Make a minor formatting adjustment in the generic constraint of
`Run<TRunnable>` in `ApplicationImpl`.

Add `SetApp(IApplication app)` to the `IRunnable` interface and
implement it in the `Runnable` class to set the `App` property
to the provided application instance.

* Improve docs, tests, and modularity across the codebase

Reorganized and updated `CONTRIBUTING.md`:
- Added **Key Architecture Concepts** section and reordered the table of contents.
- Updated testing requirements to discourage legacy patterns.
- Added instructions for replicating CI workflows locally.
- Clarified PR guidelines and coding style expectations.

Enhanced `README.md` with detailed CI/CD workflow documentation.

Refactored `ColorPicker.Prompt` to use `IApplication` for improved modularity and testability.

Introduced `IApplicationScreenChangedTests` for comprehensive testing of `ScreenChanged` events and `Screen` property.

Refactored `ApplicationScreenTests` and `TextView.PromptForColors` to align with modern patterns.

Updated `Terminal.sln` to include `.github/workflows/README.md`.

Performed general cleanup:
- Removed outdated documentation links.
- Improved XML documentation and coding consistency.

* readme tweaks

* Improve thread safety, layout, and test coverage

Refactored `OutputBufferImpl.cs` to enhance thread safety by locking shared resources and adding bounds checks for columns and rows. Improved handling of wide characters and removed outdated TODO comments.

Updated `Runnable.cs` to call `SetNeedsDraw()` on modal state changes, ensuring proper layout and drawing updates. Simplified layout handling in `ApplicationImpl.Run.cs` by replacing redundant comments with a `LayoutAndDraw()` call.

Added a check in `AllViewsTester.cs` to skip creating instances of `RunnableWrapper` types with unsatisfiable generic constraints, logging a warning when encountered.

Enhanced `ListViewTests.cs` by adding explicit `app.LayoutAndDraw()` calls to validate visual output and ensure tests reflect the updated application state.

These changes improve robustness, prevent race conditions, and ensure consistent behavior across the application.

* Refactor: Rename Toplevel to Runnable and update logic

Updated the `Border` class to use `Command.Quit` instead of
`Command.QuitToplevel` in the `CloseButton.Accept` handler.

Renamed test methods in `GetViewsAtLocationTests.cs` to replace
"Toplevel" with "Runnable" for consistency. Updated `Runnable<bool>`
instances to use "topRunnable" as the `Id` property.

These changes align the codebase with updated naming conventions
and improve clarity.

* Removed `ToplevelTests` and migrated relevant test cases to
`MouseDragTests` with improved structure and coverage. Updated
tests to use `Application.Create`, `app.Begin`, and `app.End`
for better resource management and lifecycle handling.

Replaced direct event handling with `app.Mouse.RaiseMouseEvent`
to align with the application's event-handling mechanism. Added
`Runnable` objects to ensure views are properly initialized and
disposed of within the application context.

Enhanced tests to include assertions for minimum width and
height constraints during resize operations. Removed redundant
tests and streamlined logic to reduce duplication and improve
maintainability.

* Reorged Unit Test namespaces.

* more

* Refactor tests and update namespaces for consistency

Updated namespaces in `ArrangementTests.cs` and `MouseDragTests.cs` for better organization. Enhanced `ArrangementTests.cs` with additional checks for arrangement flags. Reformatted and re-added `MouseDragTests.cs` and `SchemeTests.cs` with modern C# features like nullable annotations and object initializers. Ensured no functional changes while improving code clarity and consistency.

* Fix nullability warnings in MouseDragTests.cs

Updated `app.End` calls to use the null-forgiving operator (`!`)
on `app.SessionStack` to ensure it is treated as non-null.
This change addresses potential nullability warnings and
improves code safety and clarity. Applied consistently across
all relevant test cases in the `MouseDragTests` class.
2025-12-01 12:54:21 -07:00

22 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.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 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/>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

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();
var top = new Window();
top.Add(myView);
Application.Run(top);
top.Dispose();
Application.Shutdown(); // Obsolete - use Dispose() instead

// RECOMMENDED (v2 - instance-based with using statement):
using (var app = Application.Create().Init())
{
    var top = new Window();
    top.Add(myView);
    app.Run(top);
    top.Dispose();
} // app.Dispose() called automatically

// WITH IRunnable (fluent API with automatic disposal):
using (var app = Application.Create().Init())
{
    app.Run<ColorPickerDialog>();
    Color? result = app.GetResult<Color>();
}

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

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.

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

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 (var app = Application.Create().Init())
{
    app.Run<ColorPickerDialog>();
    Color? result = app.GetResult<Color>();
    
    if (result is { })
    {
        ApplyColor(result);
    }
}

// Alternative: Manual disposal
var 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 (var app = Application.Create().Init())
{
    app.Run<MyDialog>(); // Dialog disposed automatically when Run returns
    var result = app.GetResult<MyResultType>();
}

// Caller ownership - manual disposal
using (var app = Application.Create().Init())
{
    var dialog = new MyDialog();
    app.Run(dialog);
    var 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 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);
    }
}

Lifecycle Properties

  • IsRunning - True when runnable is on RunnableSessionStack
  • 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);
}

RunnableSessionStack

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:

  • Push: Begin(IRunnable) adds to top of stack
  • Pop: End(RunnableSessionToken) removes from stack
  • Peek: TopRunnable returns current modal runnable
  • All: RunnableSessionStack 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 (var runnable in app.SessionStack)
{
    // Process each session
}

// From within a view
int 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 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 View? TopRunnableView => ApplicationImpl.Instance.TopRunnableView;
    
    [Obsolete("The legacy static Application object is going away.")]
    public static IRunnable? TopRunnable => ApplicationImpl.Instance.TopRunnable;
    
    [Obsolete("The legacy static Application object is going away.")]
    public static ConcurrentStack<SessionToken>? 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().

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 (var runnable in Application.SessionStack)
    {
        // Process
    }
}

// NEW:
void ProcessSessions(IApplication app)
{
    foreach (var 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.Current?.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 (var app = Application.Create().Init())
{
    app.Run<MyDialog>();
    // app.Dispose() automatically called when scope exits
}

Manual Disposal

// Manual disposal
var 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 (var app = Application.Create().Init())
{
    app.Run<MyDialog>();
    var result = app.GetResult<MyResult>();
    // app.Dispose() called automatically here
}

// ALTERNATIVE (manual disposal):
var app = Application.Create().Init();
app.Run<MyDialog>();
var result = app.GetResult<MyResult>();
app.Dispose(); // Must call explicitly

// OLD (obsolete - do not use):
var result = app.Run<MyDialog>().Shutdown() as MyResult;

Input Thread Lifecycle

When you call Init(), Terminal.Gui starts a dedicated input thread that continuously polls for console input. This thread must be stopped properly:

var 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 var 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:

var app = Application.Create();
app.Init();
app.Dispose();

app.Init(); // ❌ Throws ObjectDisposedException

Session Management

Begin and End

Applications manage sessions through Begin() and End():

using var app = Application.Create ();
app.Init();

var window = new Window();

// 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 var app = Application.Create ();
app.Init();

// Session 1
var main = new Window { Title = "Main" };
var token1 = app.Begin(main);
// app.TopRunnable == main, SessionStack.Count == 1

// Session 2 (nested)
var dialog = new Dialog { Title = "Dialog" };
var 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
    var mockApp = new Mock<IApplication>();
    mockApp.Setup(a => a.Current).Returns(new Runnable());
    
    // 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!
}

Testing with Real ApplicationImpl

[Fact]
public void MyView_WorksWithRealApplication()
{
    using var app = Application.Create ();
    app.Init("fake");
    
    var view = new MyView();
    var top = new Window();
    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 var app1 = Application.Create ();
app1.Init("windows");
var top1 = new Window { Title = "App 1" };
// ... configure top1

// Application 2 (different driver!)
using var app2 = Application.Create ();
app2.Init("unix");
var top2 = new Window { 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
        var app = GetApp();
        if (app != null)
        {
            var msg = new MessageBox(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