using System.Runtime.InteropServices;
namespace WindowsBridge;
///
/// Mouse and keyboard input injection using SendInput().
/// Equivalent to macOS CoreGraphicsBridge.swift.
///
class InputBridge
{
// SendInput structures and constants
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[DllImport("user32.dll")]
private static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X, Y;
}
[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;
}
[StructLayout(LayoutKind.Sequential)]
private struct MOUSEINPUT
{
public int dx, dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
// Input type constants
private const uint INPUT_MOUSE = 0;
private const uint INPUT_KEYBOARD = 1;
// Mouse event flags
private const uint MOUSEEVENTF_MOVE = 0x0001;
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
private const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
private const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
private const uint MOUSEEVENTF_WHEEL = 0x0800;
private const uint MOUSEEVENTF_HWHEEL = 0x1000;
private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
// Keyboard event flags
private const uint KEYEVENTF_KEYUP = 0x0002;
private const uint KEYEVENTF_UNICODE = 0x0004;
// System metrics
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 1;
// Virtual key codes
private const ushort VK_SHIFT = 0x10;
private const ushort VK_CONTROL = 0x11;
private const ushort VK_MENU = 0x12; // Alt
private const ushort VK_LWIN = 0x5B;
private const ushort VK_RETURN = 0x0D;
private const ushort VK_TAB = 0x09;
private const ushort VK_ESCAPE = 0x1B;
private const ushort VK_SPACE = 0x20;
private const ushort VK_BACK = 0x08;
private const ushort VK_DELETE = 0x2E;
private const ushort VK_UP = 0x26;
private const ushort VK_DOWN = 0x28;
private const ushort VK_LEFT = 0x25;
private const ushort VK_RIGHT = 0x27;
private const ushort VK_HOME = 0x24;
private const ushort VK_END = 0x23;
private const ushort VK_PRIOR = 0x21; // Page Up
private const ushort VK_NEXT = 0x22; // Page Down
private const ushort VK_F1 = 0x70;
///
/// Click at screen coordinates.
///
public Dictionary MouseClick(double x, double y, string button, int clickCount)
{
SetCursorPos((int)x, (int)y);
System.Threading.Thread.Sleep(10);
uint downFlag, upFlag;
switch (button.ToLowerInvariant())
{
case "right":
downFlag = MOUSEEVENTF_RIGHTDOWN;
upFlag = MOUSEEVENTF_RIGHTUP;
break;
case "middle":
downFlag = MOUSEEVENTF_MIDDLEDOWN;
upFlag = MOUSEEVENTF_MIDDLEUP;
break;
default: // left
downFlag = MOUSEEVENTF_LEFTDOWN;
upFlag = MOUSEEVENTF_LEFTUP;
break;
}
for (int i = 0; i < clickCount; i++)
{
var inputs = new INPUT[]
{
new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = downFlag } } },
new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = upFlag } } },
};
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf());
if (i < clickCount - 1)
System.Threading.Thread.Sleep(30);
}
return new Dictionary { ["ok"] = true };
}
///
/// Move mouse to screen coordinates.
///
public Dictionary MouseMove(double x, double y)
{
SetCursorPos((int)x, (int)y);
return new Dictionary { ["ok"] = true };
}
///
/// Drag from one point to another with interpolation.
///
public Dictionary MouseDrag(double fromX, double fromY, double toX, double toY)
{
SetCursorPos((int)fromX, (int)fromY);
System.Threading.Thread.Sleep(50);
// Mouse down
var downInput = new INPUT[]
{
new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTDOWN } } },
};
SendInput(1, downInput, Marshal.SizeOf());
System.Threading.Thread.Sleep(50);
// Interpolate movement
int steps = 20;
for (int i = 1; i <= steps; i++)
{
double t = (double)i / steps;
int cx = (int)(fromX + (toX - fromX) * t);
int cy = (int)(fromY + (toY - fromY) * t);
SetCursorPos(cx, cy);
System.Threading.Thread.Sleep(10);
}
// Mouse up
var upInput = new INPUT[]
{
new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTUP } } },
};
SendInput(1, upInput, Marshal.SizeOf());
return new Dictionary { ["ok"] = true };
}
///
/// Press a key combination (e.g., ["ctrl", "c"], ["alt", "f4"]).
/// Maps macOS modifier names to Windows equivalents.
///
public Dictionary KeyCombo(string[] keys)
{
var modifiers = new List();
var regularKeys = new List();
foreach (var key in keys)
{
var vk = MapKeyToVK(key.Trim().ToLowerInvariant());
if (IsModifier(vk))
modifiers.Add(vk);
else
regularKeys.Add(vk);
}
var inputs = new List();
// Press modifiers down
foreach (var mod in modifiers)
{
inputs.Add(new INPUT
{
Type = INPUT_KEYBOARD,
U = new INPUTUNION { ki = new KEYBDINPUT { wVk = mod } }
});
}
// Press regular keys
foreach (var key in regularKeys)
{
inputs.Add(new INPUT
{
Type = INPUT_KEYBOARD,
U = new INPUTUNION { ki = new KEYBDINPUT { wVk = key } }
});
}
// Release regular keys
foreach (var key in regularKeys)
{
inputs.Add(new INPUT
{
Type = INPUT_KEYBOARD,
U = new INPUTUNION { ki = new KEYBDINPUT { wVk = key, dwFlags = KEYEVENTF_KEYUP } }
});
}
// Release modifiers (reverse order)
for (int i = modifiers.Count - 1; i >= 0; i--)
{
inputs.Add(new INPUT
{
Type = INPUT_KEYBOARD,
U = new INPUTUNION { ki = new KEYBDINPUT { wVk = modifiers[i], dwFlags = KEYEVENTF_KEYUP } }
});
}
SendInput((uint)inputs.Count, inputs.ToArray(), Marshal.SizeOf());
return new Dictionary { ["ok"] = true };
}
///
/// Type text character by character using Unicode input.
///
public Dictionary TypeText(string text)
{
var inputs = new List();
foreach (var ch in text)
{
// Key down
inputs.Add(new INPUT
{
Type = INPUT_KEYBOARD,
U = new INPUTUNION
{
ki = new KEYBDINPUT
{
wVk = 0,
wScan = (ushort)ch,
dwFlags = KEYEVENTF_UNICODE,
}
}
});
// Key up
inputs.Add(new INPUT
{
Type = INPUT_KEYBOARD,
U = new INPUTUNION
{
ki = new KEYBDINPUT
{
wVk = 0,
wScan = (ushort)ch,
dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
}
}
});
}
// Send in batches to avoid overflow
const int batchSize = 50;
for (int i = 0; i < inputs.Count; i += batchSize)
{
var batch = inputs.Skip(i).Take(batchSize).ToArray();
SendInput((uint)batch.Length, batch, Marshal.SizeOf());
if (i + batchSize < inputs.Count)
System.Threading.Thread.Sleep(10);
}
return new Dictionary { ["ok"] = true };
}
///
/// Scroll at a position.
///
public Dictionary Scroll(double x, double y, int deltaX, int deltaY)
{
SetCursorPos((int)x, (int)y);
System.Threading.Thread.Sleep(10);
var inputs = new List();
// Vertical scroll
if (deltaY != 0)
{
inputs.Add(new INPUT
{
Type = INPUT_MOUSE,
U = new INPUTUNION
{
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_WHEEL,
mouseData = (uint)(deltaY * 120), // 120 = WHEEL_DELTA
}
}
});
}
// Horizontal scroll
if (deltaX != 0)
{
inputs.Add(new INPUT
{
Type = INPUT_MOUSE,
U = new INPUTUNION
{
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_HWHEEL,
mouseData = (uint)(deltaX * 120),
}
}
});
}
if (inputs.Count > 0)
SendInput((uint)inputs.Count, inputs.ToArray(), Marshal.SizeOf());
return new Dictionary { ["ok"] = true };
}
// ── Key mapping ──
private ushort MapKeyToVK(string key)
{
return key switch
{
// Modifiers — map macOS names to Windows
"cmd" or "command" or "win" or "super" => VK_LWIN,
"ctrl" or "control" => VK_CONTROL,
"alt" or "option" or "opt" => VK_MENU,
"shift" => VK_SHIFT,
// Special keys
"enter" or "return" => VK_RETURN,
"tab" => VK_TAB,
"escape" or "esc" => VK_ESCAPE,
"space" => VK_SPACE,
"backspace" or "delete" => VK_BACK,
"forwarddelete" or "del" => VK_DELETE,
"up" or "uparrow" => VK_UP,
"down" or "downarrow" => VK_DOWN,
"left" or "leftarrow" => VK_LEFT,
"right" or "rightarrow" => VK_RIGHT,
"home" => VK_HOME,
"end" => VK_END,
"pageup" => VK_PRIOR,
"pagedown" => VK_NEXT,
// Function keys
"f1" => VK_F1,
"f2" => (ushort)(VK_F1 + 1),
"f3" => (ushort)(VK_F1 + 2),
"f4" => (ushort)(VK_F1 + 3),
"f5" => (ushort)(VK_F1 + 4),
"f6" => (ushort)(VK_F1 + 5),
"f7" => (ushort)(VK_F1 + 6),
"f8" => (ushort)(VK_F1 + 7),
"f9" => (ushort)(VK_F1 + 8),
"f10" => (ushort)(VK_F1 + 9),
"f11" => (ushort)(VK_F1 + 10),
"f12" => (ushort)(VK_F1 + 11),
// Single character — use virtual key code for A-Z, 0-9
_ when key.Length == 1 => CharToVK(key[0]),
_ => throw new BridgeException($"Unknown key: {key}"),
};
}
private static ushort CharToVK(char c)
{
c = char.ToUpperInvariant(c);
if (c >= 'A' && c <= 'Z') return (ushort)c; // VK_A..VK_Z = 0x41..0x5A = 'A'..'Z'
if (c >= '0' && c <= '9') return (ushort)c; // VK_0..VK_9 = 0x30..0x39 = '0'..'9'
return c switch
{
'-' => 0xBD, // VK_OEM_MINUS
'=' => 0xBB, // VK_OEM_PLUS
'[' => 0xDB, // VK_OEM_4
']' => 0xDD, // VK_OEM_6
'\\' => 0xDC, // VK_OEM_5
';' => 0xBA, // VK_OEM_1
'\'' => 0xDE, // VK_OEM_7
',' => 0xBC, // VK_OEM_COMMA
'.' => 0xBE, // VK_OEM_PERIOD
'/' => 0xBF, // VK_OEM_2
'`' => 0xC0, // VK_OEM_3
_ => 0,
};
}
private static bool IsModifier(ushort vk) =>
vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU || vk == VK_LWIN;
}