// F06 — keyboard & keymap, Win32 implementation
// (../../../features/f06-keyboard.md). Extends the scaffold
// (../scaffold/app.d) into a keyboard observatory:
//
// * The WndProc logs the full chain for every key: WM_KEYDOWN /
// WM_SYSKEYDOWN (vk from wParam; scancode, extended bit, repeat count and
// previous-state bit decoded from lParam) -> TranslateMessage ->
// WM_CHAR / WM_DEADCHAR / WM_SYSCHAR (UTF-16 code units, surrogate pairs
// recombined) -> WM_KEYUP. WSI_NO_TRANSLATE=1 skips TranslateMessage in
// the pump to prove every text-level message comes from it and it alone.
// * A SetTimer-driven script injects scancode-level input into the demo's
// own (focused) window with SendInput(KEYEVENTF_SCANCODE): a letter, a
// shifted digit (vk != text proof), a same-key keydown pair (previous-
// state bit), an Alt chord (WM_SYSKEYDOWN/WM_SYSCHAR), and — after
// LoadKeyboardLayoutW("00000407") + KLF_ACTIVATE switches the thread to
// German — the same physical scancodes again (Y/Z swap) plus the dead-key
// sequence acute (scan 0x0D on de) + E -> WM_DEADCHAR -> WM_CHAR 'é'.
// A KEYEVENTF_UNICODE pair carries U+1F600 as two surrogate WM_CHARs.
// * Repeat ownership: the system owns auto-repeat; the demo logs the
// configured rate/delay from SystemParametersInfoW(SPI_GETKEYBOARDSPEED /
// SPI_GETKEYBOARDDELAY) at startup.
//
// WSI_AUTO_EXIT=1 runs the script and exits 0 (~1.5 s); without it the window
// stays open for real typing after the script.
//
// Only druntime's built-in core.sys.windows bindings — no third-party packages.
module (module) appapp;
import (package) corecore.(package) core.syssys.(package) core.sys.windowswindows.(module) core.sys.windows.windowsWindows API header module
Translated from MinGW API for MS-Windows 4.0
Source
core/sys/windows/windows.d
windows;
import instrument;
enum UINT_PTR (constant) _error_ app.TIMER_ID = __errorTIMER_ID = 1;
enum (constant) int app.TICK_MS = 100TICK_MS = 100; // one script step per tick
// PC/AT set-1 make codes for the physical positions the script exercises.
enum (enum) app.SCSC : ushort
{
(enum value) app.SC.A = cast(ushort)30uA = 0x1e,
(enum value) app.SC.B = cast(ushort)48uB = 0x30,
(enum value) app.SC.C = cast(ushort)46uC = 0x2e,
(enum value) app.SC.E = cast(ushort)18uE = 0x12,
(enum value) app.SC.Y = cast(ushort)21uY = 0x15, // QWERTY Y position — types 'z' under de (QWERTZ)
(enum value) app.SC.Z = cast(ushort)44uZ = 0x2c, // QWERTY Z position — types 'y' under de
(enum value) app.SC.digit2 = cast(ushort)3udigit2 = 0x03,
(enum value) app.SC.acute = cast(ushort)13uacute = 0x0d, // '=' position on us; the dead acute key on de
(enum value) app.SC.lshift = cast(ushort)42ulshift = 0x2a,
(enum value) app.SC.lalt = cast(ushort)56ulalt = 0x38,
(enum value) app.SC.space = cast(ushort)57uspace = 0x39,
}
enum ushort (constant) ushort app.UP = cast(ushort)32768uUP = 0x8000; // ORed onto a SC entry: key release
struct (struct) app.DemoDemo
{
HWND (field) _error_ app.Demo.hwndhwnd;
HKL (field) _error_ app.Demo.hklStarthklStart; // layout active at startup (us under a fresh prefix)
HKL (field) _error_ app.Demo.hklDehklDe; // 00000407 once loaded
uint (field) uint app.Demo.stepstep; // script position
wchar (field) wchar app.Demo.pendingHighpendingHigh; // WM_CHAR high surrogate awaiting its low unit
bool (field) bool app.Demo.autoExitautoExit;
bool (field) bool app.Demo.noTranslatenoTranslate; // WSI_NO_TRANSLATE=1: pump skips TranslateMessage
uint (field) uint app.Demo.nKeyDownnKeyDown, (field) uint app.Demo.nKeyUpnKeyUp, (field) uint app.Demo.nSysKeyDownnSysKeyDown, (field) uint app.Demo.nSysKeyUpnSysKeyUp;
uint (field) uint app.Demo.nCharnChar, (field) uint app.Demo.nDeadCharnDeadChar, (field) uint app.Demo.nSysCharnSysChar, (field) uint app.Demo.nSysDeadCharnSysDeadChar, (field) uint app.Demo.nLangChangenLangChange;
}
__gshared (struct) app.DemoDemo _error_ app.gg;
// ---------------------------------------------------------------------------
// UTF-16 -> UTF-8 for log lines (nothrow @nogc; the WndProc may not throw).
void void app.encodeUtf8(dchar c, ref char[8] buf) nothrow @nogcencodeUtf8(dchar (parameter) dchar cc, ref char[8] (parameter) char[8] bufbuf) nothrow @nogc
{
int (local variable) int nn;
if ((parameter) dchar cc < 0x80)
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char) (parameter) dchar cc;
else if ((parameter) dchar cc < 0x800)
{
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0xc0 | ((parameter) dchar cc >> 6));
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0x80 | ((parameter) dchar cc & 0x3f));
}
else if ((parameter) dchar cc < 0x10000)
{
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0xe0 | ((parameter) dchar cc >> 12));
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0x80 | (((parameter) dchar cc >> 6) & 0x3f));
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0x80 | ((parameter) dchar cc & 0x3f));
}
else
{
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0xf0 | ((parameter) dchar cc >> 18));
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0x80 | (((parameter) dchar cc >> 12) & 0x3f));
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0x80 | (((parameter) dchar cc >> 6) & 0x3f));
(parameter) char[8] bufbuf[(local variable) int nn++] = cast(char)(0x80 | ((parameter) dchar cc & 0x3f));
}
(parameter) char[8] bufbuf[(local variable) int nn] = 0;
}
// ---------------------------------------------------------------------------
// Key-event decoding: everything but the vk lives in lParam's bitfields.
void app.logKeylogKey(const(char)* state, bool (parameter) bool syssys, WPARAM wParam, LPARAM lParam) nothrow
{
const _error_ repeatCountrepeatCount = cast(uint)(lParam & 0xffff); // bits 0-15
const _error_ scanscan = cast(uint)((lParam >> 16) & 0xff); // bits 16-23
const _error_ extext = cast(uint)((lParam >> 24) & 1); // bit 24
const _error_ prevprev = cast(uint)((lParam >> 30) & 1); // bit 30: was down before
// Layout-dependent key name straight from the scancode bits of lParam.
WCHAR[32] _error_ nameWnameW = 0;
const _error_ nn = GetKeyNameTextW(cast(LONG)(lParam & 0x03ff_0000), nameW.ptr, nameW.length);
char[64] _error_ name8name8 = 0;
int _error_ oo;
foreach ((parameter) ii; 0 .. n) // key names are ASCII-ish; non-ASCII -> '?'
name8[o++] = nameW[i] < 0x80 ? cast(char) nameW[i] : '?';
name8[o] = 0;
logEvent("key code=0x%02x ext=%u vk=0x%02x sym=%s text=- state=%s repeat=%u count=%u sys=%d",
scan, ext, cast(uint) wParam, name8.ptr, state, prev, repeatCount, sys ? 1 : 0);
}
void app.logCharlogChar(const(char)* kind, WPARAM wParam, LPARAM lParam, bool (parameter) bool syssys) nothrow
{
const _error_ unitunit = cast(ushort) wParam;
const _error_ repeatBitrepeatBit = cast(uint)((lParam >> 30) & 1);
dchar _error_ cpcp = unit;
if (unit >= 0xd800 && unit <= 0xdbff) // high surrogate: hold for the low
{
g.g.pendingHighpendingHigh = unit;
logEvent("char_unit utf16=0x%04x note=high_surrogate_pending", unit);
return;
}
if (unit >= 0xdc00 && unit <= 0xdfff)
{
cp = g.g.pendingHighpendingHigh
? 0x10000 + ((g.g.pendingHighpendingHigh - 0xd800) << 10) + (unit - 0xdc00) : 0xfffd;
g.g.pendingHighpendingHigh = 0;
}
char[8] _error_ u8u8;
encodeUtf8(cp, u8);
logEvent("%s utf16=0x%04x cp=U+%04X text=%s repeat=%u sys=%d",
kind, unit, cast(uint) cp, u8.ptr, repeatBit, sys ? 1 : 0);
}
// ---------------------------------------------------------------------------
// Injection: SendInput at scancode level (wVk=0 + KEYEVENTF_SCANCODE), so the
// active layout — not the injector — decides vk and text, exactly like a
// physical key. Each step's events go in one SendInput batch (atomic order).
void void app.injectScans(scope const(ushort)[] seq) nothrowinjectScans(scope const(ushort)[] (parameter) const(ushort)[] seqseq) nothrow
{
INPUT[16] (local variable) _error_ inpinp;
const (local variable) const(_error_) nn = cast(UINT) seq.length;
foreach ((parameter) ulong ii, (parameter) const(ushort) ee; (parameter) const(ushort)[] seqseq)
{
inp[i].type = INPUT_KEYBOARD;
inp[i].ki.wScan = e & 0xff;
inp[i].ki.dwFlags = KEYEVENTF_SCANCODE | ((e & UP) ? KEYEVENTF_KEYUP : 0);
}
const (local variable) const(_error_) sentsent = SendInput(n, inp.ptr, INPUT.sizeof);
logEvent("inject kind=scancode events=%u sent=%u err=%lu",
n, sent, sent == n ? 0 : GetLastError());
}
void void app.injectUnicode(scope const(wchar)[] units) nothrowinjectUnicode(scope const(wchar)[] (parameter) const(wchar)[] unitsunits) nothrow
{
INPUT[8] (local variable) _error_ inpinp;
UINT (local variable) _error_ nn;
foreach ((parameter) const(wchar) uu; (parameter) const(wchar)[] unitsunits) // per unit: down + up, vk = VK_PACKET internally
{
inp[n].type = INPUT_KEYBOARD;
inp[n].ki.wScan = u;
inp[n].ki.dwFlags = KEYEVENTF_UNICODE;
n++;
inp[n] = inp[n - 1];
inp[n].ki.dwFlags |= KEYEVENTF_KEYUP;
n++;
}
const (local variable) const(_error_) sentsent = SendInput(n, inp.ptr, INPUT.sizeof);
logEvent("inject kind=unicode events=%u sent=%u err=%lu",
n, sent, sent == n ? 0 : GetLastError());
}
// ---------------------------------------------------------------------------
// Direct table lookup, the no-injection fallback: scan -> vk (MapVirtualKeyEx)
// -> text (ToUnicodeEx) against the start layout and the loaded de layout.
// rc semantics: 1 = one char, 0 = no translation, -1 = DEAD KEY (the char in
// the buffer is the accent itself).
void void app.probeToUnicode(ushort scan) nothrowprobeToUnicode(ushort (parameter) ushort scanscan) nothrow
{
static foreach (which; 0 .. 2)
{
{
HKL (local variable) _error_ hklhkl = (constant) int app.probeToUnicode.which = 0which == 0 ? g.(field) _error_ g.hklStarthklStart : g.(field) _error_ g.hklDehklDe;
const (local variable) const(_error_) vkvk = MapVirtualKeyExW(scan, MAPVK_VSC_TO_VK, hkl);
BYTE[256] (local variable) _error_ kstatekstate = 0;
WCHAR[8] (local variable) _error_ out16out16 = 0;
const (local variable) const(_error_) rcrc = ToUnicodeEx(vk, scan, kstate.ptr, out16.ptr, out16.length, 0, hkl);
char[8] (local variable) char[8] u8u8 = 0;
void app.encodeUtf8(dchar c, ref char[8] buf) nothrow @nogcencodeUtf8(out16[0], (local variable) char[8] u8u8);
logEvent("tounicodeex layout=%s hkl=0x%zx scan=0x%02x vk=0x%02x rc=%d text=%s",
which == 0 ? "start".ptr : "de".ptr, cast(size_t) hkl,
scan, vk, rc, rc != 0 ? u8.ptr : "-".ptr);
}
}
}
// ---------------------------------------------------------------------------
// The script: one step per WM_TIMER tick, so each batch's messages are pumped
// (and logged) before the next batch is injected.
void app.runSteprunStep(HWND (parameter) HWND hwndhwnd) nothrow
{
switch (g.g.stepstep++)
{
case 0: // letter
logEvent("script step=letter scan=0x%02x", SC.SC.AA);
injectScans([SC.SC.AA, SC.SC.AA | UP]);
break;
case 1: // shifted digit: vk says '2', the text says otherwise
logEvent("script step=shifted_digit scan=0x%02x", SC.SC.digit2digit2);
injectScans([SC.SC.lshiftlshift, SC.SC.digit2digit2, SC.SC.digit2digit2 | UP, SC.SC.lshiftlshift | UP]);
break;
case 2: // two keydowns, no keyup between: bit 30 flips on the second
logEvent("script step=repeat_bit scan=0x%02x", SC.SC.BB);
injectScans([SC.SC.BB, SC.SC.BB, SC.SC.BB | UP]);
break;
case 3: // Alt chord -> the WM_SYS* flavor of the same chain
logEvent("script step=alt_chord scan=0x%02x", SC.SC.CC);
injectScans([SC.SC.laltlalt, SC.SC.CC, SC.SC.CC | UP, SC.SC.laltlalt | UP]);
break;
case 4: // switch the thread to German (QWERTZ + dead accents)
logEvent("script step=load_layout klid=00000407");
g.g.hklDehklDe = LoadKeyboardLayoutW("00000407"w.ptr, KLF_ACTIVATE);
if (g.g.hklDehklDe is null)
{
logEvent("error what=LoadKeyboardLayoutW code=%lu note=de_steps_will_run_on_start_layout",
GetLastError());
break;
}
ActivateKeyboardLayout(g.g.hklDehklDe, 0);
WCHAR[KL_NAMELENGTH] _error_ klidklid;
GetKeyboardLayoutNameW(klid.ptr);
// The HKL value alone does not prove the *tables* switched (Wine's
// headless null driver hands back an 0407-tagged HKL whose mapping is
// still its built-in default table). Check behaviorally: on a real de
// (QWERTZ) layout, the QWERTY-Y-position scancode maps to VK 'Z'.
const _error_ vkYdevkYde = MapVirtualKeyExW(SC.SC.YY, MAPVK_VSC_TO_VK, g.g.hklDehklDe);
logEvent("layout_active hkl=0x%zx klid=%c%c%c%c%c%c%c%c tables=%s",
cast(size_t) GetKeyboardLayout(0),
klid[0], klid[1], klid[2], klid[3], klid[4], klid[5], klid[6], klid[7],
vkYde == 'Z' ? "de".ptr : "fallback_not_de".ptr);
logEvent("layout_map scan=0x%02x vk_start=0x%02x vk_de=0x%02x", SC.SC.YY,
MapVirtualKeyExW(SC.SC.YY, MAPVK_VSC_TO_VK, g.g.hklStarthklStart), vkYde);
logEvent("layout_map scan=0x%02x vk_start=0x%02x vk_de=0x%02x", SC.SC.ZZ,
MapVirtualKeyExW(SC.SC.ZZ, MAPVK_VSC_TO_VK, g.g.hklStarthklStart),
MapVirtualKeyExW(SC.SC.ZZ, MAPVK_VSC_TO_VK, g.g.hklDehklDe));
// Text-level probes straight off the layout tables, no injection:
// ToUnicodeEx returns -1 for a dead key. Probing the dead key leaves
// a pending accent in the thread's translation state, so a space
// probe follows to flush it (its result is logged too — on a real de
// layout it yields the standalone accent).
probeToUnicode(SC.SC.YY);
probeToUnicode(SC.SC.ZZ);
probeToUnicode(SC.SC.acuteacute);
probeToUnicode(SC.SC.spacespace);
break;
case 5: // same physical key as step 0's neighbor: QWERTY Y -> de 'z'
logEvent("script step=de_y_position scan=0x%02x", SC.SC.YY);
injectScans([SC.SC.YY, SC.SC.YY | UP]);
break;
case 6: // and the mirror: QWERTY Z position -> de 'y'
logEvent("script step=de_z_position scan=0x%02x", SC.SC.ZZ);
injectScans([SC.SC.ZZ, SC.SC.ZZ | UP]);
break;
case 7: // dead key: acute accent, alone -> WM_DEADCHAR only
logEvent("script step=dead_acute scan=0x%02x", SC.SC.acuteacute);
injectScans([SC.SC.acuteacute, SC.SC.acuteacute | UP]);
break;
case 8: // ... then E composes: WM_CHAR U+00E9
logEvent("script step=dead_then_e scan=0x%02x", SC.SC.EE);
injectScans([SC.SC.EE, SC.SC.EE | UP]);
break;
case 9: // supplementary-plane text -> two WM_CHARs (surrogate pair)
logEvent("script step=unicode_surrogate cp=U+1F600");
injectUnicode([cast(wchar) 0xd83d, cast(wchar) 0xde00]);
break;
default:
KillTimer(hwnd, TIMER_ID);
logEvent("summary keydown=%u keyup=%u syskeydown=%u syskeyup=%u char=%u deadchar=%u syschar=%u sysdeadchar=%u inputlangchange=%u",
g.g.nKeyDownnKeyDown, g.g.nKeyUpnKeyUp, g.g.nSysKeyDownnSysKeyDown, g.g.nSysKeyUpnSysKeyUp,
g.g.nCharnChar, g.g.nDeadCharnDeadChar, g.g.nSysCharnSysChar, g.g.nSysDeadCharnSysDeadChar, g.g.nLangChangenLangChange);
if (g.g.autoExitautoExit)
DestroyWindow(hwnd);
else
logEvent("script_done note=window_stays_open_for_real_typing");
break;
}
}
// ---------------------------------------------------------------------------
extern (Windows) LRESULT app.wndProcwndProc(HWND (parameter) HWND hwndhwnd, UINT (parameter) UINT msgmsg, WPARAM wParam, LPARAM lParam) nothrow
{
switch (msg)
{
case WM_KEYDOWN:
++g.g.nKeyDownnKeyDown;
logKey("down", false, wParam, lParam);
return 0;
case WM_KEYUP:
++g.g.nKeyUpnKeyUp;
logKey("up", false, wParam, lParam);
return 0;
case WM_SYSKEYDOWN:
++g.g.nSysKeyDownnSysKeyDown;
logKey("down", true, wParam, lParam);
goto default; // DefWindowProcW owns Alt-menu / Alt-F4 handling
case WM_SYSKEYUP:
++g.g.nSysKeyUpnSysKeyUp;
logKey("up", true, wParam, lParam);
goto default;
case WM_CHAR:
++g.g.nCharnChar;
logChar("char", wParam, lParam, false);
return 0;
case WM_DEADCHAR:
++g.g.nDeadCharnDeadChar;
logChar("deadchar", wParam, lParam, false);
return 0;
case WM_SYSCHAR:
++g.g.nSysCharnSysChar;
logChar("char", wParam, lParam, true);
goto default;
case WM_SYSDEADCHAR:
++g.g.nSysDeadCharnSysDeadChar;
logChar("deadchar", wParam, lParam, true);
goto default;
case WM_INPUTLANGCHANGE:
++g.g.nLangChangenLangChange;
logEvent("msg name=WM_INPUTLANGCHANGE charset=%u hkl=0x%zx",
cast(uint) wParam, cast(size_t) lParam);
return 1;
case WM_TIMER:
if (wParam == TIMER_ID)
runStep(hwnd);
return 0;
case WM_PAINT:
PAINTSTRUCT _error_ psps;
HDC _error_ hdchdc = BeginPaint(hwnd, &ps);
FillRect(hdc, &ps.rcPaint, cast(HBRUSH)(COLOR_WINDOW + 1));
EndPaint(hwnd, &ps);
return 0;
case WM_CLOSE:
logEvent("close_requested");
goto default;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
// ---------------------------------------------------------------------------
bool bool app.envFlag(const(wchar)* name) nothrowenvFlag(const(wchar)* (parameter) const(wchar)* namename) nothrow
{
WCHAR[8] (local variable) _error_ bufbuf;
const (local variable) const(_error_) nn = GetEnvironmentVariableW(name, buf.ptr, buf.length);
return n >= 1 && n < buf.length && buf[0] == '1';
}
int int D main()main()
{
instrumentInit("f06_keyboard_win32");
logEvent("init_start");
g.g.autoExitautoExit = envFlag("WSI_AUTO_EXIT"w.ptr);
g.g.noTranslatenoTranslate = envFlag("WSI_NO_TRANSLATE"w.ptr);
logEvent("mode auto_exit=%d no_translate=%d", g.g.autoExitautoExit ? 1 : 0, g.g.noTranslatenoTranslate ? 1 : 0);
HINSTANCE (local variable) _error_ hInsthInst = GetModuleHandleW(null);
auto (local variable) wstring clsNameclsName = "wsi-f06-class"w;
WNDCLASSEXW (local variable) _error_ wcwc;
wc.cbSize = WNDCLASSEXW.sizeof;
wc.lpfnWndProc = &wndProc;
wc.hInstance = hInst;
wc.lpszClassName = clsName.ptr;
wc.hCursor = LoadCursorW(null, IDC_ARROW);
if (!RegisterClassExW(&wc))
{
logEvent("error what=RegisterClassExW code=%lu", GetLastError());
return 1;
}
g.g.hwndhwnd = CreateWindowExW(0, clsName.ptr, "wsi-f06-keyboard"w.ptr,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 480, 320,
null, null, hInst, null);
if (g.(field) _error_ g.hwndhwnd is null)
{
logEvent("error what=CreateWindowExW code=%lu", GetLastError());
return 1;
}
logEvent("window_created");
ShowWindow(g.g.hwndhwnd, SW_SHOW);
UpdateWindow(g.g.hwndhwnd);
// SendInput targets the foreground window's thread; make sure that's us
// (under Wine's headless null driver the fresh window is, but be explicit).
SetForegroundWindow(g.g.hwndhwnd);
SetFocus(g.g.hwndhwnd);
logEvent("focus foreground_is_self=%d focus_is_self=%d",
GetForegroundWindow() is g.g.hwndhwnd ? 1 : 0, GetFocus() is g.g.hwndhwnd ? 1 : 0);
// Who owns key repeat: the system. Its knobs (and their units) are global.
g.g.hklStarthklStart = GetKeyboardLayout(0);
WCHAR[KL_NAMELENGTH] (local variable) _error_ klidklid;
GetKeyboardLayoutNameW(klid.ptr);
logEvent("layout_start hkl=0x%zx klid=%c%c%c%c%c%c%c%c", cast(size_t) g.g.hklStarthklStart,
klid[0], klid[1], klid[2], klid[3], klid[4], klid[5], klid[6], klid[7]);
DWORD (local variable) _error_ speedspeed, (local variable) _error_ delaydelay;
SystemParametersInfoW(SPI_GETKEYBOARDSPEED, 0, &speed, 0); // 0..31 ~= 2.5..30 cps
SystemParametersInfoW(SPI_GETKEYBOARDDELAY, 0, &delay, 0); // 0..3 ~= 250..1000 ms
logEvent("repeat_config speed=%lu delay=%lu owner=system", speed, delay);
SetTimer(g.g.hwndhwnd, TIMER_ID, TICK_MS, null);
MSG (local variable) _error_ msgmsg;
while (GetMessageW(&msg, null, 0, 0) > 0)
{
// TranslateMessage is the ONLY producer of WM_CHAR/WM_DEADCHAR: it
// reads WM_(SYS)KEYDOWN + the thread's keyboard state + active layout
// and posts the text-level message(s). Skip it and the text vanishes.
if (!g.(field) _error_ g.noTranslatenoTranslate)
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
logEvent("exit code=%d", cast(int) msg.wParam);
return cast(int) msg.wParam;
}