compile-time import
Treat this like a compile-time module dependency plus a local const binding, not dlopen or a preprocessor include.
Zig difference: @import returns a module namespace; std is an immutable declaration inferred from that value.
Okay, before we talk about terminals, windows, or GPUs, let’s make sure we can build and run one program. We want the operating system to start our process, enter main, and print one line. That’s all.
chapter-00-bootstrap$ zig build run
ghostty-from-scratch: helloCaptured from the tagged reconstruction with Zig 0.16.0 on GitHub Actions Ubuntu. A decorative screenshot would add no information yet, so the exact output is the visual proof.
You may be thinking, “Where is the terminal emulator?” We haven’t built it yet. There is no shell, PTY, parser, window, font system, or GPU code. First we are checking that our toolchain and entrypoint work.
src/main.zig → executable bytesmain() runsprocess entry → print callstderr receivesUTF-8 bytes → visible lineNo terminal emulator exists inside this program yet. Your existing terminal only displays the bytes written by the new process.
Let’s draw a line around today’s code. On our side of the line is one process and one print call. Everything else stays on the other side until we actually need it.
DEPENDENCY FRONTIER
Do not solve future problems early. Chapter 01 introduces an App lifecycle only after one function has become an obvious ownership bottleneck.
ghostty-from-scratch/├── build.zig├── build.zig.zon└── src/ └── main.zigThe checkpoint is the annotated tag:
chapter-00-bootstrapThe public chapter is pinned to reconstruction commit:
00b751fbfd354a645a178542ba47bafb64e7b6dfsrc/main.zig firstA minimal process bootstrap. Chapter 02 replaces it with Ghostty-shaped entrypoint routing.
const std = @import("std");
pub fn main() !void {
std.debug.print("ghostty-from-scratch: hello\n", .{});
}This whole program is only a few lines, so let’s read it from top to bottom.
Start with the C model, then inspect each highlighted Zig line.
Open in the cumulative Zig/C referenceTreat this like a compile-time module dependency plus a local const binding, not dlopen or a preprocessor include.
Zig difference: @import returns a module namespace; std is an immutable declaration inferred from that value.
Text version:
@importbinds a compile-time module namespace.pub fn main() !voidexposes a function that either succeeds without a value or returns an error..{}is the empty formatting tuple. The call frame owns no heap allocation; static string bytes are formatted to stderr.
const std = @import("std");@import makes another Zig module available at compile time. std is only a local name. No runtime file loading occurs here.
pub fn main() !void {pub makes the declaration visible as the executable entrypoint.fn declares a function.main is the entry function Zig uses for this executable.!void means the function returns no value when successful but may return an error.We do not need the error capability yet, but retaining it gives later startup operations a natural way to fail.
std.debug.print("ghostty-from-scratch: hello\n", .{});std.debug.print formats bytes and writes them to standard error. The empty tuple, .{}, means there are no formatting arguments.
The newline is one byte:
\n → 0A hexadecimal → line feedOne easy thing to mix up: our tiny program only writes bytes. The terminal app you are already using reads those bytes and draws the letters on screen.
build.zig—think Makefile, but written in ZigIt uses Ghostty's current minimum Zig version and modern Build API, but omits every production artifact and dependency beyond one executable.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "ghostty-from-scratch",
.root_module = root_module,
});
b.installArtifact(exe);
const run = b.addRunArtifact(exe);
run.step.dependOn(b.getInstallStep());
if (b.args) |args| run.addArgs(args);
const run_step = b.step("run", "Run ghostty-from-scratch");
run_step.dependOn(&run.step);
const tests = b.addTest(.{ .root_module = root_module });
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run the Zig tests");
test_step.dependOn(&run_tests.step);
}If you come from C, treat this like a typed Makefile. It says which source file starts the program, what executable to create, and which run and test commands should depend on it:
standard target + optimization ↓ root Zig module ↓ executable ↙ ↘ install run ↓ zig build runThe test step exists even though no tests have been introduced. It establishes the command shape later pure modules will use.
From the reconstruction repository:
cd ~/ghostty-from-scratchzig build runExpected output:
ghostty-from-scratch: helloIf Zig is missing or not version 0.16.0, fix the toolchain first. Do not interpret a toolchain error as a terminal-emulator lesson.
For this chapter:
| File | Status | Why |
|---|---|---|
src/main.zig |
temporary | Simpler than any production Ghostty entrypoint; removed after entry routing is reconstructed |
build.zig |
adapted | Current Zig Build API, but only one artifact and no generated configuration |
build.zig.zon |
adapted | Correct package metadata and Zig version, without Ghostty’s production dependency set |
Nothing is labeled exact yet.
Ghostty’s first main function initialized GLFW and registered cleanup. It did not create a window yet.
b8cee0aOpen historical sourceWe stop one step earlier: compile, enter main, and make one line observable before adding a runtime.
chapter-00-bootstrapCurrent main selects a generated entrypoint; main_ghostty owns global initialization, App, runtime, and the event loop.
6ad1fe7Open current sourceGhostty’s earliest repository commit did not yet contain src/main.zig. A few commits later, the first main function initialized GLFW:
const std = @import("std");const c = @import("glfw/c.zig");
pub fn main() !void { if (c.glfwInit() != c.GLFW_TRUE) return error.GlfwInitFailed; defer c.glfwTerminate();}Historical source:
The historical project moved toward a native window very quickly. Our reconstruction starts one step earlier so the process/build boundary is explicit before adding a windowing dependency.
Current Ghostty’s main.zig is mostly an entrypoint router selected by generated build configuration.
src/main.zig:1–28Read only the entrypoint switch and exported main. Ignore generated configuration details until Chapter 02.Then main_ghostty.zig initializes global state, creates the shared App, creates the selected runtime, and enters the GUI event loop.
src/main_ghostty.zig:80–112Preview the nouns only: App, runtime App, startQuitTimer, and run. Later chapters earn each one.The production code is not “better Hello World.” It solves many problems we have not encountered yet. We will reconstruct those responsibilities when each becomes necessary.
After you can explain the program and build graph, make your own commit. The reference checkpoint is:
git tag chapter-00-bootstrapYour commit message can use your own words. The tag matters because future website snippets and visuals need a stable implementation revision.
main currently owns the entire lifetime of the program. As soon as startup, state, and cleanup grow, one function becomes a poor owner.
Chapter 01 will introduce the smallest App lifecycle:
main→ App.create→ App.run→ App.destroyIt will remain headless. No PTY, parser, or window will be introduced yet.