| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- #nullable enable
- using System.Collections.Concurrent;
- using Microsoft.Extensions.Logging;
- namespace Terminal.Gui.Drivers;
- /// <summary>
- /// Processes the queued input queue contents - which must be of Type <typeparamref name="TInputRecord"/>.
- /// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
- /// events and data models. Runs on the main loop thread.
- /// </summary>
- public abstract class InputProcessorImpl<TInputRecord> : IInputProcessor, IDisposable where TInputRecord : struct
- {
- /// <summary>
- /// Constructs base instance including wiring all relevant
- /// parser events and setting <see cref="InputQueue"/> to
- /// the provided thread safe input collection.
- /// </summary>
- /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IInput{T}"/>)</param>
- /// <param name="keyConverter">
- /// Key converter for translating driver specific
- /// <typeparamref name="TInputRecord"/> class into Terminal.Gui <see cref="Key"/>.
- /// </param>
- protected InputProcessorImpl (ConcurrentQueue<TInputRecord> inputBuffer, IKeyConverter<TInputRecord> keyConverter)
- {
- InputQueue = inputBuffer;
- Parser.HandleMouse = true;
- Parser.Mouse += (s, e) => RaiseMouseEvent (e);
- Parser.HandleKeyboard = true;
- Parser.Keyboard += (s, k) =>
- {
- RaiseKeyDownEvent (k);
- RaiseKeyUpEvent (k);
- };
- // TODO: For now handle all other escape codes with ignore
- Parser.UnexpectedResponseHandler = str =>
- {
- var cur = new string (str.Select (k => k.Item1).ToArray ());
- Logging.Logger.LogInformation ($"{nameof (InputProcessorImpl<TInputRecord>)} ignored unrecognized response '{cur}'");
- AnsiSequenceSwallowed?.Invoke (this, cur);
- return true;
- };
- KeyConverter = keyConverter;
- }
- /// <summary>
- /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
- /// </summary>
- private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
- internal AnsiResponseParser<TInputRecord> Parser { get; } = new ();
- /// <summary>
- /// Class responsible for translating the driver specific native input class <typeparamref name="TInputRecord"/> e.g.
- /// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
- /// internal library representations of Keys).
- /// </summary>
- public IKeyConverter<TInputRecord> KeyConverter { get; }
- /// <summary>
- /// The input queue which is filled by <see cref="IInput{TInputRecord}"/> implementations running on the input thread.
- /// Implementations of this class should dequeue from this queue in <see cref="ProcessQueue"/> on the main loop thread.
- /// </summary>
- public ConcurrentQueue<TInputRecord> InputQueue { get; }
- /// <inheritdoc />
- public string? DriverName { get; init; }
- /// <inheritdoc/>
- public IAnsiResponseParser GetParser () { return Parser; }
- private readonly MouseInterpreter _mouseInterpreter = new ();
- /// <inheritdoc />
- public event EventHandler<Key>? KeyDown;
- /// <inheritdoc />
- public event EventHandler<string>? AnsiSequenceSwallowed;
- /// <inheritdoc />
- public void RaiseKeyDownEvent (Key a)
- {
- KeyDown?.Invoke (this, a);
- }
- /// <inheritdoc />
- public event EventHandler<Key>? KeyUp;
- /// <inheritdoc />
- public void RaiseKeyUpEvent (Key a) { KeyUp?.Invoke (this, a); }
- /// <summary>
- ///
- /// </summary>
- public IInput<TInputRecord>? InputImpl { get; set; } // Set by MainLoopCoordinator
- /// <inheritdoc />
- public void EnqueueKeyDownEvent (Key key)
- {
- // Convert Key → TInputRecord
- TInputRecord inputRecord = KeyConverter.ToKeyInfo (key);
- // If input supports testing, use InputImplPeek/Read pipeline
- // which runs on the input thread.
- if (InputImpl is ITestableInput<TInputRecord> testableInput)
- {
- testableInput.AddInput (inputRecord);
- }
- }
- /// <inheritdoc />
- public void EnqueueKeyUpEvent (Key key)
- {
- // TODO: Determine if we can still support this on Windows
- throw new NotImplementedException ();
- }
- /// <inheritdoc />
- public event EventHandler<MouseEventArgs>? MouseEvent;
- /// <inheritdoc />
- public virtual void EnqueueMouseEvent (MouseEventArgs mouseEvent)
- {
- // Base implementation: For drivers where TInputRecord cannot represent mouse events
- // (e.g., ConsoleKeyInfo), derived classes should override this method.
- // See WindowsInputProcessor for an example implementation that converts MouseEventArgs
- // to InputRecord and enqueues it.
- Logging.Logger.LogWarning (
- $"{DriverName ?? "Unknown"} driver's InputProcessor does not support EnqueueMouseEvent. " +
- "Override this method to enable mouse event enqueueing for testing.");
- }
- /// <inheritdoc />
- public void RaiseMouseEvent (MouseEventArgs a)
- {
- // Ensure ScreenPosition is set
- a.ScreenPosition = a.Position;
- foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
- {
- // Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
- // Pass on
- MouseEvent?.Invoke (this, e);
- }
- }
- /// <inheritdoc />
- public void ProcessQueue ()
- {
- while (InputQueue.TryDequeue (out TInputRecord input))
- {
- Process (input);
- }
- foreach (TInputRecord input in ReleaseParserHeldKeysIfStale ())
- {
- ProcessAfterParsing (input);
- }
- }
- private IEnumerable<TInputRecord> ReleaseParserHeldKeysIfStale ()
- {
- if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
- && DateTime.Now - Parser.StateChangedAt > _escTimeout)
- {
- return Parser.Release ().Select (o => o.Item2);
- }
- return [];
- }
- /// <summary>
- /// Process the provided single input element <paramref name="input"/>. This method
- /// is called sequentially for each value read from <see cref="InputQueue"/>.
- /// </summary>
- /// <param name="input"></param>
- protected abstract void Process (TInputRecord input);
- /// <summary>
- /// Process the provided single input element - short-circuiting the <see cref="Parser"/>
- /// stage of the processing.
- /// </summary>
- /// <param name="input"></param>
- protected virtual void ProcessAfterParsing (TInputRecord input)
- {
- var key = KeyConverter.ToKey (input);
- // If the key is not valid, we don't want to raise any events.
- if (IsValidInput (key, out key))
- {
- RaiseKeyDownEvent (key);
- RaiseKeyUpEvent (key);
- }
- }
- private char _highSurrogate = '\0';
- /// <inheritdoc />
- public bool IsValidInput (Key key, out Key result)
- {
- result = key;
- if (char.IsHighSurrogate ((char)key))
- {
- _highSurrogate = (char)key;
- return false;
- }
- if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key))
- {
- result = (KeyCode)new Rune (_highSurrogate, (char)key).Value;
- if (key.IsAlt)
- {
- result = result.WithAlt;
- }
- if (key.IsCtrl)
- {
- result = result.WithCtrl;
- }
- if (key.IsShift)
- {
- result = result.WithShift;
- }
- _highSurrogate = '\0';
- return true;
- }
- if (char.IsSurrogate ((char)key))
- {
- return false;
- }
- if (_highSurrogate > 0)
- {
- _highSurrogate = '\0';
- }
- if (key.KeyCode == 0)
- {
- return false;
- }
- return true;
- }
- /// <inheritdoc/>
- public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
- /// <inheritdoc />
- public void Dispose () { ExternalCancellationTokenSource?.Dispose (); }
- }
|