Skip to content

Fix Windows keyboard issues with non-Latin layouts: hotkey detection, text insertion, phantom characters#134

Open
topmonroe9 wants to merge 3 commits intosavbell:mainfrom
topmonroe9:fix/windows-keyboard-input
Open

Fix Windows keyboard issues with non-Latin layouts: hotkey detection, text insertion, phantom characters#134
topmonroe9 wants to merge 3 commits intosavbell:mainfrom
topmonroe9:fix/windows-keyboard-input

Conversation

@topmonroe9
Copy link

@topmonroe9 topmonroe9 commented Mar 10, 2026

This PR fixes three related issues that make WhisperWriter unusable on Windows when a non-Latin keyboard layout (e.g., Russian, Greek, Arabic) is active. All changes are Windows-specific and guarded by sys.platform == 'win32', so Linux and macOS behavior is completely unaffected.

Problems

1. Hotkeys don't work with non-Latin layouts

When the user has a Russian layout active and presses Alt+F, pynput's internal ToUnicode() call translates the physical F key to the Russian character а and creates KeyCode(char='а', vk=None). Since key_map only has entries for Latin characters, the lookup fails and _translate_key_event was falling back to KeyCode.SPACE — causing both missed activations and false triggers.

2. Phantom characters appear when pressing hotkeys

Even after fixing detection, pressing Alt+F on a Russian layout would type а into the focused application, because pynput's default behavior forwards the key event to the OS after processing it.

3. Transcribed text is garbled with non-Latin layouts

When using pynput's character-by-character typing (keyboard.press('ж')), pynput internally calls VkKeyScan() which may fail or produce wrong results for non-Latin characters depending on the active layout. The result is garbled output (e.g., comma becomes b).

Solutions

Fix 1: Virtual Key code fallback map (_build_vk_map)

Added an explicit mapping from Windows Virtual Key codes (which are layout-independent — VK_F = 0x46 regardless of layout) to our internal KeyCode enum. When pynput's standard key_map lookup fails, we fall back to VK codes captured from the raw Win32 event.

Fix 2: win32_event_filter with SuppressException

Added a low-level event filter via pynput's win32_event_filter listener option. This filter:

  • Captures the raw VK code before pynput's ToUnicode() discards it
  • Detects when a key-down event completes the activation combo
  • Manually fires on_input_event so the chord is properly tracked
  • Raises pynput._util.win32.SystemHook.SuppressException to prevent the key from reaching other applications (the Win32 hook returns 1, telling Windows to drop the event)

Only non-modifier keys are suppressed — modifier keys (Alt, Ctrl, Shift, Win) pass through normally so they don't break other shortcuts.

Fix 3: Clipboard paste input method

Added a new clipboard option for the input_method setting that:

  • Sets clipboard content via Win32 API (GlobalAlloc / SetClipboardData with CF_UNICODETEXT)
  • Simulates Ctrl+V via keybd_event using virtual key codes (layout-independent)
  • Preserves and restores the user's previous clipboard content
  • Works instantly (no per-character delay), handles any Unicode text, and is completely layout-independent

The implementation uses ctypes directly — no external dependencies. On non-Windows platforms it falls back to pynput. The default input_method remains pynput for cross-platform compatibility.

Bonus fix: KeyCode.SPACE default removed

_translate_key_event was using self.key_map.get(pynput_key, KeyCode.SPACE) — any unrecognized key was silently mapped to SPACE, causing false chord activations. Now unrecognized keys return None and are ignored.

Files changed

  • src/key_listener.py — VK map, event filter, would_activate(), contains_key(), None handling
  • src/input_simulation.py — clipboard paste method via Win32 API
  • src/config_schema.yaml — added clipboard to input_method options with description

Testing

Tested manually on Windows 11 with Russian and English layouts:

  • Hotkey activation works with English layout
  • Hotkey activation works with Russian layout (no phantom characters)
  • Transcribed text inserted correctly via clipboard paste (English)
  • Transcribed text inserted correctly via clipboard paste (Russian)
  • Original clipboard content is preserved after paste
  • Application starts without errors
  • Verify no regressions on Linux (evdev backend — Windows code not activated)
  • Verify no regressions on macOS (pynput backend — Windows code not activated)

Notes

  • The SuppressException import uses pynput's internal API (pynput._util.win32.SystemHook), wrapped in try/except ImportError for resilience against future pynput versions. Tested with pynput 1.7.6.
  • Related to Fix PynputBackend translating non-space keys to space #75 which also fixes the KeyCode.SPACE default, but this PR goes further by solving the root cause (layout-independent key detection) and adding the clipboard paste method.

topmonroe9 and others added 3 commits March 10, 2026 12:47
…lipboard paste, event suppression

- Fix PynputBackend defaulting unknown keys to SPACE, causing false activations
- Add Windows VK code fallback map for layout-independent hotkey detection
  (e.g., Alt+F works correctly with Russian keyboard layout)
- Add win32_event_filter with SuppressException to prevent activation combo
  keys from producing characters in the active application
- Add clipboard-based text insertion method using Windows API (ctypes),
  which is instant, Unicode-aware, and layout-independent
- Keep pynput as default input method for cross-platform compatibility;
  clipboard is available as an option (Windows only, falls back to pynput)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused PynputKey import from input_simulation.py
- Don't create PynputController when using clipboard method
- Configure Win32 API ctypes signatures once (_ensure_win32_api)
- Add null checks for GlobalAlloc/GlobalLock return values
- Extract magic sleep delay into _PASTE_SETTLE_DELAY constant
- Clear _pressed_vks on listener restart to avoid stale state
- Extract chord-matching logic into KeyChord.would_activate() to
  eliminate duplication between KeyChord.is_active() and the
  win32_event_filter suppression check
- Rewrite _win32_event_filter docstring to accurately describe the
  SuppressException mechanism and thread-safety assumptions
- Move MODIFIER_VKS to a class constant _MODIFIER_VKS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move WM_ message constants to class-level (_WM_KEYDOWN etc.)
- Replace walrus operator with standard loop for broader compatibility
- Add KeyChord.contains_key() and use it for key-up forwarding instead
  of checking mutable pressed_keys state
- Wrap SuppressException import in try/except ImportError for pynput
  version resilience
- Read fallback interval from config instead of hardcoded 0.005

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@topmonroe9 topmonroe9 changed the title Fix keyboard input handling on Windows: layout-independent hotkeys and clipboard paste Fix Windows keyboard issues with non-Latin layouts: hotkey detection, text insertion, phantom characters Mar 10, 2026
@topmonroe9 topmonroe9 marked this pull request as ready for review March 10, 2026 14:07
@topmonroe9
Copy link
Author

Hi @savbell! 👋

This PR is ready for review. It fixes three issues that make WhisperWriter unusable on Windows with non-Latin keyboard layouts (Russian in my case, but the fix applies to any layout — Greek, Arabic, etc.).

Happy to address any feedback or questions. Thanks for the great project!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant