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
}