/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.PowerShell.Internal;
namespace Microsoft.PowerShell
{
///
/// A helper class for converting strings to ConsoleKey chords.
///
public static class ConsoleKeyChordConverter
{
///
/// Converts a string to a sequence of ConsoleKeyInfo.
/// Sequences are separated by ','. Modifiers
/// appear before the modified key and are separated by '+'.
/// Examples:
/// Ctrl+X
/// Ctrl+D,M
/// Escape,X
///
public static ConsoleKeyInfo[] Convert(string chord)
{
if (string.IsNullOrEmpty(chord))
{
throw new ArgumentNullException("chord");
}
var tokens = chord.Split(new[] {','});
if (tokens.Length > 2)
{
throw new ArgumentException(PSReadLineResources.ChordWithTooManyKeys);
}
var result = new ConsoleKeyInfo[tokens.Length];
for (int i = 0; i < tokens.Length; i++)
{
result[i] = ConvertOneSequence(tokens[i]);
}
return result;
}
private static ConsoleKeyInfo ConvertOneSequence(string sequence)
{
Stack tokens = null;
ConsoleModifiers modifiers = 0;
ConsoleKey key = 0;
char keyChar = '\u0000';
bool valid = !String.IsNullOrEmpty(sequence);
if (valid)
{
tokens = new Stack(
(sequence.Split(new[] {'+'})
.Select(
part => part.ToLowerInvariant().Trim())));
}
while (valid && tokens.Count > 0)
{
string token = tokens.Pop();
// sequence was something silly like "shift++"
if (token == String.Empty)
{
valid = false;
break;
}
// key should be first token to be popped
if (key == 0)
{
// the keyChar is this token, if not a special key like "Fxx" etc.
if (token.Length == 1)
{
keyChar = token[0];
}
// Enum.TryParse accepts arbitrary integers. We shouldn't,
// but single digits need to map to the correct key, e.g.
// ConsoleKey.D1
long tokenAsLong;
if (long.TryParse(token, out tokenAsLong))
{
if (tokenAsLong >= 0 && tokenAsLong <= 9)
{
token = "D" + token;
}
else
{
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.UnrecognizedKey, token));
}
}
// try simple parse for ConsoleKey enum name
valid = Enum.TryParse(token, ignoreCase: true, result: out key);
// doesn't map to ConsoleKey so convert to virtual key from char
if (!valid && token.Length == 1)
{
string failReason;
valid = TryParseCharLiteral(token[0], ref modifiers, ref key, out failReason);
if (!valid)
{
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.CantTranslateKey, token[0], failReason));
}
}
if (!valid)
{
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.UnrecognizedKey, token));
}
}
else
{
// now, parse modifier(s)
ConsoleModifiers modifier;
// courtesy translation
if (token == "ctrl")
{
token = "control";
}
if (Enum.TryParse(token, ignoreCase: true, result: out modifier))
{
// modifier already set?
if ((modifiers & modifier) != 0)
{
// either found duplicate modifier token or shift state
// was already implied from char, e.g. char is "}", which is "shift+]"
throw new ArgumentException(
String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidModifier, modifier, key));
}
modifiers |= modifier;
}
else
{
throw new ArgumentException(
String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidModifier, token, key));
}
}
}
if (!valid)
{
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidSequence, sequence));
}
return new ConsoleKeyInfo(keyChar, key,
shift: ((modifiers & ConsoleModifiers.Shift) != 0),
alt: ((modifiers & ConsoleModifiers.Alt) != 0),
control: ((modifiers & ConsoleModifiers.Control) != 0));
}
private static bool TryParseCharLiteral(char literal, ref ConsoleModifiers modifiers, ref ConsoleKey key, out string failReason)
{
bool valid = false;
#if UNIX
bool isShift;
bool isCtrl;
key = GetKeyFromCharValue(literal, out isShift, out isCtrl);
// Failure to get a key for the char just means that the key is not
// special (in the ConsoleKey enum), so we return a default key.
// Thus this never fails.
valid = true;
failReason = null;
if (isShift)
{
modifiers |= ConsoleModifiers.Shift;
}
if (isCtrl)
{
modifiers |= ConsoleModifiers.Control;
}
// alt is not possible to get
#else
// shift state will be in MSB
short virtualKey = NativeMethods.VkKeyScan(literal);
int hresult = Marshal.GetLastWin32Error();
if (virtualKey != 0)
{
// e.g. "}" = 0x01dd but "]" is 0x00dd, ergo } = shift+].
// shift = 1, control = 2, alt = 4, hankaku = 8 (ignored)
int state = virtualKey >> 8;
if ((state & 1) == 1)
{
modifiers |= ConsoleModifiers.Shift;
}
if ((state & 2) == 2)
{
modifiers |= ConsoleModifiers.Control;
}
if ((state & 4) == 4)
{
modifiers |= ConsoleModifiers.Alt;
}
virtualKey &= 0xff;
if (Enum.IsDefined(typeof (ConsoleKey), (int) virtualKey))
{
failReason = null;
key = (ConsoleKey) virtualKey;
valid = true;
}
else
{
// haven't seen this happen yet, but possible
failReason = String.Format(CultureInfo.CurrentCulture, PSReadLineResources.UnrecognizedKey, virtualKey);
}
}
else
{
Exception e = Marshal.GetExceptionForHR(hresult);
failReason = e.Message;
}
#endif
return valid;
}
// this is borrowed from the CoreFX internal System.IO.StdInReader class
// https://github.com/dotnet/corefx/blob/5b2ae6aa485773cd5569f56f446698633c9ad945/src/System.Console/src/System/IO/StdInReader.cs#L222
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':
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 (x >= 1 && x <= 26)
{
isCtrl = true;
return ConsoleKey.A + x - 1;
}
// 2. Numbers from 0 to 9.
if (x >= '0' && x <= '9')
{
return ConsoleKey.D0 + x - '0';
}
//3. A to Z
if (x >= 'A' && x <= 'Z')
{
isShift = true;
return ConsoleKey.A + (x - 'A');
}
// 4. a to z.
if (x >= 'a' && x <= 'z')
{
return ConsoleKey.A + (x - 'a');
}
break;
}
return default(ConsoleKey);
}
#if !UNIX
internal static char GetCharFromConsoleKey(ConsoleKey key, ConsoleModifiers modifiers)
{
// default for unprintables and unhandled
char keyChar = '\u0000';
// emulate GetKeyboardState bitmap - set high order bit for relevant modifier virtual keys
var state = new byte[256];
state[NativeMethods.VK_SHIFT] = (byte)(((modifiers & ConsoleModifiers.Shift) != 0) ? 0x80 : 0);
state[NativeMethods.VK_CONTROL] = (byte)(((modifiers & ConsoleModifiers.Control) != 0) ? 0x80 : 0);
state[NativeMethods.VK_ALT] = (byte)(((modifiers & ConsoleModifiers.Alt) != 0) ? 0x80 : 0);
// a ConsoleKey enum's value is a virtual key code
uint virtualKey = (uint)key;
// get corresponding scan code
uint scanCode = NativeMethods.MapVirtualKey(virtualKey, NativeMethods.MAPVK_VK_TO_VSC);
// get corresponding character - may be 0, 1 or 2 in length (diacritics)
var chars = new char[2];
int charCount = NativeMethods.ToUnicode(
virtualKey, scanCode, state, chars, chars.Length, NativeMethods.MENU_IS_INACTIVE);
// TODO: support diacritics (charCount == 2)
if (charCount == 1)
{
keyChar = chars[0];
}
return keyChar;
}
#endif
}
}