PowerShellからキーボードイベントをフックして🍺BEER🍻で会話する

お疲れ様です。金曜日です。金曜日といえばビールですね。
年度末ですが気にせずビールを飲みましょう。

さて、Unicodeで定義されているビールのEmojiには2つの種類があるのをご存知でしょうか。
🍺 BEER MUG
🍻 CLINKING BEER MUGS
「ビール」と「ビールで乾杯」です。
この2つ、何かに似ていると思いませんか？

**そう、モールス符号のトン(・)とツー(－)ですね。**

我々は2種の符号と少しの空白のみで会話ができる優れた知能を持った生命体です。アルファベットや数字などは捨て去り、すべてをビールで満たしましょう。


# コード/使い方
```powershell
Add-Type -Language VisualBasic -ReferencedAssemblies System.Windows.Forms @"
    Option Infer On
 
    Imports System
    Imports System.Diagnostics
    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Runtime.InteropServices
    Imports Microsoft.VisualBasic
    Imports System.Threading
    Imports System.Windows.Forms
 
    Public Class BeerTyper
 
        Const DIT = "🍺"
        Const DAH = "🍻"
        Const SEP = " "

        Shared morseDictSource As New List(Of Array) From { _
            ({48, "0", DAH & DAH & DAH & DAH & DAH}), _
            ({49, "1", DIT & DAH & DAH & DAH & DAH}), _
            ({50, "2", DIT & DIT & DAH & DAH & DAH}), _
            ({51, "3", DIT & DIT & DIT & DAH & DAH}), _
            ({52, "4", DIT & DIT & DIT & DIT & DAH}), _
            ({53, "5", DIT & DIT & DIT & DIT & DIT}), _
            ({54, "6", DAH & DIT & DIT & DIT & DIT}), _
            ({55, "7", DAH & DAH & DIT & DIT & DIT}), _
            ({56, "8", DAH & DAH & DAH & DIT & DIT}), _
            ({57, "9", DAH & DAH & DAH & DAH & DIT}), _
            ({65, "a", DIT & DAH}), _
            ({66, "b", DAH & DIT & DIT & DIT}), _
            ({67, "c", DAH & DIT & DAH & DIT}), _
            ({68, "d", DAH & DIT & DIT}), _
            ({69, "e", DIT}), _
            ({70, "f", DIT & DIT & DAH & DIT}), _
            ({71, "g", DAH & DAH & DIT}), _
            ({72, "h", DIT & DIT & DIT & DIT}), _
            ({73, "i", DIT & DIT}), _
            ({74, "j", DIT & DAH & DAH & DAH}), _
            ({75, "k", DAH & DIT & DAH}), _
            ({76, "l", DIT & DAH & DIT & DIT}), _
            ({77, "m", DAH & DAH}), _
            ({78, "n", DAH & DIT}), _
            ({79, "o", DAH & DAH & DAH}), _
            ({80, "p", DIT & DAH & DAH & DIT}), _
            ({81, "q", DAH & DAH & DIT & DAH}), _
            ({82, "r", DIT & DAH & DIT}), _
            ({83, "s", DIT & DIT & DIT}), _
            ({84, "t", DAH}), _
            ({85, "u", DIT & DIT & DAH}), _
            ({86, "v", DIT & DIT & DIT & DAH}), _
            ({87, "w", DIT & DAH & DAH}), _
            ({88, "x", DAH & DIT & DIT & DAH}), _
            ({89, "y", DAH & DIT & DAH & DAH}), _
            ({90, "z", DAH & DAH & DIT & DIT}), _
            ({00, " ", String.Empty}) _
        }

        Const WH_KEYBOARD_LL = &H0D
        Const WM_KEYDOWN = &H0100
        Const WM_KEYUP = &H0101
        Const INPUT_KEYBOARD = &H01
        Const KEYEVENTF_EXTENDEDKEY = &H01
        Const KEYEVENTF_KEYUP = &H02
        Const KEYEVENTF_UNICODE = &H04
        Const CALL_NEXT_HOOK = 666
 
        <DllImport("User32.dll")> _
            Public Overloads Shared Function SetWindowsHookEx( _
                ByVal idHook As Integer, _
                ByVal hookProc As CallBack, _
                ByVal hInstance As IntPtr, _
                ByVal wParam As Integer) As Integer
            End Function
 
        <DllImport("kernel32.dll")> _
            Public Overloads Shared Function GetModuleHandle( _
                ByVal lpModuleName As String) As IntPtr
            End Function
 
        <DllImport("User32.dll")> _
            Public Overloads Shared Function CallNextHookEx( _
                ByVal idHook As Integer, _
                ByVal nCode As Integer, _
                ByVal wParam As IntPtr, _
                ByVal lParam As IntPtr) As Integer
            End Function
 
        <DllImport("User32.dll")> _
            Public Overloads Shared Function UnhookWindowsHookEx( _
                ByVal idHook As Integer) As Boolean
            End Function
 
        <DllImport("User32.dll")> _
            Public Overloads Shared Function SendInput( _
                ByVal nInputs As UInteger, _
                ByVal pInputs As INPUT(), _
                ByVal cbsize As Integer) As Integer
            End Function
 
        <StructLayout(LayoutKind.Sequential)> _
            Public Structure KBDLLHOOKSTRUCT
                Public vkCode As UInteger
                Public scanCode As UInteger
                Public dwFlags As UInteger
                Public time As UInteger
                Public dwExtraInfo As IntPtr
            End Structure
 
        <StructLayout(LayoutKind.Sequential)> _
            Public Structure INPUT
                Public type As Integer
                Public iu As InputUnion
            End Structure
 
        <StructLayout(LayoutKind.Explicit)> _
            Public Structure InputUnion
                <FieldOffset(0)> Public mi As MOUSEINPUT
                <FieldOffset(0)> Public ki As KEYBDINPUT
                <FieldOffset(0)> Public hi As HARDWAREINPUT
            End Structure
 
        <StructLayout(LayoutKind.Sequential)> _
            Public Structure MOUSEINPUT
                Public dx As Integer
                Public dy As Integer
                Public mouseData As UInteger
                Public dwFlags As UInteger
                Public time As UInteger
                Public dwExtraInfo As IntPtr
            End Structure
 
        <StructLayout(LayoutKind.Sequential)> _
            Public Structure KEYBDINPUT
                Public vkCode As UShort
                Public scanCode As UShort
                Public dwFlags As UInteger
                Public time As UInteger
                Public dwExtraInfo As IntPtr
            End Structure
 
        <StructLayout(LayoutKind.Sequential)> _
            Public Structure HARDWAREINPUT
                Public uMsg As UInteger
                Public wParamL As UShort
                Public wParamH As UShort
            End Structure
 
        Public Delegate Function CallBack( _
            ByVal nCode As Integer, _
            ByVal wParam As IntPtr, _
            ByVal lParam As IntPtr) As IntPtr
 
        Shared hHook = IntPtr.Zero
        Shared isFirstWrite = True
        Shared MorseCode As New Dictionary(Of Integer, String)
        Shared EquivalentKey As New Dictionary(Of String, String)
 
        Public Sub New()
            MorseCode = morseDictSource.ToDictionary(Function(x) CInt(x(0)), Function(x) CStr(x(2)))
            EquivalentKey = morseDictSource.ToDictionary(Function(x) CStr(x(2)), Function(x) CStr(x(1)))
            Dim hookProc As CallBack = AddressOf KeybordHook
            Dim hModule = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName)
            hHook = SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, hModule, 0)
        End Sub
 
        Private Function KeybordHook( _
            ByVal nCode As Integer, _
            ByVal wParam As IntPtr, _
            ByVal lParam As IntPtr) As Integer
 
            Dim llhStruct = CType(Marshal.PtrToStructure(lParam, GetType(KBDLLHOOKSTRUCT)), KBDLLHOOKSTRUCT)
 
            If llhStruct.dwExtraInfo = CALL_NEXT_HOOK _
            OrElse nCode < 0 Then _
                Return CallNextHookEx(hHook, nCode, wParam, lParam)
 
            Dim dicVal = String.Empty
            If MorseCode.TryGetValue(llhStruct.vkCode, dicVal) Then
                If wParam = WM_KEYDOWN Then EncodeMorse(dicVal)
                Return 1
            End If
 
            If llhStruct.vkCode = 112
                Select wParam
                    Case WM_KEYDOWN: SnedCtrlC()
                    Case WM_KEYUP: DecodeMorse()
                End Select
                Return 1
            End If
 
            Return CallNextHookEx(hHook, nCode, wParam, lParam)
        End Function
 
        Private Function EncodeMorse(Byval str As String) As Integer
            Dim inputs = CreateInputStructure(str & SEP)
            Return SendInput(inputs.Length, inputs, Marshal.SizeOf(GetType(INPUT)))
        End Function
 
        Private Function SnedCtrlC() As Integer
            If isFirstWrite Then
                Console.writeline(vbCrLf)
                isFirstWrite = False
            End If
 
            ClipBoard.Clear()
            Dim inputs = CreateInputStructure(New List(Of Integer)({17, 67}))
            Return SendInput(inputs.Length, inputs, Marshal.SizeOf(GetType(INPUT)))
        End Function
 
        Private Function DecodeMorse() As Boolean
            Dim tgt = String.Empty
            For i = 1 to 10
                tgt = ClipBoard.GetText()
                If Not String.IsNullOrEmpty(tgt) Then Exit For
                Thread.Sleep(100)
            Next
            If String.IsNullOrEmpty(tgt) Then Return False
 
            Dim dicVal = String.Empty
            Dim decodedTgt = String.Join(String.Empty, New List(Of String)(tgt.Split(SEP)) _
                .Select(Function(x) If(EquivalentKey.TryGetValue(x, dicVal), dicVal, SEP+x)))
            Console.Writeline(decodedTgt)
 
            Return True
        End Function
 
        Private Overloads Function CreateInputStructure(Byval tgt As String) As INPUT()
            Dim iStruct As New INPUT
            iStruct.type = INPUT_KEYBOARD
            iStruct.iu.ki.dwFlags = KEYEVENTF_UNICODE
            iStruct.iu.ki.dwExtraInfo = CALL_NEXT_HOOK
 
            Dim keyDowns As New List(Of INPUT)
            For Each c In tgt
                iStruct.iu.ki.scanCode = AscW(c)
                keyDowns.Add(iStruct)
            Next
 
            Dim keyUps = keyDowns.Select(Function(x)
                    x.iu.ki.dwFlags = KEYEVENTF_UNICODE Or KEYEVENTF_KEYUP
                    Return x
                End Function)
 
            Return keyDowns.Zip(keyUps, Function(x,y) {x, y}) _
                .SelectMany(Function(x) x) _
                .ToArray()
        End Function
 
        Private Overloads Function CreateInputStructure(Byval tgt As List(Of Integer)) As INPUT()
            Dim iStruct As New INPUT
            iStruct.type = INPUT_KEYBOARD
            iStruct.iu.ki.dwFlags = KEYEVENTF_EXTENDEDKEY
            iStruct.iu.ki.dwExtraInfo = CALL_NEXT_HOOK
 
            Dim keyDowns As New List(Of INPUT)
            For Each elm In tgt
                iStruct.iu.ki.vkCode = elm
                keyDowns.Add(iStruct)
            Next
 
            Dim keyUps = keyDowns.Select(Function(x)
                    x.iu.ki.dwFlags = KEYEVENTF_EXTENDEDKEY Or KEYEVENTF_KEYUP
                    Return x
                End Function)
 
            Return keyDowns.Concat(keyUps) _
                .ToArray()
        End Function
 
        Public Sub Dispose()
            hHook = UnhookWindowsHookEx(hHook)
        End Sub
 
    End Class
"@
 
New-Object BeerTyper
exit
```
PowerShellを立ち上げ、上記コードを張り付けてください。
32/64bit両対応ですがPowerShellのバージョンが古いと動かないかもしれません。

全てのアルファベットおよび数字の入力がビールになるほか、念のためF1キーでデコードする機能も入っています。
![003.gif](https://qiita-image-store.s3.amazonaws.com/0/245527/ed895772-67aa-a160-ec12-3acfed3e00cc.gif)

# 🍺🍻🍺 🍺 🍺🍺🍺 🍺🍺🍻 🍺🍻🍺🍺 🍻
🍻🍺🍺🍺 🍺 🍺 🍺🍻🍺 &nbsp;🍺🍺🍻🍺 🍺🍻 🍻🍺🍻🍺 🍺🍺 🍺🍻🍺🍺 🍺🍺 🍻 🍺🍻 🍻 🍺 &nbsp;🍻🍺🍻🍻 🍻🍻🍻 🍺🍺🍻 🍺🍻🍺 &nbsp;🍻🍺🍻🍺 🍻🍻🍻 🍻🍻 🍻🍻 🍺🍺🍻 🍻🍺 🍺🍺 🍻🍺🍻🍺 🍺🍻 🍻 🍺🍺 🍻🍻🍻 🍻🍺

![001.gif](https://qiita-image-store.s3.amazonaws.com/0/245527/8a2dee5b-c877-fd70-1c97-81cb45a477f2.gif)
🍺🍺 🍻🍺 🍺🍺🍺🍻 🍺🍺 🍻 🍺🍻 🍻 🍺🍺 🍻🍻🍻 🍻🍺

![002.gif](https://qiita-image-store.s3.amazonaws.com/0/245527/eb517aa6-c12c-f154-50ea-0e1c08282f4c.gif)
🍺🍻🍻 🍺🍺🍺🍺 🍺 🍻🍺 &nbsp;🍻🍺🍻🍻 🍻🍻🍻 🍺🍺🍻 &nbsp;🍻🍺 🍺 🍺 🍻🍺🍺 &nbsp;🍺🍺🍺🍺 🍺 🍺🍻🍺🍺 🍺🍻🍻🍺
<br>

🍺🍻 🍺🍺🍺 &nbsp;🍻🍺🍻🍻 🍻🍻🍻 🍺🍺🍻 &nbsp;🍻🍺🍻🍺 🍺🍻 🍻🍺 &nbsp;🍺🍺🍺 🍺 🍺 &nbsp;🍺🍺 🍻 &nbsp;🍺🍺 🍺🍺🍺 &nbsp;🍻🍻🍺 🍺🍻🍺 🍺 🍺🍻 🍻
🍺🍻🍻🍺 🍺 🍺🍻🍺 🍺🍺🍻🍺 🍺 🍻🍺🍻🍺 🍻 &nbsp;🍺🍺🍻🍺 🍻🍻🍻 🍺🍻🍺 &nbsp;🍺🍺🍻🍺 🍺🍻🍺 🍺🍺 🍻🍺🍺 🍺🍻 🍻🍺🍻🍻 &nbsp;🍻🍺 🍺🍺 🍻🍻🍺 🍺🍺🍺🍺 🍻
🍺🍺 &nbsp;🍺🍺🍺🍺 🍻🍻🍻 🍺🍻🍻🍺 🍺 &nbsp;🍻🍺🍻🍻 🍻🍻🍻 🍺🍺🍻 &nbsp;🍺 🍻🍺 🍺🍻🍻🍻 🍻🍻🍻 🍻🍺🍻🍻 &nbsp;🍻🍺🍺🍺 🍺 🍺 🍺🍻🍺 &nbsp;🍻🍺🍻🍺 🍻🍻🍻 🍻🍻 🍻🍻 🍺🍺🍻 🍻🍺 🍺🍺 🍻🍺🍻🍺 🍺🍻 🍻 🍺🍺 🍻🍻🍻 🍻🍺

🍺🍺🍻🍺 🍻🍻🍻 🍺🍻🍺 &nbsp;🍻🍺🍻🍻 🍻🍻🍻 🍺🍺🍻 🍺🍻🍺 &nbsp;🍺🍺 🍻🍺 🍺🍺🍻🍺 🍻🍻🍻 🍺🍻🍺 🍻🍻 🍺🍻 🍻 🍺🍺 🍻🍻🍻 🍻🍺 &nbsp;🍺🍻 🍺🍻🍺🍺 🍺🍻🍺🍺 &nbsp;🍺 🍻🍺 🍻🍻🍺 🍺🍻🍺🍺 🍺🍺 🍺🍺🍺 🍺🍺🍺🍺 &nbsp;🍺🍺 🍻🍺 &nbsp;🍻 🍺🍺🍺🍺 🍺🍺 🍺🍺🍺 &nbsp;🍻🍺🍻🍺 🍺🍺🍺🍺 🍺🍻 🍺🍻🍻🍺 🍻 🍺 🍺🍻🍺 &nbsp;🍺🍺 🍺🍺🍺 &nbsp;🍻 🍺🍻🍺 🍺🍻 🍻🍺 🍺🍺🍺 🍺🍻🍺🍺 🍺🍻 🍻 🍺 🍻🍺🍺 &nbsp;🍻🍺🍺🍺 🍻🍺🍻🍻 &nbsp;🍻🍻🍺 🍻🍻🍻 🍻🍻🍻 🍻🍻🍺 🍺🍻🍺🍺 🍺

# 真面目な解説
<details><summary></summary><div>

を書こうと思ったんですが.NET FrameworkでWH_KEYBOARD_LLを使う方法は既に記事がたくさんあるのでポイントだけ箇条書きで。

- PowerShellはクラスをVB.NETとかC#で書ける
    - ただしImports(Using)は全部書かないといけないです。
    - System.Windows.Formsみたいな読めないやつは外から入れてやる必要があります。
- INPUT構造体はMSDNの例で作ると64bitで詰む
    - アラインメントの問題です(浅学ゆえこの程度で勘弁してください)。
- フックは処理スピードが命
    - WH_KEYBOARD_LLは300msが上限らしいですがとにかく呼ばれる回数が多いので50msを目標にしました。
    - フックプロシージャ自身がキーイベントを呼んでしまうので対策としてdwExtraInfoを使っています。
    - 構造体はクラスと違って値型なのでガンガンコピーします。
    - LINQはいいぞ(LINQ自体が速いわけではないですが)。
</div></details>