IInput.cs 8.3 KB

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