#nullable enable using System.Diagnostics; using System.Globalization; // ReSharper disable InconsistentNaming namespace Terminal.Gui.Drivers; /// /// 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 const char KeyEsc = (char)KeyCode.Esc; /// /// ESC [ - The CSI (Control Sequence Introducer). /// public const string CSI = "\u001B["; #region Screen Window Buffer /// /// 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 [ ? 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 [ ? 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"; /// /// ESC [ x J - Clears part of the screen. See . /// /// /// public static string CSI_ClearScreen (ClearScreenOptions option) { return $"{CSI}{(int)option}J"; } /// /// 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"; } #endregion Screen Window Buffer #region Mouse /// /// 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"; /// /// Control sequence for disabling mouse events. /// public static readonly string CSI_DisableMouseEvents = CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse; /// /// Control sequence for enabling mouse events. /// public static readonly string CSI_EnableMouseEvents = CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse; #endregion Mouse #region Keyboard /// /// Helper to set the Control key states based on the char. /// /// The char value. /// public static ConsoleKeyInfo MapChar (char ch) { return MapConsoleKeyInfo (new (ch, ConsoleKey.None, false, false, false)); } /// /// 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; var key = ConsoleKey.None; char keyChar = consoleKeyInfo.KeyChar; switch ((uint)keyChar) { case 0: if (consoleKeyInfo.Key == (ConsoleKey)64) { // Ctrl+Space in Windows. newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, ConsoleKey.Spacebar, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } else if (consoleKeyInfo.Key == ConsoleKey.None) { newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, ConsoleKey.Spacebar, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, true); } break; case uint n when n is > 0 and <= KeyEsc: if (consoleKeyInfo is { Key: 0, KeyChar: '\u001B' }) { key = ConsoleKey.Escape; newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } else if (consoleKeyInfo is { Key: 0, KeyChar: '\t' }) { key = ConsoleKey.Tab; newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } else if (consoleKeyInfo is { Key: 0, KeyChar: '\r' }) { key = ConsoleKey.Enter; newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } else if (consoleKeyInfo is { Key: 0, KeyChar: '\n' }) { key = ConsoleKey.Enter; newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, true); } else if (consoleKeyInfo is { Key: 0, KeyChar: '\b' }) { key = ConsoleKey.Backspace; newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, true); } else if (consoleKeyInfo.Key == 0) { key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1); newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, true); } break; case uint n when n is >= '\u001c' and <= '\u001f': key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + 24); newConsoleKeyInfo = new ( (char)key, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, true); break; case 127: // DEL key = ConsoleKey.Backspace; newConsoleKeyInfo = new ( consoleKeyInfo.KeyChar, key, (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); break; default: uint ck = ConsoleKeyMapping.MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey); if (isConsoleKey) { key = (ConsoleKey)ck; } newConsoleKeyInfo = new ( keyChar, key, GetShiftMod (consoleKeyInfo.Modifiers), (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); break; } return newConsoleKeyInfo; bool GetShiftMod (ConsoleModifiers modifiers) { if (consoleKeyInfo.KeyChar is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z && modifiers == ConsoleModifiers.None) { return true; } return (modifiers & ConsoleModifiers.Shift) != 0; } } internal static KeyCode MapKey (ConsoleKeyInfo keyInfo) { switch (keyInfo.Key) { case ConsoleKey.Multiply: case ConsoleKey.Add: case ConsoleKey.Separator: case ConsoleKey.Subtract: case ConsoleKey.Decimal: case ConsoleKey.Divide: case ConsoleKey.OemPeriod: case ConsoleKey.OemComma: case ConsoleKey.OemPlus: case ConsoleKey.OemMinus: case ConsoleKey.Packet: case ConsoleKey.Oem1: case ConsoleKey.Oem2: case ConsoleKey.Oem3: case ConsoleKey.Oem4: case ConsoleKey.Oem5: case ConsoleKey.Oem6: case ConsoleKey.Oem7: case ConsoleKey.Oem8: case ConsoleKey.Oem102: if (keyInfo.KeyChar == 0) { // All Oem* produce a valid KeyChar and is not guaranteed to be printable ASCII, but it’s never just '\0' (null). // If that happens it's because Console.ReadKey is misreporting for AltGr + non-character keys // or if it's a combine key waiting for the next input which will determine the respective KeyChar. // This behavior only happens on Windows and not on Unix-like systems. if (keyInfo.Key != ConsoleKey.Multiply && keyInfo.Key != ConsoleKey.Add && keyInfo.Key != ConsoleKey.Decimal && keyInfo.Key != ConsoleKey.Subtract && keyInfo.Key != ConsoleKey.Divide && keyInfo.Key != ConsoleKey.OemPeriod && keyInfo.Key != ConsoleKey.OemComma && keyInfo.Key != ConsoleKey.OemPlus && keyInfo.Key != ConsoleKey.OemMinus && keyInfo.Key != ConsoleKey.Oem1 && keyInfo.Key != ConsoleKey.Oem2 && keyInfo.Key != ConsoleKey.Oem3 && keyInfo.Key != ConsoleKey.Oem4 && keyInfo.Key != ConsoleKey.Oem5 && keyInfo.Key != ConsoleKey.Oem6 && keyInfo.Key != ConsoleKey.Oem7 && keyInfo.Key != ConsoleKey.Oem102) { // If the keyChar is 0, keyInfo.Key value is not a printable character. Debug.Assert (keyInfo.Key == 0); } return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key); } if (keyInfo.Modifiers != ConsoleModifiers.Shift) { // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); } // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç") // and passing on Shift would be redundant. return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); } // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) { if (keyInfo is { Modifiers: ConsoleModifiers.Control, Key: ConsoleKey.I }) { return KeyCode.Tab; } return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key); } // Handle control keys (e.g. CursorUp) if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) { return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); } if ((ConsoleKey)keyInfo.KeyChar is >= ConsoleKey.A and <= ConsoleKey.Z) { // Shifted keyInfo = new ( keyInfo.KeyChar, (ConsoleKey)keyInfo.KeyChar, true, keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); } if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z) { // Unshifted keyInfo = new ( keyInfo.KeyChar, (ConsoleKey)(keyInfo.KeyChar - 32), false, keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); } if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) { if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) { // DotNetDriver doesn't support Shift-Ctrl/Shift-Alt combos return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key); } if (keyInfo.Modifiers == ConsoleModifiers.Shift) { // If ShiftMask is on add the ShiftMask if (char.IsUpper (keyInfo.KeyChar)) { return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; } } return (KeyCode)keyInfo.Key; } return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); } #endregion Keyboard #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 [ < 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 [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column /// of the y line /// /// StringBuilder where to append the cursor position sequence. /// Origin is (1,1). /// Origin is (1,1). public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int col) { // InterpolatedStringHandler is composed in stack, skipping the string allocation. builder.Append ($"{CSI}{row};{col}H"); } /// /// 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 /// /// TextWriter where to write the cursor position sequence. /// Origin is (1,1). /// Origin is (1,1). public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col) { const int maxInputBufferSize = // CSI (2) + ';' + 'H' 4 + // row + col (2x int sign + int max value) 2 + 20; Span buffer = stackalloc char [maxInputBufferSize]; if (!buffer.TryWrite (CultureInfo.InvariantCulture, $"{CSI}{row};{col}H", out int charsWritten)) { var tooLongCursorPositionSequence = $"{CSI}{row};{col}H"; throw new InvalidOperationException ( $"{nameof (CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length})."); } ReadOnlySpan cursorPositionSequence = buffer [..charsWritten]; writer.Write (cursorPositionSequence); } //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 Cursor #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[38;2;{r};{g};{b}m Append foreground color as RGB to StringBuilder. /// public static void CSI_AppendForegroundColorRGB (StringBuilder builder, int r, int g, int b) { // InterpolatedStringHandler is composed in stack, skipping the string allocation. builder.Append ($"{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"; } /// /// ESC[48;2;{r};{g};{b}m Append background color as RGB to StringBuilder. /// public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, int g, int b) { // InterpolatedStringHandler is composed in stack, skipping the string allocation. builder.Append ($"{CSI}48;2;{r};{g};{b}m"); } #endregion Colors #region Text Styles /// /// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one /// to another. /// /// to add escape sequence to. /// Previous to change away from. /// Next to change to. /// /// /// Unlike colors, most text styling options are not mutually exclusive with each other, and can be applied /// independently. This creates a problem when /// switching from one style to another: For instance, if your previous style is just bold, and your next style is /// just italic, then simply adding the /// sequence to enable italic text would cause the text to remain bold. This method automatically handles this /// problem, enabling and disabling styles as /// necessary to apply exactly the next style. /// /// internal static void CSI_AppendTextStyleChange (StringBuilder output, TextStyle prev, TextStyle next) { // Do nothing if styles are the same, as no changes are necessary. if (prev == next) { return; } // Bitwise operations to determine flag changes. A ^ B are the flags different between two flag sets. These different flags that exist in the next flag // set (diff & next) are the ones that were enabled in the switch, those that exist in the previous flag set (diff & prev) are the ones that were // disabled. TextStyle diff = prev ^ next; TextStyle enabled = diff & next; TextStyle disabled = diff & prev; // List of escape codes to apply. List sgr = new (); if (disabled != TextStyle.None) { // Special case: Both bold and faint have the same disabling code. While unusual, it can be valid to have both enabled at the same time, so when // one and only one of them is being disabled, we need to re-enable the other afterward. We can check what flags remain enabled by taking // prev & next, as this is the set of flags both have. if (disabled.HasFlag (TextStyle.Bold)) { sgr.Add (22); if ((prev & next).HasFlag (TextStyle.Faint)) { sgr.Add (2); } } if (disabled.HasFlag (TextStyle.Faint)) { sgr.Add (22); if ((prev & next).HasFlag (TextStyle.Bold)) { sgr.Add (1); } } if (disabled.HasFlag (TextStyle.Italic)) { sgr.Add (23); } if (disabled.HasFlag (TextStyle.Underline)) { sgr.Add (24); } if (disabled.HasFlag (TextStyle.Blink)) { sgr.Add (25); } if (disabled.HasFlag (TextStyle.Reverse)) { sgr.Add (27); } if (disabled.HasFlag (TextStyle.Strikethrough)) { sgr.Add (29); } } if (enabled != TextStyle.None) { if (enabled.HasFlag (TextStyle.Bold)) { sgr.Add (1); } if (enabled.HasFlag (TextStyle.Faint)) { sgr.Add (2); } if (enabled.HasFlag (TextStyle.Italic)) { sgr.Add (3); } if (enabled.HasFlag (TextStyle.Underline)) { sgr.Add (4); } if (enabled.HasFlag (TextStyle.Blink)) { sgr.Add (5); } if (enabled.HasFlag (TextStyle.Reverse)) { sgr.Add (7); } if (enabled.HasFlag (TextStyle.Strikethrough)) { sgr.Add (9); } } output.Append ("\x1b["); output.Append (string.Join (';', sgr)); output.Append ('m'); } #endregion Text Styles #region Requests /// /// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR) /// https://terminalguide.namepad.de/seq/csi_sn__p-6/ /// The terminal reply to . ESC [ ? (y) ; (x) ; 1 R /// public static readonly AnsiEscapeSequence CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" }; /// /// 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 /// The terminator indicating a reply to or /// /// public static readonly AnsiEscapeSequence CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" }; /// /// 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 AnsiEscapeSequence CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" }; /// /// CSI 16 t - Request sixel resolution (width and height in pixels) /// public static readonly AnsiEscapeSequence CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" }; /// /// CSI 14 t - Request window size in pixels (width x height) /// public static readonly AnsiEscapeSequence CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" }; /// /// CSI 1 8 t | yes | yes | yes | report window size in chars /// https://terminalguide.namepad.de/seq/csi_st-18/ /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// public static readonly AnsiEscapeSequence CSI_ReportWindowSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" }; /// /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// public const string CSI_ReportWindowSizeInChars_Terminator = "t"; /// /// The value of the response to indicating value 1 and 2 are the terminal /// size in chars. /// public const string CSI_ReportWindowSizeInChars_ResponseValue = "8"; #endregion Requests #region OSC /// /// OSC (Operating System Command) escape sequence prefix. /// /// /// OSC sequences are used for operating system-specific commands like setting window title, /// hyperlinks (OSC 8), and other terminal emulator features. /// public const string OSC = "\u001B]"; /// /// String Terminator (ST) - terminates OSC sequences. /// /// /// Can also be represented as BEL (0x07) in some terminals, but ST is more modern. /// public const string ST = "\u001B\\"; /// /// Starts a hyperlink using OSC 8 escape sequence. /// /// The URL to link to (e.g., "https://github.com"). /// Optional hyperlink ID for matching start/end pairs. Use null for automatic matching. /// The OSC 8 start sequence. /// /// OSC 8 format: ESC ] 8 ; params ; URL ST /// Supported in Windows Terminal, iTerm2, and other modern terminals. /// Must be followed by visible text, then terminated with . /// public static string OSC_StartHyperlink (string url, string? id = null) { // Format: ESC ] 8 ; params ; URL ST // params can include "id=value" for matching start/end string parameters = string.IsNullOrEmpty (id) ? "" : $"id={id}"; return $"{OSC}8;{parameters};{url}{ST}"; } /// /// Ends a hyperlink using OSC 8 escape sequence. /// /// The OSC 8 end sequence. /// /// This terminates the hyperlink started by . /// Format: ESC ] 8 ; ; ST (empty URL ends the hyperlink). /// public static string OSC_EndHyperlink () { // Format: ESC ] 8 ; ; ST (empty URL ends hyperlink) return $"{OSC}8;;{ST}"; } #endregion OSC /// /// Convert a array to string. /// /// /// The string representing the array. public static string ToString (ConsoleKeyInfo [] consoleKeyInfos) { StringBuilder sb = new (); foreach (ConsoleKeyInfo keyChar in consoleKeyInfos) { sb.Append (keyChar.KeyChar); } return sb.ToString (); } }