<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daming Wu</title>
    <description>The latest articles on DEV Community by Daming Wu (@wudaming00).</description>
    <link>https://dev.to/wudaming00</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078588%2F53c9602e-c5a3-41eb-bcaa-7cc0394bdce2.png</url>
      <title>DEV Community: Daming Wu</title>
      <link>https://dev.to/wudaming00</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wudaming00"/>
    <language>en</language>
    <item>
      <title>Why PID equality breaks WebView2 focus detection—and how to verify focus safely</title>
      <dc:creator>Daming Wu</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:55:50 +0000</pubDate>
      <link>https://dev.to/wudaming00/why-pid-equality-breaks-webview2-focus-detection-and-how-to-verify-focus-safely-4ane</link>
      <guid>https://dev.to/wudaming00/why-pid-equality-breaks-webview2-focus-detection-and-how-to-verify-focus-safely-4ane</guid>
      <description>&lt;p&gt;A desktop dictation tool has a deceptively dangerous job: it waits while speech recognition runs, then writes text into another application's focused control. If focus changes during that delay, the text may land in a chat, terminal, or password field the user never intended.&lt;/p&gt;

&lt;p&gt;In VocalCode I capture a focus token when recording starts and validate it again immediately before insertion. On Windows that token includes the foreground HWND and the UI Automation runtime ID of the exact focused element.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conservative check that was still wrong
&lt;/h2&gt;

&lt;p&gt;My original code also required &lt;code&gt;IUIAutomationElement::CurrentProcessId&lt;/code&gt; to equal the PID that owns &lt;code&gt;GetForegroundWindow()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That works for ordinary Win32 controls. It fails in Chromium, Electron, and WebView2 applications because the top-level HWND belongs to the host process while the focused text control may be provided by a separate renderer process. The control is legitimate, but PID equality rejects it.&lt;/p&gt;

&lt;p&gt;The mistake was treating process identity as proof of visual/window membership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the native window tree as the membership authority
&lt;/h2&gt;

&lt;p&gt;The replacement algorithm is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capture &lt;code&gt;GetForegroundWindow()&lt;/code&gt; and reject a null HWND.&lt;/li&gt;
&lt;li&gt;Ask UI Automation for the focused element and capture its non-empty runtime ID.&lt;/li&gt;
&lt;li&gt;Walk upward with the raw-view tree walker until an element exposes &lt;code&gt;CurrentNativeWindowHandle&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Resolve &lt;code&gt;GetAncestor(native_hwnd, GA_ROOT)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Require that root HWND to equal the captured foreground HWND.&lt;/li&gt;
&lt;li&gt;Re-read both the foreground window and the focused UIA element after the ancestor walk to close the race.&lt;/li&gt;
&lt;li&gt;At delivery time, recapture the token and require the foreground HWND, renderer PID, and runtime ID to match the token captured at recording start.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In simplified Rust, the pure membership decision looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;FocusCandidate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;foreground_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;foreground_process_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;element_process_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;native_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;native_root_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;runtime_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FocusCandidate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.foreground_window&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.foreground_process_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.element_process_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.native_window&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.native_root_window&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.foreground_window&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="py"&gt;.runtime_id&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what is absent: &lt;code&gt;element_process_id == foreground_process_id&lt;/code&gt;. The renderer PID remains part of the identity token, but it is not used as the membership authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the race matters
&lt;/h2&gt;

&lt;p&gt;Walking a remote UIA provider can take time. During that walk the user can switch windows or controls. After verifying ancestry, the implementation therefore checks &lt;code&gt;GetForegroundWindow()&lt;/code&gt; again and uses &lt;code&gt;IUIAutomation::CompareElements&lt;/code&gt; against a freshly fetched focused element. At insertion time the complete token is validated again.&lt;/p&gt;

&lt;p&gt;A different field in the same WebView is not considered equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that caught the boundary
&lt;/h2&gt;

&lt;p&gt;The pure predicate and token comparison make the important cases straightforward to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a same-process Win32 input is accepted;&lt;/li&gt;
&lt;li&gt;a cross-process WebView2 input whose native root is the foreground window is accepted;&lt;/li&gt;
&lt;li&gt;an element rooted in another foreground window is rejected;&lt;/li&gt;
&lt;li&gt;changing either the window or exact runtime ID while speech recognition runs is rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader lesson is that modern desktop UI process boundaries do not necessarily match window boundaries. If the safety question is “does this control belong to the user's foreground window?”, verify that relationship in the window tree and preserve the exact control identity across the asynchronous operation.&lt;/p&gt;

&lt;p&gt;I found this while building VocalCode, an on-device push-to-talk utility for AI coding workflows. Founder disclosure: it is my product. The 30-day no-card build is at &lt;a href="https://vocalcode.app/" rel="noopener noreferrer"&gt;https://vocalcode.app/&lt;/a&gt; if you want to test the behavior in real Win32, Chrome/Electron, or WebView2 fields.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Windows silently stops delivering WH_KEYBOARD_LL hook events when a Chromium window has focus (and the diagnostic hook hides it)</title>
      <dc:creator>Daming Wu</dc:creator>
      <pubDate>Fri, 21 Aug 2026 17:20:42 +0000</pubDate>
      <link>https://dev.to/wudaming00/windows-silently-stops-delivering-whkeyboardll-hook-events-when-a-chromium-window-has-focus-and-bi8</link>
      <guid>https://dev.to/wudaming00/windows-silently-stops-delivering-whkeyboardll-hook-events-when-a-chromium-window-has-focus-and-bi8</guid>
      <description>&lt;p&gt;I ship a push-to-talk dictation app for Windows (&lt;a href="https://vocalcode.app" rel="noopener noreferrer"&gt;VocalCode&lt;/a&gt;). Hold a key, talk, release, text appears at the cursor. The "hold a key" part is a low-level keyboard hook — &lt;code&gt;SetWindowsHookExW(WH_KEYBOARD_LL, …)&lt;/code&gt; — the same primitive every global-hotkey app on Windows uses. This is the story of the week it stopped working, the part of the story that made it unfindable, and the fix. The logger I wrote to chase it is MIT on GitHub: &lt;a href="https://github.com/wudaming00/wh-keyboard-ll-chromium" rel="noopener noreferrer"&gt;wh-keyboard-ll-chromium&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;The hook worked everywhere — terminals, editors, Explorer, native apps — except when a Chromium-family window was in the foreground. My own WebView2 settings window, and separately Electron and CEF apps. With one of those focused, the hook callback simply stopped being invoked. No error. No unhook. &lt;code&gt;GetLastError&lt;/code&gt; had nothing to say. The keystrokes were delivered to the focused app just fine; my hook never saw them.&lt;/p&gt;

&lt;p&gt;Focus a native window: events flow. Focus the Chromium window: silence. Completely reproducible on my machine, and completely absent from the usual search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The observer effect (this is the part that cost the week)
&lt;/h2&gt;

&lt;p&gt;I did what you do. I installed a &lt;em&gt;second&lt;/em&gt; diagnostic hook to log what the first one was missing.&lt;/p&gt;

&lt;p&gt;The moment the diagnostic hook was in, the system delivered events to both hooks. Remove it, and the original went deaf again under identical conditions. A watched hook never misbehaves.&lt;/p&gt;

&lt;p&gt;This has a nasty consequence: &lt;strong&gt;a minimal repro does not reliably reproduce.&lt;/strong&gt; The repro program perturbs the very hook chain it is trying to observe. Every reduced test case I built passed, and the production binary kept failing. If you have hit this and concluded you were imagining it — you probably weren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I believe is going on (hypotheses, not verdicts)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The documented contract allows it. A low-level hook that exceeds the &lt;code&gt;LowLevelHooksTimeout&lt;/code&gt; budget can be silently removed or skipped, and Windows 10/11 will silently stop calling hooks it considers slow. Chromium pumps input very differently from native apps (raw input, a hot message loop, high-frequency pointer events). It is plausible that this shifts timing enough to trip the threshold — for the whole chain, or just for your position in it.&lt;/li&gt;
&lt;li&gt;Hook-chain position matters, and installing or removing &lt;em&gt;any&lt;/em&gt; hook reorders the chain. That alone would explain the observer effect: the diagnostic hook changed the chain, which changed the timing, which changed the outcome.&lt;/li&gt;
&lt;li&gt;I could not find an API that says "your hook was skipped or removed for being slow." If you know one, I would genuinely like to hear about it — it is half the reason the repo exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What actually fixed it
&lt;/h2&gt;

&lt;p&gt;I stopped depending on the global hook while my own WebView2 window had focus. A &lt;code&gt;keydown&lt;/code&gt; listener inside the page answers the push-to-talk chord itself; the global hook keeps serving every native window. Shipped, and the failure disappeared for every user.&lt;/p&gt;

&lt;p&gt;If your hotkey must work over &lt;em&gt;other people's&lt;/em&gt; Chromium windows (Chrome, VS Code, Slack…), the pragmatic rules are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the hook callback microscopic. Post to your own thread and return immediately. No logging, no allocation, no locks inside it.&lt;/li&gt;
&lt;li&gt;Detect prolonged silence while keys are clearly being typed — compare against &lt;code&gt;GetAsyncKeyState&lt;/code&gt; polling — and re-install the hook when it happens. Ugly. Effective.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The logger
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/main.rs&lt;/code&gt; installs a WH_KEYBOARD_LL hook and prints one line per event: timestamp, key, and the executable plus title of the foreground window. Run it, type into different apps, and watch whether the lines stop when a Chromium window is focused.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--bin&lt;/span&gt; khook-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lines flow in every app, Chrome included: your machine and timing do not trip it. Mine usually didn't either, until the app also had audio callbacks and a webview running.&lt;/li&gt;
&lt;li&gt;Lines stop in exactly one app family: you have reproduced my week.&lt;/li&gt;
&lt;li&gt;Your &lt;em&gt;other&lt;/em&gt; hotkey app goes deaf only while this logger is &lt;em&gt;not&lt;/em&gt; running: that is the observer effect, and I would love an issue with your setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Environment where I hit it: Windows 11 (23H2 and 24H2), Rust hook thread with a dedicated message pump, WebView2 (Edge 126+) same-process window; also reproduced against CEF-based apps.&lt;/p&gt;

&lt;p&gt;Issues and war stories welcome — especially if you can &lt;em&gt;disprove&lt;/em&gt; the timeout hypothesis with something better.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>rust</category>
      <category>debugging</category>
      <category>electron</category>
    </item>
    <item>
      <title>Windows silently stops delivering WH_KEYBOARD_LL events when a Chromium window has focus</title>
      <dc:creator>Daming Wu</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:11:42 +0000</pubDate>
      <link>https://dev.to/wudaming00/windows-silently-stops-delivering-whkeyboardll-events-when-a-chromium-window-has-focus-449f</link>
      <guid>https://dev.to/wudaming00/windows-silently-stops-delivering-whkeyboardll-events-when-a-chromium-window-has-focus-449f</guid>
      <description></description>
    </item>
    <item>
      <title>My first native-language tester said 'not usable yet' - it was the best thing that happened all week</title>
      <dc:creator>Daming Wu</dc:creator>
      <pubDate>Sun, 16 Aug 2026 20:57:44 +0000</pubDate>
      <link>https://dev.to/wudaming00/my-first-native-language-tester-said-not-usable-yet-it-was-the-best-thing-that-happened-all-week-1c95</link>
      <guid>https://dev.to/wudaming00/my-first-native-language-tester-said-not-usable-yet-it-was-the-best-thing-that-happened-all-week-1c95</guid>
      <description></description>
    </item>
    <item>
      <title>CPU-only local speech-to-text for driving coding agents: Parakeet TDT + Paraformer over Whisper</title>
      <dc:creator>Daming Wu</dc:creator>
      <pubDate>Sat, 15 Aug 2026 07:01:38 +0000</pubDate>
      <link>https://dev.to/wudaming00/cpu-only-local-speech-to-text-for-driving-coding-agents-parakeet-tdt-paraformer-over-whisper-5d61</link>
      <guid>https://dev.to/wudaming00/cpu-only-local-speech-to-text-for-driving-coding-agents-parakeet-tdt-paraformer-over-whisper-5d61</guid>
      <description>&lt;p&gt;I dictate prompts to coding agents (Claude Code, Cursor) all day. Typing paragraph-long prompts was my bottleneck, and I wanted the speech side fully local - push-to-talk, CPU-only, nothing leaving the machine. The model choices surprised me, so here's the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;Built in Rust on &lt;a href="https://github.com/k2-fsa/sherpa-onnx" rel="noopener noreferrer"&gt;sherpa-onnx&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NVIDIA Parakeet TDT v3&lt;/strong&gt; for European languages: one model covers 25 languages with automatic detection, and TDT decoding is noticeably lighter on CPU than Whisper-class encoder-decoder models in my use. It also emits its own punctuation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paraformer (zh-en bilingual)&lt;/strong&gt; for Mandarin: code-switching ("??? function ?? async") is where general multilingual models fall apart, and a dedicated bilingual model handles it far better. Punctuation comes from a separate local CT-Transformer pass.&lt;/li&gt;
&lt;li&gt;Models download on demand (a few hundred MB each); everything runs offline afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not Whisper
&lt;/h2&gt;

&lt;p&gt;Nothing wrong with Whisper - but for live push-to-talk on CPU, the latency/quality point of TDT worked better for me, and Paraformer beat everything I tried on bilingual Mandarin. I deliberately don't publish latency numbers (too hardware-dependent to be honest about), so treat this as one person's experience, not a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody talks about: insertion
&lt;/h2&gt;

&lt;p&gt;Text &lt;em&gt;insertion&lt;/em&gt; is harder than recognition. Getting clean text into whatever field has focus - terminal, editor, browser - across Windows and macOS is its own rabbit hole. The worst one: Windows silently stops delivering low-level keyboard hooks (WH_KEYBOARD_LL) when a Chromium-based window is focused. It's on record against Tauri and CEF too, and nothing you write inside the hook can fix it - the procedure is never entered. The fix is architectural: the focused window answers its own keydown events, and the global hook only owns the unfocused case. That one cost me a week.&lt;/p&gt;

&lt;p&gt;Acronym handling matters more than WER for this use case: "m c p" has to come out as MCP, and a user-editable replacement dictionary catches project jargon the model can't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;This shipped as a small paid app - VocalCode, USD 4.99 one-time with a 30-day free trial, at &lt;a href="https://vocalcode.app" rel="noopener noreferrer"&gt;vocalcode.app&lt;/a&gt; - so I'm affiliated. The model discussion is the point of this post, though: if you're running TDT-family models for live dictation, or you've gotten good zh-en code-switching out of anything else local, I'd genuinely like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>rust</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
