Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openless-all/app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ windows = { version = "0.58", features = [
"Win32_System_Ole",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_UI_Input_Ime",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_TextServices",
Expand Down
68 changes: 65 additions & 3 deletions openless-all/app/src-tauri/src/insertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,24 @@ impl TextInserter {
if text.is_empty() {
return InsertStatus::CopiedFallback;
}
match windows_unicode::send_text(text) {
// Per-codepoint KEYEVENTF_UNICODE SendInput on a Japanese host
// competes with the IME's composition state (ATOK / Microsoft IME),
// so hiragana ends up queued in the IME composition window while
// kanji / ascii get inserted ahead of it — the user sees text
// reordered with kanji pushed to the end.
//
// Fix: detach the foreground window's IME context for the duration
// of the synthetic keystrokes, then restore it. The IME never sees
// the synthesized input so it cannot re-order it, and the user's
// IME state (ATOK conversion mode etc) is left intact.
match windows_unicode::send_text_with_ime_detached(text) {
Ok(()) => InsertStatus::Inserted,
Err(err) => {
log::warn!("[insertion] Unicode SendInput failed: {err}");
InsertStatus::CopiedFallback
log::warn!(
"[insertion] Unicode SendInput (IME-detached) failed: {err}; \
falling back to clipboard paste"
);
self.insert(text, true)
}
}
}
Expand Down Expand Up @@ -290,6 +303,12 @@ fn simulate_paste() -> Result<(), String> {

#[cfg(not(target_os = "macos"))]
fn simulate_paste() -> Result<(), String> {
// Synthesize Ctrl+V (Cmd+V on macOS is handled separately above).
// Note: Ctrl+V as a *keyboard accelerator* does NOT compete with IME
// composition state — the IME treats it as a shortcut, not as text input.
// The IME-vs-text-injection bug specifically affects KEYEVENTF_UNICODE
// SendInput in `insert_via_unicode_keystrokes`, which now detaches the
// foreground window's IME context for the duration of the keystrokes.
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
let mut enigo = Enigo::new(&Settings::default()).map_err(|e| e.to_string())?;
let modifier = Key::Control;
Expand Down Expand Up @@ -318,11 +337,14 @@ fn insertion_success_status() -> InsertStatus {

#[cfg(target_os = "windows")]
mod windows_unicode {
use windows::Win32::UI::Input::Ime::{ImmAssociateContext, ImmGetContext, ImmReleaseContext};
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYBD_EVENT_FLAGS, KEYEVENTF_KEYUP,
KEYEVENTF_UNICODE, VIRTUAL_KEY,
};
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;

#[allow(dead_code)]
pub fn send_text(text: &str) -> Result<(), String> {
for unit in text.encode_utf16() {
send_utf16_unit(unit, false)?;
Expand All @@ -331,6 +353,46 @@ mod windows_unicode {
Ok(())
}

/// Send `text` as a stream of `KEYEVENTF_UNICODE` keystrokes, with the
/// foreground window's IME temporarily detached so the active IME
/// (ATOK / Microsoft IME / 等) cannot intercept and re-order the input.
///
/// Restoration is best-effort: if `ImmAssociateContext(hwnd, original)`
/// fails we still release the IMC handle and surface a warning. The
/// keystrokes themselves having gone through is the property the caller
/// cares about.
pub fn send_text_with_ime_detached(text: &str) -> Result<(), String> {
let hwnd = unsafe { GetForegroundWindow() };
if hwnd.0.is_null() {
// No foreground window we can touch; just send the keystrokes.
return send_text(text);
}

let original_imc = unsafe { ImmGetContext(hwnd) };
// ImmGetContext can return NULL on windows that never had an IME
// context (e.g. games, some elevated processes). In that case there
// is nothing to detach and nothing to restore.
let detached = !original_imc.0.is_null();

if detached {
// Detach: associate a NULL HIMC so the IME stops receiving input
// for this window during our SendInput burst.
let _ = unsafe { ImmAssociateContext(hwnd, windows::Win32::UI::Input::Ime::HIMC(std::ptr::null_mut())) };
}

let result = send_text(text);

if detached {
// Re-associate the original IMC. If this fails we log but don't
// override the send_text result — the user's input did go in.
let _ = unsafe { ImmAssociateContext(hwnd, original_imc) };
// Always release the handle we got from ImmGetContext.
let _ = unsafe { ImmReleaseContext(hwnd, original_imc) };
}

result
}

fn send_utf16_unit(unit: u16, key_up: bool) -> Result<(), String> {
let flags = if key_up {
KEYEVENTF_UNICODE | KEYEVENTF_KEYUP
Expand Down
Loading