InputProcessor.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using Microsoft.Extensions.Logging;
  4. namespace Terminal.Gui;
  5. /// <summary>
  6. /// Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>.
  7. /// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
  8. /// events and data models.
  9. /// </summary>
  10. public abstract class InputProcessor<T> : IInputProcessor
  11. {
  12. /// <summary>
  13. /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
  14. /// </summary>
  15. private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
  16. internal AnsiResponseParser<T> Parser { get; } = new ();
  17. /// <summary>
  18. /// Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g.
  19. /// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
  20. /// internal library representations of Keys).
  21. /// </summary>
  22. public IKeyConverter<T> KeyConverter { get; }
  23. /// <summary>
  24. /// Input buffer which will be drained from by this class.
  25. /// </summary>
  26. public ConcurrentQueue<T> InputBuffer { get; }
  27. /// <inheritdoc/>
  28. public IAnsiResponseParser GetParser () { return Parser; }
  29. private readonly MouseInterpreter _mouseInterpreter = new ();
  30. /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
  31. public event EventHandler<Key>? KeyDown;
  32. /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
  33. public event EventHandler<string>? AnsiSequenceSwallowed;
  34. /// <summary>
  35. /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
  36. /// <see cref="OnKeyUp"/>.
  37. /// </summary>
  38. /// <param name="a"></param>
  39. public void OnKeyDown (Key a)
  40. {
  41. Logging.Trace ($"{nameof (InputProcessor<T>)} raised {a}");
  42. KeyDown?.Invoke (this, a);
  43. }
  44. /// <summary>Event fired when a key is released.</summary>
  45. /// <remarks>
  46. /// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
  47. /// complete.
  48. /// </remarks>
  49. public event EventHandler<Key>? KeyUp;
  50. /// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
  51. /// <remarks>
  52. /// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
  53. /// is complete.
  54. /// </remarks>
  55. /// <param name="a"></param>
  56. public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
  57. /// <summary>Event fired when a mouse event occurs.</summary>
  58. public event EventHandler<MouseEventArgs>? MouseEvent;
  59. /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
  60. /// <param name="a"></param>
  61. public void OnMouseEvent (MouseEventArgs a)
  62. {
  63. // Ensure ScreenPosition is set
  64. a.ScreenPosition = a.Position;
  65. foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
  66. {
  67. Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
  68. // Pass on
  69. MouseEvent?.Invoke (this, e);
  70. }
  71. }
  72. /// <summary>
  73. /// Constructs base instance including wiring all relevant
  74. /// parser events and setting <see cref="InputBuffer"/> to
  75. /// the provided thread safe input collection.
  76. /// </summary>
  77. /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param>
  78. /// <param name="keyConverter">
  79. /// Key converter for translating driver specific
  80. /// <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.
  81. /// </param>
  82. protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter)
  83. {
  84. InputBuffer = inputBuffer;
  85. Parser.HandleMouse = true;
  86. Parser.Mouse += (s, e) => OnMouseEvent (e);
  87. Parser.HandleKeyboard = true;
  88. Parser.Keyboard += (s, k) =>
  89. {
  90. OnKeyDown (k);
  91. OnKeyUp (k);
  92. };
  93. // TODO: For now handle all other escape codes with ignore
  94. Parser.UnexpectedResponseHandler = str =>
  95. {
  96. var cur = new string (str.Select (k => k.Item1).ToArray ());
  97. Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'");
  98. AnsiSequenceSwallowed?.Invoke (this, cur);
  99. return true;
  100. };
  101. KeyConverter = keyConverter;
  102. }
  103. /// <summary>
  104. /// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes
  105. /// </summary>
  106. public void ProcessQueue ()
  107. {
  108. while (InputBuffer.TryDequeue (out T? input))
  109. {
  110. Process (input);
  111. }
  112. foreach (T input in ReleaseParserHeldKeysIfStale ())
  113. {
  114. ProcessAfterParsing (input);
  115. }
  116. }
  117. private IEnumerable<T> ReleaseParserHeldKeysIfStale ()
  118. {
  119. if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
  120. && DateTime.Now - Parser.StateChangedAt > _escTimeout)
  121. {
  122. return Parser.Release ().Select (o => o.Item2);
  123. }
  124. return [];
  125. }
  126. /// <summary>
  127. /// Process the provided single input element <paramref name="input"/>. This method
  128. /// is called sequentially for each value read from <see cref="InputBuffer"/>.
  129. /// </summary>
  130. /// <param name="input"></param>
  131. protected abstract void Process (T input);
  132. /// <summary>
  133. /// Process the provided single input element - short-circuiting the <see cref="Parser"/>
  134. /// stage of the processing.
  135. /// </summary>
  136. /// <param name="input"></param>
  137. protected abstract void ProcessAfterParsing (T input);
  138. }