using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Management; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace Terminal.Gui; /// /// Provides a platform-independent API for managing ANSI escape sequences. /// /// /// Useful resources: /// * https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences /// * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html /// * https://vt100.net/ /// public static class EscSeqUtils { /// /// Escape key code (ASCII 27/0x1B). /// public static readonly char KeyEsc = (char)Key.Esc; /// /// ESC [ - The CSI (Control Sequence Introducer). /// public static readonly string CSI = $"{KeyEsc}["; /// /// ESC [ ? 1003 h - Enable mouse event tracking. /// public static readonly string CSI_EnableAnyEventMouse = CSI + "?1003h"; /// /// ESC [ ? 1006 h - Enable SGR (Select Graphic Rendition). /// public static readonly string CSI_EnableSgrExtModeMouse = CSI + "?1006h"; /// /// ESC [ ? 1015 h - Enable URXVT (Unicode Extended Virtual Terminal). /// public static readonly string CSI_EnableUrxvtExtModeMouse = CSI + "?1015h"; /// /// ESC [ ? 1003 l - Disable any mouse event tracking. /// public static readonly string CSI_DisableAnyEventMouse = CSI + "?1003l"; /// /// ESC [ ? 1006 l - Disable SGR (Select Graphic Rendition). /// public static readonly string CSI_DisableSgrExtModeMouse = CSI + "?1006l"; /// /// ESC [ ? 1015 l - Disable URXVT (Unicode Extended Virtual Terminal). /// public static readonly string CSI_DisableUrxvtExtModeMouse = CSI + "?1015l"; /// /// Control sequence for enabling mouse events. /// public static string CSI_EnableMouseEvents { get; set; } = CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse; /// /// Control sequence for disabling mouse events. /// public static string CSI_DisableMouseEvents { get; set; } = CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse; /// /// ESC [ ? 1047 h - Activate xterm alternative buffer (no backscroll) /// public static readonly string CSI_ActivateAltBufferNoBackscroll = CSI + "?1047h"; /// /// ESC [ ? 1047 l - Restore xterm working buffer (with backscroll) /// public static readonly string CSI_RestoreAltBufferWithBackscroll = CSI + "?1047l"; /// /// ESC [ ? 1049 h - Save cursor position and activate xterm alternative buffer (no backscroll) /// public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h"; /// /// ESC [ ? 1049 l - Restore cursor position and restore xterm working buffer (with backscroll) /// public static readonly string CSI_RestoreCursorAndActivateAltBufferWithBackscroll = CSI + "?1049l"; /// /// Options for ANSI ESC "[xJ" - Clears part of the screen. /// public enum ClearScreenOptions { /// /// If n is 0 (or missing), clear from cursor to end of screen. /// CursorToEndOfScreen = 0, /// /// If n is 1, clear from cursor to beginning of the screen. /// CursorToBeginningOfScreen = 1, /// /// If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS). /// EntireScreen = 2, /// /// If n is 3, clear entire screen and delete all lines saved in the scrollback buffer /// EntireScreenAndScrollbackBuffer = 3 } /// /// ESC [ x J - Clears part of the screen. See . /// /// /// public static string CSI_ClearScreen (ClearScreenOptions option) => $"{CSI}{(int)option}J"; #region Cursor //ESC [ M - RI Reverse Index – Performs the reverse operation of \n, moves cursor up one line, maintains horizontal position, scrolls buffer if necessary* /// /// ESC [ 7 - Save Cursor Position in Memory** /// public static readonly string CSI_SaveCursorPosition = CSI + "7"; /// /// ESC [ 8 - DECSR Restore Cursor Position from Memory** /// public static readonly string CSI_RestoreCursorPosition = CSI + "8"; /// /// ESC [ 8 ; height ; width t - Set Terminal Window Size /// https://terminalguide.namepad.de/seq/csi_st-8/ /// public static string CSI_SetTerminalWindowSize (int height, int width) => $"{CSI}8;{height};{width}t"; //ESC [ < n > A - CUU - Cursor Up Cursor up by < n > //ESC [ < n > B - CUD - Cursor Down Cursor down by < n > //ESC [ < n > C - CUF - Cursor Forward Cursor forward (Right) by < n > //ESC [ < n > D - CUB - Cursor Backward Cursor backward (Left) by < n > //ESC [ < n > E - CNL - Cursor Next Line - Cursor down < n > lines from current position //ESC [ < n > F - CPL - Cursor Previous Line Cursor up < n > lines from current position //ESC [ < n > G - CHA - Cursor Horizontal Absolute Cursor moves to < n > th position horizontally in the current line //ESC [ < n > d - VPA - Vertical Line Position Absolute Cursor moves to the < n > th position vertically in the current column /// /// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column of the y line /// /// /// /// public static string CSI_SetCursorPosition (int x, int y) => $"{CSI}{y};{x}H"; //ESC [ ; f - HVP Horizontal Vertical Position* Cursor moves to; coordinate within the viewport, where is the column of the line //ESC [ s - ANSISYSSC Save Cursor – Ansi.sys emulation **With no parameters, performs a save cursor operation like DECSC //ESC [ u - ANSISYSRC Restore Cursor – Ansi.sys emulation **With no parameters, performs a restore cursor operation like DECRC //ESC [ ? 12 h - ATT160 Text Cursor Enable Blinking Start the cursor blinking //ESC [ ? 12 l - ATT160 Text Cursor Disable Blinking Stop blinking the cursor /// /// ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show Show the cursor /// public static readonly string CSI_ShowCursor = CSI + "?25h"; /// /// ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide Hide the cursor /// public static readonly string CSI_HideCursor = CSI + "?25l"; //ESC [ ? 12 h - ATT160 Text Cursor Enable Blinking Start the cursor blinking //ESC [ ? 12 l - ATT160 Text Cursor Disable Blinking Stop blinking the cursor //ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show Show the cursor //ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide Hide the cursor /// /// Styles for ANSI ESC "[x q" - Set Cursor Style /// public enum DECSCUSR_Style { /// /// DECSCUSR - User Shape - Default cursor shape configured by the user /// UserShape = 0, /// /// DECSCUSR - Blinking Block - Blinking block cursor shape /// BlinkingBlock = 1, /// /// DECSCUSR - Steady Block - Steady block cursor shape /// SteadyBlock = 2, /// /// DECSCUSR - Blinking Underline - Blinking underline cursor shape /// BlinkingUnderline = 3, /// /// DECSCUSR - Steady Underline - Steady underline cursor shape /// SteadyUnderline = 4, /// /// DECSCUSR - Blinking Bar - Blinking bar cursor shape /// BlinkingBar = 5, /// /// DECSCUSR - Steady Bar - Steady bar cursor shape /// SteadyBar = 6 } /// /// ESC [ n SP q - Select Cursor Style (DECSCUSR) /// https://terminalguide.namepad.de/seq/csi_sq_t_space/ /// /// /// public static string CSI_SetCursorStyle (DECSCUSR_Style style) => $"{CSI}{(int)style} q"; #endregion #region Colors /// /// ESC [ (n) m - SGR - Set Graphics Rendition - Set the format of the screen and text as specified by (n) /// This command is special in that the (n) position can accept between 0 and 16 parameters separated by semicolons. /// When no parameters are specified, it is treated the same as a single 0 parameter. /// https://terminalguide.namepad.de/seq/csi_sm/ /// public static string CSI_SetGraphicsRendition (params int [] parameters) => $"{CSI}{string.Join (";", parameters)}m"; /// /// ESC[38;5;{id}m - Set foreground color (256 colors) /// public static string CSI_SetForegroundColor (int id) => $"{CSI}38;5;{id}m"; /// /// ESC[48;5;{id}m - Set background color (256 colors) /// public static string CSI_SetBackgroundColor (int id) => $"{CSI}48;5;{id}m"; /// /// ESC[38;2;{r};{g};{b}m Set foreground color as RGB. /// public static string CSI_SetForegroundColorRGB (int r, int g, int b) => $"{CSI}38;2;{r};{g};{b}m"; /// /// ESC[48;2;{r};{g};{b}m Set background color as RGB. /// public static string CSI_SetBackgroundColorRGB (int r, int g, int b) => $"{CSI}48;2;{r};{g};{b}m"; #endregion #region Requests /// /// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR) /// https://terminalguide.namepad.de/seq/csi_sn__p-6/ /// public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n"; /// /// The terminal reply to . ESC [ ? (y) ; (x) R /// public const string CSI_RequestCursorPositionReport_Terminator = "R"; /// /// ESC [ 0 c - Send Device Attributes (Primary DA) /// /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Application-Program-Command-functions /// https://www.xfree86.org/current/ctlseqs.html /// Windows Terminal v1.17 and below emits “\x1b[?1;0c”, indicating "VT101 with No Options". /// Windows Terminal v1.18+ emits: \x1b[?61;6;7;22;23;24;28;32;42c" /// See https://github.com/microsoft/terminal/pull/14906 /// /// 61 - The device conforms to level 1 of the character cell display architecture /// (See https://github.com/microsoft/terminal/issues/15693#issuecomment-1633304497) /// 6 = Selective erase /// 7 = Soft fonts /// 22 = Color text /// 23 = Greek character sets /// 24 = Turkish character sets /// 28 = Rectangular area operations /// 32 = Text macros /// 42 = ISO Latin-2 character set /// /// public static readonly string CSI_SendDeviceAttributes = CSI + "0c"; /// /// ESC [ > 0 c - Send Device Attributes (Secondary DA) /// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220) /// public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c"; /// /// The terminator indicating a reply to or /// /// public const string CSI_ReportDeviceAttributes_Terminator = "c"; /// /// CSI 1 8 t | yes | yes | yes | report window size in chars /// https://terminalguide.namepad.de/seq/csi_st-18/ /// public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t"; /// /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// public const string CSI_ReportTerminalSizeInChars_Terminator = "t"; /// /// The value of the response to indicating value 1 and 2 are the terminal size in chars. /// public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8"; #endregion /// /// Ensures a console key is mapped to one that works correctly with ANSI escape sequences. /// /// The . /// The modified. public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) { ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo; ConsoleKey key; var keyChar = consoleKeyInfo.KeyChar; switch ((uint)keyChar) { case 0: if (consoleKeyInfo.Key == (ConsoleKey)64) { // Ctrl+Space in Windows. newConsoleKeyInfo = new ConsoleKeyInfo (' ', ConsoleKey.Spacebar, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } break; case uint n when n > 0 && n <= KeyEsc: if (consoleKeyInfo.Key == 0 && consoleKeyInfo.KeyChar == '\r') { key = ConsoleKey.Enter; newConsoleKeyInfo = new ConsoleKeyInfo (consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } else if (consoleKeyInfo.Key == 0) { key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1); newConsoleKeyInfo = new ConsoleKeyInfo ((char)key, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, true); } break; case 127: // DEL newConsoleKeyInfo = new ConsoleKeyInfo (consoleKeyInfo.KeyChar, ConsoleKey.Backspace, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); break; default: newConsoleKeyInfo = consoleKeyInfo; break; } return newConsoleKeyInfo; } /// /// A helper to resize the as needed. /// /// The . /// The array to resize. /// The resized. public static ConsoleKeyInfo [] ResizeArray (ConsoleKeyInfo consoleKeyInfo, ConsoleKeyInfo [] cki) { Array.Resize (ref cki, cki == null ? 1 : cki.Length + 1); cki [cki.Length - 1] = consoleKeyInfo; return cki; } /// /// Decodes an ANSI escape sequence. /// /// The which may contain a request. /// The which may changes. /// The which may changes. /// The array. /// The which may changes. /// The control returned by the method. /// The code returned by the method. /// The values returned by the method. /// The terminator returned by the method. /// Indicates if the escape sequence is a mouse event. /// The button state. /// The position. /// Indicates if the escape sequence is a response to a request. /// The handler that will process the event. public static void DecodeEscSeq (EscSeqRequests escSeqRequests, ref ConsoleKeyInfo newConsoleKeyInfo, ref ConsoleKey key, ConsoleKeyInfo [] cki, ref ConsoleModifiers mod, out string c1Control, out string code, out string [] values, out string terminator, out bool isMouse, out List buttonState, out Point pos, out bool isResponse, Action continuousButtonPressedHandler) { char [] kChars = GetKeyCharArray (cki); (c1Control, code, values, terminator) = GetEscapeResult (kChars); isMouse = false; buttonState = new List () { 0 }; pos = default; isResponse = false; switch (c1Control) { case "ESC": if (values == null && string.IsNullOrEmpty (terminator)) { key = ConsoleKey.Escape; newConsoleKeyInfo = new ConsoleKeyInfo (cki [0].KeyChar, key, (mod & ConsoleModifiers.Shift) != 0, (mod & ConsoleModifiers.Alt) != 0, (mod & ConsoleModifiers.Control) != 0); } else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26) { key = (ConsoleKey)(char)(cki [1].KeyChar + (uint)ConsoleKey.A - 1); newConsoleKeyInfo = new ConsoleKeyInfo (cki [1].KeyChar, key, false, true, true); } else { if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122) { key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0]; } else { key = (ConsoleKey)cki [1].KeyChar; } newConsoleKeyInfo = new ConsoleKeyInfo ((char)key, (ConsoleKey)Math.Min ((uint)key, 255), false, true, false); } break; case "SS3": key = GetConsoleKey (terminator [0], values [0], ref mod); newConsoleKeyInfo = new ConsoleKeyInfo ('\0', key, (mod & ConsoleModifiers.Shift) != 0, (mod & ConsoleModifiers.Alt) != 0, (mod & ConsoleModifiers.Control) != 0); break; case "CSI": if (!string.IsNullOrEmpty (code) && code == "<") { GetMouse (cki, out buttonState, out pos, continuousButtonPressedHandler); isMouse = true; return; } else if (escSeqRequests != null && escSeqRequests.HasResponse (terminator)) { isResponse = true; escSeqRequests.Remove (terminator); return; } if (!string.IsNullOrEmpty (terminator)) { key = GetConsoleKey (terminator [0], values [0], ref mod); if (key != 0 && values.Length > 1) { mod |= GetConsoleModifiers (values [1]); } newConsoleKeyInfo = new ConsoleKeyInfo ('\0', key, (mod & ConsoleModifiers.Shift) != 0, (mod & ConsoleModifiers.Alt) != 0, (mod & ConsoleModifiers.Control) != 0); } else { // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/2803 // This is caused by NetDriver depending on Console.KeyAvailable? throw new InvalidOperationException ($"CSI response, but there's no terminator"); //newConsoleKeyInfo = new ConsoleKeyInfo ('\0', // key, // (mod & ConsoleModifiers.Shift) != 0, // (mod & ConsoleModifiers.Alt) != 0, // (mod & ConsoleModifiers.Control) != 0); } break; } } /// /// Gets all the needed information about a escape sequence. /// /// The array with all chars. /// /// The c1Control returned by , code, values and terminating. /// public static (string c1Control, string code, string [] values, string terminating) GetEscapeResult (char [] kChar) { if (kChar == null || kChar.Length == 0) { return (null, null, null, null); } if (kChar [0] != KeyEsc) { throw new InvalidOperationException ("Invalid escape character!"); } if (kChar.Length == 1) { return ("ESC", null, null, null); } if (kChar.Length == 2) { return ("ESC", null, null, kChar [1].ToString ()); } string c1Control = GetC1ControlChar (kChar [1]); string code = null; int nSep = kChar.Count (x => x == ';') + 1; string [] values = new string [nSep]; int valueIdx = 0; string terminating = ""; for (int i = 2; i < kChar.Length; i++) { var c = kChar [i]; if (char.IsDigit (c)) { values [valueIdx] += c.ToString (); } else if (c == ';') { valueIdx++; } else if (valueIdx == nSep - 1 || i == kChar.Length - 1) { terminating += c.ToString (); } else { code += c.ToString (); } } return (c1Control, code, values, terminating); } /// /// Gets the c1Control used in the called escape sequence. /// /// The char used. /// The c1Control. public static string GetC1ControlChar (char c) { // These control characters are used in the vtXXX emulation. switch (c) { case 'D': return "IND"; // Index case 'E': return "NEL"; // Next Line case 'H': return "HTS"; // Tab Set case 'M': return "RI"; // Reverse Index case 'N': return "SS2"; // Single Shift Select of G2 Character Set: affects next character only case 'O': return "SS3"; // Single Shift Select of G3 Character Set: affects next character only case 'P': return "DCS"; // Device Control String case 'V': return "SPA"; // Start of Guarded Area case 'W': return "EPA"; // End of Guarded Area case 'X': return "SOS"; // Start of String case 'Z': return "DECID"; // Return Terminal ID Obsolete form of CSI c (DA) case '[': return "CSI"; // Control Sequence Introducer case '\\': return "ST"; // String Terminator case ']': return "OSC"; // Operating System Command case '^': return "PM"; // Privacy Message case '_': return "APC"; // Application Program Command default: return ""; // Not supported } } /// /// Gets the from the value. /// /// The value. /// The or zero. public static ConsoleModifiers GetConsoleModifiers (string value) { switch (value) { case "2": return ConsoleModifiers.Shift; case "3": return ConsoleModifiers.Alt; case "4": return ConsoleModifiers.Shift | ConsoleModifiers.Alt; case "5": return ConsoleModifiers.Control; case "6": return ConsoleModifiers.Shift | ConsoleModifiers.Control; case "7": return ConsoleModifiers.Alt | ConsoleModifiers.Control; case "8": return ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control; default: return 0; } } /// /// Gets the depending on terminating and value. /// /// The terminator indicating a reply to or . /// The value. /// The which may changes. /// The and probably the . public static ConsoleKey GetConsoleKey (char terminator, string value, ref ConsoleModifiers mod) { ConsoleKey key; switch (terminator) { case 'A': key = ConsoleKey.UpArrow; break; case 'B': key = ConsoleKey.DownArrow; break; case 'C': key = ConsoleKey.RightArrow; break; case 'D': key = ConsoleKey.LeftArrow; break; case 'F': key = ConsoleKey.End; break; case 'H': key = ConsoleKey.Home; break; case 'P': key = ConsoleKey.F1; break; case 'Q': key = ConsoleKey.F2; break; case 'R': key = ConsoleKey.F3; break; case 'S': key = ConsoleKey.F4; break; case 'Z': key = ConsoleKey.Tab; mod |= ConsoleModifiers.Shift; break; case '~': switch (value) { case "2": key = ConsoleKey.Insert; break; case "3": key = ConsoleKey.Delete; break; case "5": key = ConsoleKey.PageUp; break; case "6": key = ConsoleKey.PageDown; break; case "15": key = ConsoleKey.F5; break; case "17": key = ConsoleKey.F6; break; case "18": key = ConsoleKey.F7; break; case "19": key = ConsoleKey.F8; break; case "20": key = ConsoleKey.F9; break; case "21": key = ConsoleKey.F10; break; case "23": key = ConsoleKey.F11; break; case "24": key = ConsoleKey.F12; break; default: key = 0; break; } break; default: key = 0; break; } return key; } /// /// A helper to get only the from the array. /// /// /// The char array of the escape sequence. public static char [] GetKeyCharArray (ConsoleKeyInfo [] cki) { char [] kChar = new char [] { }; var length = 0; foreach (var kc in cki) { length++; Array.Resize (ref kChar, length); kChar [length - 1] = kc.KeyChar; } return kChar; } private static MouseFlags? lastMouseButtonPressed; //private static MouseFlags? lastMouseButtonReleased; private static bool isButtonPressed; //private static bool isButtonReleased; private static bool isButtonClicked; private static bool isButtonDoubleClicked; private static bool isButtonTripleClicked; private static Point point; /// /// Gets the mouse button flags and the position. /// /// The array. /// The mouse button flags. /// The mouse position. /// The handler that will process the event. public static void GetMouse (ConsoleKeyInfo [] cki, out List mouseFlags, out Point pos, Action continuousButtonPressedHandler) { MouseFlags buttonState = 0; pos = new Point (); int buttonCode = 0; bool foundButtonCode = false; int foundPoint = 0; string value = ""; var kChar = GetKeyCharArray (cki); //System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}"); for (int i = 0; i < kChar.Length; i++) { var c = kChar [i]; if (c == '<') { foundButtonCode = true; } else if (foundButtonCode && c != ';') { value += c.ToString (); } else if (c == ';') { if (foundButtonCode) { foundButtonCode = false; buttonCode = int.Parse (value); } if (foundPoint == 1) { pos.X = int.Parse (value) - 1; } value = ""; foundPoint++; } else if (foundPoint > 0 && c != 'm' && c != 'M') { value += c.ToString (); } else if (c == 'm' || c == 'M') { //pos.Y = int.Parse (value) + Console.WindowTop - 1; pos.Y = int.Parse (value) - 1; switch (buttonCode) { case 0: case 8: case 16: case 24: case 32: case 36: case 40: case 48: case 56: buttonState = c == 'M' ? MouseFlags.Button1Pressed : MouseFlags.Button1Released; break; case 1: case 9: case 17: case 25: case 33: case 37: case 41: case 45: case 49: case 53: case 57: case 61: buttonState = c == 'M' ? MouseFlags.Button2Pressed : MouseFlags.Button2Released; break; case 2: case 10: case 14: case 18: case 22: case 26: case 30: case 34: case 42: case 46: case 50: case 54: case 58: case 62: buttonState = c == 'M' ? MouseFlags.Button3Pressed : MouseFlags.Button3Released; break; case 35: //// Needed for Windows OS //if (isButtonPressed && c == 'm' // && (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed // || lastMouseEvent.ButtonState == MouseFlags.Button2Pressed // || lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) { // switch (lastMouseEvent.ButtonState) { // case MouseFlags.Button1Pressed: // buttonState = MouseFlags.Button1Released; // break; // case MouseFlags.Button2Pressed: // buttonState = MouseFlags.Button2Released; // break; // case MouseFlags.Button3Pressed: // buttonState = MouseFlags.Button3Released; // break; // } //} else { // buttonState = MouseFlags.ReportMousePosition; //} //break; case 39: case 43: case 47: case 51: case 55: case 59: case 63: buttonState = MouseFlags.ReportMousePosition; break; case 64: buttonState = MouseFlags.WheeledUp; break; case 65: buttonState = MouseFlags.WheeledDown; break; case 68: case 72: case 80: buttonState = MouseFlags.WheeledLeft; // Shift/Ctrl+WheeledUp break; case 69: case 73: case 81: buttonState = MouseFlags.WheeledRight; // Shift/Ctrl+WheeledDown break; } // Modifiers. switch (buttonCode) { case 8: case 9: case 10: case 43: buttonState |= MouseFlags.ButtonAlt; break; case 14: case 47: buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift; break; case 16: case 17: case 18: case 51: buttonState |= MouseFlags.ButtonCtrl; break; case 22: case 55: buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift; break; case 24: case 25: case 26: case 59: buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt; break; case 30: case 63: buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt; break; case 32: case 33: case 34: buttonState |= MouseFlags.ReportMousePosition; break; case 36: case 37: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift; break; case 39: case 68: case 69: buttonState |= MouseFlags.ButtonShift; break; case 40: case 41: case 42: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt; break; case 45: case 46: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift; break; case 48: case 49: case 50: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl; break; case 53: case 54: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift; break; case 56: case 57: case 58: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt; break; case 61: case 62: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt; break; } } } mouseFlags = new List () { MouseFlags.AllEvents }; if (lastMouseButtonPressed != null && !isButtonPressed && !buttonState.HasFlag (MouseFlags.ReportMousePosition) && !buttonState.HasFlag (MouseFlags.Button1Released) && !buttonState.HasFlag (MouseFlags.Button2Released) && !buttonState.HasFlag (MouseFlags.Button3Released) && !buttonState.HasFlag (MouseFlags.Button4Released)) { lastMouseButtonPressed = null; isButtonPressed = false; } if (!isButtonClicked && !isButtonDoubleClicked && ((buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed || buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed) && lastMouseButtonPressed == null) || isButtonPressed && lastMouseButtonPressed != null && buttonState.HasFlag (MouseFlags.ReportMousePosition)) { mouseFlags [0] = buttonState; lastMouseButtonPressed = buttonState; isButtonPressed = true; if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0) { point = new Point () { X = pos.X, Y = pos.Y }; Application.MainLoop.AddIdle (() => { Task.Run (async () => await ProcessContinuousButtonPressedAsync (buttonState, continuousButtonPressedHandler)); return false; }); } else if (mouseFlags [0] == MouseFlags.ReportMousePosition) { isButtonPressed = false; } } else if (isButtonDoubleClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed || buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed)) { mouseFlags [0] = GetButtonTripleClicked (buttonState); isButtonDoubleClicked = false; isButtonTripleClicked = true; } else if (isButtonClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed || buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed)) { mouseFlags [0] = GetButtonDoubleClicked (buttonState); isButtonClicked = false; isButtonDoubleClicked = true; Application.MainLoop.AddIdle (() => { Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); return false; }); } //else if (isButtonReleased && !isButtonClicked && buttonState == MouseFlags.ReportMousePosition) { // mouseFlag [0] = GetButtonClicked ((MouseFlags)lastMouseButtonReleased); // lastMouseButtonReleased = null; // isButtonReleased = false; // isButtonClicked = true; // Application.MainLoop.AddIdle (() => { // Task.Run (async () => await ProcessButtonClickedAsync ()); // return false; // }); //} else if (!isButtonClicked && !isButtonDoubleClicked && (buttonState == MouseFlags.Button1Released || buttonState == MouseFlags.Button2Released || buttonState == MouseFlags.Button3Released || buttonState == MouseFlags.Button4Released)) { mouseFlags [0] = buttonState; isButtonPressed = false; if (isButtonTripleClicked) { isButtonTripleClicked = false; } else if (pos.X == point.X && pos.Y == point.Y) { mouseFlags.Add (GetButtonClicked (buttonState)); isButtonClicked = true; Application.MainLoop.AddIdle (() => { Task.Run (async () => await ProcessButtonClickedAsync ()); return false; }); } point = pos; //if ((lastMouseButtonPressed & MouseFlags.ReportMousePosition) == 0) { // lastMouseButtonReleased = buttonState; // isButtonPressed = false; // isButtonReleased = true; //} else { // lastMouseButtonPressed = null; // isButtonPressed = false; //} } else if (buttonState == MouseFlags.WheeledUp) { mouseFlags [0] = MouseFlags.WheeledUp; } else if (buttonState == MouseFlags.WheeledDown) { mouseFlags [0] = MouseFlags.WheeledDown; } else if (buttonState == MouseFlags.WheeledLeft) { mouseFlags [0] = MouseFlags.WheeledLeft; } else if (buttonState == MouseFlags.WheeledRight) { mouseFlags [0] = MouseFlags.WheeledRight; } else if (buttonState == MouseFlags.ReportMousePosition) { mouseFlags [0] = MouseFlags.ReportMousePosition; } else { mouseFlags [0] = buttonState; //foreach (var flag in buttonState.GetUniqueFlags()) { // mouseFlag [0] |= flag; //} } mouseFlags [0] = SetControlKeyStates (buttonState, mouseFlags [0]); //buttonState = mouseFlags; //System.Diagnostics.Debug.WriteLine ($"buttonState: {buttonState} X: {pos.X} Y: {pos.Y}"); //foreach (var mf in mouseFlags) { // System.Diagnostics.Debug.WriteLine ($"mouseFlags: {mf} X: {pos.X} Y: {pos.Y}"); //} } private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action continuousButtonPressedHandler) { while (isButtonPressed) { await Task.Delay (100); //var me = new MouseEvent () { // X = point.X, // Y = point.Y, // Flags = mouseFlag //}; var view = Application.WantContinuousButtonPressedView; if (view == null) break; if (isButtonPressed && lastMouseButtonPressed != null && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { Application.MainLoop.Invoke (() => continuousButtonPressedHandler (mouseFlag, point)); } } } private static async Task ProcessButtonClickedAsync () { await Task.Delay (300); isButtonClicked = false; } private static async Task ProcessButtonDoubleClickedAsync () { await Task.Delay (300); isButtonDoubleClicked = false; } private static MouseFlags GetButtonClicked (MouseFlags mouseFlag) { MouseFlags mf = default; switch (mouseFlag) { case MouseFlags.Button1Released: mf = MouseFlags.Button1Clicked; break; case MouseFlags.Button2Released: mf = MouseFlags.Button2Clicked; break; case MouseFlags.Button3Released: mf = MouseFlags.Button3Clicked; break; } return mf; } private static MouseFlags GetButtonDoubleClicked (MouseFlags mouseFlag) { MouseFlags mf = default; switch (mouseFlag) { case MouseFlags.Button1Pressed: mf = MouseFlags.Button1DoubleClicked; break; case MouseFlags.Button2Pressed: mf = MouseFlags.Button2DoubleClicked; break; case MouseFlags.Button3Pressed: mf = MouseFlags.Button3DoubleClicked; break; } return mf; } private static MouseFlags GetButtonTripleClicked (MouseFlags mouseFlag) { MouseFlags mf = default; switch (mouseFlag) { case MouseFlags.Button1Pressed: mf = MouseFlags.Button1TripleClicked; break; case MouseFlags.Button2Pressed: mf = MouseFlags.Button2TripleClicked; break; case MouseFlags.Button3Pressed: mf = MouseFlags.Button3TripleClicked; break; } return mf; } private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlags mouseFlag) { if ((buttonState & MouseFlags.ButtonCtrl) != 0 && (mouseFlag & MouseFlags.ButtonCtrl) == 0) mouseFlag |= MouseFlags.ButtonCtrl; if ((buttonState & MouseFlags.ButtonShift) != 0 && (mouseFlag & MouseFlags.ButtonShift) == 0) mouseFlag |= MouseFlags.ButtonShift; if ((buttonState & MouseFlags.ButtonAlt) != 0 && (mouseFlag & MouseFlags.ButtonAlt) == 0) mouseFlag |= MouseFlags.ButtonAlt; return mouseFlag; } // TODO: Move this out of here and into ConsoleDriver or somewhere else. /// /// Get the terminal that holds the console driver. /// /// The process. /// If supported the executable console process, null otherwise. public static Process GetParentProcess (Process process) { if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return null; } string query = "SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = " + process.Id; using (ManagementObjectSearcher mos = new ManagementObjectSearcher (query)) { foreach (ManagementObject mo in mos.Get ()) { if (mo ["ParentProcessId"] != null) { try { var id = Convert.ToInt32 (mo ["ParentProcessId"]); return Process.GetProcessById (id); } catch { } } } } return null; } }