Search code examples
powershellhotkeyskeystrokekeylogger

How to capture global keystrokes with PowerShell?


Can Powershell listen for and capture key presses?

Is it possible to write a PowerShell script that, like AutoHotkey, sits in tray and waits until you press a predefined keyboard key to start execution? And possibly not return but fire every time you press said key?

What I would like to achieve is - perform a predefined scripted action at the press of a button only AFTER starting the script, so putting it on the desktop and defining a shortcut key doesn't work.

For example:
I'd like the text "TEST" typed 3 times every time I press the "x" key but I would like this to happen only if the script that does this is running. So when the script is not running - pressing "x" would do nothing.

Basically, AutoHotkey can do exactly that but I'd like to do it in PowerShell, if possible, without writing huge amounts of C# code, because then I'd just write a tiny C# tray application for that.


Solution

  • Not in PowerShell directly maybe, but as you can run pretty much any C# code, here is a basic working example based on an excellent solution by Peter Hinchley:

    Add-Type -TypeDefinition '
    using System;
    using System.IO;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;
    
    namespace KeyLogger {
      public static class Program {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
    
        private static HookProc hookProc = HookCallback;
        private static IntPtr hookId = IntPtr.Zero;
        private static int keyCode = 0;
    
        [DllImport("user32.dll")]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    
        [DllImport("user32.dll")]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);
    
        [DllImport("user32.dll")]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
    
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetModuleHandle(string lpModuleName);
    
        public static int WaitForKey() {
          hookId = SetHook(hookProc);
          Application.Run();
          UnhookWindowsHookEx(hookId);
          return keyCode;
        }
    
        private static IntPtr SetHook(HookProc hookProc) {
          IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
          return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, moduleHandle, 0);
        }
    
        private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    
        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
          if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
            keyCode = Marshal.ReadInt32(lParam);
            Application.Exit();
          }
          return CallNextHookEx(hookId, nCode, wParam, lParam);
        }
      }
    }
    ' -ReferencedAssemblies System.Windows.Forms
    
    while ($true) {
        $key = [System.Windows.Forms.Keys][KeyLogger.Program]::WaitForKey()
        if ($key -eq "X") {
            Write-Host "Do something now."
        }
    }
    

    Version 2

    (using a callback):

    Add-Type -TypeDefinition '
      using System;
      using System.IO;
      using System.Diagnostics;
      using System.Runtime.InteropServices;
      using System.Windows.Forms;
    
      namespace PowerShell {
        public static class KeyLogger {
          private const int WH_KEYBOARD_LL = 13;
          private const int WM_KEYDOWN = 0x0100;
    
          private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    
          private static Action<Keys> keyCallback;
          private static IntPtr hookId = IntPtr.Zero;
    
          [DllImport("user32.dll")]
          private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    
          [DllImport("user32.dll")]
          private static extern bool UnhookWindowsHookEx(IntPtr hhk);
    
          [DllImport("user32.dll")]
          private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
    
          [DllImport("kernel32.dll")]
          private static extern IntPtr GetModuleHandle(string lpModuleName);
    
          public static void Run(Action<Keys> callback) {
            keyCallback = callback;
            hookId = SetHook();
            Application.Run();
            UnhookWindowsHookEx(hookId);
          }
    
          private static IntPtr SetHook() {
            IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
            return SetWindowsHookEx(WH_KEYBOARD_LL, HookCallback, moduleHandle, 0);
          }
    
          private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
            if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
              var key = (Keys)Marshal.ReadInt32(lParam);
              keyCallback(key);
            }
            return CallNextHookEx(hookId, nCode, wParam, lParam);
          }
        }
      }
    ' -ReferencedAssemblies System.Windows.Forms
    
    [PowerShell.KeyLogger]::Run({
      param($key)
      if ($key -eq "X") {
        Write-Host "Do something now."
      }
    })