Fixes #4125 - Decouple ApplicationImpl from static Application references (#4324)

* Initial plan

* Add instance fields to ApplicationImpl and update static Application properties to delegate to ApplicationImpl.Instance

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix code review issues: use instance fields in Run() and improve Shutdown() logic

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Final code review fix: use _initialized directly in Shutdown

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix Shutdown order: call ResetState before clearing instance fields

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add MainThreadId as instance field in ApplicationImpl

Co-authored-by: tig <585482+tig@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-25 12:20:03 -06:00
committed by GitHub
parent 2d84ab4f01
commit 5199663551
7 changed files with 374 additions and 288 deletions

View File

@@ -8,7 +8,11 @@ public static partial class Application // Driver abstractions
// TODO: Add to IApplication
/// <summary>Gets the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
public static IConsoleDriver? Driver { get; internal set; }
public static IConsoleDriver? Driver
{
get => ApplicationImpl.Instance.Driver;
internal set => ApplicationImpl.Instance.Driver = value;
}
// TODO: Add to IApplication
// BUGBUG: Force16Colors should be nullable.

View File

@@ -5,42 +5,10 @@ using System.Reflection;
namespace Terminal.Gui.App;
public static partial class Application // Lifecycle (Init/Shutdown)
public static partial class Application // Initialization (Init/Shutdown)
{
// TODO: Add to IApplication
/// <summary>Gets of list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
/// <returns></returns>
[RequiresUnreferencedCode ("AOT")]
public static (List<Type?>, List<string?>) GetDriverTypes ()
{
// use reflection to get the list of drivers
List<Type?> driverTypes = new ();
// Only inspect the IConsoleDriver assembly
Assembly asm = typeof (IConsoleDriver).Assembly;
foreach (Type? type in asm.GetTypes ())
{
if (typeof (IConsoleDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true })
{
driverTypes.Add (type);
}
}
List<string?> driverTypeNames = driverTypes
.Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
.Select (d => d!.Name)
.Union (["dotnet", "windows", "unix", "fake"])
.ToList ()!;
return (driverTypes, driverTypeNames);
}
// TODO: Add to IApplicationLifecycle
/// <summary>
/// Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the
/// application is closing.
/// </summary>
/// <summary>Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the application is closing.</summary>
/// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
/// <para>
/// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
@@ -76,60 +44,31 @@ public static partial class Application // Lifecycle (Init/Shutdown)
// that isn't supported by the modern application architecture
if (driver is null)
{
string driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
var driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
if (!string.IsNullOrEmpty (driverNameToCheck))
{
(List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (driverNameToCheck, StringComparison.InvariantCultureIgnoreCase));
// If it's a legacy IConsoleDriver (not a Facade), use InternalInit which supports legacy drivers
if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
{
InternalInit (driver, driverName);
return;
}
}
}
// Otherwise delegate to the ApplicationImpl instance (which uses the modern architecture)
ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver);
}
// TODO: Add to IApplicationLifecycle
/// <summary>
/// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with
/// <see cref="Shutdown"/>.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/>
/// methods have been called.
/// </para>
/// </remarks>
public static bool Initialized { get; internal set; }
internal static int MainThreadId
{
get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId;
set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value;
}
// TODO: Add to IApplicationLifecycle
/// <summary>
/// This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
/// </summary>
/// <remarks>
/// Intended to support unit tests that need to know when the application has been initialized.
/// </remarks>
public static event EventHandler<EventArgs<bool>>? InitializedChanged;
// TODO: Add to IApplicationLifecycle
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
/// <remarks>
/// Shutdown must be called for every call to <see cref="Init"/> or
/// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
/// up (Disposed)
/// and terminal settings are restored.
/// </remarks>
public static void Shutdown () { ApplicationImpl.Instance.Shutdown (); }
// TODO: Add to IApplicationLifecycle
// INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
//
// Called from:
@@ -160,7 +99,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
if (!calledViaRunT)
{
// Reset all class variables (Application is a singleton).
ResetState (true);
ResetState (ignoreDisposed: true);
}
// For UnitTests
@@ -185,7 +124,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
//{
// (List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
// Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase));
// if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
// {
// // This is a legacy driver (not a ConsoleDriverFacade)
@@ -193,13 +132,12 @@ public static partial class Application // Lifecycle (Init/Shutdown)
// useLegacyDriver = true;
// }
//}
//// Use the modern application architecture
//if (!useLegacyDriver)
{
ApplicationImpl.Instance.Init (driver, driverName);
Debug.Assert (Driver is { });
return;
}
}
@@ -240,14 +178,6 @@ public static partial class Application // Lifecycle (Init/Shutdown)
InitializedChanged?.Invoke (null, new (init));
}
internal static int MainThreadId { get; set; } = -1;
// TODO: Add to IApplicationLifecycle
/// <summary>
/// Raises the <see cref="InitializedChanged"/> event.
/// </summary>
internal static void OnInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
internal static void SubscribeDriverEvents ()
{
ArgumentNullException.ThrowIfNull (Driver);
@@ -268,9 +198,78 @@ public static partial class Application // Lifecycle (Init/Shutdown)
Driver.MouseEvent -= Driver_MouseEvent;
}
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
/// <summary>Gets of list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
/// <returns></returns>
[RequiresUnreferencedCode ("AOT")]
public static (List<Type?>, List<string?>) GetDriverTypes ()
{
// use reflection to get the list of drivers
List<Type?> driverTypes = new ();
// Only inspect the IConsoleDriver assembly
var asm = typeof (IConsoleDriver).Assembly;
foreach (Type? type in asm.GetTypes ())
{
if (typeof (IConsoleDriver).IsAssignableFrom (type) &&
type is { IsAbstract: false, IsClass: true })
{
driverTypes.Add (type);
}
}
List<string?> driverTypeNames = driverTypes
.Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
.Select (d => d!.Name)
.Union (["dotnet", "windows", "unix", "fake"])
.ToList ()!;
return (driverTypes, driverTypeNames);
}
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
/// <remarks>
/// Shutdown must be called for every call to <see cref="Init"/> or
/// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
/// up (Disposed)
/// and terminal settings are restored.
/// </remarks>
public static void Shutdown () => ApplicationImpl.Instance.Shutdown ();
/// <summary>
/// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
/// </para>
/// </remarks>
public static bool Initialized
{
get => ApplicationImpl.Instance.Initialized;
internal set => ApplicationImpl.Instance.Initialized = value;
}
/// <summary>
/// This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
/// </summary>
/// <remarks>
/// Intended to support unit tests that need to know when the application has been initialized.
/// </remarks>
public static event EventHandler<EventArgs<bool>>? InitializedChanged;
/// <summary>
/// Raises the <see cref="InitializedChanged"/> event.
/// </summary>
internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
{
Application.InitializedChanged?.Invoke (sender, e);
}
}

View File

@@ -7,7 +7,11 @@ public static partial class Application // Navigation stuff
/// <summary>
/// Gets the <see cref="ApplicationNavigation"/> instance for the current <see cref="Application"/>.
/// </summary>
public static ApplicationNavigation? Navigation { get; internal set; }
public static ApplicationNavigation? Navigation
{
get => ApplicationImpl.Instance.Navigation;
internal set => ApplicationImpl.Instance.Navigation = value;
}
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[ConfigurationProperty (Scope = typeof (SettingsScope))]

View File

@@ -5,5 +5,9 @@ namespace Terminal.Gui.App;
public static partial class Application // Popover handling
{
/// <summary>Gets the Application <see cref="Popover"/> manager.</summary>
public static ApplicationPopover? Popover { get; internal set; }
public static ApplicationPopover? Popover
{
get => ApplicationImpl.Instance.Popover;
internal set => ApplicationImpl.Instance.Popover = value;
}
}

View File

@@ -7,22 +7,14 @@ public static partial class Application // Toplevel handling
{
// BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What
private static readonly ConcurrentStack<Toplevel> _topLevels = new ();
private static readonly object _topLevelsLock = new ();
/// <summary>Holds the stack of TopLevel views.</summary>
internal static ConcurrentStack<Toplevel> TopLevels
{
get
{
lock (_topLevelsLock)
{
return _topLevels;
}
}
}
internal static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
/// <summary>The <see cref="Toplevel"/> that is currently active.</summary>
/// <value>The top.</value>
public static Toplevel? Top { get; internal set; }
public static Toplevel? Top
{
get => ApplicationImpl.Instance.Top;
internal set => ApplicationImpl.Instance.Top = value;
}
}

View File

@@ -3,35 +3,42 @@ using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Terminal.Gui.Drivers;
namespace Terminal.Gui.App;
/// <summary>
/// Implementation of core <see cref="Application"/> methods using the modern
/// main loop architecture with component factories for different platforms.
/// Implementation of core <see cref="Application"/> methods using the modern
/// main loop architecture with component factories for different platforms.
/// </summary>
public class ApplicationImpl : IApplication
{
private readonly IComponentFactory? _componentFactory;
private IMainLoopCoordinator? _coordinator;
private string? _driverName;
private readonly ITimedEvents _timedEvents = new TimedEvents ();
private IConsoleDriver? _driver;
private bool _initialized;
private ApplicationPopover? _popover;
private ApplicationNavigation? _navigation;
private Toplevel? _top;
private readonly ConcurrentStack<Toplevel> _topLevels = new ();
private int _mainThreadId = -1;
// Private static readonly Lazy instance of Application
private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
/// <summary>
/// Creates a new instance of the Application backend.
/// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
/// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
/// </summary>
public ApplicationImpl () { }
internal ApplicationImpl (IComponentFactory componentFactory)
{
_componentFactory = componentFactory;
}
private readonly IComponentFactory? _componentFactory;
private readonly ITimedEvents _timedEvents = new TimedEvents ();
private string? _driverName;
public static IApplication Instance => _lazyInstance.Value;
/// <inheritdoc/>
public ITimedEvents? TimedEvents => _timedEvents;
internal IMainLoopCoordinator? Coordinator => _coordinator;
private IMouse? _mouse;
/// <summary>
@@ -50,6 +57,11 @@ public class ApplicationImpl : IApplication
set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
}
/// <summary>
/// Handles which <see cref="View"/> (if any) has captured the mouse
/// </summary>
public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
private IKeyboard? _keyboard;
/// <summary>
@@ -71,74 +83,74 @@ public class ApplicationImpl : IApplication
/// <inheritdoc/>
public IConsoleDriver? Driver
{
get => Application.Driver;
set => Application.Driver = value;
get => _driver;
set => _driver = value;
}
/// <inheritdoc/>
public bool Initialized
{
get => Application.Initialized;
set => Application.Initialized = value;
get => _initialized;
set => _initialized = value;
}
/// <inheritdoc/>
public ApplicationPopover? Popover
{
get => Application.Popover;
set => Application.Popover = value;
get => _popover;
set => _popover = value;
}
/// <inheritdoc/>
public ApplicationNavigation? Navigation
{
get => Application.Navigation;
set => Application.Navigation = value;
get => _navigation;
set => _navigation = value;
}
// TODO: Create an IViewHierarchy that encapsulates Top and TopLevels and LayoutAndDraw
/// <inheritdoc/>
public Toplevel? Top
{
get => Application.Top;
set => Application.Top = value;
get => _top;
set => _top = value;
}
/// <inheritdoc/>
public ConcurrentStack<Toplevel> TopLevels => Application.TopLevels;
public ConcurrentStack<Toplevel> TopLevels => _topLevels;
/// <inheritdoc />
public void LayoutAndDraw (bool forceRedraw = false)
/// <summary>
/// Gets or sets the main thread ID for the application.
/// </summary>
internal int MainThreadId
{
List<View> tops = [.. TopLevels];
get => _mainThreadId;
set => _mainThreadId = value;
}
if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
{
visiblePopover.SetNeedsDraw ();
visiblePopover.SetNeedsLayout ();
tops.Insert (0, visiblePopover);
}
/// <inheritdoc/>
public void RequestStop () => RequestStop (null);
// BUGBUG: Application.Screen needs to be moved to IApplication
bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size);
/// <summary>
/// Creates a new instance of the Application backend.
/// </summary>
public ApplicationImpl ()
{
}
// BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication
if (Application.ClearScreenNextIteration)
{
forceRedraw = true;
// BUGBUG: Application.Screen needs to be moved to IApplication
Application.ClearScreenNextIteration = false;
}
internal ApplicationImpl (IComponentFactory componentFactory)
{
_componentFactory = componentFactory;
}
if (forceRedraw)
{
Driver?.ClearContents ();
}
View.SetClipToScreen ();
View.Draw (tops, neededLayout || forceRedraw);
View.SetClipToScreen ();
Driver?.Refresh ();
/// <summary>
/// Change the singleton implementation, should not be called except before application
/// startup. This method lets you provide alternative implementations of core static gateway
/// methods of <see cref="Application"/>.
/// </summary>
/// <param name="newApplication"></param>
public static void ChangeInstance (IApplication newApplication)
{
_lazyInstance = new Lazy<IApplication> (newApplication);
}
/// <inheritdoc/>
@@ -146,7 +158,7 @@ public class ApplicationImpl : IApplication
[RequiresDynamicCode ("AOT")]
public void Init (IConsoleDriver? driver = null, string? driverName = null)
{
if (Application.Initialized)
if (_initialized)
{
Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
@@ -163,13 +175,12 @@ public class ApplicationImpl : IApplication
_driverName = Application.ForceDriver;
}
Debug.Assert (Application.Navigation is null);
Application.Navigation = new ();
Debug.Assert(_navigation is null);
_navigation = new ();
Debug.Assert (Application.Popover is null);
Application.Popover = new ();
Debug.Assert (_popover is null);
_popover = new ();
// TODO: Move this into IKeyboard and Keyboard implementation
// Preserve existing keyboard settings if they exist
bool hasExistingKeyboard = _keyboard is not null;
Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
@@ -195,13 +206,99 @@ public class ApplicationImpl : IApplication
CreateDriver (driverName ?? _driverName);
Application.Initialized = true;
_initialized = true;
Application.OnInitializedChanged (this, new (true));
Application.SubscribeDriverEvents ();
SynchronizationContext.SetSynchronizationContext (new ());
Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
}
private void CreateDriver (string? driverName)
{
// When running unit tests, always use FakeDriver unless explicitly specified
if (ConsoleDriver.RunningUnitTests &&
string.IsNullOrEmpty (driverName) &&
_componentFactory is null)
{
Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
_coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
_coordinator.StartAsync ().Wait ();
if (_driver == null)
{
throw new ("Driver was null even after booting MainLoopCoordinator");
}
return;
}
PlatformID p = Environment.OSVersion.Platform;
// Check component factory type first - this takes precedence over driverName
bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
// Then check driverName
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
// Decide which driver to use - component factory type takes priority
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
{
_coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
}
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
{
_coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
}
else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
{
_coordinator = CreateSubcomponents (() => new NetComponentFactory ());
}
else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
{
_coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
}
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
{
_coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
}
else
{
_coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
}
_coordinator.StartAsync ().Wait ();
if (_driver == null)
{
throw new ("Driver was null even after booting MainLoopCoordinator");
}
}
private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
{
ConcurrentQueue<T> inputBuffer = new ();
ApplicationMainLoop<T> loop = new ();
IComponentFactory<T> cf;
if (_componentFactory is IComponentFactory<T> typedFactory)
{
cf = typedFactory;
}
else
{
cf = fallbackFactory ();
}
return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
}
/// <summary>
@@ -228,15 +325,14 @@ public class ApplicationImpl : IApplication
public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
where T : Toplevel, new()
{
if (!Application.Initialized)
if (!_initialized)
{
// Init() has NOT been called. Auto-initialize as per interface contract.
Init (driver);
Init (driver, null);
}
T top = new ();
Run (top, errorHandler);
return top;
}
@@ -248,62 +344,74 @@ public class ApplicationImpl : IApplication
Logging.Information ($"Run '{view}'");
ArgumentNullException.ThrowIfNull (view);
if (!Application.Initialized)
if (!_initialized)
{
throw new NotInitializedException (nameof (Run));
}
if (Application.Driver == null)
if (_driver == null)
{
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
}
Application.Top = view;
_top = view;
RunState rs = Application.Begin (view);
Application.Top.Running = true;
_top.Running = true;
while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
while (_topLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
{
if (Coordinator is null)
if (_coordinator is null)
{
throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
}
Coordinator.RunIteration ();
_coordinator.RunIteration ();
}
Logging.Information ("Run - Calling End");
Logging.Information ($"Run - Calling End");
Application.End (rs);
}
/// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
public void Shutdown ()
{
Coordinator?.Stop ();
bool wasInitialized = Application.Initialized;
_coordinator?.Stop ();
bool wasInitialized = _initialized;
// Call ResetState FIRST so it can properly dispose Popover and other resources
// that are accessed via Application.* static properties that now delegate to instance fields
Application.ResetState ();
ConfigurationManager.PrintJsonErrors ();
// Clear instance fields after ResetState has disposed everything
_driver = null;
_mouse = null;
_keyboard = null;
_initialized = false;
_navigation = null;
_popover = null;
_top = null;
_topLevels.Clear ();
_mainThreadId = -1;
if (wasInitialized)
{
bool init = Application.Initialized;
bool init = _initialized; // Will be false after clearing fields above
Application.OnInitializedChanged (this, new (in init));
}
Application.Driver = null;
_keyboard = null;
_lazyInstance = new (() => new ApplicationImpl ());
}
/// <inheritdoc/>
/// <inheritdoc />
public void RequestStop (Toplevel? top)
{
Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'");
Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
top ??= Application.Top;
top ??= _top;
if (top == null)
{
@@ -321,138 +429,65 @@ public class ApplicationImpl : IApplication
top.Running = false;
}
/// <inheritdoc/>
public void RequestStop () => Application.RequestStop ();
/// <inheritdoc/>
/// <inheritdoc />
public void Invoke (Action action)
{
// If we are already on the main UI thread
if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
if (_mainThreadId == Thread.CurrentThread.ManagedThreadId)
{
action ();
return;
}
_timedEvents.Add (
TimeSpan.Zero,
() =>
{
action ();
return false;
}
);
_timedEvents.Add (TimeSpan.Zero,
() =>
{
action ();
return false;
}
);
}
/// <inheritdoc/>
/// <inheritdoc />
public bool IsLegacy => false;
/// <inheritdoc/>
/// <inheritdoc />
public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
/// <inheritdoc/>
/// <inheritdoc />
public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
/// <summary>
/// Change the singleton implementation, should not be called except before application
/// startup. This method lets you provide alternative implementations of core static gateway
/// methods of <see cref="Application"/>.
/// </summary>
/// <param name="newApplication"></param>
public static void ChangeInstance (IApplication newApplication) { _lazyInstance = new (newApplication); }
/// <summary>
/// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
/// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
/// </summary>
public static IApplication Instance => _lazyInstance.Value;
internal IMainLoopCoordinator? Coordinator { get; private set; }
private void CreateDriver (string? driverName)
/// <inheritdoc />
public void LayoutAndDraw (bool forceRedraw = false)
{
// When running unit tests, always use FakeDriver unless explicitly specified
if (ConsoleDriver.RunningUnitTests && string.IsNullOrEmpty (driverName) && _componentFactory is null)
List<View> tops = [.. _topLevels];
if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
{
Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
Coordinator.StartAsync ().Wait ();
if (Application.Driver == null)
{
throw new ("Application.Driver was null even after booting MainLoopCoordinator");
}
return;
visiblePopover.SetNeedsDraw ();
visiblePopover.SetNeedsLayout ();
tops.Insert (0, visiblePopover);
}
PlatformID p = Environment.OSVersion.Platform;
// BUGBUG: Application.Screen needs to be moved to IApplication
bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size);
// Check component factory type first - this takes precedence over driverName
bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
// Then check driverName
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
// Decide which driver to use - component factory type takes priority
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
// BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication
if (Application.ClearScreenNextIteration)
{
Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
}
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
{
Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
}
else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
{
Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
}
else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
{
Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
}
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
{
Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
}
else
{
Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
forceRedraw = true;
// BUGBUG: Application.Screen needs to be moved to IApplication
Application.ClearScreenNextIteration = false;
}
Coordinator.StartAsync ().Wait ();
if (Application.Driver == null)
if (forceRedraw)
{
throw new ("Application.Driver was null even after booting MainLoopCoordinator");
}
}
private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
{
ConcurrentQueue<T> inputBuffer = new ();
ApplicationMainLoop<T> loop = new ();
IComponentFactory<T> cf;
if (_componentFactory is IComponentFactory<T> typedFactory)
{
cf = typedFactory;
}
else
{
cf = fallbackFactory ();
_driver?.ClearContents ();
}
return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
View.SetClipToScreen ();
View.Draw (tops, neededLayout || forceRedraw);
View.SetClipToScreen ();
_driver?.Refresh ();
}
}

View File

@@ -583,4 +583,52 @@ public class ApplicationImplTests
Assert.True (result);
}
[Fact]
public void ApplicationImpl_UsesInstanceFields_NotStaticReferences()
{
// This test verifies that ApplicationImpl uses instance fields instead of static Application references
var orig = ApplicationImpl.Instance;
var v2 = NewApplicationImpl();
ApplicationImpl.ChangeInstance(v2);
// Before Init, all fields should be null/default
Assert.Null(v2.Driver);
Assert.False(v2.Initialized);
Assert.Null(v2.Popover);
Assert.Null(v2.Navigation);
Assert.Null(v2.Top);
Assert.Empty(v2.TopLevels);
// Init should populate instance fields
v2.Init();
// After Init, Driver, Navigation, and Popover should be populated
Assert.NotNull(v2.Driver);
Assert.True(v2.Initialized);
Assert.NotNull(v2.Popover);
Assert.NotNull(v2.Navigation);
Assert.Null(v2.Top); // Top is still null until Run
// Verify that static Application properties delegate to instance
Assert.Equal(v2.Driver, Application.Driver);
Assert.Equal(v2.Initialized, Application.Initialized);
Assert.Equal(v2.Popover, Application.Popover);
Assert.Equal(v2.Navigation, Application.Navigation);
Assert.Equal(v2.Top, Application.Top);
Assert.Same(v2.TopLevels, Application.TopLevels);
// Shutdown should clean up instance fields
v2.Shutdown();
Assert.Null(v2.Driver);
Assert.False(v2.Initialized);
Assert.Null(v2.Popover);
Assert.Null(v2.Navigation);
Assert.Null(v2.Top);
Assert.Empty(v2.TopLevels);
ApplicationImpl.ChangeInstance(orig);
}
}