#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; 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; } // 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 readonly IComponentFactory? _componentFactory; private readonly ITimedEvents _timedEvents = new TimedEvents (); private readonly object _lockScreen = new (); private string? _driverName; private IConsoleDriver? _driver; private Rectangle? _screen; private List? _supportedCultures; private IMouse? _mouse; private IKeyboard? _keyboard; /// public ITimedEvents? TimedEvents => _timedEvents; /// /// 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 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; set; } /// public bool Force16Colors { get; set; } /// public string ForceDriver { get; set; } = string.Empty; /// public List Sixel { get; } = []; /// 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; set; } /// public ApplicationPopover? Popover { get; set; } /// public ApplicationNavigation? Navigation { get; set; } /// public Toplevel? Top { get; set; } /// public ConcurrentStack TopLevels { get; } = new (); /// public ushort MaximumIterationsPerSecond { get; set; } = 25; /// public List? SupportedCultures { get { if (_supportedCultures is null) { _supportedCultures = GetSupportedCultures (); } return _supportedCultures; } } /// public void RequestStop () { RequestStop (null); } /// [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 { }; 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; } /// /// 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); } 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; // 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); } /// /// 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; } /// /// Gets or sets the main thread ID for the application. /// internal int MainThreadId { get; set; } = -1; /// /// Resets the Screen field to null so it will be recalculated on next access. /// internal void ResetScreen () { lock (_lockScreen) { _screen = null; } } 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); } 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 void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { Application.OnSizeChanging (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 (); } 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; } }