use actl_core::keys::{Key, KeySpec};
use actl_core::{CtlError, ErrorCode};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
KEYEVENTF_UNICODE, MAPVK_VK_TO_VSC, MapVirtualKeyW, SendInput, VIRTUAL_KEY, VK_BACK,
VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_HOME, VK_LCONTROL, VK_LEFT,
VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MENU, VK_NEXT, VK_PRIOR, VK_RCONTROL, VK_RETURN, VK_RIGHT,
VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SHIFT, VK_SPACE, VK_TAB, VK_UP, VkKeyScanW,
};
use windows::Win32::UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowTextW};
pub fn foreground_title() -> Option<String> {
unsafe {
let hwnd = GetForegroundWindow();
if hwnd.0.is_null() {
return None;
}
let mut buf = [0u16; 512];
let len = GetWindowTextW(hwnd, &mut buf);
if len <= 0 {
return None;
}
Some(String::from_utf16_lossy(&buf[..len as usize]))
}
}
pub fn focused_root_title() -> Option<String> {
use windows::Win32::UI::WindowsAndMessaging::{
GA_ROOT, GUITHREADINFO, GetAncestor, GetGUIThreadInfo,
};
unsafe {
let mut info = GUITHREADINFO {
cbSize: std::mem::size_of::<GUITHREADINFO>() as u32,
..Default::default()
};
if !GetGUIThreadInfo(0, &mut info).is_ok() || info.hwndFocus.0.is_null() {
return None;
}
let root = GetAncestor(info.hwndFocus, GA_ROOT);
if root.0.is_null() {
return None;
}
let mut buf = [0u16; 512];
let len = GetWindowTextW(root, &mut buf);
if len <= 0 {
return None;
}
Some(String::from_utf16_lossy(&buf[..len as usize]))
}
}
const KEYDOWN: KEYBD_EVENT_FLAGS = KEYBD_EVENT_FLAGS(0);
fn vk_event(vk: VIRTUAL_KEY, up: bool) -> INPUT {
let scan = unsafe { MapVirtualKeyW(vk.0 as u32, MAPVK_VK_TO_VSC) } as u16;
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: vk,
wScan: scan,
dwFlags: if up { KEYEVENTF_KEYUP } else { KEYDOWN },
time: 0,
dwExtraInfo: 0,
},
},
}
}
fn uni_event(ch: u16, up: bool) -> INPUT {
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: VIRTUAL_KEY(0),
wScan: ch,
dwFlags: if up {
KEYEVENTF_KEYUP | KEYEVENTF_UNICODE
} else {
KEYEVENTF_UNICODE
},
time: 0,
dwExtraInfo: 0,
},
},
}
}
fn modifier_vk(name: &str) -> VIRTUAL_KEY {
match name {
"ctrl" => VK_CONTROL,
"shift" => VK_SHIFT,
"alt" => VK_MENU,
_ => VK_LWIN,
}
}
fn all_modifier_vks() -> [VIRTUAL_KEY; 11] {
[
VK_CONTROL,
VK_LCONTROL,
VK_RCONTROL,
VK_SHIFT,
VK_LSHIFT,
VK_RSHIFT,
VK_MENU,
VK_LMENU,
VK_RMENU,
VK_LWIN,
VK_RWIN,
]
}
pub fn held_modifiers() -> Vec<&'static str> {
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
all_modifier_vks()
.iter()
.filter(|vk| unsafe { GetKeyState(vk.0 as i32) } as u16 & 0x8000 != 0)
.map(|vk| match vk.0 {
0x11 | 0xA2 | 0xA3 => "ctrl",
0x10 | 0xA0 | 0xA1 => "shift",
0x12 | 0xA4 | 0xA5 => "alt",
_ => "win",
})
.collect()
}
fn named_vk(name: &str) -> Option<VIRTUAL_KEY> {
let vk = match name {
"enter" => VK_RETURN,
"esc" => VK_ESCAPE,
"tab" => VK_TAB,
"space" => VK_SPACE,
"del" => VK_DELETE,
"backspace" => VK_BACK,
"up" => VK_UP,
"down" => VK_DOWN,
"left" => VK_LEFT,
"right" => VK_RIGHT,
"home" => VK_HOME,
"end" => VK_END,
"pgup" => VK_PRIOR,
"pgdn" => VK_NEXT,
_ => {
let digits = name.strip_prefix('f')?;
let n: u8 = digits.parse().ok()?;
if (1..=12).contains(&n) {
VIRTUAL_KEY(VK_F1.0 + n as u16 - 1)
} else {
return None;
}
}
};
Some(vk)
}
fn char_vk(c: char) -> Option<(VIRTUAL_KEY, bool)> {
let r = unsafe { VkKeyScanW(c as u16) };
if r == -1 {
return None;
}
let hi = (r >> 8) as u8;
if hi & 0x80 != 0 {
return None; }
Some((VIRTUAL_KEY((r & 0xFF) as u16), hi & 1 != 0))
}
fn compile_key_spec(spec: &KeySpec) -> Result<Vec<INPUT>, CtlError> {
let mut inputs: Vec<INPUT> = Vec::new();
for m in &spec.modifiers {
inputs.push(vk_event(modifier_vk(m), false));
}
for key in &spec.keys {
match key {
Key::Char(c) => {
if !spec.modifiers.is_empty() {
let (vk, needs_shift) = char_vk(*c).ok_or_else(|| {
CtlError::protocol(format!(
"char {c:?} cannot be mapped to a virtual key for combo use \
(layout or dead key); send it as text via `type` instead, \
or use a named key"
))
})?;
let shift_held = spec.modifiers.iter().any(|m| m == "shift");
if needs_shift && !shift_held {
inputs.push(vk_event(VK_SHIFT, false));
}
inputs.push(vk_event(vk, false));
inputs.push(vk_event(vk, true));
if needs_shift && !shift_held {
inputs.push(vk_event(VK_SHIFT, true));
}
} else {
let mut buf = [0u16; 2];
for unit in c.encode_utf16(&mut buf) {
if *unit == 0 {
continue;
}
inputs.push(uni_event(*unit, false));
inputs.push(uni_event(*unit, true));
}
}
}
Key::Named(n) => {
let vk = named_vk(n).ok_or_else(|| {
CtlError::new(ErrorCode::Internal, format!("unmapped named key {n:?}"))
})?;
inputs.push(vk_event(vk, false));
inputs.push(vk_event(vk, true));
}
}
}
for m in spec.modifiers.iter().rev() {
inputs.push(vk_event(modifier_vk(m), true));
}
Ok(inputs)
}
pub fn send_key_spec(spec: &KeySpec) -> Result<(), CtlError> {
wait_modifiers_clear()?;
let inputs = compile_key_spec(spec)?;
let sent = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
if sent == inputs.len() as u32 {
Ok(())
} else {
Err(CtlError::internal(format!(
"SendInput delivered {sent}/{} events; modifier/key state is now unknown \
— do not retry blindly, re-observe the target first",
inputs.len()
)))
}
}
pub fn wait_modifiers_clear() -> Result<(), CtlError> {
let deadline = std::time::Instant::now()
+ std::time::Duration::from_millis(actl_core::timing::Timing::load().modifier_wait_ms);
loop {
let held = held_modifiers();
if held.is_empty() {
return Ok(());
}
if std::time::Instant::now() >= deadline {
let held = held.join("/");
return Err(CtlError::new(
ErrorCode::NotActionable,
format!(
"modifier key(s) physically held: {held}; injection would be \
hijacked into shortcuts — release them (or tap the key to clear \
a stale state) and retry"
),
));
}
std::thread::sleep(std::time::Duration::from_millis(
actl_core::timing::Timing::load().poll_ms,
));
}
}
pub fn send_key_partial(spec: &KeySpec, down: bool) -> Result<(), CtlError> {
if down {
wait_modifiers_clear()?;
}
let inputs: Vec<INPUT> = compile_key_spec(spec)?
.into_iter()
.filter(|i| {
let ki = unsafe { i.Anonymous.ki };
ki.dwFlags.contains(KEYEVENTF_KEYUP) != down
})
.collect();
if inputs.is_empty() {
return Err(CtlError::protocol("nothing to send for this half"));
}
let sent = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
if sent == inputs.len() as u32 {
Ok(())
} else {
Err(CtlError::internal(format!(
"SendInput delivered {sent}/{} events; key state is now unknown",
inputs.len()
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use actl_core::keys::parse_key_expr;
fn compile(expr: &str) -> Vec<INPUT> {
let spec = parse_key_expr(expr).unwrap();
compile_key_spec(&spec).unwrap()
}
fn vk_seq(events: &[INPUT]) -> Vec<(u16, bool)> {
events
.iter()
.map(|i| {
let ki = unsafe { i.Anonymous.ki };
(ki.wVk.0, ki.dwFlags.contains(KEYEVENTF_KEYUP))
})
.collect()
}
fn is_unicode(i: &INPUT) -> bool {
unsafe { i.Anonymous.ki.dwFlags.contains(KEYEVENTF_UNICODE) }
}
#[test]
fn combo_chars_go_vk_channel() {
let ev = compile("ctrl+s");
assert_eq!(
ev.len(),
4,
"ctrl↓ s↓ s↑ ctrl↑;清场事件属于发送层,不进编译产物"
);
assert!(
ev.iter().all(|e| !is_unicode(e)),
"组合键成员禁止 UNICODE 通道"
);
assert_eq!(
vk_seq(&ev),
vec![(0x11, false), (0x53, false), (0x53, true), (0x11, true)]
);
}
#[test]
fn plain_text_goes_unicode_channel() {
let spec = KeySpec {
modifiers: vec![],
keys: vec![Key::Char('a'), Key::Char('b')],
};
let ev = compile_key_spec(&spec).unwrap();
assert_eq!(ev.len(), 4);
assert!(ev.iter().all(is_unicode), "纯文本固定 UNICODE 通道");
let units: Vec<u16> = ev.iter().map(|i| unsafe { i.Anonymous.ki.wScan }).collect();
assert_eq!(
units,
vec![b'a' as u16, b'a' as u16, b'b' as u16, b'b' as u16]
);
}
#[test]
fn unmappable_combo_char_is_rejected() {
let spec = KeySpec {
modifiers: vec!["ctrl".into()],
keys: vec![Key::Char('中')],
};
let err = match compile_key_spec(&spec) {
Err(e) => e,
Ok(_) => panic!("unmappable combo char must be rejected, got events"),
};
assert_eq!(err.code, ErrorCode::Protocol);
assert!(err.message.contains("type"));
}
#[test]
fn combo_with_shift_required_char_adds_shift() {
let ev = compile("ctrl+!");
let seq = vk_seq(&ev);
assert_eq!(
seq,
vec![
(0x11, false), (VK_SHIFT.0, false), (0x31, false), (0x31, true), (VK_SHIFT.0, true),
(0x11, true),
]
);
}
#[test]
fn f10_to_f12_compile_to_vk() {
for (name, vk) in [("f1", 0x70u16), ("f10", 0x79), ("f12", 0x7B)] {
let seq = vk_seq(&compile(name));
assert_eq!(seq, vec![(vk, false), (vk, true)], "{name}");
}
}
#[test]
fn named_keys_and_multi_key_sequences() {
let ev = compile("enter");
assert_eq!(vk_seq(&ev), vec![(VK_RETURN.0, false), (VK_RETURN.0, true)]);
let ev = compile("ctrl+a+b");
let seq = vk_seq(&ev);
assert_eq!(seq.first().copied(), Some((0x11, false)));
assert_eq!(seq.last().copied(), Some((0x11, true)));
assert_eq!(seq.len(), 6);
}
}