GitHub - leebase/lfznotepad

6 min read Original article ↗

A faithful, native Windows Notepad clone built in Zig using raw Win32 APIs — no .NET runtime, no GUI framework, no installer. Single .exe, zero dependencies.

Why Zig? To demonstrate that agentic AI absorbs implementation complexity. The binary is ~1.1 MB vs 154 MB for the C# WinForms sibling project (LFNotepad). The human effort to direct both was identical.


Features

Matches classic Windows Notepad (pre-AI era) feature-for-feature:

  • File: New · Open · Save · Save As (with encoding selection) · Page Setup · Print · Exit
  • Edit: Undo · Cut · Copy · Paste · Delete · Find · Find Next · Replace · Go To · Select All · Time/Date
  • Format: Word Wrap · Font
  • View: Zoom In / Out / Restore · Status Bar toggle
  • Status Bar: Ln/Col · Zoom % · Encoding · Line Endings
  • Behavior: Dirty tracking (title asterisk) · Save-changes dialog · Drag-and-drop file open · Command-line file argument · Right-click context menu

Requirements

Requirement Detail
OS Windows 10 or later (x86_64)
Zig 0.15.2 (tested)
Dependencies None at runtime — single .exe
Build deps zigwin32 (fetched automatically via zig fetch)

Quick Start

# Clone
git clone <repo-url> lfznotepad
cd lfznotepad

# Fetch zigwin32 dependency (already recorded in build.zig.zon)
zig fetch

# Build
zig build

# Run
zig build run
# or directly:
.\zig-out\bin\LFZNotepad.exe

The output binary is at zig-out\bin\LFZNotepad.exe (~1.1 MB, no installer needed).


Build Reference

zig build            # Compile only
zig build run        # Compile + launch
zig build test       # Run unit tests

Capturing build errors (recommended — PowerShell terminal can truncate stderr):

zig build 2>&1 | Set-Content -Encoding UTF8 build_errors.txt
# then open build_errors.txt

Project Structure

lfznotepad/
├── build.zig              # Zig 0.15 build script
├── build.zig.zon          # Package manifest (zigwin32 dependency + hash)
├── src/
│   ├── main.zig           # Entry point — GetModuleHandle, load riched20.dll, init comctl32
│   ├── window.zig         # Main window class, WndProc, AppState, layout
│   ├── menu.zig           # Full Win32 menu bar (CreateMenu/AppendMenuW)
│   ├── menu_ids.zig       # All WM_COMMAND IDs as a Zig enum (no magic numbers)
│   ├── statusbar.zig      # comctl32 status bar — 4 parts: Ln/Col, Zoom%, Encoding, CRLF
│   ├── helpers/
│   │   ├── file.zig       # File I/O, BOM-based encoding detection, UTF-8/16 conversion
│   │   ├── edit.zig       # Edit menu helpers (clipboard, select-all, time/date insert)
│   │   ├── format.zig     # Word wrap toggle, font dialog (ChooseFontW), zoom (EM_SETZOOM)
│   │   └── print.zig      # Win32 printing (PrintDlgExW, GDI pagination)
│   └── dialogs/
│       ├── find.zig       # Modeless Find dialog (CreateDialogParamW)
│       ├── replace.zig    # Modeless Replace dialog
│       ├── goto.zig       # Modal Go To Line dialog (DialogBoxParamW)
│       ├── opensave.zig   # Open/Save As file dialogs (GetOpenFileNameW / GetSaveFileNameW)
│       └── about.zig      # About dialog
└── tests/
    └── file_tests.zig     # Encoding detection and file round-trip tests

Key Win32 / Zig Conventions

These are critical to know before modifying the code:

Rule Detail
Wide APIs only Never use ANSI (*A) Win32 functions. Always use *W variants.
Load riched20.dll first LoadLibraryW("riched20.dll") must be called before creating any RichEdit20W window.
No magic numbers All WM_COMMAND IDs come from the MenuCmd enum in menu_ids.zig.
zigwin32 flags are packed structs Use .{ .STRING = 1 } not raw u32. E.g. MENU_ITEM_FLAGS{ .STRING = 1 }, MESSAGEBOX_STYLE{ .ICONHAND = 1 }.
Module name zigwin32 exports as "win32" — use dep.module("win32"), not "zigwin32".
Callconv WndProc uses callconv(.winapi) (lowercase, Zig 0.15).
String literals All Win32 strings are UTF-16: std.unicode.utf8ToUtf16LeStringLiteral("text").
AppState Single module-level var g: AppState in window.zig — no global scattered state.

AgentFlow Documentation

This project uses the AgentFlow methodology for human-AI collaboration. These files support autonomous AI execution and cross-session/cross-agent continuity:

File Purpose
AGENTS.md Read first. Rules of engagement, tech stack, Win32 conventions, guardrails, autonomy mode.
context.md Current session state. Sprint status, decisions locked, next actions. Updated each session.
WHERE_AM_I.md Milestone tracker — product goals vs completion.
product-definition.md Full feature list (P0/P1), non-goals, success criteria.
design.md Win32 architecture, AppState struct, encoding strategy, Win32↔WinForms mapping.
sprint-plan.md 5 sprints with Win32 concepts, task checklists, definitions of done.

AgentFlow Development Loop

CODE → BUILD (zig build) → TEST → TEST AS LEE (verify .exe runs) → FIX → LOOP → DOCUMENT → COMMIT

Sprint end-gate (required before moving to next sprint):

  1. zig build test passes
  2. Manual verify: launch .exe, exercise all new features
  3. Update context.md and WHERE_AM_I.md
  4. git commit

Continuing Work in Another Agent Harness

Orientation (do this first)

1. Read AGENTS.md          — rules and conventions
2. Read context.md         — current state, what's done, what's next
3. Read WHERE_AM_I.md      — milestone view
4. Read sprint-plan.md     — sprint tasks and definitions of done

Getting the build working

zig build 2>&1 | Set-Content -Encoding UTF8 build_errors.txt
# Read build_errors.txt for full error output (do NOT rely on terminal — it truncates)

Autonomy level

Mode 3 — Autonomous. Build, test, commit without asking for approval.
Only interrupt the human for: breaking design decisions, ambiguous requirements, blocked on user input.

Known gotchas for new agents

  1. Terminal truncation: PowerShell stderr output is cut off. Always redirect to file.
  2. zigwin32 packed structs: All flag types are packed struct(u32). Use struct literal syntax, never cast raw integers. Check field names in the zigwin32 source at %LOCALAPPDATA%\zig\p\zigwin32-*\win32\.
  3. EM_SETEVENTMASK: Must be sent to the RichEdit control after creation to receive EN_SELCHANGE via WM_NOTIFY.
  4. Modeless dialogs (Find/Replace): Must be given the RichEdit HWND, not the main window HWND, for text operations.
  5. Word Wrap: Implemented via EM_SETTARGETDEVICE on the RichEdit, not by changing window styles directly.

Current Status

Attribute Value
Version v1.0.0
Phase All sprints complete — UAT stabilization
Binary zig-out\bin\LFZNotepad.exe (~1.1 MB)
Pending Full regression pass (see context.md)

Session History

This project was built across multiple AI agent sessions on different platforms. All features were verified and tested by the human (Lee).

Session Agent Model Contribution
1 Antigravity Gemini (Google DeepMind) Project scaffolding, AgentFlow docs, Sprint 1 start
2 Kimi Code Kimi K2.5 Sprint 1 completion, Sprints 2–5
3 OpenCode GPT-5.3 Codex UAT fixes, accelerator wiring, crash fixes

Raw session notes: antigravity_session_raw.md · kimi_session_raw.md · opencode_session_raw.md


Relationship to LFNotepad (C# version)

LFNotepad LFZNotepad
Language C# Zig
Framework WinForms Raw Win32
Binary size ~154 MB ~1.1 MB
Runtime deps .NET runtime None
Features Same (P0/P1) Same (P0/P1)
Human effort Same Same

Both projects are part of the same case study demonstrating supervisor-mode agentic AI development.