Proposal (post v2) - Replace CM with MEC
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
ConfigurationManagerandSourcesManager. - 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:
Microsoft.Extensions.Configuration: For loading and aggregating configuration from any source (JSON, environment variables, etc.).ThemeManager: A new service that applies theme and color scheme logic.StyleManager: A new service that provides access to the default, configured settings for component types (e.g., the defaultShadowStylefor allButtons).
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
- Define Settings POCOs: Create classes like
ButtonSettings,WindowSettings, etc. - Create
ThemeManager: This service will takeIOptionsMonitor<RootConfiguration>and be responsible for resolvingColorSchemes based on theme and scope. - Create
StyleManager: This service will takeIServiceProviderand expose theGet<T>()method to retrieve configured default settings for component types. - Create
HostingExtensions:- Implement
ConfigureTerminalGui()to set up theM.E.C.providers, replacingSourcesManager. - This extension will register
ThemeManagerandStyleManageras singleton services.
- Implement
- Implement
AppInstance: Create theAppInstanceclass, which holds theIServiceProviderfor its scope. - Create Static Facade: Implement the
Application.Stylesstatic property that resolves theStyleManagerfromApplication.CurrentInstance.Services.
Phase 2: Refactor Components
- Refactor
Button: ModifyButtonas shown above to useIOptionsMonitor<ButtonSettings>and remove its static configuration properties. - Refactor
View: Modify the baseViewto work withThemeManagerfor applying themes. - Systematic Refactoring: Gradually refactor other components (
Window,Dialog, etc.) to follow this new pattern. Each component will get its own*SettingsPOCO.
Phase 3: Validation
- Write DI-Powered Tests: Create a test suite using a
TerminalTestHost(similar toWebAppFactory) that builds a test host and allows for mocking services and configuration. - Prove Automatic Updates: Write a test that programmatically changes a configuration source and asserts that a
Buttoninstance redraws with the new style. - 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.
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
I didn't write any of this. All AI generated so errors expected.
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.
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!
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. 😆