123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- #nullable enable
- using System.Collections.Concurrent;
- using System.Diagnostics.CodeAnalysis;
- namespace Terminal.Gui;
- internal class NetEvents : IDisposable
- {
- private readonly CancellationTokenSource _netEventsDisposed = new CancellationTokenSource ();
- //CancellationTokenSource _waitForStartCancellationTokenSource;
- private readonly ManualResetEventSlim _winChange = new (false);
- private readonly BlockingCollection<InputResult?> _inputQueue = new (new ConcurrentQueue<InputResult?> ());
- private readonly IConsoleDriver _consoleDriver;
- public AnsiResponseParser<ConsoleKeyInfo> Parser { get; private set; } = new ();
- public NetEvents (IConsoleDriver consoleDriver)
- {
- _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
- Task.Run (() =>
- {
- try
- {
- ProcessInputQueue ();
- }
- catch (OperationCanceledException)
- { }
- }, _netEventsDisposed.Token);
- Task.Run (() => {
- try
- {
- CheckWindowSizeChange ();
- }
- catch (OperationCanceledException)
- { }
- }, _netEventsDisposed.Token);
- Parser.UnexpectedResponseHandler = ProcessRequestResponse;
- }
- public InputResult? DequeueInput ()
- {
- while (!_netEventsDisposed.Token.IsCancellationRequested)
- {
- _winChange.Set ();
- try
- {
- if (_inputQueue.TryTake (out var item, -1, _netEventsDisposed.Token))
- {
- return item;
- }
- }
- catch (OperationCanceledException)
- {
- return null;
- }
- }
- return null;
- }
- private ConsoleKeyInfo ReadConsoleKeyInfo (bool intercept = true)
- {
- // if there is a key available, return it without waiting
- // (or dispatching work to the thread queue)
- if (Console.KeyAvailable)
- {
- return Console.ReadKey (intercept);
- }
- while (!_netEventsDisposed.IsCancellationRequested)
- {
- Task.Delay (100, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token);
- foreach (var k in ShouldReleaseParserHeldKeys ())
- {
- ProcessMapConsoleKeyInfo (k);
- }
- if (Console.KeyAvailable)
- {
- return Console.ReadKey (intercept);
- }
- }
- _netEventsDisposed.Token.ThrowIfCancellationRequested ();
- return default (ConsoleKeyInfo);
- }
- public IEnumerable<ConsoleKeyInfo> ShouldReleaseParserHeldKeys ()
- {
- if (Parser.State == AnsiResponseParserState.ExpectingEscapeSequence &&
- DateTime.Now - Parser.StateChangedAt > ((NetDriver)_consoleDriver).EscTimeout)
- {
- return Parser.Release ().Select (o => o.Item2);
- }
- return [];
- }
- private void ProcessInputQueue ()
- {
- while (!_netEventsDisposed.IsCancellationRequested)
- {
- if (_inputQueue.Count == 0)
- {
- while (!_netEventsDisposed.IsCancellationRequested)
- {
- ConsoleKeyInfo consoleKeyInfo;
- consoleKeyInfo = ReadConsoleKeyInfo ();
- // Parse
- foreach (var k in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo)))
- {
- ProcessMapConsoleKeyInfo (k.Item2);
- }
- }
- }
- }
- }
- void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
- {
- _inputQueue.Add (
- new InputResult
- {
- EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
- }
- );
- }
- private void CheckWindowSizeChange ()
- {
- void RequestWindowSize ()
- {
- while (!_netEventsDisposed.IsCancellationRequested)
- {
- // Wait for a while then check if screen has changed sizes
- Task.Delay (500, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token);
- int buffHeight, buffWidth;
- if (((NetDriver)_consoleDriver).IsWinPlatform)
- {
- buffHeight = Math.Max (Console.BufferHeight, 0);
- buffWidth = Math.Max (Console.BufferWidth, 0);
- }
- else
- {
- buffHeight = _consoleDriver.Rows;
- buffWidth = _consoleDriver.Cols;
- }
- if (EnqueueWindowSizeEvent (
- Math.Max (Console.WindowHeight, 0),
- Math.Max (Console.WindowWidth, 0),
- buffHeight,
- buffWidth
- ))
- {
- return;
- }
- }
- _netEventsDisposed.Token.ThrowIfCancellationRequested ();
- }
- while (!_netEventsDisposed.IsCancellationRequested)
- {
- try
- {
- _winChange.Wait (_netEventsDisposed.Token);
- _winChange.Reset ();
- RequestWindowSize ();
- }
- catch (OperationCanceledException)
- {
- return;
- }
- }
- }
- /// <summary>Enqueue a window size event if the window size has changed.</summary>
- /// <param name="winHeight"></param>
- /// <param name="winWidth"></param>
- /// <param name="buffHeight"></param>
- /// <param name="buffWidth"></param>
- /// <returns></returns>
- private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth)
- {
- if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows)
- {
- return false;
- }
- int w = Math.Max (winWidth, 0);
- int h = Math.Max (winHeight, 0);
- _inputQueue.Add (
- new InputResult
- {
- EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) }
- }
- );
- return true;
- }
- private bool ProcessRequestResponse (IEnumerable<Tuple<char, ConsoleKeyInfo>> obj)
- {
- // Added for signature compatibility with existing method, not sure what they are even for.
- ConsoleKeyInfo newConsoleKeyInfo = default;
- ConsoleKey key = default;
- ConsoleModifiers mod = default;
- ProcessRequestResponse (ref newConsoleKeyInfo, ref key, obj.Select (v => v.Item2).ToArray (), ref mod);
- // Handled
- return true;
- }
- // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
- private void ProcessRequestResponse (
- ref ConsoleKeyInfo newConsoleKeyInfo,
- ref ConsoleKey key,
- ConsoleKeyInfo [] cki,
- ref ConsoleModifiers mod
- )
- {
- // isMouse is true if it's CSI<, false otherwise
- EscSeqUtils.DecodeEscSeq (
- ref newConsoleKeyInfo,
- ref key,
- cki,
- ref mod,
- out string c1Control,
- out string code,
- out string [] values,
- out string terminating,
- out bool isMouse,
- out List<MouseFlags> mouseFlags,
- out Point pos,
- out bool isReq,
- (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
- );
- if (isMouse)
- {
- foreach (MouseFlags mf in mouseFlags)
- {
- HandleMouseEvent (MapMouseFlags (mf), pos);
- }
- return;
- }
- if (isReq)
- {
- HandleRequestResponseEvent (c1Control, code, values, terminating);
- return;
- }
- HandleKeyboardEvent (newConsoleKeyInfo);
- }
- [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
- private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
- {
- MouseButtonState mbs = default;
- foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
- {
- if (mouseFlags.HasFlag ((MouseFlags)flag))
- {
- switch (flag)
- {
- case MouseFlags.Button1Pressed:
- mbs |= MouseButtonState.Button1Pressed;
- break;
- case MouseFlags.Button1Released:
- mbs |= MouseButtonState.Button1Released;
- break;
- case MouseFlags.Button1Clicked:
- mbs |= MouseButtonState.Button1Clicked;
- break;
- case MouseFlags.Button1DoubleClicked:
- mbs |= MouseButtonState.Button1DoubleClicked;
- break;
- case MouseFlags.Button1TripleClicked:
- mbs |= MouseButtonState.Button1TripleClicked;
- break;
- case MouseFlags.Button2Pressed:
- mbs |= MouseButtonState.Button2Pressed;
- break;
- case MouseFlags.Button2Released:
- mbs |= MouseButtonState.Button2Released;
- break;
- case MouseFlags.Button2Clicked:
- mbs |= MouseButtonState.Button2Clicked;
- break;
- case MouseFlags.Button2DoubleClicked:
- mbs |= MouseButtonState.Button2DoubleClicked;
- break;
- case MouseFlags.Button2TripleClicked:
- mbs |= MouseButtonState.Button2TripleClicked;
- break;
- case MouseFlags.Button3Pressed:
- mbs |= MouseButtonState.Button3Pressed;
- break;
- case MouseFlags.Button3Released:
- mbs |= MouseButtonState.Button3Released;
- break;
- case MouseFlags.Button3Clicked:
- mbs |= MouseButtonState.Button3Clicked;
- break;
- case MouseFlags.Button3DoubleClicked:
- mbs |= MouseButtonState.Button3DoubleClicked;
- break;
- case MouseFlags.Button3TripleClicked:
- mbs |= MouseButtonState.Button3TripleClicked;
- break;
- case MouseFlags.WheeledUp:
- mbs |= MouseButtonState.ButtonWheeledUp;
- break;
- case MouseFlags.WheeledDown:
- mbs |= MouseButtonState.ButtonWheeledDown;
- break;
- case MouseFlags.WheeledLeft:
- mbs |= MouseButtonState.ButtonWheeledLeft;
- break;
- case MouseFlags.WheeledRight:
- mbs |= MouseButtonState.ButtonWheeledRight;
- break;
- case MouseFlags.Button4Pressed:
- mbs |= MouseButtonState.Button4Pressed;
- break;
- case MouseFlags.Button4Released:
- mbs |= MouseButtonState.Button4Released;
- break;
- case MouseFlags.Button4Clicked:
- mbs |= MouseButtonState.Button4Clicked;
- break;
- case MouseFlags.Button4DoubleClicked:
- mbs |= MouseButtonState.Button4DoubleClicked;
- break;
- case MouseFlags.Button4TripleClicked:
- mbs |= MouseButtonState.Button4TripleClicked;
- break;
- case MouseFlags.ButtonShift:
- mbs |= MouseButtonState.ButtonShift;
- break;
- case MouseFlags.ButtonCtrl:
- mbs |= MouseButtonState.ButtonCtrl;
- break;
- case MouseFlags.ButtonAlt:
- mbs |= MouseButtonState.ButtonAlt;
- break;
- case MouseFlags.ReportMousePosition:
- mbs |= MouseButtonState.ReportMousePosition;
- break;
- case MouseFlags.AllEvents:
- mbs |= MouseButtonState.AllEvents;
- break;
- }
- }
- }
- return mbs;
- }
- private Point _lastCursorPosition;
- private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
- {
- switch (terminating)
- {
- // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
- case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
- var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
- if (_lastCursorPosition.Y != point.Y)
- {
- _lastCursorPosition = point;
- var eventType = EventType.WindowPosition;
- var winPositionEv = new WindowPositionEvent { CursorPosition = point };
- _inputQueue.Add (
- new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
- );
- }
- else
- {
- return;
- }
- break;
- case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
- switch (values [0])
- {
- case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
- EnqueueWindowSizeEvent (
- Math.Max (int.Parse (values [1]), 0),
- Math.Max (int.Parse (values [2]), 0),
- Math.Max (int.Parse (values [1]), 0),
- Math.Max (int.Parse (values [2]), 0)
- );
- break;
- default:
- EnqueueRequestResponseEvent (c1Control, code, values, terminating);
- break;
- }
- break;
- default:
- EnqueueRequestResponseEvent (c1Control, code, values, terminating);
- break;
- }
- }
- private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
- {
- var eventType = EventType.RequestResponse;
- var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
- _inputQueue.Add (
- new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
- );
- }
- private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
- {
- var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
- _inputQueue.Add (
- new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
- );
- }
- public enum EventType
- {
- Key = 1,
- Mouse = 2,
- WindowSize = 3,
- WindowPosition = 4,
- RequestResponse = 5
- }
- [Flags]
- public enum MouseButtonState
- {
- Button1Pressed = 0x1,
- Button1Released = 0x2,
- Button1Clicked = 0x4,
- Button1DoubleClicked = 0x8,
- Button1TripleClicked = 0x10,
- Button2Pressed = 0x20,
- Button2Released = 0x40,
- Button2Clicked = 0x80,
- Button2DoubleClicked = 0x100,
- Button2TripleClicked = 0x200,
- Button3Pressed = 0x400,
- Button3Released = 0x800,
- Button3Clicked = 0x1000,
- Button3DoubleClicked = 0x2000,
- Button3TripleClicked = 0x4000,
- ButtonWheeledUp = 0x8000,
- ButtonWheeledDown = 0x10000,
- ButtonWheeledLeft = 0x20000,
- ButtonWheeledRight = 0x40000,
- Button4Pressed = 0x80000,
- Button4Released = 0x100000,
- Button4Clicked = 0x200000,
- Button4DoubleClicked = 0x400000,
- Button4TripleClicked = 0x800000,
- ButtonShift = 0x1000000,
- ButtonCtrl = 0x2000000,
- ButtonAlt = 0x4000000,
- ReportMousePosition = 0x8000000,
- AllEvents = -1
- }
- public struct MouseEvent
- {
- public Point Position;
- public MouseButtonState ButtonState;
- }
- public struct WindowSizeEvent
- {
- public Size Size;
- }
- public struct WindowPositionEvent
- {
- public int Top;
- public int Left;
- public Point CursorPosition;
- }
- public struct RequestResponseEvent
- {
- public (string c1Control, string code, string [] values, string terminating) ResultTuple;
- }
- public struct InputResult
- {
- public EventType EventType;
- public ConsoleKeyInfo ConsoleKeyInfo;
- public MouseEvent MouseEvent;
- public WindowSizeEvent WindowSizeEvent;
- public WindowPositionEvent WindowPositionEvent;
- public RequestResponseEvent RequestResponseEvent;
- public readonly override string ToString ()
- {
- return EventType switch
- {
- EventType.Key => ToString (ConsoleKeyInfo),
- EventType.Mouse => MouseEvent.ToString (),
- //EventType.WindowSize => WindowSize.ToString (),
- //EventType.RequestResponse => RequestResponse.ToString (),
- _ => "Unknown event type: " + EventType
- };
- }
- /// <summary>Prints a ConsoleKeyInfoEx structure</summary>
- /// <param name="cki"></param>
- /// <returns></returns>
- public readonly string ToString (ConsoleKeyInfo cki)
- {
- var ke = new Key ((KeyCode)cki.KeyChar);
- var sb = new StringBuilder ();
- sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
- sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
- sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
- sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
- sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
- string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
- return $"[ConsoleKeyInfo({s})]";
- }
- }
- private void HandleKeyboardEvent (ConsoleKeyInfo cki)
- {
- var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki };
- _inputQueue.Add (inputResult);
- }
- public void Dispose ()
- {
- _netEventsDisposed?.Cancel ();
- _netEventsDisposed?.Dispose ();
- try
- {
- // throws away any typeahead that has been typed by
- // the user and has not yet been read by the program.
- while (Console.KeyAvailable)
- {
- Console.ReadKey (true);
- }
- }
- catch (InvalidOperationException)
- {
- // Ignore - Console input has already been closed
- }
- }
- }
|