From 5b6a325dd1afb3cf47911002bd3df5cfdfc6a8bf Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 12:25:47 -0500 Subject: [PATCH 1/3] build: pin fizzy at main, now that it builds as a URL dependency fizzyedit/fizzy#279 merged, and its branch, which this pinned, is gone. Pins main's a2b4e0a7. Co-Authored-By: Claude Opus 5.5 --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index c99466f..9e56fe6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ // by commit; fizzy's CI builds this repo against each of its pull requests, so a change // there that breaks an outside app fails before it lands. .fizzy = .{ - .url = "https://github.com/fizzyedit/fizzy/archive/f69f7943c3a5.tar.gz", - .hash = "fizzy-0.2.0-c46aVtNWigCgXBcB-4j9c5Cv9NhTQpG72wJc6itqil2m", + .url = "https://github.com/fizzyedit/fizzy/archive/a2b4e0a7d6b3.tar.gz", + .hash = "fizzy-0.2.0-c46aVokhiwCxp5KQ-d44REid0496AzQ8hGP_DHEcyykr", }, // The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any // plugin from its own repo, and this app's own, which also builds alone as a dylib From 39412c8399e2ebc82f3b15f2baf221f578b1d8d7 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 14:19:03 -0500 Subject: [PATCH 2/3] replay: a plain dvui app with tape playback, moved from fizzy's examples fizzy's examples/replay-app, now that SDK 0.2.19 ships replay: zig build run-replay. It depends only on the plugin SDK (a second dependency, pinned to sdk-v0.2.19), builds tape and replay against dvui's SDL3 backend, plays a tape into its window and prints what is on screen. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 + README.md | 6 ++ build.zig | 28 +++++++++ build.zig.zon | 8 ++- replay/main.zig | 123 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 replay/main.zig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f4934d..f4e14be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,5 @@ jobs: echo "::endgroup::" done zig build -Dshape=studio -Dzon-layout=true --summary all + - name: Build the replay app + run: zig build replay --summary all diff --git a/README.md b/README.md index 386bda0..3f8e307 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ Each is a whole fizzy app: fizzy's frame, its built-in plugins (files, text, ima in a layout of this repo's own, with two more plugins bundled in. Each shape gets its own executable, window title and config directory (`minimalapp`, `studioapp`, `endlessapp`). +`zig build run-replay` is a plain dvui app, nothing of fizzy, with tape playback from the plugin +SDK's `replay` (`replay/main.zig`). At launch it plays a live tape into its own window (clicks, +typing, a command), draws the tape's pointer, stops if you click or type, and when the tape ends +prints what is on screen as text: the names a test or an automation client reads instead of +pixels. It depends only on the SDK package. + `zig build run-dvui` is where a plain dvui app on fizzy's backend goes: dvui's own widgets, with floating windows, menus and dialogs as OS windows of their own (Liquid Glass windows on macOS). It needs fizzy's backend as a package an app can depend on without the rest of fizzy, which is diff --git a/build.zig b/build.zig index 797e881..c921dfe 100644 --- a/build.zig +++ b/build.zig @@ -3,11 +3,15 @@ //! `zig build run-fizzy` builds a fizzy app: fizzy's framework and its plugins, in one of the //! shapes in `shapes/` (`-Dshape=`), with this repo's plugins bundled in. //! +//! `zig build run-replay` builds a plain dvui app, nothing of fizzy, with tape playback from the +//! plugin SDK's `replay`: it plays a tape into its own window and prints what is on screen. +//! //! `zig build run-dvui` will build a plain dvui app on fizzy's backend: dvui's widgets, with //! floating windows, menus and dialogs as OS windows of their own. It needs the backend as a //! package of its own first. const std = @import("std"); const fizzy = @import("fizzy"); +const fizzy_sdk = @import("fizzy_sdk"); /// The layout shapes. Each is one file in `shapes/`, read end to end and copied, never /// configured: a shape that needs something new is a new file. @@ -67,6 +71,30 @@ pub fn build(b: *std.Build) !void { if (b.args) |args| run_fizzy.addArgs(args); b.step("run-fizzy", "Run the fizzy app in the shape -Dshape picks").dependOn(&run_fizzy.step); + // A plain dvui app with tape playback: dvui as any app has it (the SDK pins it, the app picks + // its own backend) and `tape` and `replay` built against it. Not part of the default install. + const sdk = b.dependency("fizzy_sdk", .{ .target = target, .optimize = optimize }); + const dvui_dep = sdk.builder.dependency("dvui", .{ .target = target, .optimize = optimize, .backend = .sdl3 }); + const dvui_mod = dvui_dep.module("dvui_sdl3"); + const automation = fizzy_sdk.replay.modules(b, sdk.builder, dvui_mod, target, optimize); + const replay_exe = b.addExecutable(.{ + .name = "replay-app", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("replay/main.zig"), + }), + }); + replay_exe.root_module.addImport("dvui", dvui_mod); + replay_exe.root_module.addImport("tape", automation.tape); + replay_exe.root_module.addImport("replay", automation.replay); + const install_replay = b.addInstallArtifact(replay_exe, .{}); + b.step("replay", "Build the replay app").dependOn(&install_replay.step); + const run_replay = b.addRunArtifact(replay_exe); + run_replay.step.dependOn(&install_replay.step); + if (b.args) |args| run_replay.addArgs(args); + b.step("run-replay", "Run the replay app: a plain dvui app with tape playback").dependOn(&run_replay.step); + // Where the dvui app goes: dvui's own widgets on fizzy's backend. Waits on the backend // becoming a package an app depends on without the rest of fizzy. b.step("run-dvui", "Run a plain dvui app on fizzy's backend (not yet)").dependOn( diff --git a/build.zig.zon b/build.zig.zon index 9e56fe6..03b4b54 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,6 +19,12 @@ .hash = "hello-0.1.0-c1Gg4rQXAAA7Ea4vZqD4jAS7eLtXJW3QglYM4HB-17Lw", }, .shader = .{ .path = "plugins/shader" }, + // The plugin SDK alone, for the replay app: its pinned dvui and `tape` / `replay`, with + // nothing of fizzy the app. + .fizzy_sdk = .{ + .url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.2.19/fizzy-sdk-v0.2.19.tar.gz", + .hash = "fizzy-0.2.19-SGBwY8MiIQDGX5QO1AMQHemD99HG9DNnjTLQ4Pu1bpf5", + }, }, - .paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "README.md", "LICENSE" }, + .paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "replay", "README.md", "LICENSE" }, } diff --git a/replay/main.zig b/replay/main.zig new file mode 100644 index 0000000..615115d --- /dev/null +++ b/replay/main.zig @@ -0,0 +1,123 @@ +//! A plain dvui app with tape playback, and nothing of fizzy's. +//! +//! At launch it plays a live tape into its own window: two clicks on Count, a click into the field +//! and some typing, a command that resets the count, and one more click — so it ends on 1. The tape aims at widgets by their tags, so it plays at any +//! window size; the tape's pointer and its clicks are drawn over the app (`replay.overlay`); a +//! click or key from a person stops it. When it ends, the app prints what is on screen, as text +//! (`replay.Snapshot`) — the names a test or an automation client reads instead of pixels. +//! +//! What an app gives `replay`: a `Stage` (`idle` and `command` are enough for a live tape), +//! `LiveDriver.frame` and `Snapshot.beginFrame` first thing in its frame, and the overlay and +//! `Snapshot.endFrame` last. +const std = @import("std"); +const dvui = @import("dvui"); +const replay = @import("replay"); +const Script = @import("tape").Script; + +pub const dvui_app: dvui.App = .{ + .config = .{ .options = .{ + .size = .{ .w = 520, .h = 360 }, + .title = "Replay example", + } }, + .frameFn = frame, + .initFn = init, + .deinitFn = deinit, +}; +pub const main = dvui.App.main; +pub const panic = dvui.App.panic; +pub const std_options: std.Options = .{ .logFn = dvui.App.logFn }; + +var gpa: std.mem.Allocator = undefined; +var count: usize = 0; +var driver: replay.LiveDriver = undefined; +var snapshot: replay.Snapshot = undefined; +/// The snapshot asked for when the last tape ended, until it is printed. +var shot: ?u32 = null; +var played_once = false; +/// The last tape's end has been read back. +var reported = true; + +// ---- the stage: what this app is to a tape ------------------------------------------------- + +const stage_vtable: replay.Stage.VTable = .{ .idle = idle, .command = command }; + +/// Nothing here loads in the background: always settled. +fn idle(_: *anyopaque) bool { + return true; +} + +/// The one command this app offers a tape. +fn command(_: *anyopaque, id: []const u8, _: []const u8) void { + if (std.mem.eql(u8, id, "example.reset")) count = 0; +} + +// ---- the app ------------------------------------------------------------------------------- + +fn init(win: *dvui.Window) !void { + gpa = win.gpa; + driver = .init(.{ .ctx = &count, .vtable = &stage_vtable }); + snapshot = .init(gpa); +} + +fn deinit(_: *dvui.Window) void { + driver.deinit(); + snapshot.deinit(); +} + +/// The tape: written with `tape.Script`, aimed at tags rather than positions. +fn demoTape() !@import("tape").Tape.Owned { + var s: Script = .init(gpa, "example", "Count, type, reset"); + errdefer s.deinit(); + s.check = replay.Input.check; + s.check.live = true; + try s.click(.{ .tag = "example.count" }, .{}); + try s.click(.{ .tag = "example.count" }, .{}); + try s.click(.{ .tag = "example.field", .x = 0.5, .y = 0.5 }, .{}); + try s.typeText("typed by a tape", .{}); + s.pause(600); + try s.command("example.reset"); + try s.click(.{ .tag = "example.count" }, .{}); + return s.finish(); +} + +fn play() void { + driver.play(demoTape() catch return) catch |err| return std.log.err("tape refused: {t}", .{err}); + reported = false; +} + +fn frame() !dvui.App.Result { + // First, before anything reads the frame's events. + snapshot.beginFrame(); + driver.frame(); + if (!played_once) { + played_once = true; + play(); + } + + { + var box = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .both, .padding = .all(16), .background = true, .style = .window }); + defer box.deinit(); + dvui.label(@src(), "Clicked {d} times", .{count}, .{}); + if (dvui.button(@src(), "Count", .{}, .{ .tag = "example.count" })) count += 1; + var entry = dvui.textEntry(@src(), .{}, .{ .tag = "example.field", .expand = .horizontal }); + entry.deinit(); + if (dvui.button(@src(), "Play the tape again", .{}, .{}) and !driver.playing()) play(); + if (driver.outcome) |o| dvui.label(@src(), "Last tape: {t}", .{o}, .{}); + } + + // When a tape ends, read the screen back as text. + if (!driver.playing() and !reported) { + reported = true; + std.debug.print("The tape ended ({any}); the count is {d}.\n", .{ driver.outcome, count }); + shot = snapshot.request(); + } + if (shot) |n| if (snapshot.get(n)) |text| { + std.debug.print("What is on screen:\n{s}", .{text}); + shot = null; + }; + + // Last, over everything: the tape's pointer, then the snapshot written out. + replay.overlay.drawLive(&driver, .{}); + snapshot.endFrame(); + return .ok; +} From 4e091ed57793cf76621ca5e04b57093f39b1f589 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 14:23:35 -0500 Subject: [PATCH 3/3] build: pin fizzy at main with the cold-cache fix fizzyedit/fizzy#297: an app built on fizzy configures on a cold cache, which every CI run here is. Pins main's 4649893e. Co-Authored-By: Claude Opus 5.5 --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 03b4b54..7b2d321 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ // by commit; fizzy's CI builds this repo against each of its pull requests, so a change // there that breaks an outside app fails before it lands. .fizzy = .{ - .url = "https://github.com/fizzyedit/fizzy/archive/a2b4e0a7d6b3.tar.gz", - .hash = "fizzy-0.2.0-c46aVokhiwCxp5KQ-d44REid0496AzQ8hGP_DHEcyykr", + .url = "https://github.com/fizzyedit/fizzy/archive/4649893e4d27.tar.gz", + .hash = "fizzy-0.2.0-c46aVrUuiwDNap4KtjKj7wmmReYkOlSa5z4p7hDBUCgu", }, // The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any // plugin from its own repo, and this app's own, which also builds alone as a dylib