diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index ee637120..5ee51e9c 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -1,6 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Management.Automation; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Policy; namespace PSConsoleUtilities { @@ -210,11 +214,13 @@ public override object Transform(EngineIntrinsics engineIntrinsics, object input [Cmdlet("Set", "PSReadlineKeyHandler")] public class SetKeyHandlerCommand : PSCmdlet { - [Parameter(Mandatory = true)] - //[ConsoleKeyInfoConverter] + [Parameter(Position = 0, Mandatory = true)] + [ValidateNotNull] + [ConsoleKeyInfoConverter] public ConsoleKeyInfo Key { get; set; } - [Parameter(Mandatory = true)] + [Parameter(Position = 1, Mandatory = true)] + [ValidateNotNull] public Action Handler { get; set; } [Parameter(Mandatory = true)] @@ -241,5 +247,7 @@ protected override void EndProcessing() { WriteObject(PSConsoleReadLine.GetKeyHandlers(), true); } + + } } diff --git a/PSReadLine/ConsoleKeyInfoConverterAttribute.cs b/PSReadLine/ConsoleKeyInfoConverterAttribute.cs new file mode 100644 index 00000000..f696b1aa --- /dev/null +++ b/PSReadLine/ConsoleKeyInfoConverterAttribute.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management.Automation; +using System.Runtime.InteropServices; +using System.Text; + +namespace PSConsoleUtilities +{ + // $c = new-object PSConsoleUtilities.ConsoleKeyInfoConverterAttribute + [AttributeUsage(AttributeTargets.Property)] + public class ConsoleKeyInfoConverterAttribute : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + if (!(inputData is string)) + { + // pass through + return inputData; + } + + var sequence = (string)inputData; + Stack tokens = null; + ConsoleModifiers modifiers = 0; + ConsoleKey key = 0; + + 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) + { + // 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("Unable to translate '{0}' to " + + "virtual key code: {1}.", token[0], failReason)); + } + } + + if (!valid) + { + throw new ArgumentException("Unrecognized key '" + token + "'. Please use a character literal or a " + + "well-known key name from the System.ConsoleKey enumeration."); + } + } + 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("Duplicate or invalid modifier token '{0}' for key '{1}'.", modifier, key)); + } + modifiers |= modifier; + } + else + { + throw new ArgumentException("Invalid modifier token '" + token + "'. The supported modifiers are " + + "'alt', 'shift', 'control' or 'ctrl'."); + } + } + } + + if (!valid) + { + throw new ArgumentException("Invalid sequence '" + sequence + "'."); + } + + char keyChar = GetCharFromConsoleKey(key, modifiers); + + 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; + + // shift state will be in MSB + short virtualKey = NativeMethods.VkKeyScan(literal); + + 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("The virtual key code {0} does not map " + + "to a known System.ConsoleKey enumerated value.", virtualKey); + } + } + else + { + int hresult = Marshal.GetLastWin32Error(); + Exception e = Marshal.GetExceptionForHR(hresult); + failReason = e.Message; + } + + return valid; + } + + private 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); + Debug.Assert(scanCode != 0, "scanCode != 0"); + + // get corresponding character - maybe be 0, 1 or 2 in length (diacriticals) + var chars = new StringBuilder(); + int charCount = NativeMethods.ToAscii( + virtualKey, scanCode, state, chars, NativeMethods.MENU_IS_INACTIVE); + + // TODO: support diacriticals (charCount == 2) + if (charCount == 1) + { + keyChar = chars[0]; + } + + return keyChar; + } + } +} \ No newline at end of file diff --git a/PSReadLine/ConsoleLib.cs b/PSReadLine/ConsoleLib.cs index 4d812ae5..13851a27 100644 --- a/PSReadLine/ConsoleLib.cs +++ b/PSReadLine/ConsoleLib.cs @@ -8,6 +8,16 @@ namespace PSConsoleUtilities { public static class NativeMethods { + public const uint MAPVK_VK_TO_VSC = 0x00; + public const uint MAPVK_VSC_TO_VK = 0x01; + public const uint MAPVK_VK_TO_CHAR = 0x02; + + public const byte VK_SHIFT = 0x10; + public const byte VK_CONTROL = 0x11; + public const byte VK_ALT = 0x12; + public const uint MENU_IS_ACTIVE = 0x01; + public const uint MENU_IS_INACTIVE = 0x00; // windows key + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr GetStdHandle(uint handleId); @@ -29,6 +39,15 @@ public static extern bool ScrollConsoleScreenBuffer(IntPtr hConsoleOutput, [DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool ReadConsoleOutput(IntPtr consoleOutput, [Out] CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT readRegion); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint MapVirtualKey(uint uCode, uint uMapType); + + [DllImport("user32.dll")] + public static extern int ToAscii(uint uVirtKey, uint uScanCode, byte[] lpKeyState, [Out] StringBuilder lpChar, uint uFlags); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern short VkKeyScan(char @char); } public delegate bool BreakHandler(ConsoleBreakSignal ConsoleBreakSignal); diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 34229658..bf899329 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -47,6 +47,7 @@ + diff --git a/UnitTestPSReadLine/UnitTestReadLine.cs b/UnitTestPSReadLine/UnitTestReadLine.cs index 9928fb30..1eb1e8ff 100644 --- a/UnitTestPSReadLine/UnitTestReadLine.cs +++ b/UnitTestPSReadLine/UnitTestReadLine.cs @@ -1413,5 +1413,123 @@ public void TestUselessStuffForBetterCoverage() AssertCursorLeftIs(1); } } + + [TestMethod] + public void TestKeyInfoConverterSimpleCharLiteral() + { + var converter = new ConsoleKeyInfoConverterAttribute(); + + object result = converter.Transform(null, "x"); + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(ConsoleKeyInfo)); + + var key = (ConsoleKeyInfo) result; + + Assert.AreEqual(key.KeyChar, 'x'); + Assert.AreEqual(key.Key, ConsoleKey.X); + Assert.AreEqual(key.Modifiers, (ConsoleModifiers)0); + } + + [TestMethod] + public void TestKeyInfoConverterSimpleCharLiteralWithModifiers() + { + var converter = new ConsoleKeyInfoConverterAttribute(); + + object result = converter.Transform(null, "alt+shift+x"); + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(ConsoleKeyInfo)); + + var key = (ConsoleKeyInfo) result; + + Assert.AreEqual(key.KeyChar, 'X'); + Assert.AreEqual(key.Key, ConsoleKey.X); + Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift | ConsoleModifiers.Alt); + } + + [TestMethod] + public void TestKeyInfoConverterSymbolLiteral() + { + var converter = new ConsoleKeyInfoConverterAttribute(); + + object result = converter.Transform(null, "}"); + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(ConsoleKeyInfo)); + + var key = (ConsoleKeyInfo)result; + + Assert.AreEqual(key.KeyChar, '}'); + Assert.AreEqual(key.Key, ConsoleKey.Oem6); + Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift); + } + + [TestMethod] + public void TestKeyInfoConverterShiftedSymbolLiteral() + { + // } => shift+] / shift+oem6 + var converter = new ConsoleKeyInfoConverterAttribute(); + + object result = converter.Transform(null, "shift+]"); + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(ConsoleKeyInfo)); + + var key = (ConsoleKeyInfo)result; + + Assert.AreEqual(key.KeyChar, '}'); + Assert.AreEqual(key.Key, ConsoleKey.Oem6); + Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift); + } + + [TestMethod] + public void TestKeyInfoConverterWellKnownConsoleKey() + { + // oem6 + var converter = new ConsoleKeyInfoConverterAttribute(); + + object result = converter.Transform(null, "shift+oem6"); + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(ConsoleKeyInfo)); + + var key = (ConsoleKeyInfo)result; + + Assert.AreEqual(key.KeyChar, '}'); + Assert.AreEqual(key.Key, ConsoleKey.Oem6); + Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift); + } + + [TestMethod] + public void TestKeyInfoConverterPassThrough() + { + // pass through consolekeyinfo + var converter = new ConsoleKeyInfoConverterAttribute(); + + var key = new ConsoleKeyInfo('x', ConsoleKey.X, true, false, false); + object result = converter.Transform(null, key); + + Assert.AreEqual(key, result); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void TestKeyInfoConverterInvalidKey() + { + var converter = new ConsoleKeyInfoConverterAttribute(); + object result = converter.Transform(null, "escrape"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void TestKeyInfoConverterInvalidModifierTypo() + { + var converter = new ConsoleKeyInfoConverterAttribute(); + object result = converter.Transform(null, "alt+shuft+x"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void TestKeyInfoConverterInvalidModifierInapplicable() + { + var converter = new ConsoleKeyInfoConverterAttribute(); + object result = converter.Transform(null, "shift+}"); + } } }