use async_trait::async_trait;
use super::Platform;
use super::screenshot::resolve_screenshot_window;
use crate::error::PlatformError;
use crate::traits::keyboard::{
AppKeyboard, AppKeyboardAction, AppKeyboardModifier, AppKeyboardRequest, AppKeyboardResult,
};
use windows::Win32::Foundation::{HWND, LPARAM, WPARAM};
use windows::Win32::UI::Input::KeyboardAndMouse::{
MAPVK_VK_TO_VSC, MapVirtualKeyW, VIRTUAL_KEY, VK_BACK, VK_CONTROL, VK_DELETE, VK_DOWN, VK_END,
VK_ESCAPE, VK_HOME, VK_LEFT, VK_MENU, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT,
VK_SPACE, VK_TAB, VK_UP, VkKeyScanW,
};
use windows::Win32::UI::WindowsAndMessaging::{
self, GUITHREADINFO, GetGUIThreadInfo, GetWindowThreadProcessId, PostMessageW,
};
#[async_trait]
impl AppKeyboard for Platform {
async fn perform_app_keyboard(
&self,
request: AppKeyboardRequest,
) -> Result<AppKeyboardResult, PlatformError> {
let hwnd = resolve_screenshot_window(request.window_id.as_deref())?;
let window = hwnd.0 as isize;
let window_id = window.to_string();
let action_kind = request.action.kind().to_string();
let modifier_reliability = match &request.action {
AppKeyboardAction::Press { modifiers, .. } if !modifiers.is_empty() => {
Some("best_effort".to_string())
}
_ => None,
};
dispatch_key(window, &request.action)?;
Ok(AppKeyboardResult {
window_id,
action: action_kind,
modifier_reliability,
})
}
}
fn dispatch_key(window: isize, action: &AppKeyboardAction) -> Result<(), PlatformError> {
let target = focus_target(window);
match action {
AppKeyboardAction::Type { text } => {
for unit in text.encode_utf16() {
post(target, WindowsAndMessaging::WM_CHAR, unit as usize, 1)?;
}
Ok(())
}
AppKeyboardAction::Press { key, modifiers } => {
let (vk, ch) = resolve_key(key)
.ok_or_else(|| PlatformError::Platform(format!("unknown key '{key}'")))?;
let mods: Vec<u16> = modifiers.iter().map(|m| modifier_vk(*m)).collect();
for &m in &mods {
key_event(target, m, true)?;
}
key_event(target, vk, true)?;
if let Some(ch) = ch {
post(target, WindowsAndMessaging::WM_CHAR, ch as usize, 1)?;
}
key_event(target, vk, false)?;
for &m in mods.iter().rev() {
key_event(target, m, false)?;
}
Ok(())
}
}
}
fn focus_target(window: isize) -> HWND {
let top = HWND(window as *mut core::ffi::c_void);
let tid = unsafe { GetWindowThreadProcessId(top, None) };
if tid != 0 {
let mut gti = GUITHREADINFO {
cbSize: std::mem::size_of::<GUITHREADINFO>() as u32,
..Default::default()
};
unsafe {
if GetGUIThreadInfo(tid, &mut gti).is_ok() && !gti.hwndFocus.is_invalid() {
return gti.hwndFocus;
}
}
}
top
}
fn key_event(target: HWND, vk: u16, down: bool) -> Result<(), PlatformError> {
let scan = unsafe { MapVirtualKeyW(vk as u32, MAPVK_VK_TO_VSC) } & 0xFF;
let mut lparam = (scan << 16) | 1;
let msg = if down {
WindowsAndMessaging::WM_KEYDOWN
} else {
lparam |= (1 << 30) | (1 << 31);
WindowsAndMessaging::WM_KEYUP
};
post(target, msg, vk as usize, lparam as isize)
}
fn post(target: HWND, msg: u32, wparam: usize, lparam: isize) -> Result<(), PlatformError> {
unsafe {
PostMessageW(Some(target), msg, WPARAM(wparam), LPARAM(lparam))
.map_err(|err| PlatformError::Platform(format!("PostMessageW failed: {err}")))
}
}
fn modifier_vk(m: AppKeyboardModifier) -> u16 {
match m {
AppKeyboardModifier::Command | AppKeyboardModifier::Control => VK_CONTROL.0,
AppKeyboardModifier::Shift => VK_SHIFT.0,
AppKeyboardModifier::Option => VK_MENU.0,
}
}
fn resolve_key(key: &str) -> Option<(u16, Option<u16>)> {
let named: Option<(VIRTUAL_KEY, Option<u16>)> = match key.to_ascii_lowercase().as_str() {
"return" | "enter" => Some((VK_RETURN, Some(b'\r' as u16))),
"tab" => Some((VK_TAB, Some(b'\t' as u16))),
"escape" | "esc" => Some((VK_ESCAPE, Some(0x1B))),
"space" => Some((VK_SPACE, Some(b' ' as u16))),
"backspace" => Some((VK_BACK, Some(0x08))),
"delete" | "del" => Some((VK_DELETE, None)),
"up" => Some((VK_UP, None)),
"down" => Some((VK_DOWN, None)),
"left" => Some((VK_LEFT, None)),
"right" => Some((VK_RIGHT, None)),
"home" => Some((VK_HOME, None)),
"end" => Some((VK_END, None)),
"pageup" => Some((VK_PRIOR, None)),
"pagedown" => Some((VK_NEXT, None)),
_ => None,
};
if let Some((vk, ch)) = named {
return Some((vk.0, ch));
}
let mut chars = key.chars();
let ch = chars.next()?;
if chars.next().is_some() {
return None;
}
let scan = unsafe { VkKeyScanW(ch as u16) };
if scan == -1 {
return None;
}
let vk = (scan & 0xFF) as u16;
Some((vk, Some(ch as u16)))
}