#nullable enable using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace Terminal.Gui.App; /// /// Implementation of core methods using the modern /// main loop architecture with component factories for different platforms. /// public class ApplicationImpl : IApplication { // Private static readonly Lazy instance of Application private static Lazy _lazyInstance = new (() => new ApplicationImpl ()); /// /// Creates a new instance of the Application backend. /// public ApplicationImpl () { } internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; } private readonly IComponentFactory? _componentFactory; private readonly ITimedEvents _timedEvents = new TimedEvents (); private string? _driverName; /// public ITimedEvents? TimedEvents => _timedEvents; private IMouse? _mouse; /// /// Handles mouse event state and processing. /// public IMouse Mouse { get { if (_mouse is null) { _mouse = new MouseImpl { Application = this }; } return _mouse; } set => _mouse = value ?? throw new ArgumentNullException (nameof (value)); } private IKeyboard? _keyboard; /// /// Handles keyboard input and key bindings at the Application level /// public IKeyboard Keyboard { get { if (_keyboard is null) { _keyboard = new KeyboardImpl { Application = this }; } return _keyboard; } set => _keyboard = value ?? throw new ArgumentNullException (nameof (value)); } /// public IConsoleDriver? Driver { get => Application.Driver; set => Application.Driver = value; } /// public bool Initialized { get => Application.Initialized; set => Application.Initialized = value; } /// public ApplicationPopover? Popover { get => Application.Popover; set => Application.Popover = value; } /// public ApplicationNavigation? Navigation { get => Application.Navigation; set => Application.Navigation = value; } // TODO: Create an IViewHierarchy that encapsulates Top and TopLevels and LayoutAndDraw /// public Toplevel? Top { get => Application.Top; set => Application.Top = value; } /// public ConcurrentStack TopLevels => Application.TopLevels; /// public void LayoutAndDraw (bool forceRedraw = false) { List tops = [.. TopLevels]; if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { visiblePopover.SetNeedsDraw (); visiblePopover.SetNeedsLayout (); tops.Insert (0, visiblePopover); } // BUGBUG: Application.Screen needs to be moved to IApplication bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size); // 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; } if (forceRedraw) { Driver?.ClearContents (); } View.SetClipToScreen (); View.Draw (tops, neededLayout || forceRedraw); View.SetClipToScreen (); Driver?.Refresh (); } /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public void Init (IConsoleDriver? driver = null, string? driverName = null) { if (Application.Initialized) { Logging.Logger.LogError ("Init called multiple times without shutdown, aborting."); throw new InvalidOperationException ("Init called multiple times without Shutdown"); } if (!string.IsNullOrWhiteSpace (driverName)) { _driverName = driverName; } if (string.IsNullOrWhiteSpace (_driverName)) { _driverName = Application.ForceDriver; } Debug.Assert (Application.Navigation is null); Application.Navigation = new (); Debug.Assert (Application.Popover is null); Application.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; Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl; Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab; Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift; Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6; Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift; // Reset keyboard to ensure fresh state with default bindings _keyboard = new KeyboardImpl { Application = this }; // Restore previously set keys if they existed and were different from defaults if (hasExistingKeyboard) { _keyboard.QuitKey = existingQuitKey; _keyboard.ArrangeKey = existingArrangeKey; _keyboard.NextTabKey = existingNextTabKey; _keyboard.PrevTabKey = existingPrevTabKey; _keyboard.NextTabGroupKey = existingNextTabGroupKey; _keyboard.PrevTabGroupKey = existingPrevTabGroupKey; } CreateDriver (driverName ?? _driverName); Application.Initialized = true; Application.OnInitializedChanged (this, new (true)); Application.SubscribeDriverEvents (); SynchronizationContext.SetSynchronizationContext (new ()); Application.MainThreadId = Thread.CurrentThread.ManagedThreadId; } /// /// Runs the application by creating a object and calling /// . /// /// The created object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public Toplevel Run (Func? errorHandler = null, IConsoleDriver? driver = null) { return Run (errorHandler, driver); } /// /// Runs the application by creating a -derived object of type T and calling /// . /// /// /// /// The to use. If not specified the default driver for the platform will /// be used. Must be if has already been called. /// /// The created T object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public T Run (Func? errorHandler = null, IConsoleDriver? driver = null) where T : Toplevel, new() { if (!Application.Initialized) { // Init() has NOT been called. Auto-initialize as per interface contract. Init (driver); } T top = new (); Run (top, errorHandler); return top; } /// Runs the Application using the provided view. /// The to run as a modal. /// Handler for any unhandled exceptions. public void Run (Toplevel view, Func? errorHandler = null) { Logging.Information ($"Run '{view}'"); ArgumentNullException.ThrowIfNull (view); if (!Application.Initialized) { throw new NotInitializedException (nameof (Run)); } if (Application.Driver == null) { throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); } Application.Top = view; RunState rs = Application.Begin (view); Application.Top.Running = true; while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running) { if (Coordinator is null) { throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run"); } Coordinator.RunIteration (); } Logging.Information ("Run - Calling End"); Application.End (rs); } /// Shutdown an application initialized with . public void Shutdown () { Coordinator?.Stop (); bool wasInitialized = Application.Initialized; Application.ResetState (); ConfigurationManager.PrintJsonErrors (); if (wasInitialized) { bool init = Application.Initialized; Application.OnInitializedChanged (this, new (in init)); } Application.Driver = null; _keyboard = null; _lazyInstance = new (() => new ApplicationImpl ()); } /// public void RequestStop (Toplevel? top) { Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'"); top ??= Application.Top; if (top == null) { return; } ToplevelClosingEventArgs ev = new (top); top.OnClosing (ev); if (ev.Cancel) { return; } top.Running = false; } /// public void RequestStop () => Application.RequestStop (); /// public void Invoke (Action action) { // If we are already on the main UI thread if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId) { action (); return; } _timedEvents.Add ( TimeSpan.Zero, () => { action (); return false; } ); } /// public bool IsLegacy => false; /// public object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } /// public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } /// /// 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 . /// /// public static void ChangeInstance (IApplication newApplication) { _lazyInstance = new (newApplication); } /// /// Gets the currently configured backend implementation of gateway methods. /// Change to your own implementation by using (before init). /// public static IApplication Instance => _lazyInstance.Value; internal IMainLoopCoordinator? Coordinator { get; private set; } 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 (Application.Driver == null) { throw new ("Application.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; bool factoryIsDotNet = _componentFactory is IComponentFactory; bool factoryIsUnix = _componentFactory is IComponentFactory; bool factoryIsFake = _componentFactory is IComponentFactory; // 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 (Application.Driver == null) { throw new ("Application.Driver was null even after booting MainLoopCoordinator"); } } private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) { ConcurrentQueue inputBuffer = new (); ApplicationMainLoop loop = new (); IComponentFactory cf; if (_componentFactory is IComponentFactory typedFactory) { cf = typedFactory; } else { cf = fallbackFactory (); } return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); } }