private static bool TryParseTerminalInputSequence(char[] buffer, TerminalFormatStrings terminalFormatStrings, out ConsoleKeyInfo parsed, ref int startIndex, int endIndex) { ReadOnlySpan <char> 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 <ReadOnlyMemory <char>, 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);
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 <char>(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 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)); }