namespace WirelessTextSyncer.Windows.Services; using System.Runtime.InteropServices; public sealed class KeyboardInjectionService : IKeyboardInjectionService { private const uint InputKeyboard = 1; private const uint KeyEventFKeyUp = 0x0002; private const uint KeyEventFUnicode = 0x0004; private const uint KlfActivate = 0x00000001; private const uint WmInputLangChangeRequest = 0x0050; private const ushort VirtualKeyA = 0x41; private const ushort VirtualKeyBack = 0x08; private const ushort VirtualKeyControl = 0x11; private const ushort VirtualKeyReturn = 0x0D; private static readonly IntPtr EnglishKeyboardLayout = LoadKeyboardLayout("00000409", KlfActivate); public void InsertText(string text) { if (string.IsNullOrEmpty(text)) { return; } AppLogger.Info($"Injecting text length: {text.Length}"); using var keyboardLayoutScope = KeyboardLayoutScope.SwitchForegroundWindowToEnglish(); foreach (var character in text) { AppLogger.Info($"Injecting char U+{(int)character:X4}: {character}"); SendUnicode(character); Thread.Sleep(2); } } public void ReplaceFocusedText(string text) { AppLogger.Info($"Replacing focused text length: {text.Length}"); SendModifiedKey(VirtualKeyControl, VirtualKeyA); Backspace(); InsertText(text); AppLogger.Info("Finished replacing focused text."); } public void Backspace() { AppLogger.Info("Injecting backspace."); SendVirtualKey(VirtualKeyBack); } public void Enter() { AppLogger.Info("Injecting enter."); SendVirtualKey(VirtualKeyReturn); } private static void SendUnicode(char character) { Send([ CreateUnicodeInput(character, 0), CreateUnicodeInput(character, KeyEventFKeyUp) ]); } private static void SendVirtualKey(ushort virtualKey) { Send([ CreateVirtualKeyInput(virtualKey, 0), CreateVirtualKeyInput(virtualKey, KeyEventFKeyUp) ]); } private static void SendModifiedKey(ushort modifierKey, ushort key) { Send([ CreateVirtualKeyInput(modifierKey, 0), CreateVirtualKeyInput(key, 0), CreateVirtualKeyInput(key, KeyEventFKeyUp), CreateVirtualKeyInput(modifierKey, KeyEventFKeyUp) ]); } private static void Send(INPUT[] inputs) { var sent = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); if (sent != inputs.Length) { var error = Marshal.GetLastWin32Error(); AppLogger.Error( $"Windows accepted {sent}/{inputs.Length} keyboard input events. Win32 error: {error}"); } } private static INPUT CreateUnicodeInput(char character, uint flags) { return new INPUT { type = InputKeyboard, U = new InputUnion { ki = new KEYBDINPUT { wScan = character, dwFlags = KeyEventFUnicode | flags } } }; } private static INPUT CreateVirtualKeyInput(ushort virtualKey, uint flags) { return new INPUT { type = InputKeyboard, U = new InputUnion { ki = new KEYBDINPUT { wVk = virtualKey, dwFlags = flags } } }; } [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr windowHandle, out uint processId); [DllImport("user32.dll")] private static extern IntPtr GetKeyboardLayout(uint threadId); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr LoadKeyboardLayout(string keyboardLayoutId, uint flags); [DllImport("user32.dll", SetLastError = true)] private static extern bool PostMessage(IntPtr windowHandle, uint message, IntPtr wParam, IntPtr lParam); private sealed class KeyboardLayoutScope : IDisposable { private readonly IntPtr foregroundWindow; private readonly IntPtr originalKeyboardLayout; private KeyboardLayoutScope(IntPtr foregroundWindow, IntPtr originalKeyboardLayout) { this.foregroundWindow = foregroundWindow; this.originalKeyboardLayout = originalKeyboardLayout; } public static KeyboardLayoutScope SwitchForegroundWindowToEnglish() { var foregroundWindow = GetForegroundWindow(); if (foregroundWindow == IntPtr.Zero) { AppLogger.Error("Cannot switch keyboard layout because no foreground window was found."); return new KeyboardLayoutScope(IntPtr.Zero, IntPtr.Zero); } var threadId = GetWindowThreadProcessId(foregroundWindow, out _); var originalKeyboardLayout = GetKeyboardLayout(threadId); AppLogger.Info($"Switching foreground keyboard layout from 0x{originalKeyboardLayout.ToInt64():X} to 0x{EnglishKeyboardLayout.ToInt64():X}."); if (EnglishKeyboardLayout != IntPtr.Zero) { PostMessage(foregroundWindow, WmInputLangChangeRequest, IntPtr.Zero, EnglishKeyboardLayout); Thread.Sleep(50); } return new KeyboardLayoutScope(foregroundWindow, originalKeyboardLayout); } public void Dispose() { if (foregroundWindow == IntPtr.Zero || originalKeyboardLayout == IntPtr.Zero) { return; } AppLogger.Info($"Restoring foreground keyboard layout to 0x{originalKeyboardLayout.ToInt64():X}."); PostMessage(foregroundWindow, WmInputLangChangeRequest, IntPtr.Zero, originalKeyboardLayout); } } [StructLayout(LayoutKind.Sequential)] private struct INPUT { public uint type; public InputUnion U; } [StructLayout(LayoutKind.Explicit)] private struct InputUnion { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; [FieldOffset(0)] public HARDWAREINPUT hi; } [StructLayout(LayoutKind.Sequential)] private struct MOUSEINPUT { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public UIntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public UIntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct HARDWAREINPUT { public uint uMsg; public ushort wParamL; public ushort wParamH; } }