#nullable enable using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Terminal.Gui.Drivers; 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 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 _topLevels = new (); private int _mainThreadId = -1; private bool _force16Colors; private string _forceDriver = string.Empty; private readonly List _sixel = new (); private readonly object _lockScreen = new (); private Rectangle? _screen; private bool _clearScreenNextIteration; // Private static readonly Lazy instance of Application private static Lazy _lazyInstance = new (() => new ApplicationImpl ()); /// /// Gets the currently configured backend implementation of gateway methods. /// Change to your own implementation by using (before init). /// public static IApplication Instance => _lazyInstance.Value; /// public ITimedEvents? TimedEvents => _timedEvents; internal IMainLoopCoordinator? Coordinator => _coordinator; 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 => _driver; set => _driver = value; } /// public bool Initialized { get => _initialized; set => _initialized = value; } /// public bool Force16Colors { get => _force16Colors; set => _force16Colors = value; } /// public string ForceDriver { get => _forceDriver; set => _forceDriver = value; } /// public List Sixel => _sixel; /// public Rectangle Screen { get { lock (_lockScreen) { if (_screen == null) { _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048)); } return _screen.Value; } } set { if (value is { } && (value.X != 0 || value.Y != 0)) { throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported"); } lock (_lockScreen) { _screen = value; } } } /// public bool ClearScreenNextIteration { get => _clearScreenNextIteration; set => _clearScreenNextIteration = value; } /// public ApplicationPopover? Popover { get => _popover; set => _popover = value; } /// public ApplicationNavigation? Navigation { get => _navigation; set => _navigation = value; } /// public Toplevel? Top { get => _top; set => _top = value; } /// public ConcurrentStack TopLevels => _topLevels; // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. // This variable is set in `End` in this case so that `Begin` correctly sets `Top`. /// public Toplevel? CachedRunStateToplevel { get; set; } /// /// Gets or sets the main thread ID for the application. /// internal int MainThreadId { get => _mainThreadId; set => _mainThreadId = value; } /// public void RequestStop () => RequestStop (null); /// /// Creates a new instance of the Application backend. /// public ApplicationImpl () { } internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; } /// /// 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 Lazy (newApplication); } /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public void Init (IConsoleDriver? driver = null, string? driverName = null) { if (_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 = ForceDriver; } Debug.Assert (_navigation is null); _navigation = new (); Debug.Assert (_popover is null); _popover = new (); // 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); Screen = Driver!.Screen; _initialized = true; Application.OnInitializedChanged (this, new (true)); Application.SubscribeDriverEvents (); SynchronizationContext.SetSynchronizationContext (new ()); _mainThreadId = Thread.CurrentThread.ManagedThreadId; } private void CreateDriver (string? driverName) { 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)) { FakeConsoleOutput fakeOutput = new (); fakeOutput.SetConsoleSize (80, 25); _coordinator = CreateSubcomponents (() => new FakeComponentFactory (null, fakeOutput)); } 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 (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); } /// /// 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 (!_initialized) { // Init() has NOT been called. Auto-initialize as per interface contract. Init (driver, null); } 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 (!_initialized) { throw new NotInitializedException (nameof (Run)); } if (_driver == null) { throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); } _top = view; RunState rs = Application.Begin (view); _top.Running = true; while (_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 = _initialized; // Reset Screen before calling Application.ResetState to avoid circular reference ResetScreen (); // 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; CachedRunStateToplevel = null; _top = null; _topLevels.Clear (); _mainThreadId = -1; _screen = null; _clearScreenNextIteration = false; _sixel.Clear (); // Don't reset ForceDriver and Force16Colors; they need to be set before Init is called if (wasInitialized) { bool init = _initialized; // Will be false after clearing fields above Application.OnInitializedChanged (this, new (in init)); } _lazyInstance = new (() => new ApplicationImpl ()); } /// public void RequestStop (Toplevel? top) { Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'"); top ??= _top; if (top == null) { return; } ToplevelClosingEventArgs ev = new (top); top.OnClosing (ev); if (ev.Cancel) { return; } top.Running = false; } /// public void Invoke (Action action) { // If we are already on the main UI thread if (Top is { Running: true } && _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); } /// 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); } bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size); if (ClearScreenNextIteration) { forceRedraw = true; ClearScreenNextIteration = false; } if (forceRedraw) { _driver?.ClearContents (); } View.SetClipToScreen (); View.Draw (tops, neededLayout || forceRedraw); View.SetClipToScreen (); _driver?.Refresh (); } /// /// Resets the Screen field to null so it will be recalculated on next access. /// internal void ResetScreen () { lock (_lockScreen) { _screen = null; } } }