Cross-platform, open-source general editor written in Zig, UI via DVUI. Targets native (macOS/Linux/Windows) and web (wasm32). Layout/UX is IDE-shaped (VSCode-like): sidebar rail + explorer, menubar, center tabs/splits, bottom panel, infobar.
Read this file first, then go deeper via the links below — don't re-derive the architecture from scratch.
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.
Shell (Editor) ←── Host registries + EditorAPI ──→ Plugin (register(host) + vtable)
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 toowner, never inspectsptr),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).src/editor/— the shell itself:Editor.zig(frame loop, plugin registration/loading),PluginLoader.zig(dlopen),Menu.zig,Sidebar.zig,Settings.zig, etc.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.fuzzyis 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.src/plugins/— bundled built-in plugins. Each is file-for-file the same shape a third-party plugin would use: rootplugin.zig+ identity-onlyplugin.zig.zon+build.zig+build.zig.zon(optionalsrc/**), plus fizzy-internal glue instatic/. No authorroot.zigor<name>.zighub — the build helper generates the dylib entry; files use named imports (fizzy_sdk/dvui/…). Builds standalone withcd src/plugins/<name> && zig build.
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).
workbench— file tree, tabs/splits, center provider; owns no documents. Exposes aworkbench-apiservice other plugins use to open/close/manage files without importing workbench.text— generic text/code editor; fallback owner for any file extension nothing else claims. (Recently renamed fromcode.)image— read-only PNG/JPG/JPEG viewer with zoom/pan (fallback when pixi is not installed).markdown—.mdpreview utility plugin.shared— build helpers used across plugins'static/integration.zig(not a plugin itself).
Pixi (pixel-art editor) has been extracted out of this repo into an external, third-party-style plugin (fizzyedit/pixi, ~/dev/fizzyedit/pixi) — it ships and updates purely through the plugin store (docs/PLUGINS.md §6), with no special treatment in the shell. Older docs/handoffs (HANDOFF.md) still describe pixi as in-tree — that's historical, not current. Trust ls src/plugins/ and git log over any doc's plugin list.
- Copy
src/plugins/text/as your template (orsrc/plugins/image/for a document-owning viewer). - Add identity-only
plugin.zig.zon(id/name/version/min_sdk_version). Implement rootplugin.zig:Plugin+register(host)+ vtable; callhost.register{SidebarView,BottomView,CenterProvider,Menu,Command,Service,…}as needed. - 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). - Editor plugins implement the document vtable cluster; shell plugins (workbench-style) register a center provider + sidebar views instead.
- User-invoked actions are
Commands —"<active_owner_id>.<action>". zig build installdrops{id}/{id}.dylib(its own directory) into the fizzy plugins dir (no sidecar.zon).- Memory:
host.allocatorvshost.arena(); never touchdvui.currentWindow().gpadirectly. - ABI: structural fingerprint at
dlopen(fizzy_plugin_abi_fingerprint); bumps are rare/deliberate.
Full contract: docs/PLUGINS.md. Living reshape plan: docs/PLUGIN_MANIFEST_PLAN.md.
The plugin registry/install flow (author repo → release CI → fizzyedit/plugins registry →
in-app store) is fully built and is the canonical publishing path for every third-party
plugin, pixi included. It's documented end-to-end in docs/PLUGINS.md §6; the registry
repo itself is fizzyedit/plugins and the reusable
release CI is fizzyedit/plugin-build-action.
Don't trust older narrative docs that call this forward-looking/not-yet-built.
HANDOFF.md— historical Phase 4 handoff (compile-time modular separation, predates the pixi extraction and thecode→textrename). Superseded bydocs/PLUGINS.mdfor anything plugin-related; useful only for the older "how did we get here" narrative.
zig build # native exe
zig build check-web # wasm
zig build test # unit/integration tests
zig build test-sdk-version # CI lock: ABI fingerprint bump must bump sdk_version tooRun all of these after touching the SDK boundary (src/sdk/**) or a plugin's vtable usage.
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.
Pattern:
- Plugins (built-in + third-party):
.fizzy = .{ .path = ".../sdk" }locally, or thefizzy-sdk-v*release asset URL from the matchingsdk-v*tag (not the git archive — that is the monorepo root zon with Velopack). Callfizzy.plugin.create/.installas 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. - App: repo-root
zig buildas usual. Velopack stays.lazy = truein the root zon; never@import("velopack_zig")— the helper surface is vendored inbuild/velopack.zigand resolved only inbuild/app.zigvialazyDependency. - Shared
coreimport wiring lives insdk/core_module.zigand is called from the app build andsdk/plugin_sdk.zig'sexportModulesso the import set can't drift. Note thewith_tui = falseon the zf dependency: without it, zf's standalone terminal binary dragslibvaxisinto every plugin build.
Acceptance test after any build-graph change:
cd src/plugins/image && rm -rf .zig-cache zig-out zig-pkg && zig build -Doptimize=ReleaseFast
ls zig-pkg | grep -i velo # must be emptyCI builds plugins for all 6 host targets by cross-compiling with -Dtarget= (see fizzyedit/plugin-build-action); pure-Zig + vendored-C plugins don't need per-arch runners.
- Full plugin contract + lifecycle/hook tables →
docs/PLUGINS.md - Living reshape plan (identity
plugin.zig.zon, comptimesettings.Schema, ZON user config, no sidecars) →docs/PLUGIN_MANIFEST_PLAN.md