Démarrage rapide
Créez une app macOS native qui vit hors du dépôt zenit et se compile et s'exécute de façon autonome.
Prérequis
Cette version cible macOS, avec Apple Silicon vérifié en continu. Assurez-vous d'avoir Zig 0.15.2 et les Xcode Command Line Tools ; Bun n'est nécessaire que pour les tests de bout en bout.
zig version
xcode-select -pCréer un projet
Partez de templates/minimal-app dans le dépôt. C'est un projet downstream complet :
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- 1Copier le template
Copiez templates/minimal-app dans un nouveau répertoire hors du dépôt zenit.
- 2Corriger le chemin de dépendance
Le template utilise par défaut
.path = "../../", qui ne se résout qu'à l'intérieur du dépôt. Après la copie, pointez-le vers votre checkout de zenit relativement à build.zig.zon, ou épinglez une révision publiée aveczig fetch --save=zenit <url>. - 3Générer une empreinte
Ne gardez pas le .fingerprint du template : deux projets qui le partagent entrent en collision dans le cache de paquets de Zig. Supprimez la ligne, lancez zig build une fois et recollez la valeur indiquée par l'erreur. Renommez .name = .myapp au passage.
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 buildBrancher zenit
build.zig.zon déclare la dépendance ; build.zig obtient l'API de build de zenit via @import("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",
},
}Voici le build.zig du template tel quel. Il fait plus qu'un appel à attach : il transmet les options de test, installe le client Harness et définit l'étape run et le packaging .app :
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)Ajoute les imports de modules ui et zenit_app, compile les cinq ponts ObjC macOS et lie Cocoa, Metal, CoreText et les autres frameworks.@"test-mode" / .@"e2e-port"Les options de dépendance sont isolées en Zig ; transmettez-les explicitement pour que zig build -Dtest-mode=true atteigne zenitzenit.installHarnessClient(b, zenit_dep)Installe le client Harness typé dans zig-out/share/zenit/harness/client.ts pour e2e/record-demo.tszenit.bundleApp(b, .{ ... })Produit un zig-out/<display_name>.app lançable d'un double-clic, ici signé ad hocMonter votre premier arbre UI
App.runWith crée le Scope racine, appelle une fois votre fonction de montage et prend la main sur la boucle d'événements. La fonction de montage doit avoir la signature fn (cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node ; votre app se contente de renvoyer le nœud racine.
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 confie Counter au Cx, et cx.on transforme l'une de ses méthodes en callback du bouton ; test_id permet aux scripts E2E de trouver le bouton par son rôle.
Compiler et exécuter
zig build run # build and run the unbundled executable
zig build app # package zig-out/My App.app (macOS)Le template fournit aussi un script d'enregistrement : compilez avec zig build -Dtest-mode=true, lancez l'app, puis exécutez e2e/record-demo.ts pour obtenir le même type de clip — voir E2E harness.