InputProcessorImpl.cs 8.4 KB

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