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
212 changes: 212 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

209 changes: 1 addition & 208 deletions CLAUDE.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Working on fizzy

`CLAUDE.md` says what fizzy is. This file says how a change gets from an idea to `main`, for
`AGENTS.md` says what fizzy is. This file says how a change gets from an idea to `main`, for
people and agents alike, on a laptop or in a cloud session. Several agents work on this repo at
once, and these rules are what keep their work from colliding.

Expand Down Expand Up @@ -136,7 +136,7 @@ tarball is pinned by hash, so it is never replaced.

| What | Where |
|---|---|
| What fizzy is, and its architecture rules | `CLAUDE.md` |
| What fizzy is, and its architecture rules | `AGENTS.md` |
| How we work | this file |
| The plugin contract | `docs/PLUGINS.md` |
| Plans | private, in [`fizzyedit/plans`](https://github.com/fizzyedit/plans): issues, and a long design as `plans/<NAME>_PLAN.md` opening with a status line (`Status: proposed`, `in progress, fizzyedit/fizzy#N`, or `done`) |
Expand All @@ -160,7 +160,7 @@ tarball is pinned by hash, so it is never replaced.
plugin (a per-frame poll, a slice borrowed from plugin memory). Moving the fingerprint for the
clearer shape is fine.
- **Pitfalls go where they bite.** A trap that cost real time is written down in the
`CLAUDE.md` of the directory it lives in, or `docs/PITFALLS.md` if it has none.
`AGENTS.md` of the directory it lives in, or `docs/PITFALLS.md` if it has none.

## The gate on `main`

Expand Down
35 changes: 35 additions & 0 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# app/: the framework an application switches on

Compiled into an application, never into a plugin dylib, so it may use what a plugin must not
link: the network, the filesystem, an installer, `dlopen`. `root.zig`'s doc comment lists what
each directory is.

- **The module root is `root.zig`, not `app.zig`.** On a case-insensitive filesystem `app.zig` and
`App.zig` are one file (root `AGENTS.md`, "Case-renaming").
- **Nothing here names fizzy.** Where the framework needs the application it asks through a
`{ctx, vtable}` seam the app fills in. Never import `src/`; an app built on fizzy has no `src/`.
- **Layout** (`layout/`): read `layout/SPLITS.md` before changing splits, moving or floating
views. It is the contract, and it lists the rules a change must keep. `layout/LAYERS.md` is an
agreed design that isn't built yet, with the rejected alternatives. Shapes are copied, not
configured (root `AGENTS.md`, "Shipped shapes"): a shape needing something only fizzy has is
a bug in `Layout`.
- **Tests:** the std-only rule files (`layout/float_rules.zig`, `watch/folder_events.zig`,
`RecentCommands.zig`, …) each have a root in `build/app.zig`'s unit list. Pure decisions go in
such a file, so they test without a Window. Everything else is reached from `root.zig`'s
`test` block (`fizzy-app-tests`, under `zig build test-integration`).

## Pitfalls

- **macOS charges ~110–140 ms for the first `dlopen` of a newly written file,** whatever its size,
and caches the result per file. The loader therefore maps a stable per-build copy
(`.load-copy/{mtime}-{size}-{name}`) and copies again only on a genuine re-open. It never maps
the installed file: Windows couldn't rename an update into place, and on POSIX a rebuild would
rewrite bytes under a live mapping (SIGBUS). Startup that looks like N × 120 ms is this.
- **Motion handing over to an OS window must be at rest on the window's rect when it shows.** An
OS window can't animate its size per frame cheaply, so an overshoot still swinging then gets
cut. Use `core.motion.arrive(t, swing)`, and size any overshoot in points, not as a share of the
window.
- **The dragged view's glass moves only as the hand does** (`ViewDrag`'s `holdHand`, 3 pt). No
springs, easing or trails driven by the pointer: merged Liquid Glass necks re-form visibly
under tremor. A one-shot, time-driven curve started by a discrete event (the aimed bubble
changing) is fine.
36 changes: 1 addition & 35 deletions app/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,35 +1 @@
# app/: the framework an application switches on

Compiled into an application, never into a plugin dylib, so it may use what a plugin must not
link: the network, the filesystem, an installer, `dlopen`. `root.zig`'s doc comment lists what
each directory is.

- **The module root is `root.zig`, not `app.zig`.** On a case-insensitive filesystem `app.zig` and
`App.zig` are one file (root `CLAUDE.md`, "Case-renaming").
- **Nothing here names fizzy.** Where the framework needs the application it asks through a
`{ctx, vtable}` seam the app fills in. Never import `src/`; an app built on fizzy has no `src/`.
- **Layout** (`layout/`): read `layout/SPLITS.md` before changing splits, moving or floating
views. It is the contract, and it lists the rules a change must keep. `layout/LAYERS.md` is an
agreed design that isn't built yet, with the rejected alternatives. Shapes are copied, not
configured (root `CLAUDE.md`, "Shipped shapes"): a shape needing something only fizzy has is
a bug in `Layout`.
- **Tests:** the std-only rule files (`layout/float_rules.zig`, `watch/folder_events.zig`,
`RecentCommands.zig`, …) each have a root in `build/app.zig`'s unit list. Pure decisions go in
such a file, so they test without a Window. Everything else is reached from `root.zig`'s
`test` block (`fizzy-app-tests`, under `zig build test-integration`).

## Pitfalls

- **macOS charges ~110–140 ms for the first `dlopen` of a newly written file,** whatever its size,
and caches the result per file. The loader therefore maps a stable per-build copy
(`.load-copy/{mtime}-{size}-{name}`) and copies again only on a genuine re-open. It never maps
the installed file: Windows couldn't rename an update into place, and on POSIX a rebuild would
rewrite bytes under a live mapping (SIGBUS). Startup that looks like N × 120 ms is this.
- **Motion handing over to an OS window must be at rest on the window's rect when it shows.** An
OS window can't animate its size per frame cheaply, so an overshoot still swinging then gets
cut. Use `core.motion.arrive(t, swing)`, and size any overshoot in points, not as a share of the
window.
- **The dragged view's glass moves only as the hand does** (`ViewDrag`'s `holdHand`, 3 pt). No
springs, easing or trails driven by the pointer: merged Liquid Glass necks re-form visibly
under tremor. A one-shot, time-driven curve started by a discrete event (the aimed bubble
changing) is fine.
@AGENTS.md
2 changes: 1 addition & 1 deletion app/layout/Chooser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
//! picking the already-selected item means (`reselected`).
//!
//! Nothing here reaches past the public `Layout` API: a shape that wants a chooser this file
//! cannot draw copies it, per the shipped-shapes methodology in CLAUDE.md.
//! cannot draw copies it, per the shipped-shapes methodology in AGENTS.md.
const std = @import("std");
const dvui = @import("dvui");
const core = @import("core");
Expand Down
2 changes: 1 addition & 1 deletion app/layout/LAYERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ put a switch in the public API that the framework does not yet honour, which is
adding it a version later. Sequence stands as written.

**The dvui bump is the risk item, and it should land on its own.** `sdk/build.zig.zon` is the
repo's only dvui pin and is deliberately absent from the root zon (CLAUDE.md explains why adding
repo's only dvui pin and is deliberately absent from the root zon (AGENTS.md explains why adding
a second pin cannot work). Fizzy has also diverged `PanedWidget`, and the layered work diverges
it further — so bumping dvui *and* changing the widget in one change makes a regression
unbisectable. Land the bump against a green tree first.
Expand Down
2 changes: 1 addition & 1 deletion app/layout/Layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
//!
//! Note the core pieces come through the named `core` module, never by relative path: a file may
//! belong to only one module, and `@import("../../core/...")` here would claim it for the root
//! build module and break `core` as a dependency outright (CLAUDE.md).
//! build module and break `core` as a dependency outright (AGENTS.md).
const std = @import("std");
const builtin = @import("builtin");
const dvui = @import("dvui");
Expand Down
2 changes: 1 addition & 1 deletion app/layout/Region.zig
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ pub fn isPeeking(self: Region) bool {
///
/// This replaced a five-value `Chooser` enum, two of whose values (`explorer_chrome`,
/// `panel_chrome`) named *fizzy's own* furniture from inside the generic layer. That is the
/// case CLAUDE.md calls a bug in `Layout` rather than a special case: a shape is supposed to be
/// case AGENTS.md calls a bug in `Layout` rather than a special case: a shape is supposed to be
/// ordinary code over this API, and an app copying `src/editor/layout.zig` could not have written those two
/// values itself. As a function pointer it is just `explorerPane` in `src/editor/layout.zig` —
/// app code, passed in, replaceable by the app's own loop over `matching` / `selected` / `draw`,
Expand Down
52 changes: 52 additions & 0 deletions backend/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# backend/: fizzy's native backend

SDL3, the SDL_GPU renderer (`GpuRenderer.zig`) and the platform pieces (`platform/`: title bars,
the OS's glass, OS windows for floats, menus, dialogs). A package of its own (`build.zig` says
how an app wires it).

- **dvui is the caller's.** It is never pinned here; the backend compiles against the app's dvui.
- **SDL is patched here,** not in a fork: `patches/sdl/*.patch`, applied by zig-patch. How to
change, add or bump them is `docs/DEPENDENCIES.md` ("SDL", "Bumping"). Touching a patch or this
`build.zig` runs CI's `sdl` job, the suites for the patches (`zig build test-sdl` here).
- **std-only tests are listed in `build.zig`'s `unit_tests`,** and fizzy's `zig build test` runs
them. Window rules that can be stated without a window go in such a file
(`platform/window_layout.zig`, `viewport_map.zig`).
- **Verify on the platform you changed.** CI's Windows cross-compile proves fizzy links, not that
it works. `docs/MACOS_LIVE_RESIZE.md` and `docs/WINDOWS_LIVE_RESIZE.md` show how a resize
was measured.

## macOS pitfalls

The `.m` files in `src/platform/macos/` are built **without ARC**, under a frame loop that is Zig.

- **Autorelease pools:** each SDL callback (`appInit`, `appIterate`, `appEvent`, `appQuit` in
`SDLBackend.zig`) runs in a pool, so Objective-C called from the frame needs none of its own.
A thread fizzy starts, or a callback AppKit makes on its own schedule, does. Release
`alloc`/`copy` results by hand. A closed window that lingers: run with
`OBJC_DEBUG_MISSING_POOLS=YES` before suspecting the view hierarchy.
- **Secondary windows** (floats, menus, dialogs):
- AppKit's show and close animations never finish under this loop and leave ghost windows. Set
`NSWindowAnimationBehaviorNone`.
- SDL shows a non-activating window *below* the key window, so order it explicitly.
- `orderedIndex` is stale right after `setLevel:`: order after a level change, don't ask first.
- A child window (`addChildWindow:`) drops to its parent's level. Set the level after attaching,
and attach only once it is shown.
- Borderless windows join the Window menu only through `changeWindowsItem:`.
- **A press reaches SDL3View but not SDL:** SDL3View inherits NSView's `-mouseDown:`, which keeps
the press in a full-screen window. `visual_effect_view.m` adds a forwarding one. Add the
forward; don't rebuild the responder chain.
- **Full screen** (`window_monitor.m`):
- `toggleFullScreen:` called inside an open frame nests `appIterate` in it (dvui logs "window is
already the current_window"), so defer it.
- AppKit pictures the after-state synchronously *inside* `toggleFullScreen:`.
- Whoever moves a window's frame sets its drawable size, because SDL hears of the new size late.
- Take frame jumps only inside the app's frame transaction.
- Read the pixel density from `backingScaleFactor`.
- A sandbox fizzy launched from a shell isn't the active app, and AppKit stalls or refuses its
transitions.
- **Per-frame presents are what cost.** A display-sized overlay, or an extra window presented
every frame, halved drag fps (the main thread waited in `nextDrawable`), while the blur made no
difference. Size overlays to their content. Measure idle against drag fps, and `sample` for
`nextDrawable`, before blaming shaders.
- **Glass:** what each `NSGlassEffectView` variant and material actually does, measured, is in
`docs/MACOS_GLASS.md`. Don't add SwiftUI or a `swiftc` step for transitions AppKit lacks.
53 changes: 1 addition & 52 deletions backend/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,52 +1 @@
# backend/: fizzy's native backend

SDL3, the SDL_GPU renderer (`GpuRenderer.zig`) and the platform pieces (`platform/`: title bars,
the OS's glass, OS windows for floats, menus, dialogs). A package of its own (`build.zig` says
how an app wires it).

- **dvui is the caller's.** It is never pinned here; the backend compiles against the app's dvui.
- **SDL is patched here,** not in a fork: `patches/sdl/*.patch`, applied by zig-patch. How to
change, add or bump them is `docs/DEPENDENCIES.md` ("SDL", "Bumping"). Touching a patch or this
`build.zig` runs CI's `sdl` job, the suites for the patches (`zig build test-sdl` here).
- **std-only tests are listed in `build.zig`'s `unit_tests`,** and fizzy's `zig build test` runs
them. Window rules that can be stated without a window go in such a file
(`platform/window_layout.zig`, `viewport_map.zig`).
- **Verify on the platform you changed.** CI's Windows cross-compile proves fizzy links, not that
it works. `docs/MACOS_LIVE_RESIZE.md` and `docs/WINDOWS_LIVE_RESIZE.md` show how a resize
was measured.

## macOS pitfalls

The `.m` files in `src/platform/macos/` are built **without ARC**, under a frame loop that is Zig.

- **Autorelease pools:** each SDL callback (`appInit`, `appIterate`, `appEvent`, `appQuit` in
`SDLBackend.zig`) runs in a pool, so Objective-C called from the frame needs none of its own.
A thread fizzy starts, or a callback AppKit makes on its own schedule, does. Release
`alloc`/`copy` results by hand. A closed window that lingers: run with
`OBJC_DEBUG_MISSING_POOLS=YES` before suspecting the view hierarchy.
- **Secondary windows** (floats, menus, dialogs):
- AppKit's show and close animations never finish under this loop and leave ghost windows. Set
`NSWindowAnimationBehaviorNone`.
- SDL shows a non-activating window *below* the key window, so order it explicitly.
- `orderedIndex` is stale right after `setLevel:`: order after a level change, don't ask first.
- A child window (`addChildWindow:`) drops to its parent's level. Set the level after attaching,
and attach only once it is shown.
- Borderless windows join the Window menu only through `changeWindowsItem:`.
- **A press reaches SDL3View but not SDL:** SDL3View inherits NSView's `-mouseDown:`, which keeps
the press in a full-screen window. `visual_effect_view.m` adds a forwarding one. Add the
forward; don't rebuild the responder chain.
- **Full screen** (`window_monitor.m`):
- `toggleFullScreen:` called inside an open frame nests `appIterate` in it (dvui logs "window is
already the current_window"), so defer it.
- AppKit pictures the after-state synchronously *inside* `toggleFullScreen:`.
- Whoever moves a window's frame sets its drawable size, because SDL hears of the new size late.
- Take frame jumps only inside the app's frame transaction.
- Read the pixel density from `backingScaleFactor`.
- A sandbox fizzy launched from a shell isn't the active app, and AppKit stalls or refuses its
transitions.
- **Per-frame presents are what cost.** A display-sized overlay, or an extra window presented
every frame, halved drag fps (the main thread waited in `nextDrawable`), while the blur made no
difference. Size overlays to their content. Measure idle against drag fps, and `sample` for
`nextDrawable`, before blaming shaders.
- **Glass:** what each `NSGlassEffectView` variant and material actually does, measured, is in
`docs/MACOS_GLASS.md`. Don't add SwiftUI or a `swiftc` step for transitions AppKit lacks.
@AGENTS.md
2 changes: 1 addition & 1 deletion build.zig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const std = @import("std");

/// App-side re-export of the plugin build API (lives in `sdk/`). Plugins should depend on
/// the `sdk/` package directly — see CLAUDE.md — not this root package.
/// the `sdk/` package directly — see AGENTS.md — not this root package.
///
/// Reached through the dependency rather than by path (`sdk/plugin_sdk.zig`): the app consumes
/// `sdk/` as a package so the two can share one dvui pin, and a file may belong to only one module,
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
.hash = "assetpack-0.2.0-5DA2d1ZkAADJanNVdWrUBOGMhOzUENhrUiqXcHADxY2x",
},
// App-only. Plugins depend on `sdk/` (not this root package) so they never see
// Velopack — see CLAUDE.md.
// Velopack — see AGENTS.md.
.velopack_zig = .{
.url = "https://github.com/graphl-tech/velopack-zig/archive/0c2f20635a97fde38cc8000e9fb1a75f891cf37d.tar.gz",
.hash = "velopack_zig-0.0.1-LzeGcengAACU1f33uDAuLKlWXtek0KCC6i_b3XWeJMjd",
Expand Down
2 changes: 1 addition & 1 deletion build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.
// benchmark.
bench_module.addAnonymousImport("sample_huge", .{ .root_source_file = b.path("docs/PLUGINS.md") });
bench_module.addAnonymousImport("sample_prose", .{ .root_source_file = b.path("tests/data/markdown_sample_tables.md") });
bench_module.addAnonymousImport("sample_medium", .{ .root_source_file = b.path("CLAUDE.md") });
bench_module.addAnonymousImport("sample_medium", .{ .root_source_file = b.path("AGENTS.md") });
bench_module.addAnonymousImport("sample_small", .{ .root_source_file = b.path("docs/MODULARIZATION_RELEASE_NOTES.md") });

const bench_markdown = b.addTest(.{ .name = "fizzy-bench-markdown", .root_module = bench_module });
Expand Down
Loading
Loading