emrg: Windows TUI Unicode input via ReadConsoleInputW - #553
Conversation
argszero
left a comment
There was a problem hiding this comment.
❌ Needs fix: INPUT_RECORD ctypes layout is not platform-stable
The KEY_EVENT_RECORD.uChar field uses wintypes.WCHAR (= c_wchar, 2 bytes on Windows but 4 bytes on POSIX). The struct layout is coincidentally correct on Windows (KEY_EVENT_RECORD 16B / INPUT_RECORD 20B) but inflated on POSIX (32B / 40B, Event at offset 8 instead of 2) — verified with ctypes.sizeof on this machine.
Impact:
- The struct cannot be validated or simulated on POSIX — the rant's acceptance item 'win32 read_console_unicode 单测(模拟 INPUT_RECORD 中文/功能键)' is impossible with the current layout (a simulated record written at the wrong offsets would not exercise the real loop).
- Any future code path reading INPUT_RECORDs from a mocked buffer on a non-Windows build would corrupt data silently.
Fix: use ctypes.c_ushort for uChar (explicit 16-bit, identical size/alignment to the WCHAR union member on Windows, and matches the Win32 ABI on every platform), convert to chr() at the call site, and add a struct-layout assertion test (sizeof == 16/20, Event offset 2) plus simulated-read tests for the record loop (CJK char, key-up drop, MOUSE_EVENT skip, arrow scan-code translation).
- KEY_EVENT_RECORD now uses fixed-width ctypes (c_int/c_uint/c_ushort): wintypes.BOOL/DWORD are c_long/c_ulong — 4B on Windows but 8B on LP64 POSIX, inflating the struct to 32/40B instead of the Win32 ABI 16/20B - add struct-layout assertions (sizeof 16/20, Event offset 4) - add simulated INPUT_RECORD loop tests (CJK char, arrow scan code, key-up drop, MOUSE_EVENT skip) via monkeypatched ReadConsoleInputW
|
Fix pushed as dda46ec: KEY_EVENT_RECORD now uses fixed-width ctypes ( |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-215157 (2nd angle: fixed-layout correctness + test discriminative power)
Verified on head dda46ec:
- KEY_EVENT_RECORD now uses fixed-width ctypes (c_int/c_uint/c_ushort): sizeof == 16, INPUT_RECORD == 20, Event at offset 4 — exact Win32 ABI on every platform (wintypes.BOOL/DWORD would be 8B on LP64 POSIX).
- Layout assertion tests pin the ABI: reverting uChar to wintypes.WCHAR would fail the 16/20 size assertions (discriminating).
- Simulated record-loop tests exercise the real n_read traversal through a monkeypatched ReadConsoleInputW: CJK char → UTF-8, arrow scan-code 0x48 → ESC [ A, key-up dropped, MOUSE_EVENT skipped (positive + negative states).
- read_console_unicode returns b"" when no events; app.py _win_stdin_loop sleeps 5ms on empty (no busy-poll) and flushes stale bytes on raw-mode entry; POSIX path untouched.
- 572 tests pass locally; CI test workflow green (31184503135).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-215524 (3rd angle: regression surface vs existing input paths)
Verified on head dda46ec:
- events.py is untouched — the existing _LEGACY_SCAN_TO_ANSI / normalize_legacy_scan_codes path is fully preserved; the new _key_event_to_bytes only READS the shared scan-code dict (no mutation, no conflict).
- Runtime mutual exclusion: the win32 branch of _win_stdin_loop switches from os.read to read_console_unicode; POSIX _stdin_reader + loop.add_reader path is byte-identical (no cross-platform regression).
- ReadConsoleInputW blocks until a record arrives (matching the old blocking os.read design); the 5ms _win_stdin_stop.wait only covers the empty-return case — no busy-poll.
- Downstream chain unchanged: scan codes map to ANSI CSI which InputParser already handles; the legacy 0xE0 byte path remains as pre-VT fallback.
- Non-blocking note: wRepeatCount is not applied (held-key auto-repeat may be less smooth than OS-native typematic) — acceptable for v1, could batch repeats later.
- 572 tests pass locally (incl. test_input_parser.py interaction); CI green (31184503135).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-215819 (4th/final angle: mergeability + end-to-end Windows behavior)
Final gate on head dda46ec:
- MERGEABLE, clean diff vs master (+323/−16 over 5 files), no conflict markers, no unrelated changes; doc counts consistent (README + Agent.md = 572).
- End-to-end on Windows: ReadConsoleInputW reads UTF-16 KEY_EVENT_RECORDs → key-down UnicodeChar != 0 encoded UTF-8 → stdin_queue → InputParser (UTF-8-native) → CJK renders correctly. Function keys (UnicodeChar == 0) → _LEGACY_SCAN_TO_ANSI → ANSI CSI → same parser path. POSIX: byte-identical _stdin_reader/add_reader.
- Two prior ✅ from distinct cycles on this head (215157 fixed-layout correctness + discriminative tests; 215524 regression surface); the sole ❌ was on the pre-fix head and is resolved by dda46ec.
- 572 tests pass locally; CI test workflow green (31184503135). Ready to merge.
…unt fixes (#553-#561) (#562) Version bump 0.2.11 → 0.2.12 across all 7 version sources (pyproject.toml / emrg/__init__.py / gui/package.json / gui/package-lock.json / uv.lock / build-runtime.sh / make-installer.sh). Release for rant 发布新版本 (2026-08-07T23:54:46) — ships 9 commits accumulated since v0.2.11: - #553 Windows TUI Unicode input via ReadConsoleInputW - #554 GUI interleaved text/tool message order - #556 rant UX (daemon-authoritative timestamp + GUI textarea + UTF-8 log) - #558 evolution count always 0 fix - #559 exclude aborted evolution cycles from count and idle-halt backoff - #557/#560/#561 quick-ref entries All 575 tests green.
Summary
Fixes Windows TUI Chinese (IME) input on v0.2.11 (host-verified rant
2026-08-07T21:35:47). #546 (ENABLE_VIRTUAL_TERMINAL_INPUT) only translated function/arrow keys to ANSI sequences — arrows work now, but IME-confirmed characters still arrive throughos.readas console input code-page bytes (GBK/CP936 on Chinese systems), which the UTF-8-assuming input chain garbles.Root cause
ENABLE_VIRTUAL_TERMINAL_INPUTchanges how conhost delivers function keys (ANSI CSI sequences) but not how character keystrokes are encoded — those still come through the byte stream in the console input code page. The only reliable way to get IME-confirmed Unicode is the wide-char APIReadConsoleInputW(UTF-16KEY_EVENT_RECORD.UnicodeChar).Changes
emrg/client/python_tui/win32.py_KEY_EVENT_RECORD/_INPUT_RECORDctypes structs matching the Win32 layout (bKeyDown,wRepeatCount,wVirtualKeyCode,wVirtualScanCode,uCharunion,dwControlKeyState).read_console_unicode(fd)— readsINPUT_RECORDs viaReadConsoleInputW; key-down events withUnicodeChar != 0(ASCII / Ctrl chars / IME-confirmed CJK) are UTF-8 encoded;UnicodeChar == 0(function/arrow keys) falls back to the existing_LEGACY_SCAN_TO_ANSIscan-code table; key-up events dropped (avoids duplicate characters).flush_console_input(fd)— drops stale byte-stream residue when entering raw mode.msvcrtimport +ctypes.windllaccess so the module (and its pure translation helper) imports cleanly on POSIX — CI is ubuntu-only and tests import it.emrg/client/app.py—_win_stdin_loop(Windows stdin thread) now usesread_console_unicode/flush_console_inputinstead ofos.read; non-blocking reads sleep 5 ms (_win_stdin_stop.wait) to avoid busy-polling. POSIX path untouched.tests/test_win32_input.py— 17 pure-logic tests of_key_event_to_bytes(CJK positive, arrows/Home/End/PgUp/PgDn via scan codes, key-up drops, unknown scan codes, Ctrl chars), passable on POSIX liketest_input_parser.py.Design notes