diff options
| author | joonhwan <joonhwan.lee@gmail.com> | 2026-07-26 02:52:45 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-25 20:52:45 +0300 |
| commit | 6cfaa079a921e8ba7d01e0fdf14f88a13845cb9a (patch) | |
| tree | c441b13730b5c7d43aa654e7b47fcb0fdd54492e /src | |
| parent | d4e0dd3d903c50d2edb8c3cec71952a83989b310 (diff) | |
feat: switch IME to ASCII in prefix mode on Windows (Korean IME) (#1823)
* feat: switch IME to ASCII in prefix mode on Windows
Extend switch_ascii_input_source_in_prefix to Windows: detect IME state
via WM_IME_CONTROL and toggle to ASCII with SendInput for the Korean IME.
Other IME languages are detected and left unchanged.
refs #1802
* docs: clarify Windows (Korean IME) support for prefix ASCII switching
Follow-up to the Windows prefix ASCII switch, which extended
switch_ascii_input_source_in_prefix to Windows but left the surrounding
text describing macOS only.
- Settings row label: "(macOS)" -> "(macOS/Windows)", plus its test.
- Generalize the Windows behavior wording to "switches the IME to
English (ASCII) input" in the config-reference.json entry and the
model.rs doc comment, and state that Windows support is currently
limited to the Korean IME.
- Add a :::caution aside noting the Korean-IME-only limitation to the
en/ja/zh preview docs, keeping the three translations in sync.
refs #1802
* fix: warn when prefix IME toggle injection is incomplete
SendInput can queue fewer events than requested (for example when blocked
by UIPI), which would otherwise leave the IME silently stuck in ASCII or
native mode. Log a warning with the sent and expected counts so the failure
is diagnosable.
refs #1802
* fix: guard prefix IME restore and correct open-status naming
Address maintainer review on the Windows prefix ASCII switch:
- Restore only when safe: store the foreground window at switch time and,
before toggling back, require that the same window is still focused and
the IME is still in English. This stops restore from toggling another
application or overriding a manual return to Hangul during prefix mode.
- Report injection success from send_vk_tap and only arm restoration when
the initial toggle landed; on a partial injection retry the dropped
key-up so a key is not left logically held down. The injection policy is
split into send_vk_tap_with(inject) so the success, partial, and failure
branches are unit-tested without the real SendInput.
- Correct the WM_IME_CONTROL sub-command naming: 0x0005 is IMC_GETOPENSTATUS
(IME open state), not IMC_GETCONVERSIONMODE. Behavior is unchanged; the
constant, comments, and tests now describe the open-status read.
refs #1802
* fix: bound IME open-status read with a timeout
The prefix IME switch reads IMC_GETOPENSTATUS via WM_IME_CONTROL, which
crosses into the terminal-emulator process. A plain SendMessageW would block
herdr's client thread until that process responds, indefinitely if it hangs.
Read through SendMessageTimeoutW with SMTO_ABORTIFHUNG and a short timeout
instead; on timeout the switch is skipped and restore does nothing, so a hung
host process can no longer freeze prefix-mode entry or exit.
refs #1802
* fix: preserve windows ime restoration after partial injection
refs #1802
---------
Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/state.rs | 2 | ||||
| -rw-r--r-- | src/client/mod.rs | 3 | ||||
| -rw-r--r-- | src/config/model.rs | 14 | ||||
| -rw-r--r-- | src/main.rs | 10 | ||||
| -rw-r--r-- | src/platform/mod.rs | 6 | ||||
| -rw-r--r-- | src/platform/windows.rs | 431 | ||||
| -rw-r--r-- | src/ui/settings.rs | 2 |
7 files changed, 451 insertions, 17 deletions
diff --git a/src/app/state.rs b/src/app/state.rs index b9e4ed70..a3d15a8b 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1027,7 +1027,7 @@ impl ExperimentSetting { match self { Self::PaneHistory => "pane screen history", Self::SwitchAsciiInputSourceInPrefix => { - "switch to ascii input source in prefix (macOS)" + "switch to ascii input source in prefix (macOS/Windows)" } } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 23e331e8..787c0c0b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1366,7 +1366,8 @@ async fn run_client_loop( .set_nonblocking(false) .map_err(ClientError::ConnectionFailed)?; - // This (foreground) client owns the prefix ASCII input-source switch; a no-op on non-macOS. + // This (foreground) client owns the prefix ASCII input-source switch + // (implemented on macOS and Windows; a no-op on other platforms). use crate::platform::PrefixInputSource; let mut prefix_input_source = crate::platform::RealPrefixInputSource::default(); diff --git a/src/config/model.rs b/src/config/model.rs index 9fbca3a6..72c81051 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -909,11 +909,15 @@ pub struct ExperimentalConfig { /// Cursor shape rendered for the IME anchor when /// `reveal_hidden_cursor_for_cjk_ime` is enabled. Default: "steady_block". pub cjk_ime_cursor_shape: ImeCursorShape, - /// While prefix mode is active, temporarily switch the macOS host input - /// source to an ASCII-capable keyboard layout so prefix commands are read - /// as ASCII even when a CJK IME is active, then restore the previous input - /// source when prefix mode exits. macOS only; a no-op elsewhere and a - /// best-effort no-op if the switch fails. Default: false. + /// While prefix mode is active, temporarily switch the host input source + /// to an ASCII-capable mode so prefix commands are read as ASCII even when + /// an IME is active, then restore the previous input source when prefix + /// mode exits. On macOS this selects the ASCII-capable keyboard layout; on + /// Windows it switches the IME to English (ASCII) input. Windows support is + /// currently limited to the Korean IME; with an IME for any other language, + /// the input source is left unchanged. macOS and Windows only; a no-op + /// elsewhere and a best-effort no-op if the switch fails. + /// Default: false. pub switch_ascii_input_source_in_prefix: bool, } diff --git a/src/main.rs b/src/main.rs index 4e9ce815..3f1f7930 100644 --- a/src/main.rs +++ b/src/main.rs @@ -396,10 +396,12 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # kitty_graphics = false # Save recent pane screen history across full server restarts. pane_history = false -# While prefix mode is active, temporarily switch the macOS host input -# source to an ASCII-capable keyboard layout so prefix commands register -# even when a CJK IME is active, then restore the previous input source -# when prefix mode exits. macOS only; best-effort. Default: false. +# While prefix mode is active, temporarily switch the host input source to +# an ASCII-capable mode so prefix commands register even when an IME is +# active, then restore the previous input source when prefix mode exits. On +# macOS this selects the ASCII-capable keyboard layout; on Windows it toggles +# a Korean IME between Hangul and English (other IME languages are left +# unchanged). macOS and Windows only; best-effort. Default: false. # switch_ascii_input_source_in_prefix = false # Expose the focused pane's cursor to the outer terminal so macOS input # methods keep tracking the candidate window when TUIs paint their own diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fbde21b7..9b763223 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -259,16 +259,16 @@ pub(crate) fn parse_agent_env_hint(environ: &[u8]) -> Option<crate::detect::Agen None } -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "windows")))] #[derive(Debug)] pub(crate) struct InputSourceRestore; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "windows")))] pub(crate) fn switch_to_ascii_input_source() -> Option<InputSourceRestore> { None } -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "windows")))] pub(crate) fn pump_input_source_runloop() {} /// Switches the host keyboard input source while prefix mode is active. diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 317ac495..d77a30be 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,7 +12,7 @@ use windows_sys::{ Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation}, Win32::{ Foundation::{ - CloseHandle, GlobalFree, LocalFree, HANDLE, INVALID_HANDLE_VALUE, NTSTATUS, + CloseHandle, GlobalFree, LocalFree, HANDLE, HWND, INVALID_HANDLE_VALUE, NTSTATUS, STATUS_SUCCESS, UNICODE_STRING, }, System::{ @@ -34,7 +34,20 @@ use windows_sys::{ PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, }, }, - UI::Shell::{CommandLineToArgvW, ShellExecuteW}, + UI::{ + Input::{ + Ime::ImmGetDefaultIMEWnd, + KeyboardAndMouse::{ + GetKeyboardLayout, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, + KEYEVENTF_KEYUP, + }, + }, + Shell::{CommandLineToArgvW, ShellExecuteW}, + WindowsAndMessaging::{ + GetForegroundWindow, GetWindowThreadProcessId, SendMessageTimeoutW, + SMTO_ABORTIFHUNG, WM_IME_CONTROL, + }, + }, }, }; @@ -785,6 +798,309 @@ fn read_unicode_string(process: HANDLE, unicode: UNICODE_STRING) -> Option<Strin String::from_utf16(&buffer).ok() } +// Prefix-mode ASCII input source support (see `switch_ascii_input_source_in_prefix`). +// +// Windows IMEs live in the terminal-emulator process, not in herdr. Empirically: +// - `WM_IME_CONTROL` / `IMC_GETOPENSTATUS` reads whether the IME is open +// (composing native characters) reliably across the process boundary (this +// is what kren-select uses), so we detect state with it. The read goes +// through `SendMessageTimeoutW` (`SMTO_ABORTIFHUNG`) so a hung host process +// cannot block us indefinitely. +// - Writing the state back (`IMC_SETOPENSTATUS` / `IMC_SETCONVERSIONMODE`) +// changes the flag value but does NOT affect real input in terminal/TSF +// hosts, so we cannot switch by writing the mode. +// - `ImmGetContext` on the foreground window returns null across the process +// boundary, so the ImmGetOpenStatus/ImmSetOpenStatus path is unavailable. +// Therefore we switch the way kren-select does: inject the IME toggle key with +// `SendInput`, which reaches the foreground input queue like a real keypress. +// +// The toggle key is language-specific, so we pick it from the foreground +// keyboard layout's language id. Only Korean is mapped today; other IMEs are +// detected and left untouched (a no-op) rather than toggled with the wrong key. + +/// `WM_IME_CONTROL` sub-command that reads whether the IME is open, i.e. +/// composing native characters. This is `IMC_GETOPENSTATUS` (0x0005); for the +/// Korean IME "open" is exactly the Hangul state and "closed" is English/ASCII +/// direct input, which is the state we detect and toggle. +const IMC_GETOPENSTATUS: usize = 0x0005; + +/// Virtual key that toggles Hangul/English on Korean IMEs. +const VK_HANGUL: u16 = 0x15; + +/// Primary language id (low 10 bits of a LANGID) for Korean. +const LANG_KOREAN: u32 = 0x12; + +/// Whether the IME reports itself open, i.e. composing native characters +/// (Hangul for the Korean IME). `IMC_GETOPENSTATUS` returns nonzero when the +/// IME is open and zero when it is in direct English/ASCII input. +fn ime_open(open_status: isize) -> bool { + open_status != 0 +} + +/// Timeout (ms) for the cross-process IME open-status read. Short enough that a +/// hung terminal never freezes prefix-mode entry/exit. +const IME_STATUS_READ_TIMEOUT_MS: u32 = 200; + +/// Reads the IME open status (`IMC_GETOPENSTATUS`) with a bounded timeout. +/// +/// `WM_IME_CONTROL` crosses into the terminal-emulator process, and a plain +/// `SendMessageW` would block herdr's client thread until that process responds +/// (indefinitely if it is hung). `SendMessageTimeoutW` with `SMTO_ABORTIFHUNG` +/// caps the wait; on timeout or failure this returns `None` and callers leave +/// the IME untouched rather than blocking or guessing. +fn read_ime_open_status(ime_hwnd: HWND) -> Option<isize> { + let mut result: usize = 0; + // SAFETY: `ime_hwnd` is a non-null IME window from `ImmGetDefaultIMEWnd`, and + // `result` is a valid out-pointer for the message's `DWORD_PTR` result. + let ret = unsafe { + SendMessageTimeoutW( + ime_hwnd, + WM_IME_CONTROL, + IMC_GETOPENSTATUS, + 0, + SMTO_ABORTIFHUNG, + IME_STATUS_READ_TIMEOUT_MS, + &mut result, + ) + }; + if ret == 0 { + // Timed out or failed; do not block or assume a state. + return None; + } + Some(result as isize) +} + +/// The IME toggle key for a keyboard layout language id, or `None` when the +/// language's toggle key is not known. `langid` is the full LANGID (LOWORD of +/// an `HKL`); the primary language is its low 10 bits. +/// +/// Only Korean is mapped: `VK_HANGUL` is the Hangul/English toggle. Japanese +/// (half/full-width) and Chinese use different keys per IME, so they return +/// `None` and are left untouched instead of toggled incorrectly. +fn toggle_key_for_language(langid: u32) -> Option<u16> { + match langid & 0x3FF { + LANG_KOREAN => Some(VK_HANGUL), + _ => None, + } +} + +/// Builds the key-down then key-up `INPUT` pair for `vk`. +fn key_tap_inputs(vk: u16) -> [INPUT; 2] { + let key_event = |flags| INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: vk, + wScan: 0, + dwFlags: flags, + time: 0, + dwExtraInfo: 0, + }, + }, + }; + [key_event(0), key_event(KEYEVENTF_KEYUP)] +} + +/// Injects a key-down then key-up for `vk` via `SendInput`. +/// +/// Returns `true` when the key-down was queued and the IME may have toggled. +/// Thin wrapper over [`send_vk_tap_with`] that plugs in the real `SendInput`; +/// the injection policy lives there so it can be unit-tested without the OS. +fn send_vk_tap(vk: u16) -> bool { + send_vk_tap_with(vk, |events| { + // SAFETY: `events` outlives the call; its `INPUT_KEYBOARD` entries have + // the `ki` union variant fully initialized, which is the variant + // SendInput reads for keyboard input. `size_of::<INPUT>()` is the + // required `cbSize`. + unsafe { + SendInput( + events.len() as u32, + events.as_ptr(), + size_of::<INPUT>() as i32, + ) + } + }) +} + +/// Core key-tap logic with the raw event injector abstracted behind `inject`, +/// which returns how many of the passed events it actually queued. This keeps +/// the success / partial-injection / total-failure branches unit-testable +/// without touching the real `SendInput`. +/// +/// `SendInput` returns how many events it queued; a short count means injection +/// was blocked (e.g. by UIPI). Returns `true` whenever the key-down was queued, +/// because the IME may have toggled and callers must retain restoration state. +/// When only the key-down landed, the key-up is retried so the key is not left +/// logically held down. +fn send_vk_tap_with(vk: u16, mut inject: impl FnMut(&[INPUT]) -> u32) -> bool { + let inputs = key_tap_inputs(vk); + let sent = inject(&inputs); + if sent as usize == inputs.len() { + return true; + } + + if sent == 1 { + // The key-down landed and may already have toggled the IME. Retry the + // dropped key-up, but report that restoration state is still required. + let key_up = [inputs[1]]; + let up_sent = inject(&key_up); + tracing::warn!( + vk, + sent, + expected = inputs.len(), + key_up_retry_sent = up_sent, + "SendInput dropped the IME toggle key-up; retried key-up" + ); + return true; + } + + tracing::warn!( + vk, + sent, + expected = inputs.len(), + "SendInput did not inject the IME toggle key tap" + ); + false +} + +pub(crate) fn pump_input_source_runloop() {} + +/// Switch the foreground window's IME to ASCII-capable input for prefix mode. +/// +/// Returns `None` (nothing to restore) when there is no foreground IME, the +/// keyboard language has no known toggle key, or the IME is already +/// ASCII-capable, matching the macOS contract. +pub(crate) fn switch_to_ascii_input_source() -> Option<InputSourceRestore> { + // SAFETY: all calls are Win32 UI functions invoked on the client's main + // thread. Every HWND is null-checked before use; `fg_thread` is a thread id + // (not a handle) used only as `GetKeyboardLayout` input, where 0 harmlessly + // falls back to the calling thread's layout. + unsafe { + let fg = GetForegroundWindow(); + if fg.is_null() { + return None; + } + + // Pick the toggle key for the foreground keyboard language. Unknown + // languages (Japanese, Chinese, ...) are left untouched. + let fg_thread = GetWindowThreadProcessId(fg, null_mut()); + let langid = (GetKeyboardLayout(fg_thread) as usize as u32) & 0xFFFF; + let Some(toggle_vk) = toggle_key_for_language(langid) else { + tracing::debug!( + langid = format!("{langid:#06x}"), + "prefix IME switch: no toggle key for keyboard language, leaving IME as-is" + ); + return None; + }; + + // Detect the open (Hangul) state via the bounded read path. + let ime_hwnd = ImmGetDefaultIMEWnd(fg); + if ime_hwnd.is_null() { + return None; + } + let Some(open) = read_ime_open_status(ime_hwnd) else { + tracing::debug!("prefix IME switch skipped: IME open-status read timed out"); + return None; + }; + if !ime_open(open) { + // Already in English/ASCII input; nothing to switch or restore. + return None; + } + + // The bounded cross-process status read can take long enough for focus + // to change. Recheck immediately before using the global input queue. + if GetForegroundWindow() != fg { + tracing::debug!("prefix IME switch skipped: foreground window changed"); + return None; + } + + // Toggle to ASCII by injecting the language's IME toggle key. Only arm + // restoration when the toggle actually landed, so we never try to + // restore a switch that never happened. + if !send_vk_tap(toggle_vk) { + tracing::warn!( + langid = format!("{langid:#06x}"), + "prefix IME switch: toggle injection failed, leaving IME as-is" + ); + return None; + } + tracing::debug!( + langid = format!("{langid:#06x}"), + "switched host IME to ASCII for prefix mode" + ); + Some(InputSourceRestore { + toggle_vk, + origin_hwnd: fg as isize, + }) + } +} + +/// Restores the native (Hangul) IME state that was active before prefix mode. +/// +/// Only constructed by [`switch_to_ascii_input_source`] after it successfully +/// toggled the IME to English/ASCII. Dropping it re-injects the same toggle key +/// to go back, but only after two guards, so restoration never fights the user +/// or another application: +/// - the same window that was switched must still be focused, otherwise the +/// toggle would land on whatever app the user moved to; +/// - the IME must still be in English (our switch still in effect), otherwise +/// the user manually returned to Hangul during prefix mode and we must leave +/// their choice alone. +/// +/// `origin_hwnd` stores the foreground window at switch time as raw pointer bits +/// (`isize`, not `HWND`) so the guard stays `Send` when parked in the client's +/// prefix-input state across `.await` points. +#[derive(Debug)] +pub(crate) struct InputSourceRestore { + toggle_vk: u16, + origin_hwnd: isize, +} + +impl Drop for InputSourceRestore { + fn drop(&mut self) { + // SAFETY: all calls are Win32 UI functions invoked on the client's main + // thread. Every HWND is null-checked before use. + unsafe { + // Guard 1: only restore if the window we switched is still focused, + // so the toggle never lands on a different application. + let fg = GetForegroundWindow(); + if fg.is_null() || fg as isize != self.origin_hwnd { + tracing::debug!( + "prefix IME restore skipped: foreground window changed since switch" + ); + return; + } + + // Guard 2: only restore if the IME is still in English (our switch is + // still in effect). If the user manually switched back to Hangul + // during prefix mode, leave their choice untouched. + let ime_hwnd = ImmGetDefaultIMEWnd(fg); + if ime_hwnd.is_null() { + return; + } + let Some(open) = read_ime_open_status(ime_hwnd) else { + tracing::debug!("prefix IME restore skipped: IME open-status read timed out"); + return; + }; + if ime_open(open) { + tracing::debug!("prefix IME restore skipped: IME already back to native input"); + return; + } + + // The bounded cross-process status read can take long enough for + // focus to change. Recheck immediately before using SendInput. + if GetForegroundWindow() != fg { + tracing::debug!("prefix IME restore skipped: foreground window changed"); + return; + } + + if send_vk_tap(self.toggle_vk) { + tracing::debug!("restored host IME after prefix mode"); + } + } + } +} + #[cfg(test)] mod tests { use std::{ @@ -1367,4 +1683,115 @@ mod tests { cmdline: Some(argv.join(" ")), } } + + #[test] + fn ime_open_reflects_open_status() { + // IMC_GETOPENSTATUS returns nonzero when the IME is open (Hangul + // composing) and zero for direct English/ASCII input. + assert!(super::ime_open(1)); + assert!(!super::ime_open(0)); + // Any nonzero value is treated as open, not just 1. + assert!(super::ime_open(2)); + } + + #[test] + fn toggle_key_maps_korean_and_ignores_other_languages() { + // Korean (0x0412) -> Hangul/English toggle. + assert_eq!( + super::toggle_key_for_language(0x0412), + Some(super::VK_HANGUL) + ); + // Korean with a different sublanguage still resolves by primary id. + assert_eq!( + super::toggle_key_for_language(0x0812), + Some(super::VK_HANGUL) + ); + // Japanese (0x0411) and Chinese (0x0804) have no mapped key yet. + assert_eq!(super::toggle_key_for_language(0x0411), None); + assert_eq!(super::toggle_key_for_language(0x0804), None); + // English (0x0409): nothing to toggle. + assert_eq!(super::toggle_key_for_language(0x0409), None); + } + + #[test] + fn send_vk_tap_reports_success_when_full_tap_is_queued() { + let mut calls = 0; + let ok = super::send_vk_tap_with(super::VK_HANGUL, |events| { + calls += 1; + events.len() as u32 + }); + assert!(ok, "a fully queued tap is reported as success"); + assert_eq!(calls, 1, "a clean tap needs no retry"); + } + + #[test] + fn send_vk_tap_retries_keyup_and_reports_toggle_on_partial_injection() { + let mut calls = 0; + let mut retry_len = 0; + let mut retry_is_keyup = false; + let ok = super::send_vk_tap_with(super::VK_HANGUL, |events| { + calls += 1; + if calls == 1 { + // Only the key-down is queued; the key-up is dropped. + 1 + } else { + retry_len = events.len(); + // SAFETY: keyboard inputs, so reading the `ki` union is valid. + retry_is_keyup = + unsafe { events[0].Anonymous.ki.dwFlags } == super::KEYEVENTF_KEYUP; + events.len() as u32 + } + }); + assert!(ok, "the queued key-down may have toggled the IME"); + assert_eq!(calls, 2, "the dropped key-up is retried exactly once"); + assert_eq!(retry_len, 1, "only the key-up is retried"); + assert!(retry_is_keyup, "the retry injects the key-up event"); + } + + #[test] + fn send_vk_tap_reports_toggle_when_keyup_retry_fails() { + let mut calls = 0; + let ok = super::send_vk_tap_with(super::VK_HANGUL, |_events| { + calls += 1; + if calls == 1 { + 1 + } else { + 0 + } + }); + assert!(ok, "the queued key-down may have toggled the IME"); + assert_eq!(calls, 2, "the dropped key-up is retried exactly once"); + } + + #[test] + fn send_vk_tap_reports_failure_without_retry_when_nothing_is_queued() { + let mut calls = 0; + let ok = super::send_vk_tap_with(super::VK_HANGUL, |_events| { + calls += 1; + 0 + }); + assert!(!ok, "a fully blocked tap is reported as failure"); + assert_eq!( + calls, 1, + "nothing was queued, so there is no key-up to retry" + ); + } + + #[test] + fn key_tap_inputs_emit_keydown_then_keyup() { + let inputs = super::key_tap_inputs(super::VK_HANGUL); + // SAFETY: both entries are keyboard inputs, so reading the `ki` union is valid. + unsafe { + assert_eq!(inputs[0].r#type, super::INPUT_KEYBOARD); + assert_eq!(inputs[0].Anonymous.ki.wVk, super::VK_HANGUL); + assert_eq!(inputs[0].Anonymous.ki.dwFlags, 0, "first event is key-down"); + assert_eq!(inputs[1].r#type, super::INPUT_KEYBOARD); + assert_eq!(inputs[1].Anonymous.ki.wVk, super::VK_HANGUL); + assert_eq!( + inputs[1].Anonymous.ki.dwFlags, + super::KEYEVENTF_KEYUP, + "second event is key-up" + ); + } + } } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 8975d6f3..8177d874 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -526,6 +526,6 @@ mod tests { .map(|cell| cell.symbol()) .collect::<String>(); - assert!(rendered.contains("switch to ascii input source in prefix (macOS) [✓]")); + assert!(rendered.contains("switch to ascii input source in prefix (macOS/Windows) [✓]")); } } |
