2
0

ApplicationImpl.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  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 IConsoleDriver? _driver;
  19. private bool _initialized;
  20. private ApplicationPopover? _popover;
  21. private ApplicationNavigation? _navigation;
  22. private Toplevel? _top;
  23. private readonly ConcurrentStack<Toplevel> _topLevels = new ();
  24. private int _mainThreadId = -1;
  25. private bool _force16Colors;
  26. private string _forceDriver = string.Empty;
  27. private readonly List<SixelToRender> _sixel = new ();
  28. private readonly object _lockScreen = new ();
  29. private Rectangle? _screen;
  30. private bool _clearScreenNextIteration;
  31. // Private static readonly Lazy instance of Application
  32. private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
  33. /// <summary>
  34. /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
  35. /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
  36. /// </summary>
  37. public static IApplication Instance => _lazyInstance.Value;
  38. /// <inheritdoc/>
  39. public ITimedEvents? TimedEvents => _timedEvents;
  40. internal IMainLoopCoordinator? Coordinator => _coordinator;
  41. private IMouse? _mouse;
  42. /// <summary>
  43. /// Handles mouse event state and processing.
  44. /// </summary>
  45. public IMouse Mouse
  46. {
  47. get
  48. {
  49. if (_mouse is null)
  50. {
  51. _mouse = new MouseImpl { Application = this };
  52. }
  53. return _mouse;
  54. }
  55. set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
  56. }
  57. /// <summary>
  58. /// Handles which <see cref="View"/> (if any) has captured the mouse
  59. /// </summary>
  60. public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
  61. private IKeyboard? _keyboard;
  62. /// <summary>
  63. /// Handles keyboard input and key bindings at the Application level
  64. /// </summary>
  65. public IKeyboard Keyboard
  66. {
  67. get
  68. {
  69. if (_keyboard is null)
  70. {
  71. _keyboard = new KeyboardImpl { Application = this };
  72. }
  73. return _keyboard;
  74. }
  75. set => _keyboard = value ?? throw new ArgumentNullException (nameof (value));
  76. }
  77. /// <inheritdoc/>
  78. public IConsoleDriver? Driver
  79. {
  80. get => _driver;
  81. set => _driver = value;
  82. }
  83. /// <inheritdoc/>
  84. public bool Initialized
  85. {
  86. get => _initialized;
  87. set => _initialized = value;
  88. }
  89. /// <inheritdoc/>
  90. public bool Force16Colors
  91. {
  92. get => _force16Colors;
  93. set => _force16Colors = value;
  94. }
  95. /// <inheritdoc/>
  96. public string ForceDriver
  97. {
  98. get => _forceDriver;
  99. set => _forceDriver = value;
  100. }
  101. /// <inheritdoc/>
  102. public List<SixelToRender> Sixel => _sixel;
  103. /// <inheritdoc/>
  104. public Rectangle Screen
  105. {
  106. get
  107. {
  108. lock (_lockScreen)
  109. {
  110. if (_screen == null)
  111. {
  112. _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
  113. }
  114. return _screen.Value;
  115. }
  116. }
  117. set
  118. {
  119. if (value is {} && (value.X != 0 || value.Y != 0))
  120. {
  121. throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported");
  122. }
  123. lock (_lockScreen)
  124. {
  125. _screen = value;
  126. }
  127. }
  128. }
  129. /// <inheritdoc/>
  130. public bool ClearScreenNextIteration
  131. {
  132. get => _clearScreenNextIteration;
  133. set => _clearScreenNextIteration = value;
  134. }
  135. /// <inheritdoc/>
  136. public ApplicationPopover? Popover
  137. {
  138. get => _popover;
  139. set => _popover = value;
  140. }
  141. /// <inheritdoc/>
  142. public ApplicationNavigation? Navigation
  143. {
  144. get => _navigation;
  145. set => _navigation = value;
  146. }
  147. /// <inheritdoc/>
  148. public Toplevel? Top
  149. {
  150. get => _top;
  151. set => _top = value;
  152. }
  153. /// <inheritdoc/>
  154. public ConcurrentStack<Toplevel> TopLevels => _topLevels;
  155. /// <summary>
  156. /// Gets or sets the main thread ID for the application.
  157. /// </summary>
  158. internal int MainThreadId
  159. {
  160. get => _mainThreadId;
  161. set => _mainThreadId = value;
  162. }
  163. /// <inheritdoc/>
  164. public void RequestStop () => RequestStop (null);
  165. /// <summary>
  166. /// Creates a new instance of the Application backend.
  167. /// </summary>
  168. public ApplicationImpl ()
  169. {
  170. }
  171. internal ApplicationImpl (IComponentFactory componentFactory)
  172. {
  173. _componentFactory = componentFactory;
  174. }
  175. /// <summary>
  176. /// Change the singleton implementation, should not be called except before application
  177. /// startup. This method lets you provide alternative implementations of core static gateway
  178. /// methods of <see cref="Application"/>.
  179. /// </summary>
  180. /// <param name="newApplication"></param>
  181. public static void ChangeInstance (IApplication newApplication)
  182. {
  183. _lazyInstance = new Lazy<IApplication> (newApplication);
  184. }
  185. /// <inheritdoc/>
  186. [RequiresUnreferencedCode ("AOT")]
  187. [RequiresDynamicCode ("AOT")]
  188. public void Init (IConsoleDriver? driver = null, string? driverName = null)
  189. {
  190. if (_initialized)
  191. {
  192. Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
  193. throw new InvalidOperationException ("Init called multiple times without Shutdown");
  194. }
  195. if (!string.IsNullOrWhiteSpace (driverName))
  196. {
  197. _driverName = driverName;
  198. }
  199. if (string.IsNullOrWhiteSpace (_driverName))
  200. {
  201. _driverName = Application.ForceDriver;
  202. }
  203. Debug.Assert(_navigation is null);
  204. _navigation = new ();
  205. Debug.Assert (_popover is null);
  206. _popover = new ();
  207. // Preserve existing keyboard settings if they exist
  208. bool hasExistingKeyboard = _keyboard is not null;
  209. Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
  210. Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
  211. Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
  212. Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
  213. Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
  214. Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
  215. // Reset keyboard to ensure fresh state with default bindings
  216. _keyboard = new KeyboardImpl { Application = this };
  217. // Restore previously set keys if they existed and were different from defaults
  218. if (hasExistingKeyboard)
  219. {
  220. _keyboard.QuitKey = existingQuitKey;
  221. _keyboard.ArrangeKey = existingArrangeKey;
  222. _keyboard.NextTabKey = existingNextTabKey;
  223. _keyboard.PrevTabKey = existingPrevTabKey;
  224. _keyboard.NextTabGroupKey = existingNextTabGroupKey;
  225. _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
  226. }
  227. CreateDriver (driverName ?? _driverName);
  228. _initialized = true;
  229. Application.OnInitializedChanged (this, new (true));
  230. Application.SubscribeDriverEvents ();
  231. SynchronizationContext.SetSynchronizationContext (new ());
  232. _mainThreadId = Thread.CurrentThread.ManagedThreadId;
  233. }
  234. private void CreateDriver (string? driverName)
  235. {
  236. // When running unit tests, always use FakeDriver unless explicitly specified
  237. if (ConsoleDriver.RunningUnitTests &&
  238. string.IsNullOrEmpty (driverName) &&
  239. _componentFactory is null)
  240. {
  241. Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
  242. _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
  243. _coordinator.StartAsync ().Wait ();
  244. if (_driver == null)
  245. {
  246. throw new ("Driver was null even after booting MainLoopCoordinator");
  247. }
  248. return;
  249. }
  250. PlatformID p = Environment.OSVersion.Platform;
  251. // Check component factory type first - this takes precedence over driverName
  252. bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
  253. bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
  254. bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
  255. bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
  256. // Then check driverName
  257. bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
  258. bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
  259. bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
  260. bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
  261. // Decide which driver to use - component factory type takes priority
  262. if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
  263. {
  264. _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
  265. }
  266. else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
  267. {
  268. _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
  269. }
  270. else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
  271. {
  272. _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
  273. }
  274. else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
  275. {
  276. _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
  277. }
  278. else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
  279. {
  280. _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
  281. }
  282. else
  283. {
  284. _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
  285. }
  286. _coordinator.StartAsync ().Wait ();
  287. if (_driver == null)
  288. {
  289. throw new ("Driver was null even after booting MainLoopCoordinator");
  290. }
  291. }
  292. private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
  293. {
  294. ConcurrentQueue<T> inputBuffer = new ();
  295. ApplicationMainLoop<T> loop = new ();
  296. IComponentFactory<T> cf;
  297. if (_componentFactory is IComponentFactory<T> typedFactory)
  298. {
  299. cf = typedFactory;
  300. }
  301. else
  302. {
  303. cf = fallbackFactory ();
  304. }
  305. return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
  306. }
  307. /// <summary>
  308. /// Runs the application by creating a <see cref="Toplevel"/> object and calling
  309. /// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
  310. /// </summary>
  311. /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
  312. [RequiresUnreferencedCode ("AOT")]
  313. [RequiresDynamicCode ("AOT")]
  314. public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
  315. /// <summary>
  316. /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
  317. /// <see cref="Run(Toplevel, Func{Exception, bool})"/>.
  318. /// </summary>
  319. /// <param name="errorHandler"></param>
  320. /// <param name="driver">
  321. /// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
  322. /// be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
  323. /// </param>
  324. /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
  325. [RequiresUnreferencedCode ("AOT")]
  326. [RequiresDynamicCode ("AOT")]
  327. public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
  328. where T : Toplevel, new()
  329. {
  330. if (!_initialized)
  331. {
  332. // Init() has NOT been called. Auto-initialize as per interface contract.
  333. Init (driver, null);
  334. }
  335. T top = new ();
  336. Run (top, errorHandler);
  337. return top;
  338. }
  339. /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
  340. /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
  341. /// <param name="errorHandler">Handler for any unhandled exceptions.</param>
  342. public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
  343. {
  344. Logging.Information ($"Run '{view}'");
  345. ArgumentNullException.ThrowIfNull (view);
  346. if (!_initialized)
  347. {
  348. throw new NotInitializedException (nameof (Run));
  349. }
  350. if (_driver == null)
  351. {
  352. throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
  353. }
  354. _top = view;
  355. RunState rs = Application.Begin (view);
  356. _top.Running = true;
  357. while (_topLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
  358. {
  359. if (_coordinator is null)
  360. {
  361. throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
  362. }
  363. _coordinator.RunIteration ();
  364. }
  365. Logging.Information ($"Run - Calling End");
  366. Application.End (rs);
  367. }
  368. /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
  369. public void Shutdown ()
  370. {
  371. _coordinator?.Stop ();
  372. bool wasInitialized = _initialized;
  373. // Reset Screen before calling Application.ResetState to avoid circular reference
  374. ResetScreen ();
  375. // Call ResetState FIRST so it can properly dispose Popover and other resources
  376. // that are accessed via Application.* static properties that now delegate to instance fields
  377. Application.ResetState ();
  378. ConfigurationManager.PrintJsonErrors ();
  379. // Clear instance fields after ResetState has disposed everything
  380. _driver = null;
  381. _mouse = null;
  382. _keyboard = null;
  383. _initialized = false;
  384. _navigation = null;
  385. _popover = null;
  386. _top = null;
  387. _topLevels.Clear ();
  388. _mainThreadId = -1;
  389. _screen = null;
  390. _clearScreenNextIteration = false;
  391. _sixel.Clear ();
  392. // Don't reset ForceDriver and Force16Colors; they need to be set before Init is called
  393. if (wasInitialized)
  394. {
  395. bool init = _initialized; // Will be false after clearing fields above
  396. Application.OnInitializedChanged (this, new (in init));
  397. }
  398. _lazyInstance = new (() => new ApplicationImpl ());
  399. }
  400. /// <inheritdoc />
  401. public void RequestStop (Toplevel? top)
  402. {
  403. Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
  404. top ??= _top;
  405. if (top == null)
  406. {
  407. return;
  408. }
  409. ToplevelClosingEventArgs ev = new (top);
  410. top.OnClosing (ev);
  411. if (ev.Cancel)
  412. {
  413. return;
  414. }
  415. top.Running = false;
  416. }
  417. /// <inheritdoc />
  418. public void Invoke (Action action)
  419. {
  420. // If we are already on the main UI thread
  421. if (Application.Top is { Running: true } && _mainThreadId == Thread.CurrentThread.ManagedThreadId)
  422. {
  423. action ();
  424. return;
  425. }
  426. _timedEvents.Add (TimeSpan.Zero,
  427. () =>
  428. {
  429. action ();
  430. return false;
  431. }
  432. );
  433. }
  434. /// <inheritdoc />
  435. public bool IsLegacy => false;
  436. /// <inheritdoc />
  437. public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
  438. /// <inheritdoc />
  439. public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
  440. /// <inheritdoc />
  441. public void LayoutAndDraw (bool forceRedraw = false)
  442. {
  443. List<View> tops = [.. _topLevels];
  444. if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
  445. {
  446. visiblePopover.SetNeedsDraw ();
  447. visiblePopover.SetNeedsLayout ();
  448. tops.Insert (0, visiblePopover);
  449. }
  450. bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size);
  451. if (ClearScreenNextIteration)
  452. {
  453. forceRedraw = true;
  454. ClearScreenNextIteration = false;
  455. }
  456. if (forceRedraw)
  457. {
  458. _driver?.ClearContents ();
  459. }
  460. View.SetClipToScreen ();
  461. View.Draw (tops, neededLayout || forceRedraw);
  462. View.SetClipToScreen ();
  463. _driver?.Refresh ();
  464. }
  465. /// <summary>
  466. /// Resets the Screen field to null so it will be recalculated on next access.
  467. /// </summary>
  468. internal void ResetScreen ()
  469. {
  470. lock (_lockScreen)
  471. {
  472. _screen = null;
  473. }
  474. }
  475. }