I spent 4 months of nights and weekends building — caller must have received last input event, no active foreground lock, target not minimized. Any check fails → it silently returns false.
SetForegroundWindow activates the top-level frame, but keyboard focus stays elsewhere. SendInput Ctrl+V then lands on the WindowProc of a non-input frame.
Working solution: AttachThreadInput sandwich
The trick is AttachThreadInput — an API that lets your thread temporarily share input state with another thread. While attached, the OS treats both threads as one for focus/foreground purposes, bypassing the "didn't receive last input event" check.
Plus: I need to know which child HWND had keyboard focus before my modal stole it. GetGUIThreadInfo.hwndFocus returns exactly that.
Here's the production code (extracted from , not config.json. DPAPI encryption under the hood, bound to the user account, non-portable across machines by design.
Lessons after 4 months
Win32 is alive. Microsoft didn't replace it — they hid it behind WPF/WinUI. Build anything non-trivial system-side, and you're back to user32.dll. My P/Invoke list for the core workflow:
RegisterHotKey,SendInput,GetForegroundWindow,GetGUIThreadInfo,AttachThreadInput,SetFocus,BringWindowToTop,IsIconic,ShowWindowAsync. That's just the baseline.Native beats Electron for tray utilities. Not ideology — pragmatism. 49 MB vs 250 MB installer, <1s cold start vs 3-5s, ~80 MB RAM idle vs ~400 MB. For something that lives in the background, that's the difference between "I don't notice it" and "oh there you are."
Local\Mutex, notGlobal\. Singletons usually useGlobal\namespace, which requiresSeCreateGlobalPrivilege. That right is granted to interactive users by default but stripped on locked-down domain machines (kiosks, AppLocker configs). On those systems, my app crashed at startup withUnauthorizedAccessException.Local\(per-session) has no such restriction and matches the semantics I actually want (one instance per user session, not per machine):
public const string DefaultMutexName = @"Local\CapyBroV2";
var mutex = new Mutex(initiallyOwned: false, name: mutexName, createdNew: out var createdNew);
Also: initiallyOwned: false. With true, I'd get AbandonedMutexException after every crash. With false, process death cleans up silently.
Foreground-poller for popup dismiss, not Mouse.Capture. My first prompt-picker used
Mouse.Capture(this, SubTree)to detect clicks outside. WPF ListBox grabsMouse.Captureinternally for click-drag selection — myLostMouseCapturehandler closed the popup BEFORE the user's MouseLeftButtonUp reached the ListBox. Final version uses a 100msDispatcherTimer+GetForegroundWindow()poll. If foreground isn't my popup → close. Cross-process clicks (browser tabs, Notepad, Telegram) are invisible to WPF's input system — polling Win32 is the only reliable catch-all.STJ with source generation. Not
JsonSerializer.Deserialize<T>(json)(reflection-based). Instead:
[JsonSerializable(typeof(AppConfig))]
public partial class AppConfigJsonContext : JsonSerializerContext { }
var config = JsonSerializer.Deserialize(json, AppConfigJsonContext.Default.AppConfig);
One day to set up [JsonSerializable] attrs for each DTO. Result: AOT-friendly, no runtime reflection, faster parsing, cleaner stack traces on JsonException.
Tech is ~30% of the work. The other 70% is marketing, docs, screenshots, localizations, SEO, GitHub issue triage, replying on Reddit. As a solo dev, that's not "side activity" — it's the activity after MVP.
Stack receipts
- ~12,000 lines of C# (the WPF app)
- ~3,000 lines of Next.js (marketing site)
- ~4 months, nights + weekends
- ~$130 spent (domain, OpenRouter test credits, stock icons I didn't end up using)
- Coffee: uncountable
What's next
- macOS port via Avalonia (~2 months)
- Browser extension companion for web apps with shadow DOM
- Native AOT once WPF + AOT become compatible (would shave 49 MB → ~25 MB)
Code + links
Source:
Installer: https://github.com/phantasmat2018/capy-bro/releases/tag/v2.0.0 (Win 10/11 x64, 49 MB)
If you've built a similar Windows-side AI tool, I'd love to hear what Win32 weirdness you ran into. The Office (Word, Excel) paste-back behavior is something I still haven't 100% nailed — Word works, Excel works only via the F2/Esc edit-mode dance. If anyone has a clean solution, drop it in the comments 🙏
Thanks for reading.
SOCIAL SHARE CARD GENERATOR