diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadStdinUnbuffered.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadStdinUnbuffered.cs
index 844e669682b1a..b56e66ef7dee1 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadStdinUnbuffered.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadStdinUnbuffered.cs
@@ -12,7 +12,7 @@ internal static partial class Sys
internal static unsafe partial int ReadStdin(byte* buffer, int bufferSize);
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_InitializeConsoleBeforeRead")]
- internal static partial void InitializeConsoleBeforeRead(byte minChars = 1, byte decisecondsTimeout = 0);
+ internal static partial void InitializeConsoleBeforeRead([MarshalAs(UnmanagedType.Bool)] bool distinguishNewLines, byte minChars = 1, byte decisecondsTimeout = 0);
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_UninitializeConsoleAfterRead")]
internal static partial void UninitializeConsoleAfterRead();
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetSignalForBreak.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetSignalForBreak.cs
index a235e8e133a01..8ad4eba93c917 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetSignalForBreak.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SetSignalForBreak.cs
@@ -12,6 +12,6 @@ internal static partial class Sys
internal static partial int GetSignalForBreak();
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SetSignalForBreak")]
- internal static partial int SetSignalForBreak(int signalForBreak);
+ internal static partial int SetSignalForBreak(int signalForBreak, [MarshalAs(UnmanagedType.Bool)] bool distinguishNewLines);
}
}
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.StdinReady.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.StdinReady.cs
index ea191c81900e5..a5016d463c922 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.StdinReady.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.StdinReady.cs
@@ -9,6 +9,6 @@ internal static partial class Sys
{
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_StdinReady")]
[return: MarshalAs(UnmanagedType.Bool)]
- internal static partial bool StdinReady();
+ internal static partial bool StdinReady([MarshalAs(UnmanagedType.Bool)] bool distinguishNewLines);
}
}
diff --git a/src/libraries/Common/src/System/Console/ConsoleUtils.cs b/src/libraries/Common/src/System/Console/ConsoleUtils.cs
index 2cec17fb2e637..551a719018e17 100644
--- a/src/libraries/Common/src/System/Console/ConsoleUtils.cs
+++ b/src/libraries/Common/src/System/Console/ConsoleUtils.cs
@@ -10,6 +10,8 @@ internal static partial class ConsoleUtils
/// Whether to output ansi color strings.
private static volatile int s_emitAnsiColorCodes = -1;
+ private static volatile int s_useNet6KeyParser = -1;
+
/// Get whether to emit ANSI color codes.
public static bool EmitAnsiColorCodes
{
@@ -49,5 +51,31 @@ public static bool EmitAnsiColorCodes
return enabled;
}
}
+
+ internal static bool UseNet6KeyParser
+ {
+ get
+ {
+ int useNet6KeyParser = s_useNet6KeyParser;
+
+ if (useNet6KeyParser == -1)
+ {
+ useNet6KeyParser = s_useNet6KeyParser = GetNet6CompatReadKeySetting() ? 1 : 0;
+ }
+
+ return useNet6KeyParser == 1;
+
+ static bool GetNet6CompatReadKeySetting()
+ {
+ if (AppContext.TryGetSwitch("System.Console.UseNet6CompatReadKey", out bool fileConfig))
+ {
+ return fileConfig;
+ }
+
+ string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_USENET6COMPATREADKEY");
+ return envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ }
}
}
diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj
index 806daf70c3e4a..ea763aa3a4631 100644
--- a/src/libraries/System.Console/src/System.Console.csproj
+++ b/src/libraries/System.Console/src/System.Console.csproj
@@ -158,11 +158,18 @@
+
+
+
+
+
+
+ Gets the lazily-initialized terminal information for the terminal.
+ public static TerminalFormatStrings TerminalFormatStringsInstance { get { return s_terminalFormatStringsInstance.Value; } }
+ private static readonly Lazy s_terminalFormatStringsInstance = new(() => new TerminalFormatStrings(TermInfo.DatabaseFactory.ReadActiveDatabase()));
+
public static Stream OpenStandardInput()
{
return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read,
@@ -149,7 +153,7 @@ public static bool TreatControlCAsInput
if (!Console.IsInputRedirected)
{
EnsureConsoleInitialized();
- if (Interop.Sys.SetSignalForBreak(Convert.ToInt32(!value)) == 0)
+ if (Interop.Sys.SetSignalForBreak(Convert.ToInt32(!value), distinguishNewLines: !ConsoleUtils.UseNet6KeyParser) == 0)
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
}
}
@@ -198,7 +202,7 @@ public static string Title
if (Console.IsOutputRedirected)
return;
- string? titleFormat = TerminalFormatStrings.Instance.Title;
+ string? titleFormat = TerminalFormatStringsInstance.Title;
if (!string.IsNullOrEmpty(titleFormat))
{
string ansiStr = TermInfo.ParameterizedStrings.Evaluate(titleFormat, value);
@@ -211,7 +215,7 @@ public static void Beep()
{
if (!Console.IsOutputRedirected)
{
- WriteStdoutAnsiString(TerminalFormatStrings.Instance.Bell, mayChangeCursorPosition: false);
+ WriteStdoutAnsiString(TerminalFormatStringsInstance.Bell, mayChangeCursorPosition: false);
}
}
@@ -224,7 +228,7 @@ public static void Clear()
{
if (!Console.IsOutputRedirected)
{
- WriteStdoutAnsiString(TerminalFormatStrings.Instance.Clear);
+ WriteStdoutAnsiString(TerminalFormatStringsInstance.Clear);
}
}
@@ -242,7 +246,7 @@ public static void SetCursorPosition(int left, int top)
return;
}
- string? cursorAddressFormat = TerminalFormatStrings.Instance.CursorAddress;
+ string? cursorAddressFormat = TerminalFormatStringsInstance.CursorAddress;
if (!string.IsNullOrEmpty(cursorAddressFormat))
{
string ansiStr = TermInfo.ParameterizedStrings.Evaluate(cursorAddressFormat, top, left);
@@ -372,8 +376,8 @@ private static void GetWindowSize(out int width, out int height)
}
else
{
- s_windowWidth = TerminalFormatStrings.Instance.Columns;
- s_windowHeight = TerminalFormatStrings.Instance.Lines;
+ s_windowWidth = TerminalFormatStringsInstance.Columns;
+ s_windowHeight = TerminalFormatStringsInstance.Lines;
}
}
width = s_windowWidth;
@@ -399,8 +403,8 @@ public static bool CursorVisible
if (!Console.IsOutputRedirected)
{
WriteStdoutAnsiString(value ?
- TerminalFormatStrings.Instance.CursorVisible :
- TerminalFormatStrings.Instance.CursorInvisible);
+ TerminalFormatStringsInstance.CursorVisible :
+ TerminalFormatStringsInstance.CursorInvisible);
}
}
}
@@ -475,7 +479,7 @@ internal static bool TryGetCursorPosition(out int left, out int top, bool reinit
// involved in reading/writing, such as when accessing a remote system. We also extend
// the timeout on the very first request to 15 seconds, to account for potential latency
// before we know if we will receive a response.
- Interop.Sys.InitializeConsoleBeforeRead(minChars: (byte)(s_everReceivedCursorPositionResponse ? 1 : 0), decisecondsTimeout: (byte)(s_firstCursorPositionRequest ? 100 : 10));
+ Interop.Sys.InitializeConsoleBeforeRead(distinguishNewLines: !ConsoleUtils.UseNet6KeyParser, minChars: (byte)(s_everReceivedCursorPositionResponse ? 1 : 0), decisecondsTimeout: (byte)(s_firstCursorPositionRequest ? 100 : 10));
try
{
// Write out the cursor position report request.
@@ -550,7 +554,7 @@ internal static bool TryGetCursorPosition(out int left, out int top, bool reinit
{
if (reinitializeForRead)
{
- Interop.Sys.InitializeConsoleBeforeRead();
+ Interop.Sys.InitializeConsoleBeforeRead(distinguishNewLines: !ConsoleUtils.UseNet6KeyParser);
}
else
{
@@ -784,10 +788,10 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
}
// We haven't yet computed a format string. Compute it, use it, then cache it.
- string? formatString = foreground ? TerminalFormatStrings.Instance.Foreground : TerminalFormatStrings.Instance.Background;
+ string? formatString = foreground ? TerminalFormatStringsInstance.Foreground : TerminalFormatStringsInstance.Background;
if (!string.IsNullOrEmpty(formatString))
{
- int maxColors = TerminalFormatStrings.Instance.MaxColors; // often 8 or 16; 0 is invalid
+ int maxColors = TerminalFormatStringsInstance.MaxColors; // often 8 or 16; 0 is invalid
if (maxColors > 0)
{
// The values of the ConsoleColor enums unfortunately don't map to the
@@ -831,62 +835,20 @@ private static void WriteResetColorString()
{
if (ConsoleUtils.EmitAnsiColorCodes)
{
- WriteStdoutAnsiString(TerminalFormatStrings.Instance.Reset);
+ WriteStdoutAnsiString(TerminalFormatStringsInstance.Reset);
}
}
/// Cache of the format strings for foreground/background and ConsoleColor.
private static readonly string[,] s_fgbgAndColorStrings = new string[2, 16]; // 2 == fg vs bg, 16 == ConsoleColor values
- public static bool TryGetSpecialConsoleKey(char[] givenChars, int startIndex, int endIndex, out ConsoleKeyInfo key, out int keyLength)
- {
- int unprocessedCharCount = endIndex - startIndex;
-
- // First process special control character codes. These override anything from terminfo.
- if (unprocessedCharCount > 0)
- {
- // Is this an erase / backspace?
- char c = givenChars[startIndex];
- if (c != s_posixDisableValue && c == s_veraseCharacter)
- {
- key = new ConsoleKeyInfo(c, ConsoleKey.Backspace, shift: false, alt: false, control: false);
- keyLength = 1;
- return true;
- }
- }
-
- // Then process terminfo mappings.
- int minRange = TerminalFormatStrings.Instance.MinKeyFormatLength;
- if (unprocessedCharCount >= minRange)
- {
- int maxRange = Math.Min(unprocessedCharCount, TerminalFormatStrings.Instance.MaxKeyFormatLength);
-
- for (int i = maxRange; i >= minRange; i--)
- {
- var currentString = new ReadOnlyMemory(givenChars, startIndex, i);
-
- // Check if the string prefix matches.
- if (TerminalFormatStrings.Instance.KeyFormatToConsoleKey.TryGetValue(currentString, out key))
- {
- keyLength = currentString.Length;
- return true;
- }
- }
- }
-
- // Otherwise, not a known special console key.
- key = default(ConsoleKeyInfo);
- keyLength = 0;
- return false;
- }
-
/// Whether keypad_xmit has already been written out to the terminal.
private static volatile bool s_initialized;
/// Value used to indicate that a special character code isn't available.
internal static byte s_posixDisableValue;
/// Special control character code used to represent an erase (backspace).
- private static byte s_veraseCharacter;
+ internal static byte s_veraseCharacter;
/// Special control character that represents the end of a line.
internal static byte s_veolCharacter;
/// Special control character that represents the end of a line.
@@ -921,7 +883,7 @@ private static unsafe void EnsureInitializedCore()
// the native lib later to handle signals that require re-entering the mode.
if (!Console.IsOutputRedirected)
{
- string? keypadXmit = TerminalFormatStrings.Instance.KeypadXmit;
+ string? keypadXmit = TerminalFormatStringsInstance.KeypadXmit;
if (keypadXmit != null)
{
Interop.Sys.SetKeypadXmit(keypadXmit);
@@ -956,243 +918,6 @@ private static unsafe void EnsureInitializedCore()
}
}
- /// Provides format strings and related information for use with the current terminal.
- internal sealed class TerminalFormatStrings
- {
- /// Gets the lazily-initialized terminal information for the terminal.
- public static TerminalFormatStrings Instance { get { return s_instance.Value; } }
- private static readonly Lazy s_instance = new Lazy(() => new TerminalFormatStrings(TermInfo.Database.ReadActiveDatabase()));
-
- /// The format string to use to change the foreground color.
- public readonly string? Foreground;
- /// The format string to use to change the background color.
- public readonly string? Background;
- /// The format string to use to reset the foreground and background colors.
- public readonly string? Reset;
- /// The maximum number of colors supported by the terminal.
- public readonly int MaxColors;
- /// The number of columns in a format.
- public readonly int Columns;
- /// The number of lines in a format.
- public readonly int Lines;
- /// The format string to use to make cursor visible.
- public readonly string? CursorVisible;
- /// The format string to use to make cursor invisible
- public readonly string? CursorInvisible;
- /// The format string to use to set the window title.
- public readonly string? Title;
- /// The format string to use for an audible bell.
- public readonly string? Bell;
- /// The format string to use to clear the terminal.
- public readonly string? Clear;
- /// The format string to use to set the position of the cursor.
- public readonly string? CursorAddress;
- /// The format string to use to move the cursor to the left.
- public readonly string? CursorLeft;
- /// The format string to use to clear to the end of line.
- public readonly string? ClrEol;
- /// The ANSI-compatible string for the Cursor Position report request.
- ///
- /// This should really be in user string 7 in the terminfo file, but some terminfo databases
- /// are missing it. As this is defined to be supported by any ANSI-compatible terminal,
- /// we assume it's available; doing so means CursorTop/Left will work even if the terminfo database
- /// doesn't contain it (as appears to be the case with e.g. screen and tmux on Ubuntu), at the risk
- /// of outputting the sequence on some terminal that's not compatible.
- ///
- public const string CursorPositionReport = "\x1B[6n";
- ///
- /// The dictionary of keystring to ConsoleKeyInfo.
- /// Only some members of the ConsoleKeyInfo are used; in particular, the actual char is ignored.
- ///
- public readonly Dictionary, ConsoleKeyInfo> KeyFormatToConsoleKey =
- new Dictionary, ConsoleKeyInfo>(new ReadOnlyMemoryContentComparer());
-
- /// Max key length
- public readonly int MaxKeyFormatLength;
- /// Min key length
- public readonly int MinKeyFormatLength;
- /// The ANSI string used to enter "application" / "keypad transmit" mode.
- public readonly string? KeypadXmit;
-
- public TerminalFormatStrings(TermInfo.Database? db)
- {
- if (db == null)
- return;
-
- KeypadXmit = db.GetString(TermInfo.WellKnownStrings.KeypadXmit);
- Foreground = db.GetString(TermInfo.WellKnownStrings.SetAnsiForeground);
- Background = db.GetString(TermInfo.WellKnownStrings.SetAnsiBackground);
- Reset = db.GetString(TermInfo.WellKnownStrings.OrigPairs) ?? db.GetString(TermInfo.WellKnownStrings.OrigColors);
- Bell = db.GetString(TermInfo.WellKnownStrings.Bell);
- Clear = db.GetString(TermInfo.WellKnownStrings.Clear);
- Columns = db.GetNumber(TermInfo.WellKnownNumbers.Columns);
- Lines = db.GetNumber(TermInfo.WellKnownNumbers.Lines);
- CursorVisible = db.GetString(TermInfo.WellKnownStrings.CursorVisible);
- CursorInvisible = db.GetString(TermInfo.WellKnownStrings.CursorInvisible);
- CursorAddress = db.GetString(TermInfo.WellKnownStrings.CursorAddress);
- CursorLeft = db.GetString(TermInfo.WellKnownStrings.CursorLeft);
- ClrEol = db.GetString(TermInfo.WellKnownStrings.ClrEol);
-
- Title = GetTitle(db);
-
- Debug.WriteLineIf(db.GetString(TermInfo.WellKnownStrings.CursorPositionReport) != CursorPositionReport,
- "Getting the cursor position will only work if the terminal supports the CPR sequence," +
- "but the terminfo database does not contain an entry for it.");
-
- int maxColors = db.GetNumber(TermInfo.WellKnownNumbers.MaxColors);
- MaxColors = // normalize to either the full range of all ANSI colors, just the dark ones, or none
- maxColors >= 16 ? 16 :
- maxColors >= 8 ? 8 :
- 0;
-
- AddKey(db, TermInfo.WellKnownStrings.KeyF1, ConsoleKey.F1);
- AddKey(db, TermInfo.WellKnownStrings.KeyF2, ConsoleKey.F2);
- AddKey(db, TermInfo.WellKnownStrings.KeyF3, ConsoleKey.F3);
- AddKey(db, TermInfo.WellKnownStrings.KeyF4, ConsoleKey.F4);
- AddKey(db, TermInfo.WellKnownStrings.KeyF5, ConsoleKey.F5);
- AddKey(db, TermInfo.WellKnownStrings.KeyF6, ConsoleKey.F6);
- AddKey(db, TermInfo.WellKnownStrings.KeyF7, ConsoleKey.F7);
- AddKey(db, TermInfo.WellKnownStrings.KeyF8, ConsoleKey.F8);
- AddKey(db, TermInfo.WellKnownStrings.KeyF9, ConsoleKey.F9);
- AddKey(db, TermInfo.WellKnownStrings.KeyF10, ConsoleKey.F10);
- AddKey(db, TermInfo.WellKnownStrings.KeyF11, ConsoleKey.F11);
- AddKey(db, TermInfo.WellKnownStrings.KeyF12, ConsoleKey.F12);
- AddKey(db, TermInfo.WellKnownStrings.KeyF13, ConsoleKey.F13);
- AddKey(db, TermInfo.WellKnownStrings.KeyF14, ConsoleKey.F14);
- AddKey(db, TermInfo.WellKnownStrings.KeyF15, ConsoleKey.F15);
- AddKey(db, TermInfo.WellKnownStrings.KeyF16, ConsoleKey.F16);
- AddKey(db, TermInfo.WellKnownStrings.KeyF17, ConsoleKey.F17);
- AddKey(db, TermInfo.WellKnownStrings.KeyF18, ConsoleKey.F18);
- AddKey(db, TermInfo.WellKnownStrings.KeyF19, ConsoleKey.F19);
- AddKey(db, TermInfo.WellKnownStrings.KeyF20, ConsoleKey.F20);
- AddKey(db, TermInfo.WellKnownStrings.KeyF21, ConsoleKey.F21);
- AddKey(db, TermInfo.WellKnownStrings.KeyF22, ConsoleKey.F22);
- AddKey(db, TermInfo.WellKnownStrings.KeyF23, ConsoleKey.F23);
- AddKey(db, TermInfo.WellKnownStrings.KeyF24, ConsoleKey.F24);
- AddKey(db, TermInfo.WellKnownStrings.KeyBackspace, ConsoleKey.Backspace);
- AddKey(db, TermInfo.WellKnownStrings.KeyBackTab, ConsoleKey.Tab, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeyBegin, ConsoleKey.Home);
- AddKey(db, TermInfo.WellKnownStrings.KeyClear, ConsoleKey.Clear);
- AddKey(db, TermInfo.WellKnownStrings.KeyDelete, ConsoleKey.Delete);
- AddKey(db, TermInfo.WellKnownStrings.KeyDown, ConsoleKey.DownArrow);
- AddKey(db, TermInfo.WellKnownStrings.KeyEnd, ConsoleKey.End);
- AddKey(db, TermInfo.WellKnownStrings.KeyEnter, ConsoleKey.Enter);
- AddKey(db, TermInfo.WellKnownStrings.KeyHelp, ConsoleKey.Help);
- AddKey(db, TermInfo.WellKnownStrings.KeyHome, ConsoleKey.Home);
- AddKey(db, TermInfo.WellKnownStrings.KeyInsert, ConsoleKey.Insert);
- AddKey(db, TermInfo.WellKnownStrings.KeyLeft, ConsoleKey.LeftArrow);
- AddKey(db, TermInfo.WellKnownStrings.KeyPageDown, ConsoleKey.PageDown);
- AddKey(db, TermInfo.WellKnownStrings.KeyPageUp, ConsoleKey.PageUp);
- AddKey(db, TermInfo.WellKnownStrings.KeyPrint, ConsoleKey.Print);
- AddKey(db, TermInfo.WellKnownStrings.KeyRight, ConsoleKey.RightArrow);
- AddKey(db, TermInfo.WellKnownStrings.KeyScrollForward, ConsoleKey.PageDown, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeyScrollReverse, ConsoleKey.PageUp, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeySBegin, ConsoleKey.Home, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeySDelete, ConsoleKey.Delete, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeySHome, ConsoleKey.Home, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeySelect, ConsoleKey.Select);
- AddKey(db, TermInfo.WellKnownStrings.KeySLeft, ConsoleKey.LeftArrow, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeySPrint, ConsoleKey.Print, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeySRight, ConsoleKey.RightArrow, shift: true, alt: false, control: false);
- AddKey(db, TermInfo.WellKnownStrings.KeyUp, ConsoleKey.UpArrow);
- AddPrefixKey(db, "kLFT", ConsoleKey.LeftArrow);
- AddPrefixKey(db, "kRIT", ConsoleKey.RightArrow);
- AddPrefixKey(db, "kUP", ConsoleKey.UpArrow);
- AddPrefixKey(db, "kDN", ConsoleKey.DownArrow);
- AddPrefixKey(db, "kDC", ConsoleKey.Delete);
- AddPrefixKey(db, "kEND", ConsoleKey.End);
- AddPrefixKey(db, "kHOM", ConsoleKey.Home);
- AddPrefixKey(db, "kNXT", ConsoleKey.PageDown);
- AddPrefixKey(db, "kPRV", ConsoleKey.PageUp);
-
- if (KeyFormatToConsoleKey.Count > 0)
- {
- MaxKeyFormatLength = int.MinValue;
- MinKeyFormatLength = int.MaxValue;
-
- foreach (KeyValuePair, ConsoleKeyInfo> entry in KeyFormatToConsoleKey)
- {
- if (entry.Key.Length > MaxKeyFormatLength)
- {
- MaxKeyFormatLength = entry.Key.Length;
- }
- if (entry.Key.Length < MinKeyFormatLength)
- {
- MinKeyFormatLength = entry.Key.Length;
- }
- }
- }
- }
-
- private static string GetTitle(TermInfo.Database db)
- {
- // Try to get the format string from tsl/fsl and use it if they're available
- string? tsl = db.GetString(TermInfo.WellKnownStrings.ToStatusLine);
- string? fsl = db.GetString(TermInfo.WellKnownStrings.FromStatusLine);
- if (tsl != null && fsl != null)
- {
- return tsl + "%p1%s" + fsl;
- }
-
- string term = db.Term;
- if (term == null)
- {
- return string.Empty;
- }
-
- if (term.StartsWith("xterm", StringComparison.Ordinal)) // normalize all xterms to enable easier matching
- {
- term = "xterm";
- }
-
- switch (term)
- {
- case "aixterm":
- case "dtterm":
- case "linux":
- case "rxvt":
- case "xterm":
- return "\x1B]0;%p1%s\x07";
- case "cygwin":
- return "\x1B];%p1%s\x07";
- case "konsole":
- return "\x1B]30;%p1%s\x07";
- case "screen":
- return "\x1Bk%p1%s\x1B";
- default:
- return string.Empty;
- }
- }
-
- private void AddKey(TermInfo.Database db, TermInfo.WellKnownStrings keyId, ConsoleKey key)
- {
- AddKey(db, keyId, key, shift: false, alt: false, control: false);
- }
-
- private void AddKey(TermInfo.Database db, TermInfo.WellKnownStrings keyId, ConsoleKey key, bool shift, bool alt, bool control)
- {
- ReadOnlyMemory keyFormat = db.GetString(keyId).AsMemory();
- if (!keyFormat.IsEmpty)
- KeyFormatToConsoleKey[keyFormat] = new ConsoleKeyInfo('\0', key, shift, alt, control);
- }
-
- private void AddPrefixKey(TermInfo.Database db, string extendedNamePrefix, ConsoleKey key)
- {
- AddKey(db, extendedNamePrefix + "3", key, shift: false, alt: true, control: false);
- AddKey(db, extendedNamePrefix + "4", key, shift: true, alt: true, control: false);
- AddKey(db, extendedNamePrefix + "5", key, shift: false, alt: false, control: true);
- AddKey(db, extendedNamePrefix + "6", key, shift: true, alt: false, control: true);
- AddKey(db, extendedNamePrefix + "7", key, shift: false, alt: false, control: true);
- }
-
- private void AddKey(TermInfo.Database db, string extendedName, ConsoleKey key, bool shift, bool alt, bool control)
- {
- ReadOnlyMemory keyFormat = db.GetExtendedString(extendedName).AsMemory();
- if (!keyFormat.IsEmpty)
- KeyFormatToConsoleKey[keyFormat] = new ConsoleKeyInfo('\0', key, shift, alt, control);
- }
- }
-
/// Reads data from the file descriptor into the buffer.
/// The file descriptor.
/// The buffer to read into.
@@ -1428,14 +1153,5 @@ public override void Flush()
base.Flush();
}
}
-
- private sealed class ReadOnlyMemoryContentComparer : IEqualityComparer>
- {
- public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) =>
- x.Span.SequenceEqual(y.Span);
-
- public int GetHashCode(ReadOnlyMemory obj) =>
- string.GetHashCode(obj.Span);
- }
}
}
diff --git a/src/libraries/System.Console/src/System/IO/KeyParser.cs b/src/libraries/System.Console/src/System/IO/KeyParser.cs
new file mode 100644
index 0000000000000..d54e0800a42b9
--- /dev/null
+++ b/src/libraries/System.Console/src/System/IO/KeyParser.cs
@@ -0,0 +1,392 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.IO;
+
+internal static class KeyParser
+{
+ private const char Escape = '\u001B';
+ private const char Delete = '\u007F';
+ private const char VtSequenceEndTag = '~';
+ private const char ModifierSeparator = ';';
+ private const int MinimalSequenceLength = 3;
+ private const int SequencePrefixLength = 2; // ^[[ ("^[" stands for Escape)
+
+ internal static ConsoleKeyInfo Parse(char[] buffer, TerminalFormatStrings terminalFormatStrings, byte posixDisableValue, byte veraseCharacter, ref int startIndex, int endIndex)
+ {
+ int length = endIndex - startIndex;
+ Debug.Assert(length > 0);
+
+ // VERASE overrides anything from Terminfo. Both settings can be different for Linux and macOS.
+ if (buffer[startIndex] != posixDisableValue && buffer[startIndex] == veraseCharacter)
+ {
+ // the original char is preserved on purpose (backward compat + consistency)
+ return new ConsoleKeyInfo(buffer[startIndex++], ConsoleKey.Backspace, false, false, false);
+ }
+
+ // Escape Sequences start with Escape. But some terminals like PuTTY and rxvt prepend Escape to express that for given sequence Alt was pressed.
+ if (length >= MinimalSequenceLength + 1 && buffer[startIndex] == Escape && buffer[startIndex + 1] == Escape)
+ {
+ startIndex++;
+ if (TryParseTerminalInputSequence(buffer, terminalFormatStrings, out ConsoleKeyInfo parsed, ref startIndex, endIndex))
+ {
+ return new ConsoleKeyInfo(parsed.KeyChar, parsed.Key, (parsed.Modifiers & ConsoleModifiers.Shift) != 0, alt: true, (parsed.Modifiers & ConsoleModifiers.Control) != 0);
+ }
+ startIndex--;
+ }
+ else if (length >= MinimalSequenceLength && TryParseTerminalInputSequence(buffer, terminalFormatStrings, out ConsoleKeyInfo parsed, ref startIndex, endIndex))
+ {
+ return parsed;
+ }
+
+ if (length == 2 && buffer[startIndex] == Escape && buffer[startIndex + 1] != Escape)
+ {
+ startIndex++; // skip the Escape
+ return ParseFromSingleChar(buffer[startIndex++], isAlt: true);
+ }
+
+ return ParseFromSingleChar(buffer[startIndex++], isAlt: false);
+ }
+
+ private static bool TryParseTerminalInputSequence(char[] buffer, TerminalFormatStrings terminalFormatStrings, out ConsoleKeyInfo parsed, ref int startIndex, int endIndex)
+ {
+ ReadOnlySpan input = buffer.AsSpan(startIndex, endIndex - startIndex);
+ parsed = default;
+
+ // sequences start with either "^[[" or "^[O". "^[" stands for Escape (27).
+ if (input.Length < MinimalSequenceLength || input[0] != Escape || (input[1] != '[' && input[1] != 'O'))
+ {
+ return false;
+ }
+
+ Dictionary, ConsoleKeyInfo> terminfoDb = terminalFormatStrings.KeyFormatToConsoleKey; // the most important source of truth
+ ConsoleModifiers modifiers = 0;
+ ConsoleKey key;
+
+ // Is it a three character sequence? (examples: '^[[H' (Home), '^[OP' (F1))
+ if (input[1] == 'O' || char.IsAsciiLetter(input[2]) || input.Length == MinimalSequenceLength)
+ {
+ if (!terminfoDb.TryGetValue(buffer.AsMemory(startIndex, MinimalSequenceLength), out parsed))
+ {
+ // All terminals which use "^[O{letter}" escape sequences don't define conflicting mappings.
+ // Example: ^[OH either means Home or simply is not used by given terminal.
+ // But with "^[[{character}" sequences, there are conflicts between rxvt and SCO.
+ // Example: "^[[a" is Shift+UpArrow for rxvt and Shift+F3 for SCO.
+ (key, modifiers) = input[1] == 'O' || terminalFormatStrings.IsRxvtTerm
+ ? MapKeyIdOXterm(input[2], terminalFormatStrings.IsRxvtTerm)
+ : MapSCO(input[2]);
+
+ if (key == default)
+ {
+ return false; // it was not a known sequence
+ }
+
+ char keyChar = key switch
+ {
+ ConsoleKey.Enter => '\r', // "^[OM" should produce new line character (was not previously mapped this way)
+ ConsoleKey.Add => '+',
+ ConsoleKey.Subtract => '-',
+ ConsoleKey.Divide => '/',
+ ConsoleKey.Multiply => '*',
+ _ => default
+ };
+ parsed = Create(keyChar, key, modifiers);
+ }
+
+ startIndex += MinimalSequenceLength;
+ return true;
+ }
+
+ // Is it a four character sequence used by Linux Console or PuTTy configured to emulate it? (examples: '^[[[A' (F1), '^[[[B' (F2))
+ if (input[1] == '[' && input[2] == '[' && char.IsBetween(input[3], 'A', 'E'))
+ {
+ if (!terminfoDb.TryGetValue(buffer.AsMemory(startIndex, 4), out parsed))
+ {
+ parsed = new ConsoleKeyInfo(default, ConsoleKey.F1 + input[3] - 'A', false, false, false);
+ }
+
+ startIndex += 4;
+ return true;
+ }
+
+ // If sequence does not start with a letter, it must start with one or two digits that represent the Sequence Number
+ int digitCount = !char.IsBetween(input[SequencePrefixLength], '1', '9') // not using IsAsciiDigit as 0 is invalid
+ ? 0
+ : char.IsDigit(input[SequencePrefixLength + 1]) ? 2 : 1;
+
+ if (digitCount == 0 // it does not start with a digit, it's not a sequence
+ || SequencePrefixLength + digitCount >= input.Length) // it's too short to be a complete sequence
+ {
+ parsed = default;
+ return false;
+ }
+
+ if (IsSequenceEndTag(input[SequencePrefixLength + digitCount]))
+ {
+ // it's a VT Sequence like ^[[11~ or rxvt like ^[[11^
+ int sequenceLength = SequencePrefixLength + digitCount + 1;
+ if (!terminfoDb.TryGetValue(buffer.AsMemory(startIndex, sequenceLength), out parsed))
+ {
+ key = MapEscapeSequenceNumber(byte.Parse(input.Slice(SequencePrefixLength, digitCount)));
+ if (key == default)
+ {
+ return false; // it was not a known sequence
+ }
+
+ if (IsRxvtModifier(input[SequencePrefixLength + digitCount]))
+ {
+ modifiers = MapRxvtModifiers(input[SequencePrefixLength + digitCount]);
+ }
+
+ parsed = Create(default, key, modifiers);
+ }
+
+ startIndex += sequenceLength;
+ return true;
+ }
+
+ // If Sequence Number is not followed by the VT Seqence End Tag,
+ // it can be followed only by a Modifier Separator, Modifier (2-8) and Key ID or VT Sequence End Tag.
+ if (input[SequencePrefixLength + digitCount] is not ModifierSeparator
+ || SequencePrefixLength + digitCount + 2 >= input.Length
+ || !char.IsBetween(input[SequencePrefixLength + digitCount + 1], '2', '8')
+ || (!char.IsAsciiLetterUpper(input[SequencePrefixLength + digitCount + 2]) && input[SequencePrefixLength + digitCount + 2] is not VtSequenceEndTag))
+ {
+ return false;
+ }
+
+ modifiers = MapXtermModifiers(input[SequencePrefixLength + digitCount + 1]);
+
+ key = input[SequencePrefixLength + digitCount + 2] is VtSequenceEndTag
+ ? MapEscapeSequenceNumber(byte.Parse(input.Slice(SequencePrefixLength, digitCount)))
+ : MapKeyIdOXterm(input[SequencePrefixLength + digitCount + 2], terminalFormatStrings.IsRxvtTerm).key;
+
+ if (key == default)
+ {
+ return false;
+ }
+
+ startIndex += SequencePrefixLength + digitCount + 3; // 3 stands for separator, modifier and end tag or id
+ parsed = Create(default, key, modifiers);
+ return true;
+
+ // maps "^[O{character}" for all Terminals and "^[[{character}" for rxvt Terminals
+ static (ConsoleKey key, ConsoleModifiers modifiers) MapKeyIdOXterm(char character, bool isRxvt)
+ => character switch
+ {
+ 'A' or 'x' => (ConsoleKey.UpArrow, 0), // lowercase used by rxvt
+ 'a' => (ConsoleKey.UpArrow, ConsoleModifiers.Shift), // rxvt
+ 'B' or 'r' => (ConsoleKey.DownArrow, 0), // lowercase used by rxv
+ 'b' => (ConsoleKey.DownArrow, ConsoleModifiers.Shift), // used by rxvt
+ 'C' or 'v' => (ConsoleKey.RightArrow, 0), // lowercase used by rxv
+ 'c' => (ConsoleKey.RightArrow, ConsoleModifiers.Shift), // used by rxvt
+ 'D' or 't' => (ConsoleKey.LeftArrow, 0), // lowercase used by rxv
+ 'd' => (ConsoleKey.LeftArrow, ConsoleModifiers.Shift), // used by rxvt
+ 'E' => (ConsoleKey.NoName, 0), // ^[OE maps to Begin, but we don't have such Key. To reproduce press Num5.
+ 'F' or 'q' => (ConsoleKey.End, 0),
+ 'H' => (ConsoleKey.Home, 0),
+ 'j' => (ConsoleKey.Multiply, 0), // used by both xterm and rxvt
+ 'k' => (ConsoleKey.Add, 0), // used by both xterm and rxvt
+ 'm' => (ConsoleKey.Subtract, 0), // used by both xterm and rxvt
+ 'M' => (ConsoleKey.Enter, 0), // used by xterm, rxvt (they have it Terminfo) and tmux (no record in Terminfo)
+ 'n' => (ConsoleKey.Delete, 0), // rxvt
+ 'o' => (ConsoleKey.Divide, 0), // used by both xterm and rxvt
+ 'P' => (ConsoleKey.F1, 0),
+ 'p' => (ConsoleKey.Insert, 0), // rxvt
+ 'Q' => (ConsoleKey.F2, 0),
+ 'R' => (ConsoleKey.F3, 0),
+ 'S' => (ConsoleKey.F4, 0),
+ 's' => (ConsoleKey.PageDown, 0), // rxvt
+ 'T' => (ConsoleKey.F5, 0), // VT 100+
+ 'U' => (ConsoleKey.F6, 0), // VT 100+
+ 'u' => (ConsoleKey.NoName, 0), // it should be Begin, but we don't have such (press Num5 in rxvt to reproduce)
+ 'V' => (ConsoleKey.F7, 0), // VT 100+
+ 'W' => (ConsoleKey.F8, 0), // VT 100+
+ 'w' when isRxvt => (ConsoleKey.Home, 0),
+ 'w' when !isRxvt => (ConsoleKey.End, 0),
+ 'X' => (ConsoleKey.F9, 0), // VT 100+
+ 'Y' => (ConsoleKey.F10, 0), // VT 100+
+ 'y' => (ConsoleKey.PageUp, 0), // rxvt
+ 'Z' => (ConsoleKey.F11, 0), // VT 100+
+ '[' => (ConsoleKey.F12, 0), // VT 100+
+ _ => default
+ };
+
+ // maps "^[[{character}" for SCO terminals, based on https://vt100.net/docs/vt510-rm/chapter6.html
+ static (ConsoleKey key, ConsoleModifiers modifiers) MapSCO(char character)
+ => character switch
+ {
+ 'A' => (ConsoleKey.UpArrow, 0),
+ 'B' => (ConsoleKey.DownArrow, 0),
+ 'C' => (ConsoleKey.RightArrow, 0),
+ 'D' => (ConsoleKey.LeftArrow, 0),
+ 'F' => (ConsoleKey.End, 0),
+ 'G' => (ConsoleKey.PageDown, 0),
+ 'H' => (ConsoleKey.Home, 0),
+ 'I' => (ConsoleKey.PageUp, 0),
+ _ when char.IsBetween(character, 'M', 'X') => (ConsoleKey.F1 + character - 'M', 0),
+ _ when char.IsBetween(character, 'Y', 'Z') => (ConsoleKey.F1 + character - 'Y', ConsoleModifiers.Shift),
+ _ when char.IsBetween(character, 'a', 'j') => (ConsoleKey.F3 + character - 'a', ConsoleModifiers.Shift),
+ _ when char.IsBetween(character, 'k', 'v') => (ConsoleKey.F1 + character - 'k', ConsoleModifiers.Control),
+ _ when char.IsBetween(character, 'w', 'z') => (ConsoleKey.F1 + character - 'w', ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '@' => (ConsoleKey.F5, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '[' => (ConsoleKey.F6, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '<' or '\\' => (ConsoleKey.F7, ConsoleModifiers.Control | ConsoleModifiers.Shift), // the Spec says <, PuTTy uses \.
+ ']' => (ConsoleKey.F8, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '^' => (ConsoleKey.F9, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '_' => (ConsoleKey.F10, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '`' => (ConsoleKey.F11, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ '{' => (ConsoleKey.F12, ConsoleModifiers.Control | ConsoleModifiers.Shift),
+ _ => default
+ };
+
+ // based on https://en.wikipedia.org/wiki/ANSI_escape_code#Fe_Escape_sequences
+ static ConsoleKey MapEscapeSequenceNumber(byte number)
+ => number switch
+ {
+ 1 or 7 => ConsoleKey.Home,
+ 2 => ConsoleKey.Insert,
+ 3 => ConsoleKey.Delete,
+ 4 or 8 => ConsoleKey.End,
+ 5 => ConsoleKey.PageUp,
+ 6 => ConsoleKey.PageDown,
+ // Limitation: 10 is mapped to F0, ConsoleKey does not define it so it's not supported.
+ 11 => ConsoleKey.F1,
+ 12 => ConsoleKey.F2,
+ 13 => ConsoleKey.F3,
+ 14 => ConsoleKey.F4,
+ 15 => ConsoleKey.F5,
+ 17 => ConsoleKey.F6,
+ 18 => ConsoleKey.F7,
+ 19 => ConsoleKey.F8,
+ 20 => ConsoleKey.F9,
+ 21 => ConsoleKey.F10,
+ 23 => ConsoleKey.F11,
+ 24 => ConsoleKey.F12,
+ 25 => ConsoleKey.F13,
+ 26 => ConsoleKey.F14,
+ 28 => ConsoleKey.F15,
+ 29 => ConsoleKey.F16,
+ 31 => ConsoleKey.F17,
+ 32 => ConsoleKey.F18,
+ 33 => ConsoleKey.F19,
+ 34 => ConsoleKey.F20,
+ // 9, 16, 22, 27, 30 and 35 have no mapping
+ _ => default
+ };
+
+ // based on https://www.xfree86.org/current/ctlseqs.html
+ static ConsoleModifiers MapXtermModifiers(char modifier)
+ => modifier 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,
+ _ => default
+ };
+
+ static bool IsSequenceEndTag(char character) => character is VtSequenceEndTag || IsRxvtModifier(character);
+
+ static bool IsRxvtModifier(char character) => MapRxvtModifiers(character) != default;
+
+ static ConsoleModifiers MapRxvtModifiers(char modifier)
+ => modifier switch
+ {
+ '^' => ConsoleModifiers.Control,
+ '$' => ConsoleModifiers.Shift,
+ '@' => ConsoleModifiers.Control | ConsoleModifiers.Shift,
+ _ => default
+ };
+
+ static ConsoleKeyInfo Create(char keyChar, ConsoleKey key, ConsoleModifiers modifiers)
+ => new(keyChar, key, (modifiers & ConsoleModifiers.Shift) != 0, (modifiers & ConsoleModifiers.Alt) != 0, (modifiers & ConsoleModifiers.Control) != 0);
+ }
+
+ private static ConsoleKeyInfo ParseFromSingleChar(char single, bool isAlt)
+ {
+ bool isShift = false, isCtrl = false;
+ char keyChar = single;
+
+ ConsoleKey key = single switch
+ {
+ '\b' => ConsoleKey.Backspace,
+ '\t' => ConsoleKey.Tab,
+ '\r' or '\n' => ConsoleKey.Enter,
+ ' ' => ConsoleKey.Spacebar,
+ Escape => ConsoleKey.Escape, // Ctrl+[ and Ctrl+3 are also mapped to 27. Limitation: Ctrl+[ and Ctrl+3 can't be mapped.
+ Delete => ConsoleKey.Backspace, // Ctrl+8 and Backspace are mapped to 127 (ASCII Delete key). Limitation: Ctrl+8 can't be mapped.
+ '*' => ConsoleKey.Multiply, // We can't distinguish Dx+Shift and Multiply (Numeric Keypad). Limitation: Shift+Dx can't be mapped.
+ '/' => ConsoleKey.Divide, // We can't distinguish OemX and Divide (Numeric Keypad). Limitation: OemX keys can't be mapped.
+ '-' => ConsoleKey.Subtract, // We can't distinguish OemMinus and Subtract (Numeric Keypad). Limitation: OemMinus can't be mapped.
+ '+' => ConsoleKey.Add, // We can't distinguish OemPlus and Add (Numeric Keypad). Limitation: OemPlus can't be mapped.
+ '=' => default, // '+' is not mapped to OemPlus, so `=` is not mapped to Shift+OemPlus. Limitation: Shift+OemPlus can't be mapped.
+ '!' or '@' or '#' or '$' or '%' or '^' or '&' or '&' or '*' or '(' or ')' => default, // We can't make assumptions about keyboard layout neither read it. Limitation: Shift+Dx keys can't be mapped.
+ ',' => ConsoleKey.OemComma, // was not previously mapped this way
+ '.' => ConsoleKey.OemPeriod, // was not previously mapped this way
+ _ when char.IsAsciiLetterLower(single) => ConsoleKey.A + single - 'a',
+ _ when char.IsAsciiLetterUpper(single) => UppercaseCharacter(single, out isShift),
+ _ when char.IsAsciiDigit(single) => ConsoleKey.D0 + single - '0', // We can't distinguish DX and Ctrl+DX as they produce same values. Limitation: Ctrl+DX can't be mapped.
+ _ when char.IsBetween(single, (char)1, (char)26) => ControlAndLetterPressed(single, out keyChar, out isCtrl),
+ _ when char.IsBetween(single, (char)28, (char)31) => ControlAndDigitPressed(single, out keyChar, out isCtrl),
+ '\u0000' => ControlAndDigitPressed(single, out keyChar, out isCtrl),
+ _ => default
+ };
+
+ if (single is '\b' or '\n')
+ {
+ isCtrl = true; // Ctrl+Backspace is mapped to '\b' (8), Ctrl+Enter to '\n' (10)
+ }
+
+ if (isAlt)
+ {
+ isAlt = key != default; // two char sequences starting with Escape are Alt+$Key only when we can recognize the key
+ }
+
+ return new ConsoleKeyInfo(keyChar, key, isShift, isAlt, isCtrl);
+
+ static ConsoleKey UppercaseCharacter(char single, out bool isShift)
+ {
+ // Previous implementation assumed that all uppercase characters were typed using Shift.
+ // Limitation: Caps Lock+(a-z) is always mapped to Shift+(a-z).
+ isShift = true;
+ return ConsoleKey.A + single - 'A';
+ }
+
+ static ConsoleKey ControlAndLetterPressed(char single, out char keyChar, out bool isCtrl)
+ {
+ // Ctrl+(a-z) characters are mapped to values from 1 to 26.
+ // Ctrl+H is mapped to 8, which also maps to Ctrl+Backspace.
+ // Ctrl+I is mapped to 9, which also maps to Tab.
+ // Ctrl+J is mapped to 10, which also maps to Ctrl+Enter ('\n').
+ // Ctrl+M is mapped to 13, which also maps to Enter ('\r').
+ // Limitation: Ctrl+H, Ctrl+I, Ctrl+J and Crl+M can't be mapped. More: https://unix.stackexchange.com/questions/563469/conflict-ctrl-i-with-tab-in-normal-mode
+ Debug.Assert(single != 'b' && single != '\t' && single != '\n' && single != '\r');
+
+ isCtrl = true;
+ keyChar = default; // we could use the letter here, but it's impossible to distinguish upper vs lowercase (and Windows doesn't do it as well)
+ return ConsoleKey.A + single - 1;
+ }
+
+ static ConsoleKey ControlAndDigitPressed(char single, out char keyChar, out bool isCtrl)
+ {
+ // Ctrl+(D3-D7) characters are mapped to values from 27 to 31. Escape is also mapped to 27.
+ // Limitation: Ctrl+(D1, D3, D8, D9 and D0) can't be mapped.
+ Debug.Assert(single == default || char.IsBetween(single, (char)28, (char)31));
+
+ isCtrl = true;
+ keyChar = default; // consistent with Windows
+ return single switch
+ {
+ '\u0000' => ConsoleKey.D2, // was not previously mapped this way
+ _ => ConsoleKey.D4 + single - 28
+ };
+ }
+ }
+}
diff --git a/src/libraries/System.Console/src/System/IO/Net6KeyParser.cs b/src/libraries/System.Console/src/System/IO/Net6KeyParser.cs
new file mode 100644
index 0000000000000..7420978e566e4
--- /dev/null
+++ b/src/libraries/System.Console/src/System/IO/Net6KeyParser.cs
@@ -0,0 +1,180 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.IO;
+
+internal static class Net6KeyParser
+{
+ internal static ConsoleKeyInfo Parse(char[] buffer, TerminalFormatStrings terminalFormatStrings, byte posixDisableValue, byte veraseCharacter, ref int startIndex, int endIndex)
+ {
+ MapBufferToConsoleKey(buffer, terminalFormatStrings, posixDisableValue, veraseCharacter, out ConsoleKey key,
+ out char ch, out bool isShift, out bool isAlt, out bool isCtrl, ref startIndex, endIndex);
+
+ // Replace the '\n' char for Enter by '\r' to match Windows behavior.
+ if (key == ConsoleKey.Enter && ch == '\n')
+ {
+ ch = '\r';
+ }
+
+ return new ConsoleKeyInfo(ch, key, isShift, isAlt, isCtrl);
+ }
+
+ private static bool MapBufferToConsoleKey(char[] buffer, TerminalFormatStrings terminalFormatStrings, byte posixDisableValue, byte veraseCharacter,
+ out ConsoleKey key, out char ch, out bool isShift, out bool isAlt, out bool isCtrl, ref int startIndex, int endIndex)
+ {
+ // Try to get the special key match from the TermInfo static information.
+ if (TryGetSpecialConsoleKey(buffer, startIndex, endIndex, terminalFormatStrings, posixDisableValue, veraseCharacter, out ConsoleKeyInfo keyInfo, out int keyLength))
+ {
+ key = keyInfo.Key;
+ isShift = (keyInfo.Modifiers & ConsoleModifiers.Shift) != 0;
+ isAlt = (keyInfo.Modifiers & ConsoleModifiers.Alt) != 0;
+ isCtrl = (keyInfo.Modifiers & ConsoleModifiers.Control) != 0;
+
+ ch = ((keyLength == 1) ? buffer[startIndex] : '\0'); // ignore keyInfo.KeyChar
+ startIndex += keyLength;
+ return true;
+ }
+
+ // Check if we can match Esc + combination and guess if alt was pressed.
+ if (buffer[startIndex] == (char)0x1B && // Alt is send as an escape character
+ endIndex - startIndex >= 2) // We have at least two characters to read
+ {
+ startIndex++;
+ if (MapBufferToConsoleKey(buffer, terminalFormatStrings, posixDisableValue, veraseCharacter, out key, out ch, out isShift, out _, out isCtrl, ref startIndex, endIndex))
+ {
+ isAlt = true;
+ return true;
+ }
+ else
+ {
+ // We could not find a matching key here so, Alt+ combination assumption is in-correct.
+ // The current key needs to be marked as Esc key.
+ // Also, we do not increment _startIndex as we already did it.
+ key = ConsoleKey.Escape;
+ ch = (char)0x1B;
+ isAlt = false;
+ return true;
+ }
+ }
+
+ // Try reading the first char in the buffer and interpret it as a key.
+ ch = buffer[startIndex++];
+ key = GetKeyFromCharValue(ch, out isShift, out isCtrl);
+ isAlt = false;
+ return key != default(ConsoleKey);
+ }
+
+ private static bool TryGetSpecialConsoleKey(char[] givenChars, int startIndex, int endIndex,
+ TerminalFormatStrings terminalFormatStrings, byte posixDisableValue, byte veraseCharacter, out ConsoleKeyInfo key, out int keyLength)
+ {
+ int unprocessedCharCount = endIndex - startIndex;
+
+ // First process special control character codes. These override anything from terminfo.
+ if (unprocessedCharCount > 0)
+ {
+ // Is this an erase / backspace?
+ char c = givenChars[startIndex];
+ if (c != posixDisableValue && c == veraseCharacter)
+ {
+ key = new ConsoleKeyInfo(c, ConsoleKey.Backspace, shift: false, alt: false, control: false);
+ keyLength = 1;
+ return true;
+ }
+ }
+
+ // Then process terminfo mappings.
+ int minRange = terminalFormatStrings.MinKeyFormatLength;
+ if (unprocessedCharCount >= minRange)
+ {
+ int maxRange = Math.Min(unprocessedCharCount, terminalFormatStrings.MaxKeyFormatLength);
+
+ for (int i = maxRange; i >= minRange; i--)
+ {
+ var currentString = new ReadOnlyMemory(givenChars, startIndex, i);
+
+ // Check if the string prefix matches.
+ if (terminalFormatStrings.KeyFormatToConsoleKey.TryGetValue(currentString, out key))
+ {
+ keyLength = currentString.Length;
+ return true;
+ }
+ }
+ }
+
+ // Otherwise, not a known special console key.
+ key = default(ConsoleKeyInfo);
+ keyLength = 0;
+ return false;
+ }
+
+ private static ConsoleKey GetKeyFromCharValue(char x, out bool isShift, out bool isCtrl)
+ {
+ isShift = false;
+ isCtrl = false;
+
+ switch (x)
+ {
+ case '\b':
+ return ConsoleKey.Backspace;
+
+ case '\t':
+ return ConsoleKey.Tab;
+
+ case '\n':
+ case '\r':
+ return ConsoleKey.Enter;
+
+ case (char)(0x1B):
+ return ConsoleKey.Escape;
+
+ case '*':
+ return ConsoleKey.Multiply;
+
+ case '+':
+ return ConsoleKey.Add;
+
+ case '-':
+ return ConsoleKey.Subtract;
+
+ case '/':
+ return ConsoleKey.Divide;
+
+ case (char)(0x7F):
+ return ConsoleKey.Delete;
+
+ case ' ':
+ return ConsoleKey.Spacebar;
+
+ default:
+ // 1. Ctrl A to Ctrl Z.
+ if (char.IsBetween(x, (char)1, (char)26))
+ {
+ isCtrl = true;
+ return ConsoleKey.A + x - 1;
+ }
+
+ // 2. Numbers from 0 to 9.
+ if (char.IsAsciiDigit(x))
+ {
+ return ConsoleKey.D0 + x - '0';
+ }
+
+ //3. A to Z
+ if (char.IsAsciiLetterUpper(x))
+ {
+ isShift = true;
+ return ConsoleKey.A + (x - 'A');
+ }
+
+ // 4. a to z.
+ if (char.IsAsciiLetterLower(x))
+ {
+ return ConsoleKey.A + (x - 'a');
+ }
+
+ break;
+ }
+
+ return default(ConsoleKey);
+ }
+}
diff --git a/src/libraries/System.Console/src/System/IO/StdInReader.cs b/src/libraries/System.Console/src/System/IO/StdInReader.cs
index e20465b226567..7b0105c9de087 100644
--- a/src/libraries/System.Console/src/System/IO/StdInReader.cs
+++ b/src/libraries/System.Console/src/System/IO/StdInReader.cs
@@ -151,7 +151,7 @@ private bool ReadLineCore(bool consumeKeys)
// Don't carry over chars from previous ReadLine call.
_readLineSB.Clear();
- Interop.Sys.InitializeConsoleBeforeRead();
+ Interop.Sys.InitializeConsoleBeforeRead(distinguishNewLines: !ConsoleUtils.UseNet6KeyParser);
try
{
// Read key-by-key until we've read a line.
@@ -205,7 +205,7 @@ private bool ReadLineCore(bool consumeKeys)
if (ConsolePal.TryGetCursorPosition(out int left, out int top, reinitializeForRead: true) &&
left == 0 && top > 0)
{
- s_clearToEol ??= ConsolePal.TerminalFormatStrings.Instance.ClrEol ?? string.Empty;
+ s_clearToEol ??= ConsolePal.TerminalFormatStringsInstance.ClrEol ?? string.Empty;
// Move to end of previous line
ConsolePal.SetCursorPosition(ConsolePal.WindowWidth - 1, top - 1);
@@ -216,7 +216,7 @@ private bool ReadLineCore(bool consumeKeys)
{
if (s_moveLeftString == null)
{
- string? moveLeft = ConsolePal.TerminalFormatStrings.Instance.CursorLeft;
+ string? moveLeft = ConsolePal.TerminalFormatStringsInstance.CursorLeft;
s_moveLeftString = !string.IsNullOrEmpty(moveLeft) ? moveLeft + " " + moveLeft : string.Empty;
}
@@ -301,125 +301,6 @@ private static bool IsEol(char c)
(c == ConsolePal.s_veolCharacter || c == ConsolePal.s_veol2Character || c == ConsolePal.s_veofCharacter);
}
- internal static ConsoleKey GetKeyFromCharValue(char x, out bool isShift, out bool isCtrl)
- {
- isShift = false;
- isCtrl = false;
-
- switch (x)
- {
- case '\b':
- return ConsoleKey.Backspace;
-
- case '\t':
- return ConsoleKey.Tab;
-
- case '\n':
- case '\r':
- return ConsoleKey.Enter;
-
- case (char)(0x1B):
- return ConsoleKey.Escape;
-
- case '*':
- return ConsoleKey.Multiply;
-
- case '+':
- return ConsoleKey.Add;
-
- case '-':
- return ConsoleKey.Subtract;
-
- case '/':
- return ConsoleKey.Divide;
-
- case (char)(0x7F):
- return ConsoleKey.Delete;
-
- case ' ':
- return ConsoleKey.Spacebar;
-
- default:
- // 1. Ctrl A to Ctrl Z.
- if (char.IsBetween(x, (char)1, (char)26))
- {
- isCtrl = true;
- return ConsoleKey.A + x - 1;
- }
-
- // 2. Numbers from 0 to 9.
- if (char.IsAsciiDigit(x))
- {
- return ConsoleKey.D0 + x - '0';
- }
-
- //3. A to Z
- if (char.IsAsciiLetterUpper(x))
- {
- isShift = true;
- return ConsoleKey.A + (x - 'A');
- }
-
- // 4. a to z.
- if (char.IsAsciiLetterLower(x))
- {
- return ConsoleKey.A + (x - 'a');
- }
-
- break;
- }
-
- return default(ConsoleKey);
- }
-
- internal bool MapBufferToConsoleKey(out ConsoleKey key, out char ch, out bool isShift, out bool isAlt, out bool isCtrl)
- {
- Debug.Assert(!IsUnprocessedBufferEmpty());
-
- // Try to get the special key match from the TermInfo static information.
- ConsoleKeyInfo keyInfo;
- int keyLength;
- if (ConsolePal.TryGetSpecialConsoleKey(_unprocessedBufferToBeRead, _startIndex, _endIndex, out keyInfo, out keyLength))
- {
- key = keyInfo.Key;
- isShift = (keyInfo.Modifiers & ConsoleModifiers.Shift) != 0;
- isAlt = (keyInfo.Modifiers & ConsoleModifiers.Alt) != 0;
- isCtrl = (keyInfo.Modifiers & ConsoleModifiers.Control) != 0;
-
- ch = ((keyLength == 1) ? _unprocessedBufferToBeRead[_startIndex] : '\0'); // ignore keyInfo.KeyChar
- _startIndex += keyLength;
- return true;
- }
-
- // Check if we can match Esc + combination and guess if alt was pressed.
- if (_unprocessedBufferToBeRead[_startIndex] == (char)0x1B && // Alt is send as an escape character
- _endIndex - _startIndex >= 2) // We have at least two characters to read
- {
- _startIndex++;
- if (MapBufferToConsoleKey(out key, out ch, out isShift, out _, out isCtrl))
- {
- isAlt = true;
- return true;
- }
- else
- {
- // We could not find a matching key here so, Alt+ combination assumption is in-correct.
- // The current key needs to be marked as Esc key.
- // Also, we do not increment _startIndex as we already did it.
- key = ConsoleKey.Escape;
- ch = (char)0x1B;
- isAlt = false;
- return true;
- }
- }
-
- // Try reading the first char in the buffer and interpret it as a key.
- ch = _unprocessedBufferToBeRead[_startIndex++];
- key = GetKeyFromCharValue(ch, out isShift, out isCtrl);
- isAlt = false;
- return key != default(ConsoleKey);
- }
-
///
/// Try to intercept the key pressed.
///
@@ -446,13 +327,10 @@ private unsafe ConsoleKeyInfo ReadKey()
{
Debug.Assert(_availableKeys.Count == 0);
- Interop.Sys.InitializeConsoleBeforeRead();
+ bool useNet6KeyParser = ConsoleUtils.UseNet6KeyParser;
+ Interop.Sys.InitializeConsoleBeforeRead(distinguishNewLines: !useNet6KeyParser);
try
{
- ConsoleKey key;
- char ch;
- bool isAlt, isCtrl, isShift;
-
if (IsUnprocessedBufferEmpty())
{
// Read in bytes
@@ -476,15 +354,9 @@ private unsafe ConsoleKeyInfo ReadKey()
}
}
- MapBufferToConsoleKey(out key, out ch, out isShift, out isAlt, out isCtrl);
-
- // Replace the '\n' char for Enter by '\r' to match Windows behavior.
- if (key == ConsoleKey.Enter && ch == '\n')
- {
- ch = '\r';
- }
-
- return new ConsoleKeyInfo(ch, key, isShift, isAlt, isCtrl);
+ return useNet6KeyParser
+ ? Net6KeyParser.Parse(_unprocessedBufferToBeRead, ConsolePal.TerminalFormatStringsInstance, ConsolePal.s_posixDisableValue, ConsolePal.s_veraseCharacter, ref _startIndex, _endIndex)
+ : KeyParser.Parse(_unprocessedBufferToBeRead, ConsolePal.TerminalFormatStringsInstance, ConsolePal.s_posixDisableValue, ConsolePal.s_veraseCharacter, ref _startIndex, _endIndex);
}
finally
{
@@ -493,6 +365,6 @@ private unsafe ConsoleKeyInfo ReadKey()
}
/// Gets whether there's input waiting on stdin.
- internal static bool StdinReady => Interop.Sys.StdinReady();
+ internal static bool StdinReady => Interop.Sys.StdinReady(distinguishNewLines: !ConsoleUtils.UseNet6KeyParser);
}
}
diff --git a/src/libraries/System.Console/src/System/TermInfo.Database.cs b/src/libraries/System.Console/src/System/TermInfo.Database.cs
new file mode 100644
index 0000000000000..de09ea072c3d8
--- /dev/null
+++ b/src/libraries/System.Console/src/System/TermInfo.Database.cs
@@ -0,0 +1,310 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace System;
+
+internal static partial class TermInfo
+{
+ /// Provides a terminfo database.
+ internal sealed class Database
+ {
+ /// The name of the terminfo file.
+ private readonly string _term;
+ /// Raw data of the database instance.
+ private readonly byte[] _data;
+
+ /// The number of bytes in the names section of the database.
+ private readonly int _nameSectionNumBytes;
+ /// The number of bytes in the Booleans section of the database.
+ private readonly int _boolSectionNumBytes;
+ /// The number of integers in the numbers section of the database.
+ private readonly int _numberSectionNumInts;
+ /// The number of offsets in the strings section of the database.
+ private readonly int _stringSectionNumOffsets;
+ /// The number of bytes in the strings table of the database.
+ private readonly int _stringTableNumBytes;
+ /// Whether or not to read the number section as 32-bit integers.
+ private readonly bool _readAs32Bit;
+ /// The size of the integers on the number section.
+ private readonly int _sizeOfInt;
+
+ /// Extended / user-defined entries in the terminfo database.
+ private readonly Dictionary _extendedStrings;
+
+ /// Initializes the database instance.
+ /// The name of the terminal.
+ /// The data from the terminfo file.
+ internal Database(string term, byte[] data)
+ {
+ _term = term;
+ _data = data;
+
+ const int MagicLegacyNumber = 0x11A; // magic number octal 0432 for legacy ncurses terminfo
+ const int Magic32BitNumber = 0x21E; // magic number octal 01036 for new ncruses terminfo
+ short magic = ReadInt16(data, 0);
+ _readAs32Bit =
+ magic == MagicLegacyNumber ? false :
+ magic == Magic32BitNumber ? true :
+ throw new InvalidOperationException(SR.Format(SR.IO_TermInfoInvalidMagicNumber, "O" + Convert.ToString(magic, 8))); // magic number was not recognized. Printing the magic number in octal.
+ _sizeOfInt = (_readAs32Bit) ? 4 : 2;
+
+ _nameSectionNumBytes = ReadInt16(data, 2);
+ _boolSectionNumBytes = ReadInt16(data, 4);
+ _numberSectionNumInts = ReadInt16(data, 6);
+ _stringSectionNumOffsets = ReadInt16(data, 8);
+ _stringTableNumBytes = ReadInt16(data, 10);
+ if (_nameSectionNumBytes < 0 ||
+ _boolSectionNumBytes < 0 ||
+ _numberSectionNumInts < 0 ||
+ _stringSectionNumOffsets < 0 ||
+ _stringTableNumBytes < 0)
+ {
+ throw new InvalidOperationException(SR.IO_TermInfoInvalid);
+ }
+
+ // In addition to the main section of bools, numbers, and strings, there is also
+ // an "extended" section. This section contains additional entries that don't
+ // have well-known indices, and are instead named mappings. As such, we parse
+ // all of this data now rather than on each request, as the mapping is fairly complicated.
+ // This function relies on the data stored above, so it's the last thing we run.
+ // (Note that the extended section also includes other Booleans and numbers, but we don't
+ // have any need for those now, so we don't parse them.)
+ int extendedBeginning = RoundUpToEven(StringsTableOffset + _stringTableNumBytes);
+ _extendedStrings = ParseExtendedStrings(data, extendedBeginning, _readAs32Bit) ?? new Dictionary();
+ }
+
+ /// The name of the associated terminfo, if any.
+ public string Term { get { return _term; } }
+
+ /// The offset into data where the names section begins.
+ private const int NamesOffset = 12; // comes right after the header, which is always 12 bytes
+
+ /// The offset into data where the Booleans section begins.
+ private int BooleansOffset { get { return NamesOffset + _nameSectionNumBytes; } } // after the names section
+
+ /// The offset into data where the numbers section begins.
+ private int NumbersOffset { get { return RoundUpToEven(BooleansOffset + _boolSectionNumBytes); } } // after the Booleans section, at an even position
+
+ ///
+ /// The offset into data where the string offsets section begins. We index into this section
+ /// to find the location within the strings table where a string value exists.
+ ///
+ private int StringOffsetsOffset { get { return NumbersOffset + (_numberSectionNumInts * _sizeOfInt); } }
+
+ /// The offset into data where the string table exists.
+ private int StringsTableOffset { get { return StringOffsetsOffset + (_stringSectionNumOffsets * 2); } }
+
+ /// Gets a string from the strings section by the string's well-known index.
+ /// The index of the string to find.
+ /// The string if it's in the database; otherwise, null.
+ public string? GetString(WellKnownStrings stringTableIndex)
+ {
+ int index = (int)stringTableIndex;
+ Debug.Assert(index >= 0);
+
+ if (index >= _stringSectionNumOffsets)
+ {
+ // Some terminfo files may not contain enough entries to actually
+ // have the requested one.
+ return null;
+ }
+
+ int tableIndex = ReadInt16(_data, StringOffsetsOffset + (index * 2));
+ if (tableIndex == -1)
+ {
+ // Some terminfo files may have enough entries, but may not actually
+ // have it filled in for this particular string.
+ return null;
+ }
+
+ return ReadString(_data, StringsTableOffset + tableIndex);
+ }
+
+ /// Gets a string from the extended strings section.
+ /// The name of the string as contained in the extended names section.
+ /// The string if it's in the database; otherwise, null.
+ public string? GetExtendedString(string name)
+ {
+ Debug.Assert(name != null);
+
+ string? value;
+ return _extendedStrings.TryGetValue(name, out value) ?
+ value :
+ null;
+ }
+
+ /// Gets a number from the numbers section by the number's well-known index.
+ /// The index of the string to find.
+ /// The number if it's in the database; otherwise, -1.
+ public int GetNumber(WellKnownNumbers numberIndex)
+ {
+ int index = (int)numberIndex;
+ Debug.Assert(index >= 0);
+
+ if (index >= _numberSectionNumInts)
+ {
+ // Some terminfo files may not contain enough entries to actually
+ // have the requested one.
+ return -1;
+ }
+
+ return ReadInt(_data, NumbersOffset + (index * _sizeOfInt), _readAs32Bit);
+ }
+
+ /// Parses the extended string information from the terminfo data.
+ ///
+ /// A dictionary of the name to value mapping. As this section of the terminfo isn't as well
+ /// defined as the earlier portions, and may not even exist, the parsing is more lenient about
+ /// errors, returning an empty collection rather than throwing.
+ ///
+ private static Dictionary? ParseExtendedStrings(byte[] data, int extendedBeginning, bool readAs32Bit)
+ {
+ const int ExtendedHeaderSize = 10;
+ int sizeOfIntValuesInBytes = (readAs32Bit) ? 4 : 2;
+ if (extendedBeginning + ExtendedHeaderSize >= data.Length)
+ {
+ // Exit out as there's no extended information.
+ return null;
+ }
+
+ // Read in extended counts, and exit out if we got any incorrect info
+ int extendedBoolCount = ReadInt16(data, extendedBeginning);
+ int extendedNumberCount = ReadInt16(data, extendedBeginning + (2 * 1));
+ int extendedStringCount = ReadInt16(data, extendedBeginning + (2 * 2));
+ int extendedStringNumOffsets = ReadInt16(data, extendedBeginning + (2 * 3));
+ int extendedStringTableByteSize = ReadInt16(data, extendedBeginning + (2 * 4));
+ if (extendedBoolCount < 0 ||
+ extendedNumberCount < 0 ||
+ extendedStringCount < 0 ||
+ extendedStringNumOffsets < 0 ||
+ extendedStringTableByteSize < 0)
+ {
+ // The extended header contained invalid data. Bail.
+ return null;
+ }
+
+ // Skip over the extended bools. We don't need them now and can add this in later
+ // if needed. Also skip over extended numbers, for the same reason.
+
+ // Get the location where the extended string offsets begin. These point into
+ // the extended string table.
+ int extendedOffsetsStart =
+ extendedBeginning + // go past the normal data
+ ExtendedHeaderSize + // and past the extended header
+ RoundUpToEven(extendedBoolCount) + // and past all of the extended Booleans
+ (extendedNumberCount * sizeOfIntValuesInBytes); // and past all of the extended numbers
+
+ // Get the location where the extended string table begins. This area contains
+ // null-terminated strings.
+ int extendedStringTableStart =
+ extendedOffsetsStart +
+ (extendedStringCount * 2) + // and past all of the string offsets
+ ((extendedBoolCount + extendedNumberCount + extendedStringCount) * 2); // and past all of the name offsets
+
+ // Get the location where the extended string table ends. We shouldn't read past this.
+ int extendedStringTableEnd =
+ extendedStringTableStart +
+ extendedStringTableByteSize;
+
+ if (extendedStringTableEnd > data.Length)
+ {
+ // We don't have enough data to parse everything. Bail.
+ return null;
+ }
+
+ // Now we need to parse all of the extended string values. These aren't necessarily
+ // "in order", meaning the offsets aren't guaranteed to be increasing. Instead, we parse
+ // the offsets in order, pulling out each string it references and storing them into our
+ // results list in the order of the offsets.
+ var values = new List(extendedStringCount);
+ int lastEnd = 0;
+ for (int i = 0; i < extendedStringCount; i++)
+ {
+ int offset = extendedStringTableStart + ReadInt16(data, extendedOffsetsStart + (i * 2));
+ if (offset < 0 || offset >= data.Length)
+ {
+ // If the offset is invalid, bail.
+ return null;
+ }
+
+ // Add the string
+ int end = FindNullTerminator(data, offset);
+ values.Add(Encoding.ASCII.GetString(data, offset, end - offset));
+
+ // Keep track of where the last string ends. The name strings will come after that.
+ lastEnd = Math.Max(end, lastEnd);
+ }
+
+ // Now parse all of the names.
+ var names = new List(extendedBoolCount + extendedNumberCount + extendedStringCount);
+ for (int pos = lastEnd + 1; pos < extendedStringTableEnd; pos++)
+ {
+ int end = FindNullTerminator(data, pos);
+ names.Add(Encoding.ASCII.GetString(data, pos, end - pos));
+ pos = end;
+ }
+
+ // The names are in order for the Booleans, then the numbers, and then the strings.
+ // Skip over the bools and numbers, and associate the names with the values.
+ var extendedStrings = new Dictionary(extendedStringCount);
+ for (int iName = extendedBoolCount + extendedNumberCount, iValue = 0;
+ iName < names.Count && iValue < values.Count;
+ iName++, iValue++)
+ {
+ extendedStrings.Add(names[iName], values[iValue]);
+ }
+
+ return extendedStrings;
+ }
+
+ private static int RoundUpToEven(int i) { return i % 2 == 1 ? i + 1 : i; }
+
+ /// Read a 16-bit or 32-bit value from the buffer starting at the specified position.
+ /// The buffer from which to read.
+ /// The position at which to read.
+ /// Whether or not to read value as 32-bit. Will read as 16-bit if set to false.
+ /// The value read.
+ private static int ReadInt(byte[] buffer, int pos, bool readAs32Bit) =>
+ readAs32Bit ? ReadInt32(buffer, pos) : ReadInt16(buffer, pos);
+
+ /// Read a 16-bit value from the buffer starting at the specified position.
+ /// The buffer from which to read.
+ /// The position at which to read.
+ /// The 16-bit value read.
+ private static short ReadInt16(byte[] buffer, int pos)
+ {
+ return unchecked((short)
+ ((((int)buffer[pos + 1]) << 8) |
+ ((int)buffer[pos] & 0xff)));
+ }
+
+ /// Read a 32-bit value from the buffer starting at the specified position.
+ /// The buffer from which to read.
+ /// The position at which to read.
+ /// The 32-bit value read.
+ private static int ReadInt32(byte[] buffer, int pos)
+ => BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(pos));
+
+ /// Reads a string from the buffer starting at the specified position.
+ /// The buffer from which to read.
+ /// The position at which to read.
+ /// The string read from the specified position.
+ private static string ReadString(byte[] buffer, int pos)
+ {
+ int end = FindNullTerminator(buffer, pos);
+ return Encoding.ASCII.GetString(buffer, pos, end - pos);
+ }
+
+ /// Finds the null-terminator for a string that begins at the specified position.
+ private static int FindNullTerminator(byte[] buffer, int pos)
+ {
+ int i = buffer.AsSpan(pos).IndexOf((byte)'\0');
+ return i >= 0 ? pos + i : buffer.Length;
+ }
+ }
+}
diff --git a/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs b/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs
new file mode 100644
index 0000000000000..844475e4851d5
--- /dev/null
+++ b/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Microsoft.Win32.SafeHandles;
+
+namespace System;
+
+internal static partial class TermInfo
+{
+ internal sealed class DatabaseFactory
+ {
+ ///
+ /// The default locations in which to search for terminfo databases.
+ /// This is the ordering of well-known locations used by ncurses.
+ ///
+ private static readonly string[] _terminfoLocations = new string[] {
+ "/etc/terminfo",
+ "/lib/terminfo",
+ "/usr/share/terminfo",
+ "/usr/share/misc/terminfo",
+ "/usr/local/share/terminfo"
+ };
+
+ /// Read the database for the current terminal as specified by the "TERM" environment variable.
+ /// The database, or null if it could not be found.
+ internal static Database? ReadActiveDatabase()
+ {
+ string? term = Environment.GetEnvironmentVariable("TERM");
+ return !string.IsNullOrEmpty(term) ? ReadDatabase(term) : null;
+ }
+
+ /// Read the database for the specified terminal.
+ /// The identifier for the terminal.
+ /// The database, or null if it could not be found.
+ private static Database? ReadDatabase(string term)
+ {
+ // This follows the same search order as prescribed by ncurses.
+ Database? db;
+
+ // First try a location specified in the TERMINFO environment variable.
+ string? terminfo = Environment.GetEnvironmentVariable("TERMINFO");
+ if (!string.IsNullOrWhiteSpace(terminfo) && (db = ReadDatabase(term, terminfo)) != null)
+ {
+ return db;
+ }
+
+ // Then try in the user's home directory.
+ string? home = PersistedFiles.GetHomeDirectory();
+ if (!string.IsNullOrWhiteSpace(home) && (db = ReadDatabase(term, home + "/.terminfo")) != null)
+ {
+ return db;
+ }
+
+ // Then try a set of well-known locations.
+ foreach (string terminfoLocation in _terminfoLocations)
+ {
+ if ((db = ReadDatabase(term, terminfoLocation)) != null)
+ {
+ return db;
+ }
+ }
+
+ // Couldn't find one
+ return null;
+ }
+
+ /// Attempt to open as readonly the specified file path.
+ /// The path to the file to open.
+ /// If successful, the opened file descriptor; otherwise, -1.
+ /// true if the file was successfully opened; otherwise, false.
+ private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHandle? fd)
+ {
+ fd = Interop.Sys.Open(filePath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
+ if (fd.IsInvalid)
+ {
+ // Don't throw in this case, as we'll be polling multiple locations looking for the file.
+ fd.Dispose();
+ fd = null;
+ return false;
+ }
+
+ return true;
+ }
+
+ /// Read the database for the specified terminal from the specified directory.
+ /// The identifier for the terminal.
+ /// The path to the directory containing terminfo database files.
+ /// The database, or null if it could not be found.
+ private static Database? ReadDatabase(string? term, string? directoryPath)
+ {
+ if (string.IsNullOrEmpty(term) || string.IsNullOrEmpty(directoryPath))
+ {
+ return null;
+ }
+
+ Span stackBuffer = stackalloc char[256];
+ SafeFileHandle? fd;
+ if (!TryOpen(string.Create(null, stackBuffer, $"{directoryPath}/{term[0]}/{term}"), out fd) && // /directory/termFirstLetter/term (Linux)
+ !TryOpen(string.Create(null, stackBuffer, $"{directoryPath}/{(int)term[0]:X}/{term}"), out fd)) // /directory/termFirstLetterAsHex/term (Mac)
+ {
+ return null;
+ }
+
+ using (fd)
+ {
+ // Read in all of the terminfo data
+ long termInfoLength = Interop.CheckIo(Interop.Sys.LSeek(fd, 0, Interop.Sys.SeekWhence.SEEK_END)); // jump to the end to get the file length
+ Interop.CheckIo(Interop.Sys.LSeek(fd, 0, Interop.Sys.SeekWhence.SEEK_SET)); // reset back to beginning
+ const int MaxTermInfoLength = 4096; // according to the term and tic man pages, 4096 is the terminfo file size max
+ const int HeaderLength = 12;
+ if (termInfoLength <= HeaderLength || termInfoLength > MaxTermInfoLength)
+ {
+ throw new InvalidOperationException(SR.IO_TermInfoInvalid);
+ }
+
+ byte[] data = new byte[(int)termInfoLength];
+ if (ConsolePal.Read(fd, data) != data.Length)
+ {
+ throw new InvalidOperationException(SR.IO_TermInfoInvalid);
+ }
+
+ // Create the database from the data
+ return new Database(term, data);
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Console/src/System/TermInfo.WellKnownNumbers.cs b/src/libraries/System.Console/src/System/TermInfo.WellKnownNumbers.cs
new file mode 100644
index 0000000000000..f8cd8d51e49fb
--- /dev/null
+++ b/src/libraries/System.Console/src/System/TermInfo.WellKnownNumbers.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System;
+
+internal static partial class TermInfo
+{
+ internal enum WellKnownNumbers
+ {
+ Columns = 0,
+ Lines = 2,
+ MaxColors = 13,
+ }
+}
diff --git a/src/libraries/System.Console/src/System/TermInfo.WellKnownStrings.cs b/src/libraries/System.Console/src/System/TermInfo.WellKnownStrings.cs
new file mode 100644
index 0000000000000..20502b3b5f8c4
--- /dev/null
+++ b/src/libraries/System.Console/src/System/TermInfo.WellKnownStrings.cs
@@ -0,0 +1,77 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System;
+
+internal static partial class TermInfo
+{
+ internal enum WellKnownStrings
+ {
+ Bell = 1,
+ Clear = 5,
+ ClrEol = 6,
+ CursorAddress = 10,
+ CursorLeft = 14,
+ CursorPositionReport = 294,
+ OrigPairs = 297,
+ OrigColors = 298,
+ SetAnsiForeground = 359,
+ SetAnsiBackground = 360,
+ CursorInvisible = 13,
+ CursorVisible = 16,
+ FromStatusLine = 47,
+ ToStatusLine = 135,
+ KeyBackspace = 55,
+ KeyClear = 57,
+ KeyDelete = 59,
+ KeyDown = 61,
+ KeyF1 = 66,
+ KeyF10 = 67,
+ KeyF2 = 68,
+ KeyF3 = 69,
+ KeyF4 = 70,
+ KeyF5 = 71,
+ KeyF6 = 72,
+ KeyF7 = 73,
+ KeyF8 = 74,
+ KeyF9 = 75,
+ KeyHome = 76,
+ KeyInsert = 77,
+ KeyLeft = 79,
+ KeyPageDown = 81,
+ KeyPageUp = 82,
+ KeyRight = 83,
+ KeyScrollForward = 84,
+ KeyScrollReverse = 85,
+ KeyUp = 87,
+ KeypadXmit = 89,
+ KeyBackTab = 148,
+ KeyBegin = 158,
+ KeyEnd = 164,
+ KeyEnter = 165,
+ KeyHelp = 168,
+ KeyPrint = 176,
+ KeySBegin = 186,
+ KeySDelete = 191,
+ KeySelect = 193,
+ KeySHelp = 198,
+ KeySHome = 199,
+ KeySLeft = 201,
+ KeySPrint = 207,
+ KeySRight = 210,
+ KeyF11 = 216,
+ KeyF12 = 217,
+ KeyF13 = 218,
+ KeyF14 = 219,
+ KeyF15 = 220,
+ KeyF16 = 221,
+ KeyF17 = 222,
+ KeyF18 = 223,
+ KeyF19 = 224,
+ KeyF20 = 225,
+ KeyF21 = 226,
+ KeyF22 = 227,
+ KeyF23 = 228,
+ KeyF24 = 229,
+ }
+}
diff --git a/src/libraries/System.Console/src/System/TermInfo.cs b/src/libraries/System.Console/src/System/TermInfo.cs
index d44e99afa1c24..0241e77bf43fc 100644
--- a/src/libraries/System.Console/src/System/TermInfo.cs
+++ b/src/libraries/System.Console/src/System/TermInfo.cs
@@ -1,509 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Text;
-using Microsoft.Win32.SafeHandles;
namespace System
{
/// Provides access to and processing of a terminfo database.
- internal static class TermInfo
+ internal static partial class TermInfo
{
- internal enum WellKnownNumbers
- {
- Columns = 0,
- Lines = 2,
- MaxColors = 13,
- }
-
- internal enum WellKnownStrings
- {
- Bell = 1,
- Clear = 5,
- ClrEol = 6,
- CursorAddress = 10,
- CursorLeft = 14,
- CursorPositionReport = 294,
- OrigPairs = 297,
- OrigColors = 298,
- SetAnsiForeground = 359,
- SetAnsiBackground = 360,
- CursorInvisible = 13,
- CursorVisible = 16,
- FromStatusLine = 47,
- ToStatusLine = 135,
- KeyBackspace = 55,
- KeyClear = 57,
- KeyDelete = 59,
- KeyDown = 61,
- KeyF1 = 66,
- KeyF10 = 67,
- KeyF2 = 68,
- KeyF3 = 69,
- KeyF4 = 70,
- KeyF5 = 71,
- KeyF6 = 72,
- KeyF7 = 73,
- KeyF8 = 74,
- KeyF9 = 75,
- KeyHome = 76,
- KeyInsert = 77,
- KeyLeft = 79,
- KeyPageDown = 81,
- KeyPageUp = 82,
- KeyRight = 83,
- KeyScrollForward = 84,
- KeyScrollReverse = 85,
- KeyUp = 87,
- KeypadXmit = 89,
- KeyBackTab = 148,
- KeyBegin = 158,
- KeyEnd = 164,
- KeyEnter = 165,
- KeyHelp = 168,
- KeyPrint = 176,
- KeySBegin = 186,
- KeySDelete = 191,
- KeySelect = 193,
- KeySHelp = 198,
- KeySHome = 199,
- KeySLeft = 201,
- KeySPrint = 207,
- KeySRight = 210,
- KeyF11 = 216,
- KeyF12 = 217,
- KeyF13 = 218,
- KeyF14 = 219,
- KeyF15 = 220,
- KeyF16 = 221,
- KeyF17 = 222,
- KeyF18 = 223,
- KeyF19 = 224,
- KeyF20 = 225,
- KeyF21 = 226,
- KeyF22 = 227,
- KeyF23 = 228,
- KeyF24 = 229,
- }
-
- /// Provides a terminfo database.
- internal sealed class Database
- {
- /// The name of the terminfo file.
- private readonly string _term;
- /// Raw data of the database instance.
- private readonly byte[] _data;
-
- /// The number of bytes in the names section of the database.
- private readonly int _nameSectionNumBytes;
- /// The number of bytes in the Booleans section of the database.
- private readonly int _boolSectionNumBytes;
- /// The number of integers in the numbers section of the database.
- private readonly int _numberSectionNumInts;
- /// The number of offsets in the strings section of the database.
- private readonly int _stringSectionNumOffsets;
- /// The number of bytes in the strings table of the database.
- private readonly int _stringTableNumBytes;
- /// Whether or not to read the number section as 32-bit integers.
- private readonly bool _readAs32Bit;
- /// The size of the integers on the number section.
- private readonly int _sizeOfInt;
-
- /// Extended / user-defined entries in the terminfo database.
- private readonly Dictionary _extendedStrings;
-
- /// Initializes the database instance.
- /// The name of the terminal.
- /// The data from the terminfo file.
- private Database(string term, byte[] data)
- {
- _term = term;
- _data = data;
-
- const int MagicLegacyNumber = 0x11A; // magic number octal 0432 for legacy ncurses terminfo
- const int Magic32BitNumber = 0x21E; // magic number octal 01036 for new ncruses terminfo
- short magic = ReadInt16(data, 0);
- _readAs32Bit =
- magic == MagicLegacyNumber ? false :
- magic == Magic32BitNumber ? true :
- throw new InvalidOperationException(SR.Format(SR.IO_TermInfoInvalidMagicNumber, "O" + Convert.ToString(magic, 8))); // magic number was not recognized. Printing the magic number in octal.
- _sizeOfInt = (_readAs32Bit) ? 4 : 2;
-
- _nameSectionNumBytes = ReadInt16(data, 2);
- _boolSectionNumBytes = ReadInt16(data, 4);
- _numberSectionNumInts = ReadInt16(data, 6);
- _stringSectionNumOffsets = ReadInt16(data, 8);
- _stringTableNumBytes = ReadInt16(data, 10);
- if (_nameSectionNumBytes < 0 ||
- _boolSectionNumBytes < 0 ||
- _numberSectionNumInts < 0 ||
- _stringSectionNumOffsets < 0 ||
- _stringTableNumBytes < 0)
- {
- throw new InvalidOperationException(SR.IO_TermInfoInvalid);
- }
-
- // In addition to the main section of bools, numbers, and strings, there is also
- // an "extended" section. This section contains additional entries that don't
- // have well-known indices, and are instead named mappings. As such, we parse
- // all of this data now rather than on each request, as the mapping is fairly complicated.
- // This function relies on the data stored above, so it's the last thing we run.
- // (Note that the extended section also includes other Booleans and numbers, but we don't
- // have any need for those now, so we don't parse them.)
- int extendedBeginning = RoundUpToEven(StringsTableOffset + _stringTableNumBytes);
- _extendedStrings = ParseExtendedStrings(data, extendedBeginning, _readAs32Bit) ?? new Dictionary();
- }
-
- /// The name of the associated terminfo, if any.
- public string Term { get { return _term; } }
-
- /// Read the database for the current terminal as specified by the "TERM" environment variable.
- /// The database, or null if it could not be found.
- internal static Database? ReadActiveDatabase()
- {
- string? term = Environment.GetEnvironmentVariable("TERM");
- return !string.IsNullOrEmpty(term) ? ReadDatabase(term) : null;
- }
-
- ///
- /// The default locations in which to search for terminfo databases.
- /// This is the ordering of well-known locations used by ncurses.
- ///
- private static readonly string[] _terminfoLocations = new string[] {
- "/etc/terminfo",
- "/lib/terminfo",
- "/usr/share/terminfo",
- "/usr/share/misc/terminfo",
- "/usr/local/share/terminfo"
- };
-
- /// Read the database for the specified terminal.
- /// The identifier for the terminal.
- /// The database, or null if it could not be found.
- private static Database? ReadDatabase(string term)
- {
- // This follows the same search order as prescribed by ncurses.
- Database? db;
-
- // First try a location specified in the TERMINFO environment variable.
- string? terminfo = Environment.GetEnvironmentVariable("TERMINFO");
- if (!string.IsNullOrWhiteSpace(terminfo) && (db = ReadDatabase(term, terminfo)) != null)
- {
- return db;
- }
-
- // Then try in the user's home directory.
- string? home = PersistedFiles.GetHomeDirectory();
- if (!string.IsNullOrWhiteSpace(home) && (db = ReadDatabase(term, home + "/.terminfo")) != null)
- {
- return db;
- }
-
- // Then try a set of well-known locations.
- foreach (string terminfoLocation in _terminfoLocations)
- {
- if ((db = ReadDatabase(term, terminfoLocation)) != null)
- {
- return db;
- }
- }
-
- // Couldn't find one
- return null;
- }
-
- /// Attempt to open as readonly the specified file path.
- /// The path to the file to open.
- /// If successful, the opened file descriptor; otherwise, -1.
- /// true if the file was successfully opened; otherwise, false.
- private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHandle? fd)
- {
- fd = Interop.Sys.Open(filePath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
- if (fd.IsInvalid)
- {
- // Don't throw in this case, as we'll be polling multiple locations looking for the file.
- fd.Dispose();
- fd = null;
- return false;
- }
-
- return true;
- }
-
- /// Read the database for the specified terminal from the specified directory.
- /// The identifier for the terminal.
- /// The path to the directory containing terminfo database files.
- /// The database, or null if it could not be found.
- private static Database? ReadDatabase(string? term, string? directoryPath)
- {
- if (string.IsNullOrEmpty(term) || string.IsNullOrEmpty(directoryPath))
- {
- return null;
- }
-
- Span stackBuffer = stackalloc char[256];
- SafeFileHandle? fd;
- if (!TryOpen(string.Create(null, stackBuffer, $"{directoryPath}/{term[0]}/{term}"), out fd) && // /directory/termFirstLetter/term (Linux)
- !TryOpen(string.Create(null, stackBuffer, $"{directoryPath}/{(int)term[0]:X}/{term}"), out fd)) // /directory/termFirstLetterAsHex/term (Mac)
- {
- return null;
- }
-
- using (fd)
- {
- // Read in all of the terminfo data
- long termInfoLength = Interop.CheckIo(Interop.Sys.LSeek(fd, 0, Interop.Sys.SeekWhence.SEEK_END)); // jump to the end to get the file length
- Interop.CheckIo(Interop.Sys.LSeek(fd, 0, Interop.Sys.SeekWhence.SEEK_SET)); // reset back to beginning
- const int MaxTermInfoLength = 4096; // according to the term and tic man pages, 4096 is the terminfo file size max
- const int HeaderLength = 12;
- if (termInfoLength <= HeaderLength || termInfoLength > MaxTermInfoLength)
- {
- throw new InvalidOperationException(SR.IO_TermInfoInvalid);
- }
-
- byte[] data = new byte[(int)termInfoLength];
- if (ConsolePal.Read(fd, data) != data.Length)
- {
- throw new InvalidOperationException(SR.IO_TermInfoInvalid);
- }
-
- // Create the database from the data
- return new Database(term, data);
- }
- }
-
- /// The offset into data where the names section begins.
- private const int NamesOffset = 12; // comes right after the header, which is always 12 bytes
-
- /// The offset into data where the Booleans section begins.
- private int BooleansOffset { get { return NamesOffset + _nameSectionNumBytes; } } // after the names section
-
- /// The offset into data where the numbers section begins.
- private int NumbersOffset { get { return RoundUpToEven(BooleansOffset + _boolSectionNumBytes); } } // after the Booleans section, at an even position
-
- ///
- /// The offset into data where the string offsets section begins. We index into this section
- /// to find the location within the strings table where a string value exists.
- ///
- private int StringOffsetsOffset { get { return NumbersOffset + (_numberSectionNumInts * _sizeOfInt); } }
-
- /// The offset into data where the string table exists.
- private int StringsTableOffset { get { return StringOffsetsOffset + (_stringSectionNumOffsets * 2); } }
-
- /// Gets a string from the strings section by the string's well-known index.
- /// The index of the string to find.
- /// The string if it's in the database; otherwise, null.
- public string? GetString(WellKnownStrings stringTableIndex)
- {
- int index = (int)stringTableIndex;
- Debug.Assert(index >= 0);
-
- if (index >= _stringSectionNumOffsets)
- {
- // Some terminfo files may not contain enough entries to actually
- // have the requested one.
- return null;
- }
-
- int tableIndex = ReadInt16(_data, StringOffsetsOffset + (index * 2));
- if (tableIndex == -1)
- {
- // Some terminfo files may have enough entries, but may not actually
- // have it filled in for this particular string.
- return null;
- }
-
- return ReadString(_data, StringsTableOffset + tableIndex);
- }
-
- /// Gets a string from the extended strings section.
- /// The name of the string as contained in the extended names section.
- /// The string if it's in the database; otherwise, null.
- public string? GetExtendedString(string name)
- {
- Debug.Assert(name != null);
-
- string? value;
- return _extendedStrings.TryGetValue(name, out value) ?
- value :
- null;
- }
-
- /// Gets a number from the numbers section by the number's well-known index.
- /// The index of the string to find.
- /// The number if it's in the database; otherwise, -1.
- public int GetNumber(WellKnownNumbers numberIndex)
- {
- int index = (int)numberIndex;
- Debug.Assert(index >= 0);
-
- if (index >= _numberSectionNumInts)
- {
- // Some terminfo files may not contain enough entries to actually
- // have the requested one.
- return -1;
- }
-
- return ReadInt(_data, NumbersOffset + (index * _sizeOfInt), _readAs32Bit);
- }
-
- /// Parses the extended string information from the terminfo data.
- ///
- /// A dictionary of the name to value mapping. As this section of the terminfo isn't as well
- /// defined as the earlier portions, and may not even exist, the parsing is more lenient about
- /// errors, returning an empty collection rather than throwing.
- ///
- private static Dictionary? ParseExtendedStrings(byte[] data, int extendedBeginning, bool readAs32Bit)
- {
- const int ExtendedHeaderSize = 10;
- int sizeOfIntValuesInBytes = (readAs32Bit) ? 4 : 2;
- if (extendedBeginning + ExtendedHeaderSize >= data.Length)
- {
- // Exit out as there's no extended information.
- return null;
- }
-
- // Read in extended counts, and exit out if we got any incorrect info
- int extendedBoolCount = ReadInt16(data, extendedBeginning);
- int extendedNumberCount = ReadInt16(data, extendedBeginning + (2 * 1));
- int extendedStringCount = ReadInt16(data, extendedBeginning + (2 * 2));
- int extendedStringNumOffsets = ReadInt16(data, extendedBeginning + (2 * 3));
- int extendedStringTableByteSize = ReadInt16(data, extendedBeginning + (2 * 4));
- if (extendedBoolCount < 0 ||
- extendedNumberCount < 0 ||
- extendedStringCount < 0 ||
- extendedStringNumOffsets < 0 ||
- extendedStringTableByteSize < 0)
- {
- // The extended header contained invalid data. Bail.
- return null;
- }
-
- // Skip over the extended bools. We don't need them now and can add this in later
- // if needed. Also skip over extended numbers, for the same reason.
-
- // Get the location where the extended string offsets begin. These point into
- // the extended string table.
- int extendedOffsetsStart =
- extendedBeginning + // go past the normal data
- ExtendedHeaderSize + // and past the extended header
- RoundUpToEven(extendedBoolCount) + // and past all of the extended Booleans
- (extendedNumberCount * sizeOfIntValuesInBytes); // and past all of the extended numbers
-
- // Get the location where the extended string table begins. This area contains
- // null-terminated strings.
- int extendedStringTableStart =
- extendedOffsetsStart +
- (extendedStringCount * 2) + // and past all of the string offsets
- ((extendedBoolCount + extendedNumberCount + extendedStringCount) * 2); // and past all of the name offsets
-
- // Get the location where the extended string table ends. We shouldn't read past this.
- int extendedStringTableEnd =
- extendedStringTableStart +
- extendedStringTableByteSize;
-
- if (extendedStringTableEnd > data.Length)
- {
- // We don't have enough data to parse everything. Bail.
- return null;
- }
-
- // Now we need to parse all of the extended string values. These aren't necessarily
- // "in order", meaning the offsets aren't guaranteed to be increasing. Instead, we parse
- // the offsets in order, pulling out each string it references and storing them into our
- // results list in the order of the offsets.
- var values = new List(extendedStringCount);
- int lastEnd = 0;
- for (int i = 0; i < extendedStringCount; i++)
- {
- int offset = extendedStringTableStart + ReadInt16(data, extendedOffsetsStart + (i * 2));
- if (offset < 0 || offset >= data.Length)
- {
- // If the offset is invalid, bail.
- return null;
- }
-
- // Add the string
- int end = FindNullTerminator(data, offset);
- values.Add(Encoding.ASCII.GetString(data, offset, end - offset));
-
- // Keep track of where the last string ends. The name strings will come after that.
- lastEnd = Math.Max(end, lastEnd);
- }
-
- // Now parse all of the names.
- var names = new List(extendedBoolCount + extendedNumberCount + extendedStringCount);
- for (int pos = lastEnd + 1; pos < extendedStringTableEnd; pos++)
- {
- int end = FindNullTerminator(data, pos);
- names.Add(Encoding.ASCII.GetString(data, pos, end - pos));
- pos = end;
- }
-
- // The names are in order for the Booleans, then the numbers, and then the strings.
- // Skip over the bools and numbers, and associate the names with the values.
- var extendedStrings = new Dictionary(extendedStringCount);
- for (int iName = extendedBoolCount + extendedNumberCount, iValue = 0;
- iName < names.Count && iValue < values.Count;
- iName++, iValue++)
- {
- extendedStrings.Add(names[iName], values[iValue]);
- }
-
- return extendedStrings;
- }
-
- private static int RoundUpToEven(int i) { return i % 2 == 1 ? i + 1 : i; }
-
- /// Read a 16-bit or 32-bit value from the buffer starting at the specified position.
- /// The buffer from which to read.
- /// The position at which to read.
- /// Whether or not to read value as 32-bit. Will read as 16-bit if set to false.
- /// The value read.
- private static int ReadInt(byte[] buffer, int pos, bool readAs32Bit) =>
- readAs32Bit ? ReadInt32(buffer, pos) : ReadInt16(buffer, pos);
-
- /// Read a 16-bit value from the buffer starting at the specified position.
- /// The buffer from which to read.
- /// The position at which to read.
- /// The 16-bit value read.
- private static short ReadInt16(byte[] buffer, int pos)
- {
- return unchecked((short)
- ((((int)buffer[pos + 1]) << 8) |
- ((int)buffer[pos] & 0xff)));
- }
-
- /// Read a 32-bit value from the buffer starting at the specified position.
- /// The buffer from which to read.
- /// The position at which to read.
- /// The 32-bit value read.
- private static int ReadInt32(byte[] buffer, int pos)
- => BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(pos));
-
- /// Reads a string from the buffer starting at the specified position.
- /// The buffer from which to read.
- /// The position at which to read.
- /// The string read from the specified position.
- private static string ReadString(byte[] buffer, int pos)
- {
- int end = FindNullTerminator(buffer, pos);
- return Encoding.ASCII.GetString(buffer, pos, end - pos);
- }
-
- /// Finds the null-terminator for a string that begins at the specified position.
- private static int FindNullTerminator(byte[] buffer, int pos)
- {
- int i = buffer.AsSpan(pos).IndexOf((byte)'\0');
- return i >= 0 ? pos + i : buffer.Length;
- }
- }
-
/// Provides support for evaluating parameterized terminfo database format strings.
internal static class ParameterizedStrings
{
diff --git a/src/libraries/System.Console/src/System/TerminalFormatStrings.cs b/src/libraries/System.Console/src/System/TerminalFormatStrings.cs
new file mode 100644
index 0000000000000..897f67c733e8b
--- /dev/null
+++ b/src/libraries/System.Console/src/System/TerminalFormatStrings.cs
@@ -0,0 +1,256 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System;
+
+/// Provides format strings and related information for use with the current terminal.
+internal sealed class TerminalFormatStrings
+{
+ /// The format string to use to change the foreground color.
+ public readonly string? Foreground;
+ /// The format string to use to change the background color.
+ public readonly string? Background;
+ /// The format string to use to reset the foreground and background colors.
+ public readonly string? Reset;
+ /// The maximum number of colors supported by the terminal.
+ public readonly int MaxColors;
+ /// The number of columns in a format.
+ public readonly int Columns;
+ /// The number of lines in a format.
+ public readonly int Lines;
+ /// The format string to use to make cursor visible.
+ public readonly string? CursorVisible;
+ /// The format string to use to make cursor invisible
+ public readonly string? CursorInvisible;
+ /// The format string to use to set the window title.
+ public readonly string? Title;
+ /// The format string to use for an audible bell.
+ public readonly string? Bell;
+ /// The format string to use to clear the terminal.
+ public readonly string? Clear;
+ /// The format string to use to set the position of the cursor.
+ public readonly string? CursorAddress;
+ /// The format string to use to move the cursor to the left.
+ public readonly string? CursorLeft;
+ /// The format string to use to clear to the end of line.
+ public readonly string? ClrEol;
+ /// The ANSI-compatible string for the Cursor Position report request.
+ ///
+ /// This should really be in user string 7 in the terminfo file, but some terminfo databases
+ /// are missing it. As this is defined to be supported by any ANSI-compatible terminal,
+ /// we assume it's available; doing so means CursorTop/Left will work even if the terminfo database
+ /// doesn't contain it (as appears to be the case with e.g. screen and tmux on Ubuntu), at the risk
+ /// of outputting the sequence on some terminal that's not compatible.
+ ///
+ public const string CursorPositionReport = "\x1B[6n";
+ ///
+ /// The dictionary of keystring to ConsoleKeyInfo.
+ /// Only some members of the ConsoleKeyInfo are used; in particular, the actual char is ignored.
+ ///
+ public readonly Dictionary, ConsoleKeyInfo> KeyFormatToConsoleKey =
+ new Dictionary, ConsoleKeyInfo>(new ReadOnlyMemoryContentComparer());
+
+ /// Max key length
+ public readonly int MaxKeyFormatLength;
+ /// Min key length
+ public readonly int MinKeyFormatLength;
+ /// The ANSI string used to enter "application" / "keypad transmit" mode.
+ public readonly string? KeypadXmit;
+ /// Indicates that it was created out of rxvt TERM
+ public readonly bool IsRxvtTerm;
+
+ public TerminalFormatStrings(TermInfo.Database? db)
+ {
+ if (db == null)
+ return;
+
+ KeypadXmit = db.GetString(TermInfo.WellKnownStrings.KeypadXmit);
+ Foreground = db.GetString(TermInfo.WellKnownStrings.SetAnsiForeground);
+ Background = db.GetString(TermInfo.WellKnownStrings.SetAnsiBackground);
+ Reset = db.GetString(TermInfo.WellKnownStrings.OrigPairs) ?? db.GetString(TermInfo.WellKnownStrings.OrigColors);
+ Bell = db.GetString(TermInfo.WellKnownStrings.Bell);
+ Clear = db.GetString(TermInfo.WellKnownStrings.Clear);
+ Columns = db.GetNumber(TermInfo.WellKnownNumbers.Columns);
+ Lines = db.GetNumber(TermInfo.WellKnownNumbers.Lines);
+ CursorVisible = db.GetString(TermInfo.WellKnownStrings.CursorVisible);
+ CursorInvisible = db.GetString(TermInfo.WellKnownStrings.CursorInvisible);
+ CursorAddress = db.GetString(TermInfo.WellKnownStrings.CursorAddress);
+ CursorLeft = db.GetString(TermInfo.WellKnownStrings.CursorLeft);
+ ClrEol = db.GetString(TermInfo.WellKnownStrings.ClrEol);
+
+ IsRxvtTerm = !string.IsNullOrEmpty(db.Term) && db.Term.Contains("rxvt", StringComparison.OrdinalIgnoreCase);
+ Title = GetTitle(db);
+
+ Debug.WriteLineIf(db.GetString(TermInfo.WellKnownStrings.CursorPositionReport) != CursorPositionReport,
+ "Getting the cursor position will only work if the terminal supports the CPR sequence," +
+ "but the terminfo database does not contain an entry for it.");
+
+ int maxColors = db.GetNumber(TermInfo.WellKnownNumbers.MaxColors);
+ MaxColors = // normalize to either the full range of all ANSI colors, just the dark ones, or none
+ maxColors >= 16 ? 16 :
+ maxColors >= 8 ? 8 :
+ 0;
+
+ AddKey(db, TermInfo.WellKnownStrings.KeyF1, ConsoleKey.F1);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF2, ConsoleKey.F2);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF3, ConsoleKey.F3);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF4, ConsoleKey.F4);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF5, ConsoleKey.F5);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF6, ConsoleKey.F6);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF7, ConsoleKey.F7);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF8, ConsoleKey.F8);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF9, ConsoleKey.F9);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF10, ConsoleKey.F10);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF11, ConsoleKey.F11);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF12, ConsoleKey.F12);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF13, ConsoleKey.F13);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF14, ConsoleKey.F14);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF15, ConsoleKey.F15);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF16, ConsoleKey.F16);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF17, ConsoleKey.F17);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF18, ConsoleKey.F18);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF19, ConsoleKey.F19);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF20, ConsoleKey.F20);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF21, ConsoleKey.F21);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF22, ConsoleKey.F22);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF23, ConsoleKey.F23);
+ AddKey(db, TermInfo.WellKnownStrings.KeyF24, ConsoleKey.F24);
+ AddKey(db, TermInfo.WellKnownStrings.KeyBackspace, ConsoleKey.Backspace);
+ AddKey(db, TermInfo.WellKnownStrings.KeyBackTab, ConsoleKey.Tab, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeyBegin, ConsoleKey.Home);
+ AddKey(db, TermInfo.WellKnownStrings.KeyClear, ConsoleKey.Clear);
+ AddKey(db, TermInfo.WellKnownStrings.KeyDelete, ConsoleKey.Delete);
+ AddKey(db, TermInfo.WellKnownStrings.KeyDown, ConsoleKey.DownArrow);
+ AddKey(db, TermInfo.WellKnownStrings.KeyEnd, ConsoleKey.End);
+ AddKey(db, TermInfo.WellKnownStrings.KeyEnter, ConsoleKey.Enter);
+ AddKey(db, TermInfo.WellKnownStrings.KeyHelp, ConsoleKey.Help);
+ AddKey(db, TermInfo.WellKnownStrings.KeyHome, ConsoleKey.Home);
+ AddKey(db, TermInfo.WellKnownStrings.KeyInsert, ConsoleKey.Insert);
+ AddKey(db, TermInfo.WellKnownStrings.KeyLeft, ConsoleKey.LeftArrow);
+ AddKey(db, TermInfo.WellKnownStrings.KeyPageDown, ConsoleKey.PageDown);
+ AddKey(db, TermInfo.WellKnownStrings.KeyPageUp, ConsoleKey.PageUp);
+ AddKey(db, TermInfo.WellKnownStrings.KeyPrint, ConsoleKey.Print);
+ AddKey(db, TermInfo.WellKnownStrings.KeyRight, ConsoleKey.RightArrow);
+ AddKey(db, TermInfo.WellKnownStrings.KeyScrollForward, ConsoleKey.PageDown, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeyScrollReverse, ConsoleKey.PageUp, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeySBegin, ConsoleKey.Home, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeySDelete, ConsoleKey.Delete, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeySHome, ConsoleKey.Home, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeySelect, ConsoleKey.Select);
+ AddKey(db, TermInfo.WellKnownStrings.KeySLeft, ConsoleKey.LeftArrow, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeySPrint, ConsoleKey.Print, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeySRight, ConsoleKey.RightArrow, shift: true, alt: false, control: false);
+ AddKey(db, TermInfo.WellKnownStrings.KeyUp, ConsoleKey.UpArrow);
+ AddPrefixKey(db, "kLFT", ConsoleKey.LeftArrow);
+ AddPrefixKey(db, "kRIT", ConsoleKey.RightArrow);
+ AddPrefixKey(db, "kUP", ConsoleKey.UpArrow);
+ AddPrefixKey(db, "kDN", ConsoleKey.DownArrow);
+ AddPrefixKey(db, "kDC", ConsoleKey.Delete);
+ AddPrefixKey(db, "kEND", ConsoleKey.End);
+ AddPrefixKey(db, "kHOM", ConsoleKey.Home);
+ AddPrefixKey(db, "kNXT", ConsoleKey.PageDown);
+ AddPrefixKey(db, "kPRV", ConsoleKey.PageUp);
+
+ if (KeyFormatToConsoleKey.Count > 0)
+ {
+ MaxKeyFormatLength = int.MinValue;
+ MinKeyFormatLength = int.MaxValue;
+
+ foreach (KeyValuePair, ConsoleKeyInfo> entry in KeyFormatToConsoleKey)
+ {
+ if (entry.Key.Length > MaxKeyFormatLength)
+ {
+ MaxKeyFormatLength = entry.Key.Length;
+ }
+ if (entry.Key.Length < MinKeyFormatLength)
+ {
+ MinKeyFormatLength = entry.Key.Length;
+ }
+ }
+ }
+ }
+
+ private static string GetTitle(TermInfo.Database db)
+ {
+ // Try to get the format string from tsl/fsl and use it if they're available
+ string? tsl = db.GetString(TermInfo.WellKnownStrings.ToStatusLine);
+ string? fsl = db.GetString(TermInfo.WellKnownStrings.FromStatusLine);
+ if (tsl != null && fsl != null)
+ {
+ return tsl + "%p1%s" + fsl;
+ }
+
+ string term = db.Term;
+ if (term == null)
+ {
+ return string.Empty;
+ }
+
+ if (term.StartsWith("xterm", StringComparison.Ordinal)) // normalize all xterms to enable easier matching
+ {
+ term = "xterm";
+ }
+ else if (term.StartsWith("screen", StringComparison.Ordinal)) // normalize all tmux configs
+ {
+ term = "screen";
+ }
+
+ switch (term)
+ {
+ case "aixterm":
+ case "dtterm":
+ case "linux":
+ case "rxvt":
+ case "xterm":
+ return "\x1B]0;%p1%s\x07";
+ case "cygwin":
+ return "\x1B];%p1%s\x07";
+ case "konsole":
+ return "\x1B]30;%p1%s\x07";
+ case "screen":
+ return "\x1Bk%p1%s\x1B";
+ default:
+ return string.Empty;
+ }
+ }
+
+ private void AddKey(TermInfo.Database db, TermInfo.WellKnownStrings keyId, ConsoleKey key)
+ {
+ AddKey(db, keyId, key, shift: false, alt: false, control: false);
+ }
+
+ private void AddKey(TermInfo.Database db, TermInfo.WellKnownStrings keyId, ConsoleKey key, bool shift, bool alt, bool control)
+ {
+ ReadOnlyMemory keyFormat = db.GetString(keyId).AsMemory();
+ if (!keyFormat.IsEmpty)
+ KeyFormatToConsoleKey[keyFormat] = new ConsoleKeyInfo(key == ConsoleKey.Enter ? '\r' : '\0', key, shift, alt, control);
+ }
+
+ private void AddPrefixKey(TermInfo.Database db, string extendedNamePrefix, ConsoleKey key)
+ {
+ AddKey(db, extendedNamePrefix + "3", key, shift: false, alt: true, control: false);
+ AddKey(db, extendedNamePrefix + "4", key, shift: true, alt: true, control: false);
+ AddKey(db, extendedNamePrefix + "5", key, shift: false, alt: false, control: true);
+ AddKey(db, extendedNamePrefix + "6", key, shift: true, alt: false, control: true);
+ AddKey(db, extendedNamePrefix + "7", key, shift: false, alt: false, control: true);
+ }
+
+ private void AddKey(TermInfo.Database db, string extendedName, ConsoleKey key, bool shift, bool alt, bool control)
+ {
+ ReadOnlyMemory keyFormat = db.GetExtendedString(extendedName).AsMemory();
+ if (!keyFormat.IsEmpty)
+ KeyFormatToConsoleKey[keyFormat] = new ConsoleKeyInfo('\0', key, shift, alt, control);
+ }
+
+ private sealed class ReadOnlyMemoryContentComparer : IEqualityComparer>
+ {
+ public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) =>
+ x.Span.SequenceEqual(y.Span);
+
+ public int GetHashCode(ReadOnlyMemory obj) =>
+ string.GetHashCode(obj.Span);
+ }
+}
diff --git a/src/libraries/System.Console/tests/KeyParserTests.cs b/src/libraries/System.Console/tests/KeyParserTests.cs
new file mode 100644
index 0000000000000..f5cebdeeff17e
--- /dev/null
+++ b/src/libraries/System.Console/tests/KeyParserTests.cs
@@ -0,0 +1,1866 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace System.Tests;
+
+public class KeyParserTests
+{
+ private static readonly TerminalData[] Terminals =
+ {
+ new XTermData(),
+ new GNOMETerminalData(),
+ new LinuxConsole(),
+ new PuTTYData_xterm(),
+ new PuTTYData_linux(),
+ new PuTTYData_putty(),
+ new WindowsTerminalData(),
+ new TmuxData(),
+ new Tmux256ColorData(),
+ new RxvtUnicode(),
+ };
+
+ private static IEnumerable<(char ch, ConsoleKey key)> AsciiKeys
+ {
+ get
+ {
+ yield return (' ', ConsoleKey.Spacebar);
+ yield return ('\t', ConsoleKey.Tab);
+ yield return ('\r', ConsoleKey.Enter);
+
+ yield return ('+', ConsoleKey.Add);
+ yield return ('-', ConsoleKey.Subtract);
+ yield return ('*', ConsoleKey.Multiply);
+ yield return ('/', ConsoleKey.Divide);
+
+ yield return ('.', ConsoleKey.OemPeriod);
+ yield return (',', ConsoleKey.OemComma);
+
+ yield return ('\u001B', ConsoleKey.Escape);
+
+ for (char i = '0'; i <= '9'; i++)
+ {
+ yield return (i, ConsoleKey.D0 + i - '0');
+ }
+ for (char i = 'a'; i <= 'z'; i++)
+ {
+ yield return (i, ConsoleKey.A + i - 'a');
+ }
+ for (char i = 'A'; i <= 'Z'; i++)
+ {
+ yield return (i, ConsoleKey.A + i - 'A');
+ }
+ }
+ }
+
+ public static IEnumerable