AI Types Faster Than I Do
Have you heard about this AI thing? I know it doesn’t get much press and hardly anyone is talking about it. So, what better way to give it some more attention than to write an article about it for my blog that hasn’t been updated in 8 years?
Let’s begin.
The Experiment
I wanted to try building a full Windows application using AI as my coding partner. Since Notepad gets more and more bloated and more and more filled with AI, how about turning the tables and using AI to build a no-bloat Notepad replacement?
The result is Nanopad, a Notepad replacement in pure Win32 C++ with no installer or runtime dependencies.
| Dark | Light |
|---|---|
![]() |
![]() |
What I Was Going For
Pure Notepad, but with some modern conveniences. Altogther, that means:
- Full dark mode that follows the system theme
- Per-monitor DPI scaling
- Portable settings (INI file next to the EXE, no registry)
- System integration (Replace Notepad, Open With, context menu)
- Font selection
- Encoding detection (UTF-8, UTF-16, ANSI)
- Find & Replace, Print, Word Wrap, Go To Line
The Stack
- Language: Modern C++
- UI: Win32 API only -
CreateWindowExW,WndProc, message loop - Edit control: Standard multiline
EDITclass (same one original Notepad uses) - Build: Visual Studio 2022+, MSBuild, v143 toolset
- CI/CD: GitHub Actions - builds on push, version-stamped releases on tag
- Dark mode: Undocumented Windows APIs (more on this below)
This ends up being about 3,000 lines of C++ across 12 source files.
How It Actually Worked
I started with this prompt:
Let’s create the simplest Win32 text editor. This would be a replacement for modern-day Notepad that goes back to that simplicity. Light and dark mode. Font selection. File open and save. Dead simple text editor, but should be able to handle large files smoothly. ask questions.
I answered some questions about language, build setup, etc., and we began.
TL;DR: AI types faster than I do, but it’s as falliable as any human developer.
The AI didn’t decide to use an EDIT control instead of RichEdit. It didn’t decide to store settings in an INI file. It didn’t decide to use undocumented UAH messages for dark mode menus. It royally screwed up per-monitor DPI the first time. And second time. And mutliple times thereafter. Knowing how these things work and how to fix them were all on me, based on years of working with Windows. In short, I already knew how to build this app, but AI let me build it faster.
What Went Well
The initial project setup of solution file, vcxproj, manifest, resource script, window class, message loop, working editor with menus, etc. was generated in one shot and compiled on the first try. Boilerplate code and already solved problems are where AI shines. There’s a right way to call CreateWindowExW and set up an accelerator table, and AI already knows that.
What Needed Human Eyes
Almost everything needed iteration. Some examples:
-
DPI scaling was a mess. The AI hardcoded the font height When I asked “are we handling realtime DPI changes?” the AI admitted we were not. If I didn’t already know what needed to be written here, this would have shipped broken.
-
Visual failures needed screenshots. I often had to paste screenshots showing problems, like a white border under the dark menu bar, inconsistent menu spacing, too much padding. AI can’t “see” your app. You have to be its eyes and be able to determine what’s wrong and explain it. Screenshots help.
-
I had to recognize and reject AI-suggested bad ideas. When implementing the “Edit with Nanopad” Explorer context menu feature, at one point the AI suggested building a COM DLL shell extension to get into the Windows 11 modern context menu. Wanting a portable single EXE and knowing the pain that is required to partake in the new context menu (application identity,
IExplorerComamndin a DLL, ertc.) this would severely complicate things and not easily allow the single EXE I wanted.
The Pain Points
DPI Handling
DPI handling turned out to be a recurring theme, not because the AI didn’t know about DPI, but because it would fix it in one place and not think about the others.
First, the font. The AI hardcoded lfHeight = -15 (11pt at 96 DPI). This is an easy fix, just query the actual DPI and use MulDiv.
Then I asked “are we handling realtime DPI changes?” But, the generated code was not. PerMonitorV2 was in the manifest, but there was no WM_DPICHANGED handler. So the AI added one, which scaled the editor font and resized the window.
But the menu bar font wasn’t scaling. The AI had cached it at startup and never recreated it. Once I pointed this out, it added InvalidateMenuFont().
But then it was using SystemParametersInfoW to get the new font metrics, which returns values for the system DPI, not the monitor you just dragged to. I had to point that out too, and we switched to SystemParametersInfoForDpi.
Then, after shipping v1.0.0, I tested printing. The text was way too large on paper. Here’s the code the AI wrote:
int printerDpi = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
...
lf.lfHeight = MulDiv(lf.lfHeight, printerDpi, 96);
As with the very first issue above, it’s scaling the font from the printer’s DPI relative to 96, not my actual screen DPI. The fix is easy, get the actual DPI instead of hardcoding:
int screenDpi = GetDeviceCaps(hScreenDC, LOGPIXELSY);
int printerDpi = GetDeviceCaps(hPrinterDC, LOGPIXELSY);
...
lf.lfHeight = MulDiv(lf.lfHeight, printerDpi, screenDpi);
The AI knew how to handle DPI. It got it right for the editor font on the first try. But it didn’t propagate that knowledge to every other place DPI matters. The menu font, the SystemParametersInfo variant, and the print scaling were all still busted. Each one required a separate “oh, what about this?” from me. The AI fixed each one correctly when asked (mostly), but it never said “hey, we should also check the printer code.” Without actually looking at the built result and testing across a variety of scaling values, and printing to my printer, these would also have just shipped broken. In fact the version 1.0 I released has this print issue. I missed it the first time through.
Dark Mode
This one was painful. There is no official Microsoft API for dark Win32 menus. This took several iterations where I had to know right from wrong.
Attempt 1: Owner-Draw
The AI defaulted to owner-drawing the menus. It wrote an implementation using classic MFT_OWNERDRAW with WM_MEASUREITEM/WM_DRAWITEM. It worked, but it was ~200 lines of code that fought with Windows on spacing, DPI, and a white border between the title bar and menu bar.
But, I know the community solved this long ago with undocumented Win32 stuffs.
Attempt 2: UAH Messages (What Actually Works)
After giving it info on the known-but-undocumented WM_UAHDRAW* messages, it was able to produce a working, system-level menubar.
// Undocumented Windows messages for menu bar painting.
// Part of UAH (User32 Aero Hooks), present since Vista.
// See: https://github.com/adzm/win32-custom-menubar-aero-theme
#define WM_UAHDRAWMENU 0x0091
#define WM_UAHDRAWMENUITEM 0x0092
These are undocumented messages that Windows sends to paint the menu bar. They use reverse-engineered structs:
struct UAHMENUITEM
{
int iPosition;
UINT32 dwFlags;
HMENU hMenu;
RECT rcItem;
};
struct UAHMENU
{
HMENU hMenu;
HDC hdc;
DWORD dwFlags;
};
struct UAHDRAWMENUITEM
{
DRAWITEMSTRUCT dis;
UAHMENU um;
UAHMENUITEM umi;
};
By handling these messages and filling in the structs with “dark” values, you get a fully dark menu bar without any owner-draw flags on the menu items. Windows still handles keyboard navigation, sizing, and accessibility. These have been stable since Vista and are used by tons of Win32 apps, even though Microsoft has never documented them.
You can learn far more about this undocumented API here.
The full dark mode stack ended up touching five different undocumented or semi-documented APIs (use at your own risk, of course):
| Component | API |
|---|---|
| Title bar | DwmSetWindowAttribute(hwnd, 20, ...) |
| Popup menus | SetPreferredAppMode() (uxtheme ordinal 135) |
| Context menus | AllowDarkModeForWindow() (uxtheme ordinal 133) |
| Scrollbars | SetWindowTheme(L"DarkMode_Explorer") |
| Menu bar | WM_UAHDRAWMENU + WM_UAHDRAWMENUITEM |
These are loaded dynamically via LoadLibrary/GetProcAddress. On Windows versions that don’t support them, the calls silently fail and you get a light-themed app.
Font Size Setting
I wanted settings to go in nanopad.ini next to the EXE instead of into the registry. This way you can throw Nanopad on a USB stick with your config and it would work on any machine. I had AI write a simple custom parser instead of using GetPrivateProfileString knowing that this Windows 3.x era API has all kinds of issues with character limits, encoding quirks, and caching.
But, in creating this, AI saved the FontSize entry to the logical unit size, not the user-friendly size from the font chooser dialog, making hand-editing the file difficult. This required some additional prompting and we changed to:
[Font]
FontFace=Consolas
FontSize=11
FontWeight=400
The conversion happens at load/save time:
// Load: points → lfHeight
font.lfHeight = -MulDiv(pts, dpi, 72);
// Save: lfHeight → points
int pointSize = MulDiv(-font.lfHeight, 72, dpi);
AI Wrote Bugs, But Then Found Them (After Prompting)
Interestingly, during security review passes, the AI found real vulnerabilities in code it had written earlier in the same session:
EM_GETLINEbuffers that weren’t null-terminated.EM_GETLINEdoesn’t null-terminate its output. Three locations in the find/replace code used the buffer as a C string. Real bug.LoadLibraryW("uxtheme.dll")without a full path. DLL hijacking vulnerability. Fixed to useGetSystemDirectoryW+ full path.PrintDlgcancel path leakedDEVMODEhandles.- Integer overflow on files >2GB in
MultiByteToWideCharsize casts.
The AI that wrote the bugs also found the bugs when asked to look. Of course, we all write bugs, it’s part of the process. AI is no different. But, AI required me to prompt it to “look for security vulnerabilities” before they were found. Also, I had to do this multiple times. Each time it would find other things to change.
The same flow occurred when I asked it to optimize code, remove dead code, etc. It would perform the task, and then after immediately asking again, it would find more.
The Takeaway
TL;DR: If your project lives in a well-documented, widely-used technology space, AI can accelerate your efforts.
For me, AI can sometimes be a force multiplier. In this experiment, it cut what would have been a multi-day project down to hours. It amplified my existing knowledge, but it didn’t replace it. If I didn’t know how to write this already, going to production with that first result, or even the first few results, would have been gross. And…
This Was the Easy Case
Something very important to consider here is that a Notepad replacement is a solved problem. Win32 is one of the most thoroughly documented APIs (even if it’s not always perfectly accurate). C++ has decades of Stack Overflow answers, textbooks, and training data. The Windows EDIT control, CreateWindowExW, WM_PAINT, etc. are well-trodden paths. AI had an enormous body of work to draw from, and it showed.
I would wager if you opened your AI tool of choice and prompted it with my first prompt, and you answered its questiosn the same way, you’d get a version of this very code that was also 90% right. And with that comes the legal and ethical concerns, coupled with the ecological and energy concerns, but I’ll save that for a future post.
A Much Less Successful Hard Case
For comparison, I build a lot of retro gaming stuff. I have dabbled with AI to help build tools for the Atari Lynx, a handheld console from 1989 with a 65C02 CPU. The documentation is sparse, sometimes wrong, and scattered across decades-old fan sites and reverse-engineered datasheets. The toolchain I use is cc65, which has a fraction of the community around it that Visual Studio/MSVC does.
The sound hardware on this machine is poorly documented. I’ve been building a VGM-to-Lynx audio converter, and the AI regularly produces code that looks right but generates garbled audio on real hardware. Register values are wrong, timing assumptions are off, and AI confidently hallucinates behaviors for hardware it clearly has no training data on.
For Nanopad, AI was a genuine productivity multiplier. For Lynx development, it’s more like a sometimes-helpful, often-dangerous assistant that needs constant supervision and frequently needs to be told “no, that’s not how this hardware works.” The difference isn’t the AI, it’s the training data. Win32 has a million examples. The Atari Lynx sound chip has maybe a dozen, and half of them disagree with each other.
Simply, if you’re working in a niche area with sparse documentation or building something uniquely and wholly new, expect pain and to spend as much time correcting the AI as you would writing the code yourself.
The Human Is Still The Expert
I couldn’t have built Nanopad without understanding Win32, C++, DPI scaling, the Windows message loop, GDI object lifetimes, registry semantics, and all the other domain knowledge that informed every decision in the project. The AI can write the code for WM_DPICHANGED, but it can’t tell you that your font looks wrong on a 150% display. It can implement SetPreferredAppMode, but it can’t tell you that Windows 11’s Store Notepad is intercepting your IFEO hook. It can write a LoadLibrary call, but it won’t think about DLL hijacking until you ask it to.
AI types faster than I do. That’s valuable. But, the hard part of software development is knowing what to type, knowing when what you typed is wrong or could be improved, and knowing what questions to ask throughout the process.
Try It
Feel free to give Nanopad a try. I’m successfully using it on my machines as an actual Notepad replacement.
If you see anything completely busted, open an issue or ping me on Bluesky.

