InputProcessorImpl.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. using System.Collections.Concurrent;
  2. using Microsoft.Extensions.Logging;
  3. namespace Terminal.Gui.Drivers;
  4. /// <summary>
  5. /// Processes the queued input queue contents - which must be of Type <typeparamref name="TInputRecord"/>.
  6. /// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
  7. /// events and data models. Runs on the main loop thread.
  8. /// </summary>
  9. public abstract class InputProcessorImpl<TInputRecord> : IInputProcessor, IDisposable where TInputRecord : struct
  10. {
  11. /// <summary>
  12. /// Constructs base instance including wiring all relevant
  13. /// parser events and setting <see cref="InputQueue"/> to
  14. /// the provided thread safe input collection.
  15. /// </summary>
  16. /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IInput{T}"/>)</param>
  17. /// <param name="keyConverter">
  18. /// Key converter for translating driver specific
  19. /// <typeparamref name="TInputRecord"/> class into Terminal.Gui <see cref="Key"/>.
  20. /// </param>
  21. protected InputProcessorImpl (ConcurrentQueue<TInputRecord> inputBuffer, IKeyConverter<TInputRecord> keyConverter)
  22. {
  23. InputQueue = inputBuffer;
  24. Parser.HandleMouse = true;
  25. Parser.Mouse += (s, e) => RaiseMouseEvent (e);
  26. Parser.HandleKeyboard = true;
  27. Parser.Keyboard += (s, k) =>
  28. {
  29. RaiseKeyDownEvent (k);
  30. RaiseKeyUpEvent (k);
  31. };
  32. // TODO: For now handle all other escape codes with ignore
  33. Parser.UnexpectedResponseHandler = str =>
  34. {
  35. var cur = new string (str.Select (k => k.Item1).ToArray ());
  36. Logging.Logger.LogInformation ($"{nameof (InputProcessorImpl<TInputRecord>)} ignored unrecognized response '{cur}'");
  37. AnsiSequenceSwallowed?.Invoke (this, cur);
  38. return true;
  39. };
  40. KeyConverter = keyConverter;
  41. }
  42. /// <summary>
  43. /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
  44. /// </summary>
  45. private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
  46. internal AnsiResponseParser<TInputRecord> Parser { get; } = new ();
  47. /// <summary>
  48. /// Class responsible for translating the driver specific native input class <typeparamref name="TInputRecord"/> e.g.
  49. /// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
  50. /// internal library representations of Keys).
  51. /// </summary>
  52. public IKeyConverter<TInputRecord> KeyConverter { get; }
  53. /// <summary>
  54. /// The input queue which is filled by <see cref="IInput{TInputRecord}"/> implementations running on the input thread.
  55. /// Implementations of this class should dequeue from this queue in <see cref="ProcessQueue"/> on the main loop thread.
  56. /// </summary>
  57. public ConcurrentQueue<TInputRecord> InputQueue { get; }
  58. /// <inheritdoc />
  59. public string? DriverName { get; init; }
  60. /// <inheritdoc/>
  61. public IAnsiResponseParser GetParser () { return Parser; }
  62. private readonly MouseInterpreter _mouseInterpreter = new ();
  63. /// <inheritdoc />
  64. public event EventHandler<Key>? KeyDown;
  65. /// <inheritdoc />
  66. public event EventHandler<string>? AnsiSequenceSwallowed;
  67. /// <inheritdoc />
  68. public void RaiseKeyDownEvent (Key a)
  69. {
  70. KeyDown?.Invoke (this, a);
  71. }
  72. /// <inheritdoc />
  73. public event EventHandler<Key>? KeyUp;
  74. /// <inheritdoc />
  75. public void RaiseKeyUpEvent (Key a) { KeyUp?.Invoke (this, a); }
  76. /// <summary>
  77. ///
  78. /// </summary>
  79. public IInput<TInputRecord>? InputImpl { get; set; } // Set by MainLoopCoordinator
  80. /// <inheritdoc />
  81. public void EnqueueKeyDownEvent (Key key)
  82. {
  83. // Convert Key → TInputRecord
  84. TInputRecord inputRecord = KeyConverter.ToKeyInfo (key);
  85. // If input supports testing, use InputImplPeek/Read pipeline
  86. // which runs on the input thread.
  87. if (InputImpl is ITestableInput<TInputRecord> testableInput)
  88. {
  89. testableInput.AddInput (inputRecord);
  90. }
  91. }
  92. /// <inheritdoc />
  93. public void EnqueueKeyUpEvent (Key key)
  94. {
  95. // TODO: Determine if we can still support this on Windows
  96. throw new NotImplementedException ();
  97. }
  98. /// <inheritdoc />
  99. public event EventHandler<MouseEventArgs>? MouseEvent;
  100. /// <inheritdoc />
  101. public virtual void EnqueueMouseEvent (IApplication? app, MouseEventArgs mouseEvent)
  102. {
  103. // Base implementation: For drivers where TInputRecord cannot represent mouse events
  104. // (e.g., ConsoleKeyInfo), derived classes should override this method.
  105. // See WindowsInputProcessor for an example implementation that converts MouseEventArgs
  106. // to InputRecord and enqueues it.
  107. Logging.Logger.LogWarning (
  108. $"{DriverName ?? "Unknown"} driver's InputProcessor does not support EnqueueMouseEvent. " +
  109. "Override this method to enable mouse event enqueueing for testing.");
  110. }
  111. /// <inheritdoc />
  112. public void RaiseMouseEvent (MouseEventArgs a)
  113. {
  114. // Ensure ScreenPosition is set
  115. a.ScreenPosition = a.Position;
  116. foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
  117. {
  118. // Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
  119. // Pass on
  120. MouseEvent?.Invoke (this, e);
  121. }
  122. }
  123. /// <inheritdoc />
  124. public void ProcessQueue ()
  125. {
  126. while (InputQueue.TryDequeue (out TInputRecord input))
  127. {
  128. Process (input);
  129. }
  130. foreach (TInputRecord input in ReleaseParserHeldKeysIfStale ())
  131. {
  132. ProcessAfterParsing (input);
  133. }
  134. }
  135. private IEnumerable<TInputRecord> ReleaseParserHeldKeysIfStale ()
  136. {
  137. if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
  138. && DateTime.Now - Parser.StateChangedAt > _escTimeout)
  139. {
  140. return Parser.Release ().Select (o => o.Item2);
  141. }
  142. return [];
  143. }
  144. /// <summary>
  145. /// Process the provided single input element <paramref name="input"/>. This method
  146. /// is called sequentially for each value read from <see cref="InputQueue"/>.
  147. /// </summary>
  148. /// <param name="input"></param>
  149. protected abstract void Process (TInputRecord input);
  150. /// <summary>
  151. /// Process the provided single input element - short-circuiting the <see cref="Parser"/>
  152. /// stage of the processing.
  153. /// </summary>
  154. /// <param name="input"></param>
  155. protected virtual void ProcessAfterParsing (TInputRecord input)
  156. {
  157. var key = KeyConverter.ToKey (input);
  158. // If the key is not valid, we don't want to raise any events.
  159. if (IsValidInput (key, out key))
  160. {
  161. RaiseKeyDownEvent (key);
  162. RaiseKeyUpEvent (key);
  163. }
  164. }
  165. private char _highSurrogate = '\0';
  166. /// <inheritdoc />
  167. public bool IsValidInput (Key key, out Key result)
  168. {
  169. result = key;
  170. if (char.IsHighSurrogate ((char)key))
  171. {
  172. _highSurrogate = (char)key;
  173. return false;
  174. }
  175. if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key))
  176. {
  177. result = (KeyCode)new Rune (_highSurrogate, (char)key).Value;
  178. if (key.IsAlt)
  179. {
  180. result = result.WithAlt;
  181. }
  182. if (key.IsCtrl)
  183. {
  184. result = result.WithCtrl;
  185. }
  186. if (key.IsShift)
  187. {
  188. result = result.WithShift;
  189. }
  190. _highSurrogate = '\0';
  191. return true;
  192. }
  193. if (char.IsSurrogate ((char)key))
  194. {
  195. return false;
  196. }
  197. if (_highSurrogate > 0)
  198. {
  199. _highSurrogate = '\0';
  200. }
  201. if (key.KeyCode == 0)
  202. {
  203. return false;
  204. }
  205. return true;
  206. }
  207. /// <inheritdoc/>
  208. public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
  209. /// <inheritdoc />
  210. public void Dispose () { ExternalCancellationTokenSource?.Dispose (); }
  211. }