Skip to content

Chapter 00: A process exists

Let’s start with the smallest thing that can work

Section titled “Let’s start with the smallest thing that can work”

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.

VISIBLE RESULTThe first observable result
chapter-00-bootstrap
$ zig build run
ghostty-from-scratch: hello

Captured 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.

VISIBLE PIPELINEOne source file becomes one visible line
  1. 01Zig buildssrc/main.zig → executable bytes
  2. 02The OS startsexecutable bytes → running process
  3. 03main() runsprocess entry → print call
  4. 04stderr receivesUTF-8 bytes → visible line

No terminal emulator exists inside this program yet. Your existing terminal only displays the bytes written by the new process.

What are we deliberately not building yet?

Section titled “What are we deliberately not building yet?”

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

Build only what the current result needs

  1. 00Process entryYou are here
  2. 01App lifecycleNext limitation
  3. 02Runtime + SurfaceNot introduced
  4. 03Child process + PTYNot introduced
  5. 04I/O + parserNot introduced
  6. 05Terminal stateNot introduced
  7. 06Window + GPU + fontsNot introduced

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.zig

The checkpoint is the annotated tag:

chapter-00-bootstrap

The public chapter is pinned to reconstruction commit:

00b751fbfd354a645a178542ba47bafb64e7b6df
temporarysrc/main.zig

A 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.

ZIG SYNTAX BRIDGE

Read the first Zig function from C

Start with the C model, then inspect each highlighted Zig line.

Open in the cumulative Zig/C reference
C MENTAL MODEL

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.

MEMORY / LIFETIME FLOW
  1. executable imageformat string bytes
  2. main call frameno heap allocation
  3. stderrformatted bytes + LF

Text version: @import binds a compile-time module namespace. pub fn main() !void exposes 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 feed

One 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.

Now open build.zig—think Makefile, but written in Zig

Section titled “Now open build.zig—think Makefile, but written in Zig”
adaptedbuild.zig

It 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 run

The test step exists even though no tests have been introduced. It establishes the command shape later pure modules will use.

From the reconstruction repository:

Terminal window
cd ~/ghostty-from-scratch
zig build run

Expected output:

ghostty-from-scratch: hello

If 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.

SOURCE ARCHAEOLOGYThen, reconstruction, and now solve different-sized problems
  1. 01
    THEN · 2022-03-29Initialize a runtime

    Ghostty’s first main function initialized GLFW and registered cleanup. It did not create a window yet.

    b8cee0aOpen historical source
  2. 02
    RECONSTRUCTION · CHAPTER 00Prove the process boundary

    We stop one step earlier: compile, enter main, and make one line observable before adding a runtime.

    chapter-00-bootstrap
  3. 03
    NOW · PINNED MAINRoute into production startup

    Current main selects a generated entrypoint; main_ghostty owns global initialization, App, runtime, and the event loop.

    6ad1fe7Open current source

Ghostty’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.

</>
Current Ghostty entrypoint routingsrc/main.zig:1–28Read only the entrypoint switch and exported main. Ignore generated configuration details until Chapter 02.
Read excerptGitHub

Then main_ghostty.zig initializes global state, creates the shared App, creates the selected runtime, and enters the GUI event loop.

</>
Current App and runtime startupsrc/main_ghostty.zig:80–112Preview the nouns only: App, runtime App, startQuitTimer, and run. Later chapters earn each one.
Read excerptGitHub

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:

Terminal window
git tag chapter-00-bootstrap

Your 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.destroy

It will remain headless. No PTY, parser, or window will be introduced yet.