_____ ___ _   _ _  ______   ___  _____ _____
|_   _|_ _| \ | | |/ / ___| / _ \|  ___|_   _|
  | |  | ||  \| | ' /\___ \| | | | |_    | |
  | |  | || |\  | . \ ___) | |_| |  _|   | |
  |_| |___|_| \_|_|\_\____/ \___/|_|     |_|

// building software in public. logs, bugs, small victories. est. 2026


« back to the log

tping: the tiny frontend quest

2026-06-11 · #tping · .txt


I had a tiny window that watched 23 store devices, and it worked. That should have been the end of it. But a number got into my head and would not leave: the size of that window, in pixels. Twenty-three rows of IP addresses, every one visible, no scrollbar, packed into the smallest rectangle I could manage. A tool window at 7pt font, built with WS_EX_TOOLWINDOW (a Windows flag that strips the title bar down to a thin sliver). Honest work. I was proud of it right up until I wasn’t.

Then MiniMax suggested Dear ImGui, an open-source, game-style drawing library, and I rebuilt the same window in it. The same 23 devices fit in a noticeably tighter box, barely any wider but dramatically shorter. This window drew itself from scratch: no Windows chrome tax, no SysListView32 (Windows’ built-in row list, the thing that wedges a tiny, unavoidable gap between rows) to argue with.

Something about watching all that wasted height vanish lit a fuse in me. I should have talked myself out of it faster. I did not.

what I tried

Three frontends live in the tree now.

The Walk build (Walk is a Go toolkit that wraps Windows’ built-in window controls) is the conservative one. Pure Go, 9.3 MB, builds in under a second, and runs anywhere a Walk binary runs, including ancient PCs.

The ImGui build is the dense one. It leans on Cgo (a Go feature that lets Go call into C libraries) and Dear ImGui through github.com/AllenDang/cimgui-go, comes in at 14.7 MB, and is statically linked so it doesn’t drag MinGW DLLs around behind it. It has an adaptive frame rate now: 30 frames a second when active, 5 when idle, 2 when minimized. Add a scratch buffer, pushed IDs, and a cached CalcTextSize, and the per-frame allocation comes down to one slice and zero string formats. This is the build I run on shift. I like it the way you like a tool you sharpened yourself.

The third frontend is a Direct2D experiment. Direct2D is Windows’ built-in 2D drawing engine, the thing that paints pixels to the screen. This one is a Cgo shim into the COM vtables (a way of calling into Windows’ component system from Go), with a D2D factory, a DWrite text format, and a wndproc that draws every pixel by hand. The package compiles. The test passes. The binary launches, opens a small window up in the corner of the screen, draws a control strip and a label area, and respects double-click, the R and G keys, and the topmost flag. It is the first frontend I have ever written where I owned the entire paint.

It was horrible. I spun MiniMax out completely on this one. Twice. That is a LOT of tokens blasted into the abyss to chase a couple of pixels. I do not know why I kept going as long as I did. Actually I do. I wanted the pixels to obey me, and they almost did, and “almost” is the most expensive word in this hobby.

the number that wouldn’t move

The Walk build carries a tax I could not shake. Windows’ built-in row list insists on a couple of pixels of padding between every row, and no theme flag, no LVS_OWNERDRAWFIXED, no LVM_SETITEMSPACING would let me strip it out. I measured the real scroll values against the client area and worked through the math, and the rows simply refuse to divide evenly into the space. The arithmetic is what it is. You can be furious at arithmetic, but it doesn’t care.

So I killed the theme, set the background to a flat RGB(245, 245, 245), accepted the gap, and made it look like I meant it. Then I went after Direct2D anyway, because Direct2D has no list view at all. Direct2D is a paintbrush. A paintbrush draws exactly the pixels you tell it to, and that promise was too pretty to leave alone.

the wall I hit

I told Direct2D to redraw at the new window size. It drew to the old one. I poked a different corner of the API, the place where the factory is supposed to hand me a resized drawing target, and it crashed outright. The window resizes just fine. The paint stops at the original rectangle and the rest stays black. A little dead zone, sitting there mocking me.

The honest fixes are all big. One path drops down into a lower-level API called ID2D1DeviceContext and hand-rolls a swap chain (the underlying buffer that talks to the graphics card), roughly 500 lines of swap-chain management. Another waits for a specific error code at the end of every frame, then tears the whole drawing target down and rebuilds it, which is what production Direct2D apps do, and which I could not make stable inside one evening. The last option falls back to GDI (Windows’ older drawing system) for the resize case, which would mean admitting the question in the title was wrong.

None of those fit into a few more hours. Each one is a day or two of focused work. And I would rather ship the thing I have than keep feeding tokens to a window that paints itself wrong out of spite.

the state of the tree

Three binaries in the tree now. Walk, for that retro feel. ImGui, for me. Direct2D, preserved for whoever wants to pick up the resize thread. The Direct2D package is a genuinely working Cgo COM layer: factory, render target, brush, text format, every vtable index checked against the MinGW d2d1.h. It’s real, and it’s reusable. The next person won’t have to relearn the calling convention from scratch, and I hope they read this first and budget two days instead of one evening.

ImGui stays the production build, after much pain and suffering. The Direct2D code sits in the tree like an unfinished miniature on the painting desk. The foundation is sound. Some future version of me with a free weekend will finish the resize story, or some future stranger will, and honestly either one is fine by me.