Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!--
Title: `area: what changes`, 72 characters at most. It becomes the commit on main.
How we work: CONTRIBUTING.md.
-->

Part of #<!-- the plan's issue, if there is one -->

## What changes

<!-- What a user or plugin author sees. Then why, what was measured, what was tried and dropped. -->

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`, `sdk_version` untouched, PR labelled `sdk`
- [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag

## Verified

<!-- How, per platform: a test, a demo tape, a screenshot, a measurement. Leave a platform unticked rather than claim it. -->

- [ ] macOS:
- [ ] Windows:
- [ ] Linux:
- [ ] Web:

## Follow-ups

<!-- What was deliberately left out, with an issue if it outlives this PR. -->
41 changes: 41 additions & 0 deletions .github/rulesets/main.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "main",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"bypass_actors": [
{
"actor_id": 1,
"actor_type": "OrganizationAdmin",
"bypass_mode": "pull_request"
}
],
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"allowed_merge_methods": ["squash"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"do_not_enforce_on_create": false,
"required_status_checks": [{ "context": "ci-ok" }]
}
}
]
}
89 changes: 77 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
name: CI

# PR / manual: three native test jobs (no skipped rows). Push to main: Linux-only (fast).
# PR / merge queue / manual: three native test jobs (no skipped rows). Push to main: Linux-only
# (fast).
#
# `ci-ok` is the one check `main`'s ruleset requires (`.github/rulesets/main.json`), so the
# workflow runs for every change. A workflow-level `paths-ignore` would never start for a
# Markdown-only PR, leaving its required check waiting for good; `changes` works out instead
# whether anything besides docs changed, and the builds skip themselves when nothing did.
on:
push:
branches: [main]
paths-ignore:
- "doc/**"
- "README.md"
- "**.md"
- "LICENSE**"
pull_request:
paths-ignore:
- "doc/**"
- "README.md"
- "**.md"
- "LICENSE**"
merge_group:
workflow_dispatch:

concurrency:
Expand All @@ -25,8 +22,53 @@ env:
ZIG_VERSION: "0.16.0"

jobs:
changes:
name: Changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
# A pull request checks out its merge commit, whose first parent is the base: depth 2
# holds both.
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Anything besides docs?
id: filter
shell: bash
env:
EVENT: ${{ github.event_name }}
PUSH_BEFORE: ${{ github.event.before }}
QUEUE_BASE: ${{ github.event.merge_group.base_sha }}
run: |
set -euo pipefail
case "$EVENT" in
pull_request) base=HEAD^1 ;;
merge_group) base=$QUEUE_BASE ;;
push) base=$PUSH_BEFORE ;;
*) echo "code=true" >> "$GITHUB_OUTPUT"; exit 0 ;;
esac
# A base that can't be fetched (a force push's old tip) builds everything.
if ! git cat-file -e "$base^{commit}" 2>/dev/null && ! git fetch --depth=1 origin "$base"; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Assigned first, so a failing diff fails the job (and `ci-ok`) rather than reading as
# "nothing changed".
files=$(git diff --name-only "$base" HEAD)
code=false
while IFS= read -r f; do
case "$f" in
"" | *.md | doc/* | LICENSE*) ;;
*) code=true; echo "builds: $f changed" ;;
esac
done <<< "$files"
echo "code=$code" >> "$GITHUB_OUTPUT"

test:
name: Tests (${{ matrix.name }})
needs: changes
if: needs.changes.outputs.code == 'true'
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -115,7 +157,8 @@ jobs:
# builds do). It shows the app compiles and links, not that it runs.
windows-fizzy-backend:
name: Windows build (fizzy backend, cross-compiled)
if: github.event_name != 'push'
needs: changes
if: github.event_name != 'push' && needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -129,3 +172,25 @@ jobs:
run: bash scripts/ci-retry.sh zig build --fetch
- name: Build for x86_64-windows-gnu with -Dnative-backend=fizzy
run: bash scripts/ci-retry.sh zig build -Dtarget=x86_64-windows-gnu -Dnative-backend=fizzy -Dno-emit --summary all

# The required check. Passes when every job above passed or was skipped (a docs-only change
# skips the builds); fails on any failure or cancellation, `changes` included. A job added to
# this workflow joins the gate by being added to `needs`.
ci-ok:
name: ci-ok
if: always()
needs: [changes, test, windows-fizzy-backend]
runs-on: ubuntu-latest
steps:
- name: Every job passed or was skipped
shell: bash
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
echo "results: $RESULTS"
for r in $RESULTS; do
case "$r" in
success | skipped) ;;
*) exit 1 ;;
esac
done
19 changes: 13 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Cross-platform, open-source general editor written in Zig, UI via [DVUI](https:/

**Read this file first, then go deeper via the links below — don't re-derive the architecture from scratch.**

**How we work** — one plan step per PR, claiming work before building it, jj, verifying, the SDK
release train — is [`CONTRIBUTING.md`](CONTRIBUTING.md), imported here so every session reads it:

@CONTRIBUTING.md

## The core idea: fizzy + plugins

Fizzy the app is itself a near-empty host (window, frame loop, layout shape, 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.
Expand Down Expand Up @@ -41,7 +46,7 @@ Fizzy (Editor) ←── Host registries + EditorAPI ──→ Plugin (register(
5. User-invoked actions are **`Command`s** — `"<active_owner_id>.<action>"`.
6. `zig build install` drops `{id}/{id}.dylib` (its own directory) into the fizzy plugins dir (no sidecar `.zon`).
7. Memory: `host.allocator` vs `host.arena()`; never touch `dvui.currentWindow().gpa` directly.
8. ABI: structural fingerprint at `dlopen` (`fizzy_plugin_abi_fingerprint`). The SDK is at 0.2.0, unreleased: the fingerprint may move freely under it (update `recorded_sdk_shape_fingerprint`), the version does not until it ships.
8. ABI: structural fingerprint at `dlopen` (`fizzy_plugin_abi_fingerprint`). A change that moves the boundary's shape records the new `recorded_sdk_shape_fingerprint` and leaves `sdk_version` alone; only an SDK release PR bumps the version (`CONTRIBUTING.md`, "Changing the SDK").

Full contract: **[`docs/PLUGINS.md`](docs/PLUGINS.md)**. Living reshape plan: **[`docs/PLUGIN_MANIFEST_PLAN.md`](docs/PLUGIN_MANIFEST_PLAN.md)**.

Expand Down Expand Up @@ -131,8 +136,9 @@ one directory — that is why the `app` module root is `app/root.zig`, not `app/
```sh
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 too
zig build test # unit tests
zig build test-integration # headless integration tests (not run by CI: run it yourself)
zig build test-sdk-version # CI lock: the recorded fingerprint matches the live plugin boundary
```

Run all of these after touching the SDK boundary (`sdk/src/**`) or a plugin's vtable usage.
Expand All @@ -145,7 +151,7 @@ Pattern:

- **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`.
- **App**: repo-root `zig build` as usual. The app **consumes `sdk/` as a dependency** (`.fizzy_sdk = .{ .path = "sdk/" }`), so build scripts reach `plugin`/`core_module`/`sdk_version` through `@import("fizzy_sdk")` and never by relative path into `sdk/` — a file may belong to only one module, so a path import claims it for the root build module and breaks the dependency outright. The same applies in reverse: nothing under `src/` may relative-import an `sdk/` file. 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`.
- **dvui is pinned in exactly one place — `sdk/build.zig.zon` — and is deliberately absent from the root zon.** The app borrows it via `build/sdk.zig`'s `dvuiDependency` (which forwards backend/target/optimize normally), and build scripts get dvui's build API from `@import("fizzy_sdk").dvui`. Do **not** "fix" the missing root dep by re-adding `.dvui`: two pins that drift make `recorded_sdk_shape_fingerprint` unsatisfiable by *both* the app and plugin-SDK builds at once, and the resulting error tells you to bump `sdk_version`, which cannot help. Bump or swap to a local checkout in `sdk/build.zig.zon` only.
- **dvui is pinned in exactly one place — `sdk/build.zig.zon` — and is deliberately absent from the root zon.** The app borrows it via `build/sdk.zig`'s `dvuiDependency` (which forwards backend/target/optimize normally), and build scripts get dvui's build API from `@import("fizzy_sdk").dvui`. Do **not** "fix" the missing root dep by re-adding `.dvui`: two pins that drift make `recorded_sdk_shape_fingerprint` unsatisfiable by *both* the app and plugin-SDK builds at once, and the resulting error asks for a new recorded fingerprint, which cannot satisfy both. Bump or swap to a local checkout in `sdk/build.zig.zon` only.
- 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.

Acceptance test after any build-graph change:
Expand All @@ -159,8 +165,9 @@ CI builds plugins for all 6 host targets by cross-compiling with `-Dtarget=` (se

## When you need more than this file

- **Resuming the library/framework work (bookmark `fizzy-lib`)** → [`docs/LIB_CHECKPOINT.md`](docs/LIB_CHECKPOINT.md):
ground rules, what is done, the verification workflow, and the agreed next steps.
- **The library/framework work (merged from bookmark `fizzy-lib` in #194)** → [`docs/LIB_CHECKPOINT.md`](docs/LIB_CHECKPOINT.md):
what was done and the agreed next steps. Its ground rules predate `CONTRIBUTING.md`, which wins
where they differ.

- Demos that play the real app (tapes, the player, rewind, writing a demo, the anchors widgets
publish) → [`docs/AUTOMATION.md`](docs/AUTOMATION.md); where it is going (recording, seeking,
Expand Down
Loading
Loading