Files
desktop/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs
2026-05-15 22:50:09 +08:00

306 lines
9.0 KiB
C#

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 const ushort VirtualKeyV = 0x56;
private static readonly IntPtr EnglishKeyboardLayout = LoadKeyboardLayout("00000409", KlfActivate);
public void InsertText(string text)
{
if (string.IsNullOrEmpty(text))
{
return;
}
AppLogger.Info($"Pasting text length: {text.Length}");
PasteTextViaClipboard(text);
}
private static void TypeTextWithSendInput(string text)
{
AppLogger.Info($"Falling back to SendInput 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);
}
}
private static void PasteTextViaClipboard(string text)
{
Exception? failure = null;
var thread = new Thread(() =>
{
try
{
PasteTextOnStaThread(text);
}
catch (Exception ex)
{
failure = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (failure is not null)
{
AppLogger.Error("Clipboard paste failed.", failure);
TypeTextWithSendInput(text);
}
}
private static void PasteTextOnStaThread(string text)
{
var hadText = Clipboard.ContainsText(TextDataFormat.UnicodeText);
var previousText = hadText ? Clipboard.GetText(TextDataFormat.UnicodeText) : null;
try
{
Clipboard.SetText(text, TextDataFormat.UnicodeText);
Thread.Sleep(30);
SendModifiedKey(VirtualKeyControl, VirtualKeyV);
Thread.Sleep(150);
}
finally
{
RestoreClipboardText(hadText, previousText);
}
}
private static void RestoreClipboardText(bool hadText, string? previousText)
{
try
{
if (hadText && previousText is not null)
{
Clipboard.SetText(previousText, TextDataFormat.UnicodeText);
return;
}
Clipboard.Clear();
}
catch (Exception ex)
{
AppLogger.Error("Failed to restore clipboard text.", ex);
}
}
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<INPUT>());
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;
}
}