ApplicationImpl.cs 15 KB

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