|
|
|
|
@@ -1,38 +1,141 @@
|
|
|
|
|
use std::ffi::OsString;
|
|
|
|
|
use std::os::windows::ffi::OsStringExt;
|
|
|
|
|
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
|
|
|
|
|
|
|
|
|
use tracing::debug;
|
|
|
|
|
use windows::Win32::Foundation::{GetLastError, HWND};
|
|
|
|
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|
|
|
|
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE,
|
|
|
|
|
VIRTUAL_KEY,
|
|
|
|
|
};
|
|
|
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
|
|
|
GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW,
|
|
|
|
|
use anyhow::{anyhow, Result};
|
|
|
|
|
use tracing::{debug, error, warn};
|
|
|
|
|
use windows::Win32::{
|
|
|
|
|
Foundation::{GetLastError, SetLastError, HWND, WIN32_ERROR},
|
|
|
|
|
UI::{
|
|
|
|
|
Input::KeyboardAndMouse::{
|
|
|
|
|
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP,
|
|
|
|
|
KEYEVENTF_UNICODE, VIRTUAL_KEY,
|
|
|
|
|
},
|
|
|
|
|
WindowsAndMessaging::{GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
|
|
|
|
|
|
|
|
|
|
fn clear_last_error() {
|
|
|
|
|
debug!("Clearing last error with SetLastError.");
|
|
|
|
|
unsafe {
|
|
|
|
|
SetLastError(WIN32_ERROR(0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_last_error() -> WIN32_ERROR {
|
|
|
|
|
let last_err = unsafe { GetLastError() };
|
|
|
|
|
debug!("GetLastError(): {}", last_err.to_hresult().message());
|
|
|
|
|
last_err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The handle should be validated before any unsafe calls referencing it.
|
|
|
|
|
fn validate_window_handle(handle: &HWND) -> Result<()> {
|
|
|
|
|
if handle.is_invalid() {
|
|
|
|
|
error!("Window handle is invalid.");
|
|
|
|
|
return Err(anyhow!("Window handle is invalid."));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Window title --------------
|
|
|
|
|
|
|
|
|
|
/// Gets the title bar string for the foreground window.
|
|
|
|
|
pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
|
|
|
|
|
let Ok(window_handle) = get_foreground_window() else {
|
|
|
|
|
return Err(());
|
|
|
|
|
};
|
|
|
|
|
let Ok(Some(window_title)) = get_window_title(window_handle) else {
|
|
|
|
|
return Err(());
|
|
|
|
|
};
|
|
|
|
|
pub fn get_foreground_window_title() -> Result<String> {
|
|
|
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
|
|
|
|
|
let window_handle = unsafe { GetForegroundWindow() };
|
|
|
|
|
|
|
|
|
|
Ok(window_title)
|
|
|
|
|
debug!("GetForegroundWindow() called.");
|
|
|
|
|
|
|
|
|
|
validate_window_handle(&window_handle)?;
|
|
|
|
|
|
|
|
|
|
get_window_title(&window_handle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the length of the window title bar text.
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
|
|
|
|
|
fn get_window_title_length(window_handle: &HWND) -> Result<usize> {
|
|
|
|
|
// GetWindowTextLengthW does not itself clear the last error so we must do it ourselves.
|
|
|
|
|
clear_last_error();
|
|
|
|
|
|
|
|
|
|
validate_window_handle(window_handle)?;
|
|
|
|
|
|
|
|
|
|
let length = unsafe { GetWindowTextLengthW(*window_handle) };
|
|
|
|
|
|
|
|
|
|
let length = usize::try_from(length)?;
|
|
|
|
|
|
|
|
|
|
debug!(length, "window text length retrieved from handle.");
|
|
|
|
|
|
|
|
|
|
if length == 0 {
|
|
|
|
|
// attempt to retreive win32 error
|
|
|
|
|
let last_err = get_last_error();
|
|
|
|
|
if last_err != WIN32_SUCCESS {
|
|
|
|
|
let last_err = last_err.to_hresult().message();
|
|
|
|
|
error!(last_err, "Error getting window text length.");
|
|
|
|
|
return Err(anyhow!("Error getting window text length: {last_err}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(length)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the window title bar title.
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
|
|
|
|
|
fn get_window_title(window_handle: &HWND) -> Result<String> {
|
|
|
|
|
let expected_window_title_length = get_window_title_length(window_handle)?;
|
|
|
|
|
|
|
|
|
|
// This isn't considered an error by the windows API, but in practice it means we can't
|
|
|
|
|
// match against the title so we'll stop here.
|
|
|
|
|
// The upstream will make a contains comparison on what we return, so an empty string
|
|
|
|
|
// will not result on a match.
|
|
|
|
|
if expected_window_title_length == 0 {
|
|
|
|
|
warn!("Window title length is zero.");
|
|
|
|
|
return Ok(String::from(""));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut buffer: Vec<u16> = vec![0; expected_window_title_length + 1]; // add extra space for the null character
|
|
|
|
|
|
|
|
|
|
validate_window_handle(window_handle)?;
|
|
|
|
|
|
|
|
|
|
let actual_window_title_length = unsafe { GetWindowTextW(*window_handle, &mut buffer) };
|
|
|
|
|
|
|
|
|
|
debug!(actual_window_title_length, "window title retrieved.");
|
|
|
|
|
|
|
|
|
|
if actual_window_title_length == 0 {
|
|
|
|
|
// attempt to retreive win32 error
|
|
|
|
|
let last_err = get_last_error();
|
|
|
|
|
if last_err != WIN32_SUCCESS {
|
|
|
|
|
let last_err = last_err.to_hresult().message();
|
|
|
|
|
error!(last_err, "Error retrieving window title.");
|
|
|
|
|
return Err(anyhow!("Error retrieving window title. {last_err}"));
|
|
|
|
|
}
|
|
|
|
|
// in practice, we should not get to the below code, since we asserted the len > 0
|
|
|
|
|
// above. but it is an extra protection in case the windows API didn't set an error.
|
|
|
|
|
warn!(expected_window_title_length, "No window title retrieved.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let window_title = OsString::from_wide(&buffer);
|
|
|
|
|
|
|
|
|
|
Ok(window_title.to_string_lossy().into_owned())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Type Input --------------
|
|
|
|
|
|
|
|
|
|
/// Attempts to type the input text wherever the user's cursor is.
|
|
|
|
|
///
|
|
|
|
|
/// `input` must be a vector of utf-16 encoded characters to insert.
|
|
|
|
|
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
|
|
|
|
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<(), ()> {
|
|
|
|
|
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
|
|
|
|
const TAB_KEY: u8 = 9;
|
|
|
|
|
|
|
|
|
|
let mut keyboard_inputs: Vec<INPUT> = Vec::new();
|
|
|
|
|
// the length of this vec is always shortcut keys to release + (2x length of input chars)
|
|
|
|
|
let mut keyboard_inputs: Vec<INPUT> =
|
|
|
|
|
Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2));
|
|
|
|
|
|
|
|
|
|
debug!(?keyboard_shortcut, "Converting keyboard shortcut to input.");
|
|
|
|
|
|
|
|
|
|
// Add key "up" inputs for the shortcut
|
|
|
|
|
for key in keyboard_shortcut {
|
|
|
|
|
@@ -63,7 +166,7 @@ pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<(),
|
|
|
|
|
/// Converts a valid shortcut key to an "up" keyboard input.
|
|
|
|
|
///
|
|
|
|
|
/// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z]
|
|
|
|
|
fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT, ()> {
|
|
|
|
|
fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
|
|
|
|
const SHIFT_KEY: u8 = 0x10;
|
|
|
|
|
const SHIFT_KEY_STR: &str = "Shift";
|
|
|
|
|
const CONTROL_KEY: u8 = 0x11;
|
|
|
|
|
@@ -89,9 +192,15 @@ fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT, ()> {
|
|
|
|
|
/// Because we only accept [a-z][A-Z], the decimal u16
|
|
|
|
|
/// cast of the letter is safe because the unicode code point
|
|
|
|
|
/// of these characters fits in a u16.
|
|
|
|
|
fn get_alphabetic_hotkey(letter: String) -> Result<u16, ()> {
|
|
|
|
|
fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
|
|
|
|
if letter.len() != 1 {
|
|
|
|
|
return Err(());
|
|
|
|
|
error!(
|
|
|
|
|
len = letter.len(),
|
|
|
|
|
"Final keyboard shortcut key should be a single character."
|
|
|
|
|
);
|
|
|
|
|
return Err(anyhow!(
|
|
|
|
|
"Final keyboard shortcut key should be a single character: {letter}"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let c = letter.chars().next().expect("letter is size 1");
|
|
|
|
|
@@ -99,65 +208,20 @@ fn get_alphabetic_hotkey(letter: String) -> Result<u16, ()> {
|
|
|
|
|
// is_ascii_alphabetic() checks for:
|
|
|
|
|
// U+0041 `A` ..= U+005A `Z`, or U+0061 `a` ..= U+007A `z`
|
|
|
|
|
if !c.is_ascii_alphabetic() {
|
|
|
|
|
return Err(());
|
|
|
|
|
error!(letter = %c, "Letter is not ASCII Alphabetic ([a-z][A-Z]).");
|
|
|
|
|
return Err(anyhow!(
|
|
|
|
|
"Letter is not ASCII Alphabetic ([a-z][A-Z]): '{letter}'",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(c as u16)
|
|
|
|
|
let c = c as u16;
|
|
|
|
|
|
|
|
|
|
debug!(c, letter, "Got alphabetic hotkey.");
|
|
|
|
|
|
|
|
|
|
Ok(c)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the foreground window handle.
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
|
|
|
|
|
fn get_foreground_window() -> Result<HWND, ()> {
|
|
|
|
|
let foreground_window_handle = unsafe { GetForegroundWindow() };
|
|
|
|
|
|
|
|
|
|
if foreground_window_handle.is_invalid() {
|
|
|
|
|
return Err(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(foreground_window_handle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the length of the window title bar text.
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
|
|
|
|
|
fn get_window_title_length(window_handle: HWND) -> Result<usize, ()> {
|
|
|
|
|
if window_handle.is_invalid() {
|
|
|
|
|
return Err(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match usize::try_from(unsafe { GetWindowTextLengthW(window_handle) }) {
|
|
|
|
|
Ok(length) => Ok(length),
|
|
|
|
|
Err(_) => Err(()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the window title bar title.
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
|
|
|
|
|
fn get_window_title(window_handle: HWND) -> Result<Option<String>, ()> {
|
|
|
|
|
if window_handle.is_invalid() {
|
|
|
|
|
return Err(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let window_title_length = get_window_title_length(window_handle)?;
|
|
|
|
|
if window_title_length == 0 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut buffer: Vec<u16> = vec![0; window_title_length + 1]; // add extra space for the null character
|
|
|
|
|
|
|
|
|
|
let window_title_length = unsafe { GetWindowTextW(window_handle, &mut buffer) };
|
|
|
|
|
if window_title_length == 0 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let window_title = OsString::from_wide(&buffer);
|
|
|
|
|
|
|
|
|
|
Ok(Some(window_title.to_string_lossy().into_owned()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Used in build_input() to specify if an input key is being pressed (down) or released (up).
|
|
|
|
|
/// An input key can be either pressed (down), or released (up).
|
|
|
|
|
enum InputKeyPress {
|
|
|
|
|
Down,
|
|
|
|
|
Up,
|
|
|
|
|
@@ -233,18 +297,29 @@ fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
|
|
|
|
|
/// Attempts to type the provided input wherever the user's cursor is.
|
|
|
|
|
///
|
|
|
|
|
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
|
|
|
|
fn send_input(inputs: Vec<INPUT>) -> Result<(), ()> {
|
|
|
|
|
fn send_input(inputs: Vec<INPUT>) -> Result<()> {
|
|
|
|
|
let insert_count = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
|
|
|
|
|
|
|
|
|
|
let e = unsafe { GetLastError().to_hresult().message() };
|
|
|
|
|
debug!("type_input() called, GetLastError() is: {:?}", e);
|
|
|
|
|
debug!("SendInput() called.");
|
|
|
|
|
|
|
|
|
|
if insert_count == 0 {
|
|
|
|
|
return Err(()); // input was blocked by another thread
|
|
|
|
|
let last_err = get_last_error().to_hresult().message();
|
|
|
|
|
error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread.");
|
|
|
|
|
|
|
|
|
|
return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}"));
|
|
|
|
|
} else if insert_count != inputs.len() as u32 {
|
|
|
|
|
return Err(()); // input insertion not completed
|
|
|
|
|
let last_err = get_last_error().to_hresult().message();
|
|
|
|
|
error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err,
|
|
|
|
|
"SendInput sent does not match expected."
|
|
|
|
|
);
|
|
|
|
|
return Err(anyhow!(
|
|
|
|
|
"SendInput does not match expected. sent: {insert_count}, expected: {}",
|
|
|
|
|
inputs.len()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debug!(insert_count, "Autotype sent input.");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -263,16 +338,16 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic = ""]
|
|
|
|
|
#[should_panic = "Final keyboard shortcut key should be a single character: foo"]
|
|
|
|
|
fn get_alphabetic_hot_key_fail_not_single_char() {
|
|
|
|
|
let letter = String::from("foo");
|
|
|
|
|
get_alphabetic_hotkey(letter).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic = ""]
|
|
|
|
|
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '}'"]
|
|
|
|
|
fn get_alphabetic_hot_key_fail_not_alphabetic() {
|
|
|
|
|
let letter = String::from("🚀");
|
|
|
|
|
let letter = String::from("}");
|
|
|
|
|
get_alphabetic_hotkey(letter).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|