#nullable enable using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Resources; 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 ushort _maximumIterationsPerSecond = 25; // Default value for MaximumIterationsPerSecond private List? _supportedCultures; // 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`. internal Toplevel? _cachedRunStateToplevel; // 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)); } /// /// Handles which (if any) has captured the mouse /// public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler (); 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; /// public ushort MaximumIterationsPerSecond { get => _maximumIterationsPerSecond; set => _maximumIterationsPerSecond = value; } /// public List? SupportedCultures { get { if (_supportedCultures is null) { _supportedCultures = GetSupportedCultures (); } return _supportedCultures; } } /// /// Internal helper to raise InitializedChanged static event. Used by both legacy and modern Init paths. /// internal void RaiseInitializedChanged (bool initialized) { Application.OnInitializedChanged (this, new (initialized)); } /// /// 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); _initialized = true; Application.OnInitializedChanged (this, new (true)); SubscribeDriverEvents (); SynchronizationContext.SetSynchronizationContext (new ()); _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; 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 (_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 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 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; _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 (); } /// public void ResetState (bool ignoreDisposed = false) { // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 foreach (Toplevel? t in _topLevels) { t!.Running = false; } if (_popover?.GetActivePopover () is View popover) { // This forcefully closes the popover; invoking Command.Quit would be more graceful // but since this is shutdown, doing this is ok. popover.Visible = false; } _popover?.Dispose (); _popover = null; _topLevels.Clear (); #if DEBUG_IDISPOSABLE // Don't dispose the Top. It's up to caller dispose it if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && _top is { }) { Debug.Assert (_top.WasDisposed, $"Title = {_top.Title}, Id = {_top.Id}"); // If End wasn't called _cachedRunStateToplevel may be null if (_cachedRunStateToplevel is { }) { Debug.Assert (_cachedRunStateToplevel.WasDisposed); Debug.Assert (_cachedRunStateToplevel == _top); } } #endif _top = null; _cachedRunStateToplevel = null; _mainThreadId = -1; // These static properties need to be reset Application.EndAfterFirstIteration = false; Application.ClearScreenNextIteration = false; Application.ClearForceFakeConsole (); // Driver stuff if (_driver is { }) { UnsubscribeDriverEvents (); _driver?.End (); _driver = null; } // Reset Screen to null so it will be recalculated on next access ResetScreen (); // Run State stuff - these are static events on Application class Application.ClearRunStateEvents (); // Mouse and Keyboard will be lazy-initialized in ApplicationImpl on next access _initialized = false; // Mouse // Do not clear _lastMousePosition; Popovers require it to stay set with // last mouse pos. //_lastMousePosition = null; Application.CachedViewsUnderMouse.Clear (); Application.ResetMouseState (); // Keyboard events and bindings are now managed by the Keyboard instance Application.ClearSizeChangingEvent (); _navigation = null; // Reset SupportedCultures so it's re-cached on next access _supportedCultures = null; // Reset synchronization context to allow the user to run async/await, // as the main loop has been ended, the synchronization context from // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); } /// /// Resets the Screen field to null so it will be recalculated on next access. /// internal void ResetScreen () { lock (_lockScreen) { _screen = null; } } private void SubscribeDriverEvents () { if (_driver is null) { throw new ArgumentNullException (nameof (_driver)); } _driver.SizeChanged += Driver_SizeChanged; _driver.KeyDown += Driver_KeyDown; _driver.KeyUp += Driver_KeyUp; _driver.MouseEvent += Driver_MouseEvent; } private void UnsubscribeDriverEvents () { if (_driver is null) { throw new ArgumentNullException (nameof (_driver)); } _driver.SizeChanged -= Driver_SizeChanged; _driver.KeyDown -= Driver_KeyDown; _driver.KeyUp -= Driver_KeyUp; _driver.MouseEvent -= Driver_MouseEvent; } private void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { Application.OnSizeChanging (e); } private void Driver_KeyDown (object? sender, Key e) { Application.RaiseKeyDownEvent (e); } private void Driver_KeyUp (object? sender, Key e) { Application.RaiseKeyUpEvent (e); } private void Driver_MouseEvent (object? sender, MouseEventArgs e) { Application.RaiseMouseEvent (e); } private static List GetAvailableCulturesFromEmbeddedResources () { ResourceManager rm = new (typeof (Strings)); CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); return cultures.Where ( cultureInfo => !cultureInfo.Equals (CultureInfo.InvariantCulture) && rm.GetResourceSet (cultureInfo, true, false) is { } ) .ToList (); } // BUGBUG: This does not return en-US even though it's supported by default private static List GetSupportedCultures () { CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); // Get the assembly var assembly = Assembly.GetExecutingAssembly (); //Find the location of the assembly string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory; // Find the resource file name of the assembly var resourceFilename = $"{assembly.GetName ().Name}.resources.dll"; if (cultures.Length > 1 && Directory.Exists (Path.Combine (assemblyLocation, "pt-PT"))) { // Return all culture for which satellite folder found with culture code. return cultures.Where ( cultureInfo => Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) && File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) ) .ToList (); } // It's called from a self-contained single-file and get available cultures from the embedded resources strings. return GetAvailableCulturesFromEmbeddedResources (); } }