IInput.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. namespace Terminal.Gui.Drivers;
  4. /// <summary>
  5. /// Interface for reading console input in a perpetual loop on a dedicated input thread.
  6. /// </summary>
  7. /// <remarks>
  8. /// <para>
  9. /// Implementations run on a separate thread (started by
  10. /// <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>)
  11. /// and continuously read platform-specific input from the console, placing it into a thread-safe queue
  12. /// for processing by <see cref="IInputProcessor"/> on the main UI thread.
  13. /// </para>
  14. /// <para>
  15. /// <b>Architecture:</b>
  16. /// </para>
  17. /// <code>
  18. /// Input Thread: Main UI Thread:
  19. /// ┌─────────────────┐ ┌──────────────────────┐
  20. /// │ IInput.Run() │ │ IInputProcessor │
  21. /// │ ├─ Peek() │ │ ├─ ProcessQueue() │
  22. /// │ ├─ Read() │──Enqueue──→ │ ├─ Process() │
  23. /// │ └─ Enqueue │ │ ├─ ToKey() │
  24. /// └─────────────────┘ │ └─ Raise Events │
  25. /// └──────────────────────┘
  26. /// </code>
  27. /// <para>
  28. /// <b>Lifecycle:</b>
  29. /// </para>
  30. /// <list type="number">
  31. /// <item><see cref="Initialize"/> - Set the shared input queue</item>
  32. /// <item><see cref="Run"/> - Start the perpetual read loop (blocks until cancelled)</item>
  33. /// <item>
  34. /// Loop calls <see cref="InputImpl{TInputRecord}.Peek"/> and <see cref="InputImpl{TInputRecord}.Read"/>
  35. /// </item>
  36. /// <item>Cancellation via `runCancellationToken` or <see cref="ExternalCancellationTokenSource"/></item>
  37. /// </list>
  38. /// <para>
  39. /// <b>Implementations:</b>
  40. /// </para>
  41. /// <list type="bullet">
  42. /// <item><see cref="WindowsInput"/> - Uses Windows Console API (<c>ReadConsoleInput</c>)</item>
  43. /// <item><see cref="NetInput"/> - Uses .NET <see cref="System.Console"/> API</item>
  44. /// <item><see cref="UnixInput"/> - Uses Unix terminal APIs</item>
  45. /// <item><see cref="FakeInput"/> - For testing, implements <see cref="ITestableInput{TInputRecord}"/></item>
  46. /// </list>
  47. /// <para>
  48. /// <b>Testing Support:</b> See <see cref="ITestableInput{TInputRecord}"/> for programmatic input injection
  49. /// in test scenarios.
  50. /// </para>
  51. /// </remarks>
  52. /// <typeparam name="TInputRecord">
  53. /// The platform-specific input record type:
  54. /// <list type="bullet">
  55. /// <item><see cref="ConsoleKeyInfo"/> - for .NET and Fake drivers</item>
  56. /// <item><see cref="WindowsConsole.InputRecord"/> - for Windows driver</item>
  57. /// <item><see cref="char"/> - for Unix driver</item>
  58. /// </list>
  59. /// </typeparam>
  60. public interface IInput<TInputRecord> : IDisposable
  61. {
  62. /// <summary>
  63. /// Gets or sets an external cancellation token source that can stop the <see cref="Run"/> loop
  64. /// in addition to the `runCancellationToken` passed to <see cref="Run"/>.
  65. /// </summary>
  66. /// <remarks>
  67. /// <para>
  68. /// This property allows external code (e.g., test harnesses like <c>GuiTestContext</c>) to
  69. /// provide additional cancellation signals such as timeouts or hard-stop conditions.
  70. /// </para>
  71. /// <para>
  72. /// <b>Ownership:</b> The setter does NOT transfer ownership of the <see cref="CancellationTokenSource"/>.
  73. /// The creator is responsible for disposal. <see cref="IInput{TInputRecord}"/> implementations
  74. /// should NOT dispose this token source.
  75. /// </para>
  76. /// <para>
  77. /// <b>How it works:</b> <see cref="InputImpl{TInputRecord}.Run"/> creates a linked token that
  78. /// responds to BOTH the `runCancellationToken` AND this external token:
  79. /// </para>
  80. /// <code>
  81. /// var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(
  82. /// runCancellationToken,
  83. /// ExternalCancellationTokenSource.Token);
  84. /// </code>
  85. /// </remarks>
  86. /// <example>
  87. /// Test scenario with timeout:
  88. /// <code>
  89. /// var input = new FakeInput();
  90. /// input.ExternalCancellationTokenSource = new CancellationTokenSource(
  91. /// TimeSpan.FromSeconds(30)); // 30-second timeout
  92. ///
  93. /// // Run will stop if either:
  94. /// // 1. runCancellationToken is cancelled (normal shutdown)
  95. /// // 2. 30 seconds elapse (timeout)
  96. /// input.Run(normalCancellationToken);
  97. /// </code>
  98. /// </example>
  99. CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
  100. /// <summary>
  101. /// Initializes the input reader with the thread-safe queue where read input will be stored.
  102. /// </summary>
  103. /// <param name="inputQueue">
  104. /// The shared <see cref="ConcurrentQueue{T}"/> that both <see cref="Run"/> (producer)
  105. /// and <see cref="IInputProcessor"/> (consumer) use for passing input records between threads.
  106. /// </param>
  107. /// <remarks>
  108. /// <para>
  109. /// This queue is created by <see cref="Terminal.Gui.App.MainLoopCoordinator{TInputRecord}"/>
  110. /// and shared between the input thread and main UI thread.
  111. /// </para>
  112. /// <para>
  113. /// <b>Must be called before <see cref="Run"/>.</b> Calling <see cref="Run"/> without
  114. /// initialization will throw an exception.
  115. /// </para>
  116. /// </remarks>
  117. void Initialize (ConcurrentQueue<TInputRecord> inputQueue);
  118. /// <summary>
  119. /// Runs the input loop, continuously reading input and placing it into the queue
  120. /// provided by <see cref="Initialize"/>.
  121. /// </summary>
  122. /// <param name="runCancellationToken">
  123. /// The primary cancellation token that stops the input loop. Provided by
  124. /// <see cref="Terminal.Gui.App.MainLoopCoordinator{TInputRecord}"/> and triggered
  125. /// during application shutdown.
  126. /// </param>
  127. /// <remarks>
  128. /// <para>
  129. /// <b>Threading:</b> This method runs on a dedicated input thread created by
  130. /// <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>. and blocks until
  131. /// cancellation is requested. It should never be called from the main UI thread.
  132. /// </para>
  133. /// <para>
  134. /// <b>Cancellation:</b> The loop stops when either <paramref name="runCancellationToken"/>
  135. /// or <see cref="ExternalCancellationTokenSource"/> (if set) is cancelled.
  136. /// </para>
  137. /// <para>
  138. /// <b>Base Implementation:</b> <see cref="InputImpl{TInputRecord}.Run"/> provides the
  139. /// standard loop logic:
  140. /// </para>
  141. /// <code>
  142. /// while (!cancelled)
  143. /// {
  144. /// while (Peek()) // Check for available input
  145. /// {
  146. /// foreach (var input in Read()) // Read all available
  147. /// {
  148. /// inputQueue.Enqueue(input); // Store for processing
  149. /// }
  150. /// }
  151. /// Task.Delay(20ms); // Throttle to ~50 polls/second
  152. /// }
  153. /// </code>
  154. /// <para>
  155. /// <b>Testing:</b> For <see cref="ITestableInput{TInputRecord}"/> implementations,
  156. /// test input injected via <see cref="ITestableInput{TInputRecord}.AddInput"/>
  157. /// flows through the same <c>Peek/Read</c> pipeline.
  158. /// </para>
  159. /// </remarks>
  160. /// <exception cref="OperationCanceledException">
  161. /// Thrown when <paramref name="runCancellationToken"/> or <see cref="ExternalCancellationTokenSource"/>
  162. /// is cancelled. This is the normal/expected means of exiting the input loop.
  163. /// </exception>
  164. /// <exception cref="InvalidOperationException">
  165. /// Thrown if <see cref="Initialize"/> was not called before <see cref="Run"/>.
  166. /// </exception>
  167. void Run (CancellationToken runCancellationToken);
  168. }