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 { /// /// 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 } /// /// Escape key code (ASCII 27/0x1B). /// public const char KeyEsc = (char)KeyCode.Esc; /// /// ESC [ - The CSI (Control Sequence Introducer). /// public const string CSI = "\u001B["; /// /// ESC [ ? 1047 h - Activate xterm alternative buffer (no backscroll) /// /// /// From /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ /// Use Alternate Screen Buffer, xterm. /// public static readonly string CSI_ActivateAltBufferNoBackscroll = CSI + "?1047h"; /// /// 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"; /// /// 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 [ ? 1047 l - Restore xterm working buffer (with backscroll) /// /// /// From /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ /// Use Normal Screen Buffer, xterm. Clear the screen first if in the Alternate Screen Buffer. /// public static readonly string CSI_RestoreAltBufferWithBackscroll = CSI + "?1047l"; /// /// ESC [ ? 1049 l - Restore cursor position and restore xterm working buffer (with backscroll) /// /// /// From /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ /// Use Normal Screen Buffer and restore cursor as in DECRC, xterm. /// resource.This combines the effects of the 1047 and 1048 modes. /// public static readonly string CSI_RestoreCursorAndRestoreAltBufferWithBackscroll = CSI + "?1049l"; /// /// ESC [ ? 1049 h - Save cursor position and activate xterm alternative buffer (no backscroll) /// /// /// From /// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ /// Save cursor as in DECSC, xterm. After saving the cursor, switch to the Alternate Screen Buffer, /// clearing it first. /// This control combines the effects of the 1047 and 1048 modes. /// Use this with terminfo-based applications rather than the 47 mode. /// public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h"; //private static bool isButtonReleased; private static bool isButtonClicked; private static bool isButtonDoubleClicked; //private static MouseFlags? lastMouseButtonReleased; // QUESTION: What's the difference between isButtonClicked and isButtonPressed? // Some clarity or comments would be handy, here. // It also seems like some enforcement of valid states might be a good idea. private static bool isButtonPressed; private static bool isButtonTripleClicked; private static MouseFlags? lastMouseButtonPressed; private static Point? point; /// /// Control sequence for disabling mouse events. /// public static string CSI_DisableMouseEvents { get; set; } = CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse; /// /// Control sequence for enabling mouse events. /// public static string CSI_EnableMouseEvents { get; set; } = CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse; /// /// ESC [ x J - Clears part of the screen. See . /// /// /// public static string CSI_ClearScreen (ClearScreenOptions option) { return $"{CSI}{(int)option}J"; } /// /// Decodes an ANSI escape sequence. /// /// The which may contain a request. /// The which may change. /// The which may change. /// The array. /// The which may change. /// 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 (Point); isResponse = false; char keyChar = '\0'; switch (c1Control) { case "ESC": if (values is 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, ref keyChar); newConsoleKeyInfo = new ConsoleKeyInfo ( keyChar, 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; } if (escSeqRequests is { } && escSeqRequests.HasResponse (terminator)) { isResponse = true; escSeqRequests.Remove (terminator); return; } if (!string.IsNullOrEmpty (terminator)) { key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar); if (key != 0 && values.Length > 1) { mod |= GetConsoleModifiers (values [1]); } newConsoleKeyInfo = new ConsoleKeyInfo ( keyChar, 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; } } #nullable enable /// /// Gets the c1Control used in the called escape sequence. /// /// The char used. /// The c1Control. [Pure] public static string GetC1ControlChar (in char c) { // These control characters are used in the vtXXX emulation. return c switch { 'D' => "IND", // Index 'E' => "NEL", // Next Line 'H' => "HTS", // Tab Set 'M' => "RI", // Reverse Index 'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only 'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only 'P' => "DCS", // Device Control String 'V' => "SPA", // Start of Guarded Area 'W' => "EPA", // End of Guarded Area 'X' => "SOS", // Start of String 'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA) '[' => "CSI", // Control Sequence Introducer '\\' => "ST", // String Terminator ']' => "OSC", // Operating System Command '^' => "PM", // Privacy Message '_' => "APC", // Application Program Command _ => string.Empty }; } /// /// Gets the depending on terminating and value. /// /// /// The terminator indicating a reply to or /// . /// /// The value. /// The which may change. /// Normally is '\0' but on some cases may need other value. /// The and probably the . public static ConsoleKey GetConsoleKey (char terminator, string? value, ref ConsoleModifiers mod, ref char keyChar) { if (terminator == 'Z') { mod |= ConsoleModifiers.Shift; } if (terminator == 'l') { keyChar = '+'; } if (terminator == 'm') { keyChar = '-'; } return (terminator, value) switch { ('A', _) => ConsoleKey.UpArrow, ('B', _) => ConsoleKey.DownArrow, ('C', _) => ConsoleKey.RightArrow, ('D', _) => ConsoleKey.LeftArrow, ('F', _) => ConsoleKey.End, ('H', _) => ConsoleKey.Home, ('P', _) => ConsoleKey.F1, ('Q', _) => ConsoleKey.F2, ('R', _) => ConsoleKey.F3, ('S', _) => ConsoleKey.F4, ('Z', _) => ConsoleKey.Tab, ('~', "2") => ConsoleKey.Insert, ('~', "3") => ConsoleKey.Delete, ('~', "5") => ConsoleKey.PageUp, ('~', "6") => ConsoleKey.PageDown, ('~', "15") => ConsoleKey.F5, ('~', "17") => ConsoleKey.F6, ('~', "18") => ConsoleKey.F7, ('~', "19") => ConsoleKey.F8, ('~', "20") => ConsoleKey.F9, ('~', "21") => ConsoleKey.F10, ('~', "23") => ConsoleKey.F11, ('~', "24") => ConsoleKey.F12, ('l', _) => ConsoleKey.Add, ('m', _) => ConsoleKey.Subtract, ('p', _) => ConsoleKey.Insert, ('q', _) => ConsoleKey.End, ('r', _) => ConsoleKey.DownArrow, ('s', _) => ConsoleKey.PageDown, ('t', _) => ConsoleKey.LeftArrow, ('u', _) => ConsoleKey.Clear, ('v', _) => ConsoleKey.RightArrow, ('w', _) => ConsoleKey.Home, ('x', _) => ConsoleKey.UpArrow, ('y', _) => ConsoleKey.PageUp, (_, _) => 0 }; } /// /// Gets the from the value. /// /// The value. /// The or zero. public static ConsoleModifiers GetConsoleModifiers (string? value) { return value switch { "2" => ConsoleModifiers.Shift, "3" => ConsoleModifiers.Alt, "4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt, "5" => ConsoleModifiers.Control, "6" => ConsoleModifiers.Shift | ConsoleModifiers.Control, "7" => ConsoleModifiers.Alt | ConsoleModifiers.Control, "8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control, _ => 0 }; } #nullable restore /// /// Gets all the needed information about an 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 is 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 (static x => x == ';') + 1; var values = new string [nSep]; var valueIdx = 0; var terminating = string.Empty; for (var i = 2; i < kChar.Length; i++) { char c = kChar [i]; if (char.IsDigit (c)) { // PERF: Ouch values [valueIdx] += c.ToString (); } else if (c == ';') { valueIdx++; } else if (valueIdx == nSep - 1 || i == kChar.Length - 1) { // PERF: Ouch terminating += c.ToString (); } else { // PERF: Ouch code += c.ToString (); } } return (c1Control, code, values, terminating); } /// /// A helper to get only the from the array. /// /// /// The char array of the escape sequence. // PERF: This is expensive public static char [] GetKeyCharArray (ConsoleKeyInfo [] cki) { char [] kChar = { }; var length = 0; foreach (ConsoleKeyInfo kc in cki) { length++; Array.Resize (ref kChar, length); kChar [length - 1] = kc.KeyChar; } return kChar; } /// /// 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 = Point.Empty; var buttonCode = 0; var foundButtonCode = false; var foundPoint = 0; string value = string.Empty; char [] kChar = GetKeyCharArray (cki); // PERF: This loop could benefit from use of Spans and other strategies to avoid copies. //System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}"); for (var i = 0; i < kChar.Length; i++) { // PERF: Copy char c = kChar [i]; if (c == '<') { foundButtonCode = true; } else if (foundButtonCode && c != ';') { // PERF: Ouch value += c.ToString (); } else if (c == ';') { if (foundButtonCode) { foundButtonCode = false; buttonCode = int.Parse (value); } if (foundPoint == 1) { pos.X = int.Parse (value) - 1; } value = string.Empty; 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 = [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 is null) || (isButtonPressed && lastMouseButtonPressed is { } && buttonState.HasFlag (MouseFlags.ReportMousePosition))) { mouseFlags [0] = buttonState; lastMouseButtonPressed = buttonState; isButtonPressed = true; point = pos; if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0) { Application.MainLoop.AddIdle ( () => { // INTENT: What's this trying to do? // The task itself is not awaited. Task.Run ( async () => await ProcessContinuousButtonPressedAsync ( buttonState, continuousButtonPressedHandler)); return false; }); } else if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition)) { point = pos; // The isButtonPressed must always be true, otherwise we can lose the feature // If mouse flags has ReportMousePosition this feature won't run // but is always prepared with the new location //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}"); //} } /// /// 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; char 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 is null ? 1 : cki.Length + 1); cki [cki.Length - 1] = consoleKeyInfo; return cki; } 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 async Task ProcessButtonClickedAsync () { await Task.Delay (300); isButtonClicked = false; } private static async Task ProcessButtonDoubleClickedAsync () { await Task.Delay (300); isButtonDoubleClicked = false; } private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action continuousButtonPressedHandler) { // PERF: Pause and poll in a hot loop. // This should be replaced with event dispatch and a synchronization primitive such as AutoResetEvent. // Will make a massive difference in responsiveness. while (isButtonPressed) { await Task.Delay (100); View view = Application.WantContinuousButtonPressedView; if (view is null) { break; } if (isButtonPressed && lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, point ?? Point.Empty)); } } } 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; } #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) { return $"{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 /// /// Origin is (1,1). /// Origin is (1,1). /// public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}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) { return $"{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) { return $"{CSI}{string.Join (";", parameters)}m"; } /// /// ESC [ (n) m - Uses to set the foreground color. /// /// One of the 16 color codes. /// public static string CSI_SetForegroundColor (AnsiColorCode code) { return CSI_SetGraphicsRendition ((int)code); } /// /// ESC [ (n) m - Uses to set the background color. /// /// One of the 16 color codes. /// public static string CSI_SetBackgroundColor (AnsiColorCode code) { return CSI_SetGraphicsRendition ((int)code + 10); } /// /// ESC[38;5;{id}m - Set foreground color (256 colors) /// public static string CSI_SetForegroundColor256 (int color) { return $"{CSI}38;5;{color}m"; } /// /// ESC[48;5;{id}m - Set background color (256 colors) /// public static string CSI_SetBackgroundColor256 (int color) { return $"{CSI}48;5;{color}m"; } /// /// ESC[38;2;{r};{g};{b}m Set foreground color as RGB. /// public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{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) { return $"{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 }