The 30-day ledger
One short entry per day. What landed, what clicked, what got chased and didn't pan out.
Project start: 2026-05-05. Day 30: 2026-06-03.
The goal was to find out whether one person with an agentic coding setup could build a usable X server on macOS in a month. Not a feature-complete clone of XQuartz, and not a research prototype either. Something I would actually use to display real X clients from my Sun workstations on my Mac, with modern rendering and display scaling that I could live with day to day.
One short entry per day. What landed, and the occasional moment where
something clicked. Hours are estimated from git timestamps (first commit
to last commit, with adjustments for obvious meal breaks and the days
where most of the work happened before the first push). The opcode
count is the cumulative number of rows tracked in OPCODE_STATUS.md
by end of day, where a row is one X11 wire interaction (setup, a core
opcode, an event, or an extension entry). Three of the jumps were
audit-driven, not pure implementation work — the entries call those
out. Velocity is a rough four-tier read on the day: high (a major
unlock or a big block of work that all stuck), solid (productive,
everything landed), mixed (net forward, but some hours went into
work that got reverted or hit a dead end), or off. I never regressed
the test suite, but I did chase a few things that didn’t pan out — those
are the mixed days.
Day 1 — 2026-05-05
Started with the capture tool, not the server. The idea: before I write a single byte of server code, I want to record exactly what real X clients are sending over the wire. So Day 1 is a framer plus a proxy that sits between client and server and dumps every request and reply to disk. No rendering yet. No NSWindow. Just bytes in, bytes out, decoded.
Day 2 — 2026-05-06
Capture v1 closed: replay subcommand, round-trip test against a real X server, a small corpus of captured sessions. Then I pivoted to Product 2 (the server itself): reference infrastructure (X11R6 source, the protocol spec, XQuartz source as a comparison), the first design docs, and the review-gate convention. Scaffolding day, but a necessary one. Capture was going to be the lens I used to debug everything that came after.
Day 3 — 2026-05-07
First pixels on screen. M1 (socket + handshake), M2 (first NSWindow), and M3 (xclock rendering) all landed on the same day. Then display-adaptive integer scaling, an XLFD parser, a font resolver that maps X font names to Core Text, ImageText8, PolyFillRectangle, and ListFonts. By evening live xterm was working: keyboard input, scrolling, resize, focus cursor. Eight hours from “blank window” to “I’m typing into it.” The XLFD parser and the font resolver are the start of what becomes the multi-day xterm text rendering arc; today they map the names, the cell-size question gets answered over the next two days.
Day 4 — 2026-05-08
Color xterm with ANSI palette parsed per-bit out of the GC. xcalc from my
Ultra 5 running live with PolyText8 and AllocNamedColor. Scrollback fixed
after I tracked down a ChangeWindowAttributes path that was auto-repainting
and wiping scrolled content. Cut-to-clipboard round-trip working. The
+0.5 user-pixel CTM shift on the stroke plane killed the cursor-fragment
ghosts I’d been chasing all afternoon. Multi-client server now identifies
sessions by WM_CLASS in the logs. The xterm text arc gets its first
attempt today too: fit Monaco into xterm’s requested cell by computing
the pointSize from min(cellW/advance_ratio, cellH/lineHeight_ratio)
and correcting Monaco’s line-height ratio from 1.07 to 1.2. It rendered,
but Monaco at fractional pointSize was reading as “too bold” because
Core Text’s hinter can’t quite save fractional sizes. That set up
tomorrow.
Day 5 — 2026-05-09
The xterm text arc resolves. Cell follows the selected font, not the
other way around — iTerm2’s playbook. When xterm asks for 7x14, pick
the integer pointSize closest to what fits, instantiate Monaco at that
size, ask Core Text for its actual advance / ascent / descent /
lineHeight, and report Monaco’s actual cell in QueryFont. The XLFD
becomes a hint, not a contract. Integer pointSize is the unlock:
Core Text’s hinter does its best work there, and the “too bold” stem
asymmetry from yesterday’s fractional sizes disappears. Reported metrics
equal rendered metrics by construction. EnterNotify and LeaveNotify
wired up so Athena widgets hover correctly. NSCursor substitution gives
me an I-beam over the xterm text area. Then I broke through Motif /
quickplot init: GrabPointer, QueryBestSize, override_redirect,
SetInputFocus, real event time and state. By end of day quickplot from
the Ultra 5 was rendering and accepting clicks.
Day 6 — 2026-05-10
Big architectural day. I had been running per-session split read/write
threads, and there was a race I couldn’t make go away. So I consolidated
to a single Grand Central Dispatch protocolQueue per session that owns the socket and the
session state. Same day: Xt menu support, fake CDE customization daemon
plus a SelectionNotify time-field fix that I would later discover was
the actual unlock for the dt-apps. Captured a corpus of SS2 gold pairs
to test against. Planned the week ahead because I was about to be away
from the workshop.
Day 7 — 2026-05-11
Day off
Day 8 — 2026-05-12
Day off
Day 9 — 2026-05-13
Back at it. Started the region work: built a small region algebra (type
plus the four set ops), wired clipList and borderClip into every
window entry, and made Expose emission use the rect list rather than
the bounding box. Locked Expose-event counts per captured app in the
test suite so I’d catch any regressions immediately. Also shipped
swiftx-capture diff: a markdown gold-vs-swiftx comparison tool with
LCS alignment. Adopted “XError honesty is the default” as policy:
silent drops are now a test failure.
Day 10 — 2026-05-14
XError emission sweep. A wire-level XError is the protocol-correct way to tell a client “I can’t serve this request.” Every handler that had been silently dropping bad inputs got converted: 22 window handlers, 13 GC handlers, drawable validators on CopyArea / GetGeometry / ClearArea / PolyText, atom and font and pixmap and cursor validation on top. Most real clients handle XErrors gracefully — what they don’t handle is being lied to. About forty silent-drop bugs surfaced and closed.
Day 11 — 2026-05-15
Ran a comparison study against XQuartz and Xvfb on the same captured corpus. Spec-honesty sweep on colormap opcodes, DestroyWindow, CirculateWindow. ChangeWindowAttributes now honors and round-trips all 13 CW bits. QueryTextExtents fix turned out to be the unlock for Motif menu-title spacing on quickplot. Audit of recent “good enough” shortcuts caught four real bugs.
Day 12 — 2026-05-16
Visual day. Disabled anti-aliasing and interpolation on rect fills, then
baked it into the withClip helper for every non-text draw. Rectangles
were soft-edging at scale 3 because Core Graphics was happily resampling
them; the fix is one line per call site but I’d been seeing the ghosted
edges for a week before I traced it.
Day 13 — 2026-05-17
Pixmap rendering. Built a PixelBuffer plus a DrawTarget that gives
the same drawing surface whether the destination is a window or an
off-screen pixmap. CopyArea now works across all five spec variants
(window→window, window→pixmap, pixmap→window, pixmap→pixmap, same
drawable). QueryFont got a charset-aware reply with proper CHARSET
FONTPROPS. Rebased the gold capture set onto SS2 + X.org R6 + MWM
(no CDE), which is closer to what most readers will recognize than the
dt-app-heavy Ultra 5 captures.
Day 14 — 2026-05-18
Retired the CDE customization-daemon impersonation. It had served its
purpose on Day 6, but it was a lie I couldn’t keep paying for. Instead,
I established the MOTIF_TEXT_QUALITY invariant: a single source of
truth for glyph advances, so the metrics we report to the client and
the metrics we render with are identical by construction. Tier 1
RESOURCE_MANAGER landed: curated Motif widget-class font defaults
shipped in the binary. The XmText caret now renders correctly because
pixmaps allocate at the window’s device scale.
Day 15 — 2026-05-19
Honored the X server bg-paint contract end to end. Athena and Motif
widgets count on the server to paint window backgrounds — they set
background_pixel and trust us. We weren’t, in a few specific cases
(clipping, paint-on-grow, GC bg default), and “white where the bg
should be” was the symptom. dtcalc LCD rendering fixed by translating
clip rects by the widget’s window offset. 542 tests green by end of day.
Day 16 — 2026-05-20
Ah-ha day. Motif menu clicks were silently dropping and I couldn’t see why. The unlock: ICCCM 4.1.5 says that when a rootless server acts as the window manager, it has to emit a synthetic ConfigureNotify after MapNotify and after any move. Motif caches each widget’s root coordinates at realization and only invalidates that cache on a synthetic event. Without it, the click coordinates the toolkit compares against are stale by exactly the frame offset, and every menu click misses. One protocol detail, weeks of latent bugs gone.
Day 17 — 2026-05-21
PutImage with format=Bitmap (1bpp source rendered into the destination via GC foreground/background). Motif submenus unblocked after four event-emission fixes plus drag-mode submenu transitions matching Sun’s boundary-crossing semantics. TranslateCoordinates now folds the top-level’s root position into the reply. ConfigureWindow on top-levels pushes through to the actual NSWindow frame.
Day 18 — 2026-05-22
x11perf -all clean sweep: 254 of 254 tests pass against the server.
Same day, 69 new error-path tests caught six silent-drop bugs that the
happy-path coverage was missing. Theme pass on the CDE dt-apps: dtcalc,
dtterm, dthelpview, dtpad all looking right. Per-app dialog chrome
rules in place.
Day 19 — 2026-05-23
Multi-session lookup-registry fix: invisible menu text in the second
running app was a bridge state collision between sessions. Themes
Phase 1: user-editable resource file in ~/, with a Cocoa editor
window (line-number gutter and a dark code-editor theme). Font
substitution table also promoted to a user-editable file. Then the
capture v2 push: extracted a CaptureSink protocol, added a
SessionCapture path with a Listener byte tee, ListFontsWithInfo (op 50),
and the first six steps of the SwiftUI capture app: Record / Open / Replay.
Day 20 — 2026-05-24
Capture v2 done: one binary that routes between CLI and GUI. README
refreshed. Xcode workspace pipeline (xcodegen). Symmetric rename to
MacXServer / MacXCapture. Optional Motif window-frame for X
top-levels: MotifFrameView, opt-in via Preferences. Three subagent-
driven fixes for dtpad theming, resize-uncover Expose, and an idle-loop
diagnosis.
Day 21 — 2026-05-25
The resize-cascade marathon. The original day’s expose-on-resize
extension regressed dtpad badly, so I reverted it and worked the
problem from scratch. Took most of the day, but the result was a clean
“delta cascade”: when a window’s clipList changes, paint background and
emit Expose over only the new clipList, walk siblings whose clipList
grew, and walk parent chain for the right effective background when a
widget has none. Quickplot at 100% by evening. Three resize bugs gone.
Also ported the rest of miregion (inverse, reset, rects, append,
normalize) straight from the X11R6 source instead of re-deriving.
Day 22 — 2026-05-26
Pivoted off the Dropbox-shared bare git repo. Dropbox was conflict-copying
LMDB lock files in .build/ and silently corrupting pack files in the
shared bare repo. Cut over to a private GitHub remote and moved the
working tree out of ~/Library/CloudStorage/ to a local path on both
Macs. Same day: a y-flipped CopyArea fix that gave the horizontal
scrollbar thumb its proper shadow, and a GC dash-state leak across
draws.
Day 23 — 2026-05-27
Y-flip primer doc + an asymmetric-source orientation test that every new graphics op now has to pass. Closed four verified-fixed bugs and three SHORTCUTS entries. Architectural: moved root-window properties up to the ServerCoordinator so they’re server-global rather than per-session. That unblocked Motif clipboard cross-session copy/paste. Shipped a remote app launcher: one-click X-app launch from a vintage Sun over telnet with passwords in Keychain. Motif frame chrome configurable from the resource file.
Day 24 — 2026-05-28
SHAPE extension (major opcode 128). oclock renders round, xeyes
renders as a bare oval (it reshapes its top-level via Xmu + a 1-bit
pixmap mask), and the Motif frame integrates with SetFrameShape so
shaped clients get appropriate frame chrome. Negotiated TERM=xterm
over telnet on the launcher so the remote shell’s prompt setup runs.
Day 25 — 2026-05-29
Capture viewer windows with syntax-highlighted decoded chrono output.
Extracted SwiftXCaptureUI as a shared library so the capture app and
the server’s debug viewer use the same widget. Redesigned the Record
screen as a stacked 6-step wizard. Distinct red XTAP icon for the
capture app vs the blue X icon for the server. Wrote the macXcapture
mission doc plus the phased decoder coverage plan.
Day 26 — 2026-05-30
Decoder coverage push. Phase 1 of the framer closed 16 requests, 6
replies, and 5 events. Built an extension dumper registry. Added
BIG-REQUESTS, MIT-SHM, XKB, XInput v1, and RENDER decoders — the last
three are large surfaces and took three sessions each. Capture v2 picked
up a request/reply pairing glyph and field-level semantic diff. Also
shipped the inline narrative landmark detector: # story-form callouts
in the capture, with viewer-side navigation (sidebar + Cmd-] / Cmd-[).
Day 27 — 2026-05-31
Capture decode polish day. Keysyms and modifiers now show symbolic
names instead of raw codes. Type-aware decode for ICCCM WM_* and
_MOTIF_* properties, ClientMessage payloads, and a type-driven
fallback for unknown properties. XC-MISC, XTEST, and RECORD decoders.
Session-wide resource registry with lineage and leak annotations
(promoted into live XError landmarks the same day). Then a server-side
afternoon: PutImage ZPixmap depth=1 + depth=8, CopyGC (op 57),
GetKeyboardControl (op 103), four bug closures from the audit.
Day 28 — 2026-06-01
Curated the captures corpus for the open-source launch, with a two-pass
fact-check. SHAPE bounding-and-clip shapes now apply on descendant
windows via clipList. Shipped a --scale {2,3} CLI flag with
auto-scaled Motif chrome and integer-point bevel snapping. WM_NAME
fallback for capture identification when no WM_CLASS is present, plus a
batch rename of eight previously-unidentified captures by wire
fingerprint.
Day 29 — 2026-06-02
Display Size radio in Preferences (Auto / Comfortable / Compact) wires
through to the --scale flag. Wrote the device-coords refactor plan:
internal regions had been carrying user-coord values that were getting
re-scaled at every draw site, which was the underlying cause of a
recurring class of resize bugs.
Day 30 — 2026-06-03
Device-coords refactor end to end in six phases on one day. ClipListEngine
and the SHAPE extension converted to device-coord regions, the drawing
handler did a unit sweep, and the dual-representation patches from the
prior day were deleted clean. Closing fixes: server-global pointer
cache plus Motif-frame mouseMoved so xeyes tracks the cursor across
sessions, and per-app dialog enumeration replaced with a composite
chrome-thinning rule + borderWidth.
Threads that spanned multiple days
A few pieces of the project were arcs, not single-day landings. Worth calling out because the daily entries only show fragments.
xterm text quality (Days 3–5). The cell-fits-font story. Day 3 built
the XLFD parser and the font resolver. Day 4 tried fitting Monaco into
xterm’s requested cell at fractional pointSize and rendered correctly
but read as too bold. Day 5 flipped it: pick the integer pointSize that
fits, instantiate Monaco at that size, and report Monaco’s actual cell
back to the client. Cell follows font. Reported metrics equal rendered
metrics by construction. This is the principle that lets xterm sit
comfortably next to an iTerm2 window, which was the bar for the project.
The same invariant generalized to proportional fonts on Day 11
(QueryTextExtents) and got hardened into the MOTIF_TEXT_QUALITY
contract on Day 14 (per-character characterWidth, two-sided enforcement
of reporting versus rendering).
Region tracking and bg-paint (Days 9–21). The region work landed
incrementally. Day 9 built the algebra and wired clipList and
borderClip into every window entry. Day 15 honored the X server’s
bg-paint contract end-to-end. Day 16 added the ICCCM 4.1.5 synthetic
ConfigureNotify that Motif relies on. Day 17 routed ConfigureWindow
through to the actual NSWindow frame. Day 21 was the resize-cascade
marathon that produced the delta-cascade rule (sibling whose clipList
grew gets bg-paint plus Expose), which closed the last of the visible
resize bugs across dtpad and quickplot.
Pixmap rendering and graphics y-flip (Days 13, 22–23). Day 13 built
the PixelBuffer plus DrawTarget foundation and made CopyArea work
across all five spec variants. The y-flip gotchas surfaced on Days 22
and 23 (the horizontal scrollbar thumb shadow, then the pixmap-writer
needing a counter-flip). Day 23 wrote the y-flip primer doc plus an
asymmetric-source orientation test that every new graphics op now has
to pass.
Capture v1 → v2 (Days 1–2, 19–20, 25–27). Day 1 was the framer plus the CLI proxy. Day 2 was replay and the corpus round-trip. Day 19 was the v2 architecture push (CaptureSink protocol, SessionCapture, Listener byte tee, and the SwiftUI app skeleton). Day 20 unified it into one binary. Days 25–27 were the decoder coverage push: SwiftXCaptureUI as a shared library, the 6-step Record wizard, framer Phase 1, the extension dumper registry, and the type-aware decoders for keysyms / modifiers / ICCCM / Motif / RENDER / XKB / XInput.
Where it landed on Day 30
Working server with display-adaptive integer scaling, anti-aliased font
rendering through Core Text, ICCCM 4.1.5 WM emulation, the SHAPE
extension, optional Motif window frames, multi-client and multi-session,
remote app launching from vintage Suns, server-side and client-side
session capture with a decoded chrono viewer, and x11perf -all clean.
Live X clients running so far: xterm, xcalc, xeyes, xclock, oclock,
quickplot, dtcalc, dtterm, dthelpview, dtpad, dticon, and a long tail of
smaller tools.
What’s still rough is its own list. The challenge was whether 30 days was enough to get a real X server I could use day to day. The answer turned out to be yes.
Total time: roughly 205 hours over 28 working days. About five and a half standard workweeks of focused effort, spread across the calendar month.