From fd727ffc39d2e9af6bc0001824c83a6afcbec387 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 28 Jul 2025 16:26:48 -0400 Subject: [PATCH] [PM-22790] Adds SendInput() Functionality to the Autotype Crate (#15751) * [PM-22790] Adds SendInput() functionality to the autotype crate * [PM-22790] Fixes unused variable linting errors * [PM-22790] Updated autotype lib.rs comment --- .../desktop_native/autotype/src/lib.rs | 10 ++ .../desktop_native/autotype/src/linux.rs | 4 + .../desktop_native/autotype/src/macos.rs | 4 + .../desktop_native/autotype/src/windows.rs | 101 +++++++++++++++++- apps/desktop/desktop_native/napi/index.d.ts | 1 + apps/desktop/desktop_native/napi/src/lib.rs | 9 +- 6 files changed, 123 insertions(+), 6 deletions(-) diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs index e3083422eb2..6d7b9f9db85 100644 --- a/apps/desktop/desktop_native/autotype/src/lib.rs +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -10,3 +10,13 @@ mod windowing; pub fn get_foreground_window_title() -> std::result::Result { windowing::get_foreground_window_title() } + +/// Attempts to type the input text wherever the user's cursor is. +/// +/// `input` must be an array of utf-16 encoded characters to insert. +/// +/// TODO: The error handling will be improved in a future PR: PM-23615 +#[allow(clippy::result_unit_err)] +pub fn type_input(input: Vec) -> std::result::Result<(), ()> { + windowing::type_input(input) +} diff --git a/apps/desktop/desktop_native/autotype/src/linux.rs b/apps/desktop/desktop_native/autotype/src/linux.rs index aa06da21a49..d53d7af0bd9 100644 --- a/apps/desktop/desktop_native/autotype/src/linux.rs +++ b/apps/desktop/desktop_native/autotype/src/linux.rs @@ -1,3 +1,7 @@ pub fn get_foreground_window_title() -> std::result::Result { todo!("Bitwarden does not yet support Linux autotype"); } + +pub fn type_input(_input: Vec) -> std::result::Result<(), ()> { + todo!("Bitwarden does not yet support Linux autotype"); +} diff --git a/apps/desktop/desktop_native/autotype/src/macos.rs b/apps/desktop/desktop_native/autotype/src/macos.rs index 12a4ca08d3e..7ab9f5441b7 100644 --- a/apps/desktop/desktop_native/autotype/src/macos.rs +++ b/apps/desktop/desktop_native/autotype/src/macos.rs @@ -1,3 +1,7 @@ pub fn get_foreground_window_title() -> std::result::Result { todo!("Bitwarden does not yet support Mac OS autotype"); } + +pub fn type_input(_input: Vec) -> std::result::Result<(), ()> { + todo!("Bitwarden does not yet support Mac OS autotype"); +} diff --git a/apps/desktop/desktop_native/autotype/src/windows.rs b/apps/desktop/desktop_native/autotype/src/windows.rs index d86d5dd35ae..931c111e2f5 100644 --- a/apps/desktop/desktop_native/autotype/src/windows.rs +++ b/apps/desktop/desktop_native/autotype/src/windows.rs @@ -1,7 +1,11 @@ use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; -use windows::Win32::Foundation::HWND; +use windows::Win32::Foundation::{GetLastError, HWND}; +use windows::Win32::UI::Input::KeyboardAndMouse::{ + BlockInput, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, + KEYEVENTF_UNICODE, +}; use windows::Win32::UI::WindowsAndMessaging::{ GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW, }; @@ -18,6 +22,29 @@ pub fn get_foreground_window_title() -> std::result::Result { Ok(window_title) } +/// Attempts to type the input text wherever the user's cursor is. +/// +/// `input` must be an array of utf-16 encoded characters to insert. +/// +/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput +pub fn type_input(input: Vec) -> Result<(), ()> { + let mut keyboard_inputs: Vec = Vec::new(); + + for i in input { + let next_down_input = build_input(InputKeyPress::Down, i); + let next_up_input = build_input(InputKeyPress::Up, i); + + keyboard_inputs.push(next_down_input); + keyboard_inputs.push(next_up_input); + } + + let _ = block_input(true); + let result = send_input(keyboard_inputs); + let _ = block_input(false); + + result +} + /// Gets the foreground window handle. /// /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow @@ -33,8 +60,6 @@ fn get_foreground_window() -> Result { /// Gets the length of the window title bar text. /// -/// TODO: Future improvement is to use GetLastError for better error handling -/// /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw fn get_window_title_length(window_handle: HWND) -> Result { if window_handle.is_invalid() { @@ -49,8 +74,6 @@ fn get_window_title_length(window_handle: HWND) -> Result { /// Gets the window title bar title. /// -/// TODO: Future improvement is to use GetLastError for better error handling -/// /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw fn get_window_title(window_handle: HWND) -> Result, ()> { if window_handle.is_invalid() { @@ -73,3 +96,71 @@ fn get_window_title(window_handle: HWND) -> Result, ()> { 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). +enum InputKeyPress { + Down, + Up, +} + +/// A function for easily building keyboard INPUT structs used in SendInput(). +/// +/// Before modifying this function, make sure you read the SendInput() documentation: +/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput +fn build_input(key_press: InputKeyPress, character: u16) -> INPUT { + match key_press { + InputKeyPress::Down => INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: Default::default(), + wScan: character, + dwFlags: KEYEVENTF_UNICODE, + time: 0, + dwExtraInfo: 0, + }, + }, + }, + InputKeyPress::Up => INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: Default::default(), + wScan: character, + dwFlags: KEYEVENTF_KEYUP | KEYEVENTF_UNICODE, + time: 0, + dwExtraInfo: 0, + }, + }, + }, + } +} + +/// Block keyboard and mouse input events. This prevents the hotkey +/// key presses from interfering with the input sent via SendInput(). +/// +/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-blockinput +fn block_input(block: bool) -> Result<(), ()> { + match unsafe { BlockInput(block) } { + Ok(()) => Ok(()), + Err(_) => Err(()), + } +} + +/// 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) -> Result<(), ()> { + let insert_count = unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; + + let e = unsafe { GetLastError().to_hresult().message() }; + println!("type_input() called, GetLastError() is: {:?}", e); + + if insert_count == 0 { + return Err(()); // input was blocked by another thread + } else if insert_count != inputs.len() as u32 { + return Err(()); // input insertion not completed + } + + Ok(()) +} diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index f554fdb12e8..5ea75bd6120 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -210,4 +210,5 @@ export declare namespace logging { } export declare namespace autotype { export function getForegroundWindowTitle(): string + export function typeInput(input: Array): void } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index aa271b335ad..d0a57b5632a 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -872,8 +872,15 @@ pub mod autotype { pub fn get_foreground_window_title() -> napi::Result { autotype::get_foreground_window_title().map_err(|_| { napi::Error::from_reason( - "Autotype Error: faild to get foreground window title".to_string(), + "Autotype Error: failed to get foreground window title".to_string(), ) }) } + + #[napi] + pub fn type_input(input: Vec) -> napi::Result<(), napi::Status> { + autotype::type_input(input).map_err(|_| { + napi::Error::from_reason("Autotype Error: failed to type input".to_string()) + }) + } }