Schnellstart
Erstellen Sie eine native macOS-App, die außerhalb des zenit-Repos liegt und eigenständig baut und läuft.
Voraussetzungen
Diese Version zielt auf macOS, Apple Silicon wird laufend verifiziert. Stellen Sie sicher, dass Zig 0.15.2 und die Xcode Command Line Tools installiert sind; Bun wird nur für End-to-End-Tests benötigt.
zig version
xcode-select -pProjekt anlegen
Starten Sie mit templates/minimal-app im Repo. Es ist ein vollständiges Downstream-Projekt:
minimal-app/
├── README.md
├── build.zig # zenit.attach() + run / app steps
├── build.zig.zon # declares the zenit dependency
├── e2e/
│ └── record-demo.ts # drives and records the app through the Harness
└── src/
└── main.zig # counter-button app- 1Template kopieren
Kopieren Sie templates/minimal-app in ein neues Verzeichnis außerhalb des zenit-Repos.
- 2Abhängigkeitspfad anpassen
Das Template verwendet standardmäßig
.path = "../../", was nur innerhalb des Repos auflöst. Richten Sie es nach dem Kopieren relativ zu build.zig.zon auf Ihren zenit-Checkout aus oder pinnen Sie eine veröffentlichte Revision mitzig fetch --save=zenit <url>. - 3Fingerprint erzeugen
Übernehmen Sie nicht den .fingerprint des Templates – zwei Projekte mit demselben Wert kollidieren im Paket-Cache von Zig. Löschen Sie die Zeile, führen Sie zig build einmal aus und fügen Sie den Wert aus der Fehlermeldung ein. Benennen Sie dabei auch .name = .myapp um.
git clone https://github.com/version-next/zenit.git
cp -R zenit/templates/minimal-app ./myapp
cd myapp # then set .zenit = .{ .path = "../zenit" } in build.zig.zon
zig buildzenit einbinden
build.zig.zon deklariert die Abhängigkeit; build.zig erhält über @import("zenit") die Build-API von zenit.
.{
.name = .myapp,
.version = "0.1.0",
.fingerprint = 0x8798022a7f8e220e, // replace: delete this line, run zig build once
.minimum_zig_version = "0.15.2",
.dependencies = .{
// Relative to this file. Absolute paths are rejected by Zig 0.15.2.
.zenit = .{ .path = "../zenit" },
},
.paths = .{
"README.md",
"build.zig",
"build.zig.zon",
"e2e",
"src",
},
}Hier ist das build.zig des Templates im Wortlaut. Es ist mehr als ein attach-Aufruf: Es leitet die Test-Schalter weiter, installiert den Harness-Client und definiert den run-Step und das .app-Packaging:
const std = @import("std");
const zenit = @import("zenit");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const test_mode = b.option(bool, "test-mode", "Enable the Zenit automation harness") orelse false;
const e2e_port = b.option(u16, "e2e-port", "Zenit Harness RPC directory suffix") orelse 19816;
const zenit_dep = b.dependency("zenit", .{
.target = target,
.optimize = optimize,
// Dependency build options are isolated in Zig. Forward these
// explicitly so `zig build -Dtest-mode=true` reaches Zenit.
.@"test-mode" = test_mode,
.@"e2e-port" = e2e_port,
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
zenit.attach(zenit_dep, exe);
zenit.installHarnessClient(b, zenit_dep);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// Optional: `zig build app` packages a double-clickable .app bundle.
if (target.result.os.tag == .macos) {
const bundled = zenit.bundleApp(b, .{
.exe = exe,
.display_name = "My App",
.bundle_id = "com.example.myapp",
.version = "0.1.0",
.signing = .ad_hoc,
});
const app_step = b.step("app", "Build the .app bundle");
app_step.dependOn(bundled.final_step);
}
}zenit.attach(zenit_dep, exe)Fügt die Modul-Imports ui und zenit_app hinzu, kompiliert die fünf macOS-ObjC-Bridges und linkt Cocoa, Metal, CoreText und die übrigen Frameworks.@"test-mode" / .@"e2e-port"Abhängigkeitsoptionen sind in Zig isoliert; leiten Sie sie explizit weiter, damit zig build -Dtest-mode=true zenit erreichtzenit.installHarnessClient(b, zenit_dep)Installiert den typisierten Harness-Client nach zig-out/share/zenit/harness/client.ts für e2e/record-demo.tszenit.bundleApp(b, .{ ... })Erzeugt ein per Doppelklick startbares zig-out/<display_name>.app, hier ad-hoc signiertDen ersten UI-Baum mounten
App.runWith erzeugt den Root-Scope, ruft Ihre Mount-Funktion einmal auf und übernimmt die Event-Loop. Die Mount-Funktion muss die Signatur fn (cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node haben; Ihre App gibt nur den Wurzelknoten zurück.
const std = @import("std");
const ui = @import("ui");
const App = @import("zenit_app").App;
const Counter = struct {
n: u32 = 0,
label: ?*ui.Node = null,
buf: [32]u8 = undefined,
pub fn increment(self: *Counter) void {
self.n += 1;
const node = self.label orelse return;
const content = std.fmt.bufPrint(&self.buf, "Clicked {d} times", .{self.n}) catch return;
if (node.getText()) |old| {
var t = old;
t.content = content;
node.setText(t);
}
node.markRenderDirty();
}
};
fn mountUi(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
const allocator = cx.allocator;
const counter = try cx.bindState(Counter, .{});
const click = cx.on(Counter, counter, Counter.increment);
const root = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
.direction = .column,
.gap = 16,
.padding = ui.Padding.all(40),
.background = cx.tokens.color.bg_primary,
.align_items = .center,
.justify = .center,
}, .{});
try root.appendChild(allocator, try ui.text(cx, "Hello, zenit!", .{
.font_size = 24,
.font_weight = 600,
.color = cx.tokens.color.fg_primary,
}));
const button = try ui.widgets.Button(.{
.label = "Click me",
.variant = .primary,
.on_click = click,
}).mount(scope, cx);
button.meta.ownership.meta.test_id = "counter.increment";
try root.appendChild(allocator, button);
const label = try ui.text(cx, "Clicked 0 times", .{
.font_size = 14,
.color = cx.tokens.color.fg_secondary,
});
try root.appendChild(allocator, label);
counter.label = label;
return root;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const app = try App.init(gpa.allocator(), .{
.window = .{ .width = 640, .height = 480, .title = "Hello, zenit" },
});
defer app.deinit();
try app.runWith(mountUi);
}cx.bindState übergibt Counter an den Cx, und cx.on macht eine seiner Methoden zum Button-Callback; mit test_id finden E2E-Skripte den Button über seine Bedeutung.
Bauen und ausführen
zig build run # build and run the unbundled executable
zig build app # package zig-out/My App.app (macOS)Das Template enthält auch ein Aufnahmeskript: Bauen Sie mit zig build -Dtest-mode=true, starten Sie die App und führen Sie dann e2e/record-demo.ts aus, um einen ähnlichen Clip zu erhalten – siehe E2E harness.