Terminal.Gui icon indicating copy to clipboard operation
Terminal.Gui copied to clipboard

Proposal (post v2) - Replace CM with MEC

Open tig opened this issue 1 month ago • 5 comments

Our long departed friend @dodexahedron will love this:

1. The Vision: Modernizing Terminal.Gui

The current architecture of Terminal.Gui, built on static classes like Application and ConfigurationManager, has served us well. However, to enable critical features like test parallelization and to align with modern .NET development practices, we must evolve.

This proposal outlines a comprehensive plan to refactor Terminal.Gui into a modern, DI-centric application framework. This will involve replacing static singletons with services, leveraging the .NET Generic Host for lifecycle management, and providing a powerful, flexible, and familiar developer experience.

This is a holistic vision that addresses:

  • Instance-based application lifecycles.
  • A complete replacement for ConfigurationManager and SourcesManager.
  • A clean way for components to define and consume configuration.
  • Live, automatic updates for configuration changes.

2. Core Concepts

A. The AppInstance and the Generic Host

  • #4366

The static Application will be replaced by a non-static AppInstance class, which will be managed as a service within the .NET Generic Host (IHost). This provides true application isolation.

B. Dependency Injection as the Foundation

All core services (AppInstance, IConsoleDriver, MainLoop, etc.) and component settings will be managed by a DI container. Views and other components will receive their dependencies via constructor injection.

C. The ThemeManager and StyleManager Services

ConfigurationManager and SourcesManager will be retired. Their responsibilities will be split between:

  1. Microsoft.Extensions.Configuration: For loading and aggregating configuration from any source (JSON, environment variables, etc.).
  2. ThemeManager: A new service that applies theme and color scheme logic.
  3. StyleManager: A new service that provides access to the default, configured settings for component types (e.g., the default ShadowStyle for all Buttons).

D. The IOptionsMonitor<T> Pattern

Components will use IOptionsMonitor<T> to subscribe to live configuration changes, allowing them to automatically update their appearance when a setting is modified.

3. The New Developer Experience

A. Application Startup (Program.cs)

Developers will compose and launch their applications using the familiar IHostBuilder pattern.

// Program.cs
public static class Program
{
    public static async Task Main(string[] args)
    {
        await Host.CreateDefaultBuilder(args)
            // 1. Configure Terminal.Gui services and settings providers.
            .ConfigureTerminalGui() 
            .ConfigureServices((hostContext, services) =>
            {
                // 2. Register component-specific settings from the config file.
                services.Configure<ButtonSettings>(hostContext.Configuration.GetSection("ButtonSettings"));
                services.Configure<WindowSettings>(hostContext.Configuration.GetSection("WindowSettings"));

                // 3. Register your application's views and services.
                services.AddSingleton<MyAppToplevel>();
                services.AddTransient<IMyApiService, MyApiService>();
            })
            // 4. Run the application with a specific Toplevel view.
            .RunTerminalAsync<MyAppToplevel>();
    }
}

B. Component Configuration (ButtonSettings)

Configuration is decoupled from the component into simple POCO classes.

// Terminal.Gui/Hosting/Configuration/ButtonSettings.cs
public class ButtonSettings
{
    public ShadowStyle ShadowStyle { get; set; } = ShadowStyle.None;
    public bool UseContentForHotKey { get; set; } = true;
}

C. Component Implementation (Button)

Components declare their dependencies via the constructor and use IOptionsMonitor<T> to react to live changes.

// Terminal.Gui/Views/Button.cs
public partial class Button : View, IDisposable
{
    private readonly IOptionsMonitor<ButtonSettings> _settingsMonitor;
    private readonly IDisposable _settingsSubscription;
    private ShadowStyle? _instanceShadowStyle;

    public Button(IOptionsMonitor<ButtonSettings> settingsMonitor)
    {
        _settingsMonitor = settingsMonitor;
        _settingsSubscription = _settingsMonitor.OnChange(OnSettingsChanged);
    }

    public ShadowStyle ShadowStyle
    {
        get => _instanceShadowStyle ?? _settingsMonitor.CurrentValue.ShadowStyle;
        set
        {
            if (_instanceShadowStyle != value)
            {
                _instanceShadowStyle = value;
                SetNeedsDisplay();
            }
        }
    }

    private void OnSettingsChanged(ButtonSettings newSettings)
    {
        if (_instanceShadowStyle is null)
        {
            this.SetNeedsDisplay();
        }
    }

    public void Dispose()
    {
        _settingsSubscription?.Dispose();
    }
}

D. Accessing Default Styles

A developer can easily access the configured default for any component type via a new static facade.

// Get the configured default shadow style for all buttons.
ShadowStyle defaultBtnStyle = Application.Styles.Get<ButtonSettings>().ShadowStyle;

4. Implementation Plan

Phase 1: Create Core Services and Hosting Extensions

  1. Define Settings POCOs: Create classes like ButtonSettings, WindowSettings, etc.
  2. Create ThemeManager: This service will take IOptionsMonitor<RootConfiguration> and be responsible for resolving ColorSchemes based on theme and scope.
  3. Create StyleManager: This service will take IServiceProvider and expose the Get<T>() method to retrieve configured default settings for component types.
  4. Create HostingExtensions:
    • Implement ConfigureTerminalGui() to set up the M.E.C. providers, replacing SourcesManager.
    • This extension will register ThemeManager and StyleManager as singleton services.
  5. Implement AppInstance: Create the AppInstance class, which holds the IServiceProvider for its scope.
  6. Create Static Facade: Implement the Application.Styles static property that resolves the StyleManager from Application.CurrentInstance.Services.

Phase 2: Refactor Components

  1. Refactor Button: Modify Button as shown above to use IOptionsMonitor<ButtonSettings> and remove its static configuration properties.
  2. Refactor View: Modify the base View to work with ThemeManager for applying themes.
  3. Systematic Refactoring: Gradually refactor other components (Window, Dialog, etc.) to follow this new pattern. Each component will get its own *Settings POCO.

Phase 3: Validation

  1. Write DI-Powered Tests: Create a test suite using a TerminalTestHost (similar to WebAppFactory) that builds a test host and allows for mocking services and configuration.
  2. Prove Automatic Updates: Write a test that programmatically changes a configuration source and asserts that a Button instance redraws with the new style.
  3. Prove Default Style Access: Write a test that asserts Application.Styles.Get<ButtonSettings>() returns the correctly configured default.

This proposal represents a significant but necessary evolution. By embracing these modern .NET patterns, we will make Terminal.Gui more powerful, more testable, and more familiar to the next generation of .NET developers, securing its future as a premier framework for building terminal applications.

tig avatar Oct 28 '25 06:10 tig

This is a great improvement indeed. Isn't View class already disposable?

public partial class View : IDisposable, ISupportInitializeNotification

I think you meant:

public partial class Button : View, IDesignable

BDisp avatar Oct 28 '25 10:10 BDisp

I didn't write any of this. All AI generated so errors expected.

tig avatar Oct 28 '25 18:10 tig

Hey guys.

Sorry I've been AWOL for quite a while. Only fairly recently been able to put time into hobby/non-work-critical programming again.

This one showed up in my feed, and I'd be happy to take a look at what has come about on this one once I finish with a couple other items (non-TG). Probably later this week or some time next week.

dodexahedron avatar Nov 11 '25 16:11 dodexahedron

Hey guys.

Sorry I've been AWOL for quite a while. Only fairly recently been able to put time into hobby/non-work-critical programming again.

This one showed up in my feed, and I'd be happy to take a look at what has come about on this one once I finish with a couple other items (non-TG). Probably later this week or some time next week.

We've missed you! Hope all is well.

Tons has happened on the project since we last saw you. I think you'll appreciate most of it!

tig avatar Nov 11 '25 17:11 tig

I've noticed!

The notifications have been constantly rolling in, though I haven't had the chance to look at the hundreds of them that have stacked up. 😆

dodexahedron avatar Nov 11 '25 21:11 dodexahedron