Skip to content

Commit 3766dd4

Browse files
committed
rework plugin and app settings
1 parent 45a978e commit 3766dd4

70 files changed

Lines changed: 4862 additions & 1184 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/sdk-tag.yml

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,33 @@ name: SDK tag
22

33
# Auto-pins the plugin ABI contract: on every push to main, if src/sdk/sdk_version.zig's
44
# `sdk_version` doesn't already have a matching `sdk-v<major>.<minor>.<patch>` tag, create and
5-
# push one at this commit. This is the pin point third-party plugins' build.zig.zon `fizzy`
6-
# dependency uses (docs/PLUGINS.md §2.3) instead of an arbitrary commit SHA.
5+
# push one at this commit. Then pack the plugin-facing `sdk/` package (plus vendored
6+
# `src/core` + `src/sdk`) into `fizzy-sdk-v*.tar.gz` and publish it as a GitHub release asset
7+
# on that tag — the URL third-party plugins pin (docs/PLUGINS.md §2.3). The git archive of the
8+
# monorepo is *not* a valid plugin pin: its root zon owns Velopack.
79
#
810
# `sdk-v*` is a separate namespace from the app's `v*` release tags (release.yml triggers on
911
# `tags: ["v*"]`, a glob on the ref's start — `sdk-v...` doesn't start with `v`, so this can
10-
# never fire the app build/package pipeline). No build/package step here: just a ref push.
12+
# never fire the app build/package pipeline).
1113

1214
on:
1315
push:
1416
branches: [main]
1517
paths:
1618
- "src/sdk/sdk_version.zig"
1719
- "src/sdk/version.zig"
20+
- "sdk/sdk_version.zig"
21+
- "sdk/**"
22+
- "scripts/pack-sdk.sh"
23+
- ".github/workflows/sdk-tag.yml"
1824
workflow_dispatch:
1925

2026
concurrency:
2127
group: ${{ github.workflow }}
2228
cancel-in-progress: false
2329

2430
jobs:
25-
tag:
31+
tag-and-release:
2632
runs-on: ubuntu-latest
2733
permissions:
2834
contents: write
@@ -32,7 +38,8 @@ jobs:
3238
fetch-depth: 0
3339
fetch-tags: true
3440

35-
- name: Compute sdk_version and tag if missing
41+
- name: Compute sdk_version
42+
id: ver
3643
shell: bash
3744
run: |
3845
set -euo pipefail
@@ -48,15 +55,77 @@ jobs:
4855
fi
4956
5057
tag="sdk-v${major}.${minor}.${patch}"
58+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
59+
echo "version=${major}.${minor}.${patch}" >> "$GITHUB_OUTPUT"
5160
echo "Resolved sdk_version -> $tag"
5261
62+
- name: Create tag if missing
63+
shell: bash
64+
run: |
65+
set -euo pipefail
66+
tag="${{ steps.ver.outputs.tag }}"
67+
5368
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
54-
echo "Tag $tag already exists, nothing to do."
69+
echo "Tag $tag already exists."
5570
exit 0
5671
fi
5772
5873
git config user.name "github-actions[bot]"
5974
git config user.email "github-actions[bot]@users.noreply.github.com"
60-
git tag -a "$tag" -m "SDK $major.$minor.$patch (ABI fingerprint pin point)"
75+
git tag -a "$tag" -m "SDK ${{ steps.ver.outputs.version }} (plugin pin + ABI fingerprint)"
6176
git push origin "$tag"
6277
echo "Pushed $tag"
78+
79+
- name: Pack SDK tarball
80+
shell: bash
81+
run: |
82+
set -euo pipefail
83+
chmod +x scripts/pack-sdk.sh
84+
./scripts/pack-sdk.sh "$PWD/zig-out/sdk"
85+
ls -la zig-out/sdk/
86+
87+
- name: Publish GitHub release asset
88+
env:
89+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90+
shell: bash
91+
run: |
92+
set -euo pipefail
93+
tag="${{ steps.ver.outputs.tag }}"
94+
version="${{ steps.ver.outputs.version }}"
95+
asset="zig-out/sdk/fizzy-sdk-v${version}.tar.gz"
96+
test -f "$asset"
97+
98+
notes_file="$(mktemp)"
99+
cat >"$notes_file" <<EOF
100+
Plugin-facing SDK package for Fizzy \`${version}\`.
101+
102+
Pin in your plugin's \`build.zig.zon\`:
103+
104+
\`\`\`zig
105+
.fizzy = .{
106+
.url = "https://github.com/fizzyedit/fizzy/releases/download/${tag}/fizzy-sdk-v${version}.tar.gz",
107+
.hash = "…", // zig fetch --save=fizzy <url>
108+
},
109+
\`\`\`
110+
111+
Do **not** pin the git archive of this tag — that is the full monorepo (app zon / Velopack).
112+
This asset is the \`sdk/\` package with \`src/core\` + \`src/sdk\` vendored in.
113+
Locally, \`.path = "../fizzy/sdk"\` is fine while developing against an unreleased SDK.
114+
115+
See [docs/PLUGINS.md](https://github.com/fizzyedit/fizzy/blob/main/docs/PLUGINS.md) §2.3.
116+
EOF
117+
# Strip the common leading indent from the YAML-embedded heredoc.
118+
sed -i 's/^ //' "$notes_file"
119+
120+
# Idempotent: create the release if missing, else replace the sdk asset.
121+
if gh release view "$tag" >/dev/null 2>&1; then
122+
echo "Release $tag exists — uploading/replacing asset."
123+
gh release upload "$tag" "$asset" --clobber
124+
else
125+
gh release create "$tag" "$asset" \
126+
--title "Fizzy SDK $version" \
127+
--notes-file "$notes_file"
128+
fi
129+
130+
echo "Published $asset on $tag"
131+
gh release view "$tag" --json assets --jq '.assets[].name'

CLAUDE.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ Cross-platform, open-source general editor written in Zig, UI via [DVUI](https:/
99
Fizzy the app is a near-empty **shell** (window, frame loop, menu/sidebar/panel layout, document model) that owns **no editing features**. Everything the user sees — pixel-art editing, the file explorer/tabs/splits, text editing — is contributed by **plugins** that register against a stable SDK. Plugins never import each other; they meet only at the SDK.
1010

1111
```
12-
Shell (Editor) ←── Host registries + EditorAPI ──→ Plugin (register(host) + vtable)
12+
Shell (Editor) ←── Host registries + EditorAPI ──→ Plugin (register(host) + vtable)
1313
```
1414

1515
- **`src/sdk/`** — the entire contract. `Host` (registries + service locator), `Plugin` (identity + vtable of hooks the shell calls), `DocHandle` (opaque `{ptr, id, owner}` — shell routes every doc op to `owner`, never inspects `ptr`), `EditorAPI` (shell read/util surface plugins reach back through), `regions.zig` (sidebar/bottom/center/menu/settings/command contribution structs), `dylib.zig`/`dvui_context.zig` (runtime-library C-ABI + dvui injection).
1616
- **`src/editor/`** — the shell itself: `Editor.zig` (frame loop, plugin registration/loading), `PluginLoader.zig` (dlopen), `Menu.zig`, `Sidebar.zig`, `Settings.zig`, etc.
17-
- **`src/core/`** — shared infra (Atlas/Sprite, math, gfx, fs, paths, platform detection) used by shell *and* plugins. Not plugin-owned; don't move it.
17+
- **`src/core/`** — shared infra (Atlas/Sprite, math, gfx, fs, paths, platform detection) used by shell *and* plugins. Not plugin-owned; don't move it. `core.fuzzy` is the one matcher behind every filter box in the app (settings tree, file tree, plugin store, LSP completions) — wrap zf through it rather than matching by hand, and remember **lower scores are better**.
1818
- **`src/plugins/`** — bundled built-in plugins. Each is file-for-file the **same shape a third-party plugin would use**: root `plugin.zig` + identity-only `plugin.zig.zon` + `build.zig` + `build.zig.zon` (optional `src/**`), plus fizzy-internal glue in `static/`. No author `root.zig` or `<name>.zig` hub — the build helper generates the dylib entry; files use named imports (`fizzy_sdk`/`dvui`/…). Builds standalone with `cd src/plugins/<name> && zig build`.
1919

2020
**Two link modes, one source:** built-in plugins compile **static** (linked directly, all targets incl. web) or **dynamic** (`.dylib`/`.so`/`.dll`, desktop-only, `dlopen`'d — this is how third-party plugins ship too). `FIZZY_STATIC_<NAME>=1` env var forces static for a given built-in (useful when debugging dylib loading).
@@ -36,7 +36,7 @@ Shell (Editor) ←── Host registries + EditorAPI ──→ Plugin (registe
3636
3. Plugin prefs: `sdk.settings.Schema(struct { … })` then `.register(host, &plugin, …)` — shell draws them only while the plugin is loaded (no SettingsSection). User config on disk is ZON (`settings.zon` / `recents.zon`).
3737
4. Editor plugins implement the document vtable cluster; shell plugins (workbench-style) register a center provider + sidebar views instead.
3838
5. User-invoked actions are **`Command`s**`"<active_owner_id>.<action>"`.
39-
6. `zig build install` drops `{id}.dylib` into the fizzy plugins dir (no sidecar `.zon`).
39+
6. `zig build install` drops `{id}/{id}.dylib` (its own directory) into the fizzy plugins dir (no sidecar `.zon`).
4040
7. Memory: `host.allocator` vs `host.arena()`; never touch `dvui.currentWindow().gpa` directly.
4141
8. ABI: structural fingerprint at `dlopen` (`fizzy_plugin_abi_fingerprint`); bumps are rare/deliberate.
4242

@@ -60,25 +60,25 @@ Don't trust older narrative docs that call this forward-looking/not-yet-built.
6060
## Build
6161

6262
```sh
63-
zig build # native exe
64-
zig build check-web # wasm
65-
zig build test # unit/integration tests
66-
zig build test-sdk-version # CI lock: ABI fingerprint bump must bump sdk_version too
63+
zig build # native exe
64+
zig build check-web # wasm
65+
zig build test # unit/integration tests
66+
zig build test-sdk-version # CI lock: ABI fingerprint bump must bump sdk_version too
6767
```
6868

6969
Run all of these after touching the SDK boundary (`src/sdk/**`) or a plugin's vtable usage.
7070

7171
### Keep the plugin build free of app-only dependencies
7272

73-
Every plugin build (including third-party ones like pixi) compiles this repo's root `build.zig` via `b.dependency("fizzy", .{ .plugin_sdk = true })`. Because `@import` is **comptime + transitive**, anything the root `build()` can reach at comptime is pulled into *every* plugin build — even code guarded by a runtime `if (plugin_sdk) return;`. So an app-only dependency reached through a plain top-level `@import("<dep>")` (like Velopack, the host's self-installer/updater) leaks its whole graph — wrapper + prebuilt archives — into plugin builds that never use it.
73+
Plugins depend on the **`sdk/` package** (its own `build.zig` + `build.zig.zon`), not the repo root. The root zon owns the editor build and may list app-only deps (Velopack, nightwatch, …). A root-zon `.lazy = true` URL dep is **not** enough by itself: Zig eagerly unpacks lazy URL deps that already sit in the global cache into every consumer's `zig-pkg`, so after any app build a plugin depending on the root package would grow a Velopack tree even though `lazyDependency` never runs on the plugin path.
7474

75-
Rule: **app-only deps must never be reachable via comptime `@import` from the root build graph.** The pattern (see Velopack):
75+
Pattern:
7676

77-
- Mark the dep `.lazy = true` in `build.zig.zon`.
78-
- Never `@import("velopack_zig")` anywhere in the build graph. The helper surface is **vendored** in `build/velopack.zig` (pure `std.Build` glue) and takes the dependency as a handle.
79-
- Resolve it lazily *inside the app build only*: `const vz = b.lazyDependency("velopack_zig", .{}) orelse return;` in `build/app.zig`, then thread `vz` through `build/exe.zig` / `build/package.zig`.
77+
- **Plugins** (built-in + third-party): `.fizzy = .{ .path = ".../sdk" }` locally, or the `fizzy-sdk-v*` **release asset** URL from the matching `sdk-v*` tag (not the git archive — that is the monorepo root zon with Velopack). Call `fizzy.plugin.create` / `.install` as before; `b.dependency("fizzy", .{ .plugin_sdk = true })` still works (the option is accepted and ignored — `sdk/` always exports modules). Packing: `scripts/pack-sdk.sh` / `.github/workflows/sdk-tag.yml`.
78+
- **App**: repo-root `zig build` as usual. Velopack stays `.lazy = true` in the root zon; never `@import("velopack_zig")` the helper surface is vendored in `build/velopack.zig` and resolved only in `build/app.zig` via `lazyDependency`.
79+
- Shared `core` import wiring lives in `sdk/core_module.zig` and is called from the app build *and* `sdk/plugin_sdk.zig`'s `exportModules` so the import set can't drift. Note the `with_tui = false` on the zf dependency: without it, zf's standalone terminal binary drags `libvaxis` into every plugin build.
8080

81-
Acceptance test after any build-graph change: cross-build the image plugin and confirm no leak —
81+
Acceptance test after any build-graph change:
8282

8383
```sh
8484
cd src/plugins/image && rm -rf .zig-cache zig-out zig-pkg && zig build -Doptimize=ReleaseFast

build.zig

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const std = @import("std");
22

3-
pub const plugin = @import("plugin_sdk.zig");
3+
/// App-side re-export of the plugin build API (lives in `sdk/`). Plugins should depend on
4+
/// the `sdk/` package directly — see CLAUDE.md — not this root package.
5+
pub const plugin = @import("sdk/plugin_sdk.zig");
46

57
pub fn build(b: *std.Build) !void {
68
const windows_msvc_libc_opt = b.option([]const u8, "windows-msvc-libc", "zig libc manifest for *-windows-msvc when cross-compiling; forwarded by packageall for Windows children") orelse null;
@@ -16,21 +18,11 @@ pub fn build(b: *std.Build) !void {
1618
const target = b.standardTargetOptions(.{});
1719
const optimize = b.standardOptimizeOption(.{});
1820

19-
const plugin_sdk = b.option(
20-
bool,
21-
"plugin_sdk",
22-
"Export core/sdk modules for third-party plugin builds; skips the fizzy app",
23-
) orelse false;
24-
if (plugin_sdk) {
25-
try plugin.exportModules(b, target, optimize);
26-
return;
27-
}
28-
2921
try @import("build/app.zig").build(b, target, optimize, .{
3022
.windows_msvc_libc_opt = windows_msvc_libc_opt,
3123
.fetch_msvc_opt = fetch_msvc_opt,
3224
.macos_sign_app_identity = macos_sign_app_identity,
3325
.macos_sign_install_identity = macos_sign_install_identity,
3426
.macos_notary_profile = macos_notary_profile,
3527
});
36-
}
28+
}

build.zig.zon

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"build.zig",
66
"build.zig.zon",
77
"update.zig",
8-
"build",
9-
"plugin_sdk.zig",
8+
"sdk",
109
"assets",
1110
"libs",
1211
},
@@ -39,6 +38,8 @@
3938
.url = "https://github.com/foxnne/assetpack/archive/ac7592f3f5988857840d0df4610e1e1fad690e2e.tar.gz",
4039
.hash = "assetpack-0.2.0-5DA2d1ZkAADJanNVdWrUBOGMhOzUENhrUiqXcHADxY2x",
4140
},
41+
// App-only. Plugins depend on `sdk/` (not this root package) so they never see
42+
// Velopack — see CLAUDE.md.
4243
.velopack_zig = .{
4344
.url = "https://github.com/graphl-tech/velopack-zig/archive/0c2f20635a97fde38cc8000e9fb1a75f891cf37d.tar.gz",
4445
.hash = "velopack_zig-0.0.1-LzeGcengAACU1f33uDAuLKlWXtek0KCC6i_b3XWeJMjd",
@@ -52,5 +53,14 @@
5253
.hash = "cmark_gfm-0.1.0-uQgTKymaFwDK0jVZszXfDtQtW6BnS44Bp0GS55TVBD4p",
5354
.lazy = true,
5455
},
56+
.nightwatch = .{
57+
.url = "git+https://github.com/neurocyte/nightwatch#cac3b9edb3a59f5b3a290cf3b76044ad9b09ef28",
58+
.hash = "nightwatch-1.0.0-uXzeHyOgAgCvaBhschfx3LA48n01bLYOVBQUXYjvrREM",
59+
.lazy = true,
60+
},
61+
.zf = .{
62+
.url = "git+https://github.com/natecraddock/zf#c35c421f84895193246db06c40683c1a30e616ef",
63+
.hash = "zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW",
64+
},
5565
},
5666
}

build/app.zig

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
const std = @import("std");
22

3-
const plugin = @import("../plugin_sdk.zig");
3+
const plugin = @import("../sdk/plugin_sdk.zig");
4+
const core_mod = @import("../sdk/core_module.zig");
45
const dvui = @import("dvui");
56
const velopack = @import("velopack.zig");
67

8+
// `sdk/sdk_version.zig` mirrors `src/sdk/sdk_version.zig` for the plugin package boundary.
9+
comptime {
10+
const pack = @import("../sdk/sdk_version.zig").sdk_version;
11+
const runtime = @import("../src/sdk/sdk_version.zig").sdk_version;
12+
if (pack.major != runtime.major or pack.minor != runtime.minor or pack.patch != runtime.patch)
13+
@compileError("sdk/sdk_version.zig drifted from src/sdk/sdk_version.zig — bump both");
14+
}
15+
716
pub const Options = struct {
817
windows_msvc_libc_opt: ?[]const u8 = null,
918
fetch_msvc_opt: ?bool = null,
@@ -19,10 +28,9 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
1928
const macos_sign_install_identity = opts.macos_sign_install_identity;
2029
const macos_notary_profile = opts.macos_notary_profile;
2130

22-
// Resolve Velopack lazily. This runs only on app builds (never on the plugin-SDK path,
23-
// which returns from the root build before reaching here), so plugin builds never fetch
24-
// it. On the first configure pass this returns null → Zig fetches velopack_zig and
25-
// re-runs build(); the second pass proceeds with a valid handle.
31+
// Resolve Velopack lazily (app-only; plugins depend on `sdk/` which has no Velopack).
32+
// First configure pass returns null → Zig fetches velopack_zig and re-runs build();
33+
// the second pass proceeds with a valid handle.
2634
const vz = b.lazyDependency("velopack_zig", .{}) orelse return;
2735

2836
const common = @import("common.zig");
@@ -425,28 +433,26 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
425433
fizzy_test_module.addImport("backend", dvui_testing_dep.module("testing"));
426434
fizzy_test_module.addImport("assets", assets_module);
427435
fizzy_test_module.addOptions("build_opts", build_opts);
428-
if (b.lazyDependency("icons", .{ .target = target, .optimize = optimize })) |dep| {
429-
fizzy_test_module.addImport("icons", dep.module("icons"));
430-
}
431436

432437
// Shared `core` module for the test build (dvui testing backend variant).
433438
const core_module_test = b.createModule(.{
434439
.target = target,
435440
.optimize = optimize,
436441
.root_source_file = b.path("src/core/core.zig"),
437442
});
438-
core_module_test.addImport("dvui", dvui_testing_dep.module("dvui_testing"));
439-
if (b.lazyDependency("icons", .{ .target = target, .optimize = optimize })) |dep| {
440-
core_module_test.addImport("icons", dep.module("icons"));
441-
}
443+
const icons_test = core_mod.addImports(b, core_module_test, dvui_testing_dep.module("dvui_testing"), target, optimize);
442444
fizzy_test_module.addImport("core", core_module_test);
445+
if (icons_test) |icons| fizzy_test_module.addImport("icons", icons);
446+
if (b.lazyDependency("nightwatch", .{ .target = target, .optimize = optimize })) |dep| {
447+
fizzy_test_module.addImport("nightwatch", dep.module("nightwatch"));
448+
}
443449

444450
const sdk_module_test = sdk.wireSdkModule(b, target, optimize, dvui_testing_dep.module("dvui_testing"), dvui_test_proxy_bridge, core_module_test, fizzy_test_module);
445451
_ = workbench_plugin.addStaticModule(b, target, optimize, .{
446452
.dvui = dvui_testing_dep.module("dvui_testing"),
447453
.core = core_module_test,
448454
.sdk = sdk_module_test,
449-
.icons = if (b.lazyDependency("icons", .{ .target = target, .optimize = optimize })) |dep| dep.module("icons") else null,
455+
.icons = icons_test,
450456
.backend = dvui_testing_dep.module("testing"),
451457
}, workbench_opts, fizzy_test_module);
452458
_ = text_plugin.addStaticModule(b, target, optimize, .{

build/common.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const std = @import("std");
22

3-
const plugin = @import("../plugin_sdk.zig");
3+
const plugin = @import("../sdk/plugin_sdk.zig");
44
const update = @import("../update.zig");
55
const GitDependency = update.GitDependency;
66

0 commit comments

Comments
 (0)