Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ff12fab
app: Undo names whose step it takes, and the bubble after it has a Re…
foxnne Oct 18, 2026
f892f9b
about: the About dialog and Help menu follow the app (#533)
foxnne Oct 18, 2026
99f330a
app: control server streams the app's events (SSE) (#499)
foxnne Oct 18, 2026
f847dc2
layout: corner button stays dark under a modal; Output tabs on the le…
foxnne Oct 18, 2026
04b2f4f
dvui: front-to-back blocks stay above later focus rings; snapshot bui…
foxnne Oct 18, 2026
75ab578
app: document ids are unique when two load at once (#531)
foxnne Oct 18, 2026
bf1b173
menus: plugin menus are clickable in the title bar, and sit before He…
foxnne Oct 18, 2026
573afed
app: a folder walker that watches a large tree within limits (#543)
foxnne Oct 18, 2026
6c6a5d4
crash: report the caller when the PC is outside every module (#539)
foxnne Oct 18, 2026
ef86a08
linux: opening a large folder no longer hangs watching every director…
foxnne Oct 18, 2026
0c4d03a
dvui: front-to-back blocks close by append; one replay order (#544)
foxnne Oct 18, 2026
ad7b518
build: an app built on fizzy packages under its own name and icons (#…
foxnne Oct 18, 2026
f60c1c7
automation: key pills are the transport bar's glass (#545)
foxnne Oct 18, 2026
42215d2
core: tabs and rail icons show a hover look (#542)
foxnne Oct 18, 2026
9df1a04
app: the scrub bar grows into a strip of the document's history as pi…
foxnne Oct 18, 2026
43c816c
app: export a window of the strip, dragged from both sides (#502)
foxnne Oct 18, 2026
6155170
web: start fetching the editor before the page is shown (#515)
foxnne Oct 18, 2026
ae60b4f
web: play a tape from a link, loading the plugins it needs (#514)
foxnne Oct 18, 2026
68b4f6c
sdk: a plugin can replace a command it names, and gets it back on unl…
foxnne Oct 18, 2026
a14e8b1
app: control server describes itself (/openapi.json) (#508)
foxnne Oct 18, 2026
e3a71ea
web: a tape link's plugin wait builds again (#554)
foxnne Oct 18, 2026
4623dd0
app: control server on by default, socket in a private folder (#509)
foxnne Oct 18, 2026
44849f2
zig/0.17: merge main (22 commits, to #509)
foxnne Oct 18, 2026
e8b7133
zig: settings own only strings, and builds read through b.root
foxnne Oct 18, 2026
b9e2bef
Merge zig/0.17 into zig/017-review-fixes
foxnne Oct 18, 2026
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
2 changes: 1 addition & 1 deletion build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options
// package's, so its VERSION file is out of reach here), else 0.0.0.
const app_version: []const u8 = if (app_version_opt) |v| v else if (opts.own) |own| blk: {
b.dependOnFileContents(b.path(own.version_file));
const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, own.version_file, b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e});
const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, own.version_file) catch @panic("OOM"), b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e});
version_owned = raw;
break :blk std.mem.trimEnd(u8, raw, "\r\n");
} else "0.0.0";
Expand Down
2 changes: 1 addition & 1 deletion build/web.zig
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ fn stampStorage(b: *std.Build) std.Build.LazyPath {
};
const token = "__FIZZY_WEB_STORAGE__";
b.dependOnFileContents(b.path("web/index.html"));
const html = b.root.root_dir.handle.readFileAlloc(b.graph.io, "web/index.html", b.allocator, .unlimited) catch |err|
const html = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, "web/index.html") catch @panic("OOM"), b.allocator, .unlimited) catch |err|
std.debug.panic("web/index.html: {t}", .{err});
if (std.mem.indexOf(u8, html, token) == null) std.debug.panic("web/index.html: no {s} to stamp", .{token});
const stamped = std.mem.replaceOwned(u8, b.allocator, html, token, name) catch @panic("OOM");
Expand Down
2 changes: 1 addition & 1 deletion plugins/shared/build/helpers.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub const IdentityManifest = @import("../../../sdk/src/manifest_identity.zig").I
/// `@import`s it via `plugin_sdk.readManifest`). Panics clearly on a missing/invalid manifest.
pub fn readManifestAt(b: *std.Build, zon_rel_path: []const u8) IdentityManifest {
b.dependOnFileContents(b.path(zon_rel_path));
const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, zon_rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e|
const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, zon_rel_path) catch @panic("OOM"), b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e|
std.debug.panic("fizzy: could not read {s}: {}", .{ zon_rel_path, e });
const src = b.allocator.dupeSentinel(u8, raw, 0) catch @panic("OOM");
const Target = struct {
Expand Down
5 changes: 2 additions & 3 deletions sdk/plugin_sdk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ const core_module = @import("core_module.zig");
/// package — so the access check below picks whichever layout is present.
fn repoPath(b: *std.Build, sub_path: []const u8) std.Build.LazyPath {
b.root.access(b.graph.io, sub_path, .{}) catch {
const root = b.root.root_dir.path orelse @panic("fizzy sdk: missing build_root");
return .{ .cwd_relative = b.pathJoin(&.{ root, "..", sub_path }) };
return .{ .cwd_relative = b.root.joinString(b.allocator, b.pathJoin(&.{ "..", sub_path })) catch @panic("OOM") };
};
return b.path(sub_path);
}
Expand Down Expand Up @@ -99,7 +98,7 @@ pub fn readManifest(b: *std.Build) IdentityManifest {

fn readManifestAt(b: *std.Build, rel_path: []const u8) IdentityManifest {
b.dependOnFileContents(b.path(rel_path));
const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e|
const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, rel_path) catch @panic("OOM"), b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e|
std.debug.panic(
"fizzy: could not read {s}: {}\nEvery plugin needs a `plugin.zig.zon` manifest at its root (id/name/version) — see docs/PLUGINS.md.",
.{ rel_path, e },
Expand Down
63 changes: 54 additions & 9 deletions sdk/src/settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ pub fn isCell(comptime T: type) bool {

/// Compile error for a settings struct field that isn't a `Value(...)` cell. Bare fields used to
/// be the whole API, so this names the fix rather than just the rule.
/// True when a value of `T` holds a pointer anywhere in it. A settings payload may hold one only
/// when it is a string itself: the schema copies strings out of a parse and frees them, and owns
/// nothing else, so any other pointer would point into a parse's arena after it is freed.
fn holdsPointers(comptime T: type) bool {
return comptime switch (@typeInfo(T)) {
.pointer => true,
.optional => |o| holdsPointers(o.child),
.array => |a| holdsPointers(a.child),
.@"struct" => |st| for (st.field_types) |F| {
if (holdsPointers(F)) break true;
} else false,
.@"union" => |u| for (u.field_types) |F| {
if (holdsPointers(F)) break true;
} else false,
else => false,
};
}

fn requireCell(comptime T: type, comptime field_name: []const u8, comptime owner: type) void {
if (!isCell(T)) @compileError(
"sdk.settings.Schema: field '" ++ field_name ++ "' of " ++ @typeName(owner) ++
Expand Down Expand Up @@ -247,6 +265,11 @@ fn Plain(comptime T: type) type {
inline for (info.field_names, info.field_types, info.field_attrs, 0..) |f_name, Cell, cell_attrs, i| {
requireCell(Cell, f_name, T);
const P = Cell.Payload;
if (typeTagFor(P) != .string and holdsPointers(P)) @compileError(
"sdk.settings.Schema: field '" ++ f_name ++ "' of " ++ @typeName(T) ++ " is a " ++
@typeName(P) ++ ", which holds pointers. A setting owns strings and plain values " ++
"only; keep a list as a string, or as several settings.",
);
const cell_default = cell_attrs.defaultValue(Cell) orelse @compileError(
"sdk.settings.Schema: field '" ++ f_name ++ "' of " ++ @typeName(T) ++
" needs a default value (`= .init(<default>)`) — required so settings.zon only " ++
Expand Down Expand Up @@ -475,17 +498,29 @@ pub fn Schema(comptime T: type) type {
return;
};
// The parse's strings are in `arena`. One that is not the declared default is copied
// out, so it is this schema's to free (`freeOwned`); a default stays the default.
inline for (info.field_names, info.field_types) |f_name, Cell| {
if (comptime typeTagFor(Cell.Payload) == .string) {
const parsed_str = @field(parsed, f_name);
if (parsed_str.ptr != @field(default_payloads, f_name).ptr) {
@field(parsed, f_name) = gpa.dupe(u8, parsed_str) catch |err| {
dvui.log.warn("sdk.settings: out of memory keeping settings: {s}", .{@errorName(err)});
return;
};
// out, so it is this schema's to free (`freeOwned`); a default stays the default. A
// copy that fails frees the ones before it, and the live value is left as it was.
var copied: usize = 0;
const all_copied = copy: {
inline for (info.field_names, info.field_types, 0..) |f_name, Cell, i| {
if (comptime typeTagFor(Cell.Payload) == .string) {
const parsed_str = @field(parsed, f_name);
if (parsed_str.ptr != @field(default_payloads, f_name).ptr) {
@field(parsed, f_name) = gpa.dupe(u8, parsed_str) catch break :copy false;
}
}
copied = i + 1;
}
break :copy true;
};
if (!all_copied) {
inline for (info.field_names, info.field_types, 0..) |f_name, Cell, i| {
if (comptime typeTagFor(Cell.Payload) == .string) {
if (i < copied and @field(parsed, f_name).ptr != @field(default_payloads, f_name).ptr) gpa.free(@field(parsed, f_name));
}
}
dvui.log.warn("sdk.settings: out of memory keeping settings", .{});
return;
}
freeOwned(gpa, out.*);
fromPayloads(out, parsed);
Expand Down Expand Up @@ -1021,3 +1056,13 @@ test "a type with no dedicated control is still a legal setting, edited as zon t
// `.other` payloads take part in diff-vs-default like anything else.
try testing.expect(try S.diffSerialize(testing.allocator, .{}) == null);
}

test "a settings payload holds pointers only as a string" {
try std.testing.expect(!holdsPointers(u32));
try std.testing.expect(!holdsPointers(?[4]f32));
try std.testing.expect(!holdsPointers(enum { a, b }));
try std.testing.expect(holdsPointers([]const u8));
try std.testing.expect(holdsPointers([]const []const u8));
try std.testing.expect(holdsPointers(struct { n: u8, name: []const u8 }));
try std.testing.expect(holdsPointers(?struct { inner: [2]?*const u8 }));
}
Loading