ApplicationImpl.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using Microsoft.Extensions.Logging;
  6. using Terminal.Gui.Drivers;
  7. namespace Terminal.Gui.App;
  8. /// <summary>
  9. /// Implementation of core <see cref="Application"/> methods using the modern
  10. /// main loop architecture with component factories for different platforms.
  11. /// </summary>
  12. public class ApplicationImpl : IApplication
  13. {
  14. private readonly IComponentFactory? _componentFactory;
  15. private IMainLoopCoordinator? _coordinator;
  16. private string? _driverName;
  17. private readonly ITimedEvents _timedEvents = new TimedEvents ();
  18. // Private static readonly Lazy instance of Application
  19. private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
  20. /// <summary>
  21. /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
  22. /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
  23. /// </summary>
  24. public static IApplication Instance => _lazyInstance.Value;
  25. /// <inheritdoc/>
  26. public ITimedEvents? TimedEvents => _timedEvents;
  27. internal IMainLoopCoordinator? Coordinator => _coordinator;
  28. /// <summary>
  29. /// Handles which <see cref="View"/> (if any) has captured the mouse
  30. /// </summary>
  31. public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
  32. private IKeyboard? _keyboard;
  33. /// <summary>
  34. /// Handles keyboard input and key bindings at the Application level
  35. /// </summary>
  36. public IKeyboard Keyboard
  37. {
  38. get
  39. {
  40. if (_keyboard is null)
  41. {
  42. _keyboard = new Keyboard { Application = this };
  43. }
  44. return _keyboard;
  45. }
  46. set => _keyboard = value ?? throw new ArgumentNullException (nameof (value));
  47. }
  48. /// <inheritdoc/>
  49. public IConsoleDriver? Driver
  50. {
  51. get => Application.Driver;
  52. set => Application.Driver = value;
  53. }
  54. /// <inheritdoc/>
  55. public bool Initialized
  56. {
  57. get => Application.Initialized;
  58. set => Application.Initialized = value;
  59. }
  60. /// <inheritdoc/>
  61. public ApplicationPopover? Popover
  62. {
  63. get => Application.Popover;
  64. set => Application.Popover = value;
  65. }
  66. /// <inheritdoc/>
  67. public ApplicationNavigation? Navigation
  68. {
  69. get => Application.Navigation;
  70. set => Application.Navigation = value;
  71. }
  72. /// <inheritdoc/>
  73. public Toplevel? Top
  74. {
  75. get => Application.Top;
  76. set => Application.Top = value;
  77. }
  78. /// <inheritdoc/>
  79. public ConcurrentStack<Toplevel> TopLevels => Application.TopLevels;
  80. /// <inheritdoc/>
  81. public void RequestStop () => Application.RequestStop ();
  82. /// <summary>
  83. /// Creates a new instance of the Application backend.
  84. /// </summary>
  85. public ApplicationImpl ()
  86. {
  87. }
  88. internal ApplicationImpl (IComponentFactory componentFactory)
  89. {
  90. _componentFactory = componentFactory;
  91. }
  92. /// <summary>
  93. /// Change the singleton implementation, should not be called except before application
  94. /// startup. This method lets you provide alternative implementations of core static gateway
  95. /// methods of <see cref="Application"/>.
  96. /// </summary>
  97. /// <param name="newApplication"></param>
  98. public static void ChangeInstance (IApplication newApplication)
  99. {
  100. _lazyInstance = new Lazy<IApplication> (newApplication);
  101. }
  102. /// <inheritdoc/>
  103. [RequiresUnreferencedCode ("AOT")]
  104. [RequiresDynamicCode ("AOT")]
  105. public void Init (IConsoleDriver? driver = null, string? driverName = null)
  106. {
  107. if (Application.Initialized)
  108. {
  109. Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
  110. throw new InvalidOperationException ("Init called multiple times without Shutdown");
  111. }
  112. if (!string.IsNullOrWhiteSpace (driverName))
  113. {
  114. _driverName = driverName;
  115. }
  116. if (string.IsNullOrWhiteSpace (_driverName))
  117. {
  118. _driverName = Application.ForceDriver;
  119. }
  120. Debug.Assert(Application.Navigation is null);
  121. Application.Navigation = new ();
  122. Debug.Assert (Application.Popover is null);
  123. Application.Popover = new ();
  124. // Preserve existing keyboard settings if they exist
  125. bool hasExistingKeyboard = _keyboard is not null;
  126. Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
  127. Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
  128. Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
  129. Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
  130. Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
  131. Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
  132. // Reset keyboard to ensure fresh state with default bindings
  133. _keyboard = new Keyboard { Application = this };
  134. // Restore previously set keys if they existed and were different from defaults
  135. if (hasExistingKeyboard)
  136. {
  137. _keyboard.QuitKey = existingQuitKey;
  138. _keyboard.ArrangeKey = existingArrangeKey;
  139. _keyboard.NextTabKey = existingNextTabKey;
  140. _keyboard.PrevTabKey = existingPrevTabKey;
  141. _keyboard.NextTabGroupKey = existingNextTabGroupKey;
  142. _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
  143. }
  144. CreateDriver (driverName ?? _driverName);
  145. Application.Initialized = true;
  146. Application.OnInitializedChanged (this, new (true));
  147. Application.SubscribeDriverEvents ();
  148. SynchronizationContext.SetSynchronizationContext (new ());
  149. Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
  150. }
  151. private void CreateDriver (string? driverName)
  152. {
  153. // When running unit tests, always use FakeDriver unless explicitly specified
  154. if (ConsoleDriver.RunningUnitTests &&
  155. string.IsNullOrEmpty (driverName) &&
  156. _componentFactory is null)
  157. {
  158. Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
  159. _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
  160. _coordinator.StartAsync ().Wait ();
  161. if (Application.Driver == null)
  162. {
  163. throw new ("Application.Driver was null even after booting MainLoopCoordinator");
  164. }
  165. return;
  166. }
  167. PlatformID p = Environment.OSVersion.Platform;
  168. // Check component factory type first - this takes precedence over driverName
  169. bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
  170. bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
  171. bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
  172. bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
  173. // Then check driverName
  174. bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
  175. bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
  176. bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
  177. bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
  178. // Decide which driver to use - component factory type takes priority
  179. if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
  180. {
  181. _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
  182. }
  183. else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
  184. {
  185. _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
  186. }
  187. else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
  188. {
  189. _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
  190. }
  191. else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
  192. {
  193. _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
  194. }
  195. else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
  196. {
  197. _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
  198. }
  199. else
  200. {
  201. _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
  202. }
  203. _coordinator.StartAsync ().Wait ();
  204. if (Application.Driver == null)
  205. {
  206. throw new ("Application.Driver was null even after booting MainLoopCoordinator");
  207. }
  208. }
  209. private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
  210. {
  211. ConcurrentQueue<T> inputBuffer = new ();
  212. ApplicationMainLoop<T> loop = new ();
  213. IComponentFactory<T> cf;
  214. if (_componentFactory is IComponentFactory<T> typedFactory)
  215. {
  216. cf = typedFactory;
  217. }
  218. else
  219. {
  220. cf = fallbackFactory ();
  221. }
  222. return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
  223. }
  224. /// <summary>
  225. /// Runs the application by creating a <see cref="Toplevel"/> object and calling
  226. /// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
  227. /// </summary>
  228. /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
  229. [RequiresUnreferencedCode ("AOT")]
  230. [RequiresDynamicCode ("AOT")]
  231. public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
  232. /// <summary>
  233. /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
  234. /// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
  235. /// </summary>
  236. /// <param name="errorHandler"></param>
  237. /// <param name="driver">
  238. /// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
  239. /// be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
  240. /// </param>
  241. /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
  242. [RequiresUnreferencedCode ("AOT")]
  243. [RequiresDynamicCode ("AOT")]
  244. public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
  245. where T : Toplevel, new()
  246. {
  247. if (!Application.Initialized)
  248. {
  249. // Init() has NOT been called. Auto-initialize as per interface contract.
  250. Init (driver, null);
  251. }
  252. T top = new ();
  253. Run (top, errorHandler);
  254. return top;
  255. }
  256. /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
  257. /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
  258. /// <param name="errorHandler">Handler for any unhandled exceptions.</param>
  259. public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
  260. {
  261. Logging.Information ($"Run '{view}'");
  262. ArgumentNullException.ThrowIfNull (view);
  263. if (!Application.Initialized)
  264. {
  265. throw new NotInitializedException (nameof (Run));
  266. }
  267. if (Application.Driver == null)
  268. {
  269. throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
  270. }
  271. Application.Top = view;
  272. RunState rs = Application.Begin (view);
  273. Application.Top.Running = true;
  274. while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
  275. {
  276. if (_coordinator is null)
  277. {
  278. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  279. }
  280. _coordinator.RunIteration ();
  281. }
  282. Logging.Information ($"Run - Calling End");
  283. Application.End (rs);
  284. }
  285. /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
  286. public void Shutdown ()
  287. {
  288. _coordinator?.Stop ();
  289. bool wasInitialized = Application.Initialized;
  290. Application.ResetState ();
  291. ConfigurationManager.PrintJsonErrors ();
  292. if (wasInitialized)
  293. {
  294. bool init = Application.Initialized;
  295. Application.OnInitializedChanged (this, new (in init));
  296. }
  297. Application.Driver = null;
  298. _keyboard = null;
  299. _lazyInstance = new (() => new ApplicationImpl ());
  300. }
  301. /// <inheritdoc />
  302. public void RequestStop (Toplevel? top)
  303. {
  304. Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
  305. top ??= Application.Top;
  306. if (top == null)
  307. {
  308. return;
  309. }
  310. ToplevelClosingEventArgs ev = new (top);
  311. top.OnClosing (ev);
  312. if (ev.Cancel)
  313. {
  314. return;
  315. }
  316. top.Running = false;
  317. }
  318. /// <inheritdoc />
  319. public void Invoke (Action action)
  320. {
  321. // If we are already on the main UI thread
  322. if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
  323. {
  324. action ();
  325. return;
  326. }
  327. _timedEvents.Add (TimeSpan.Zero,
  328. () =>
  329. {
  330. action ();
  331. return false;
  332. }
  333. );
  334. }
  335. /// <inheritdoc />
  336. public bool IsLegacy => false;
  337. /// <inheritdoc />
  338. public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
  339. /// <inheritdoc />
  340. public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
  341. /// <inheritdoc />
  342. public void LayoutAndDraw (bool forceDraw)
  343. {
  344. Application.Top?.SetNeedsDraw();
  345. Application.Top?.SetNeedsLayout ();
  346. }
  347. }