diff --git a/sdk/remote/Client.zig b/sdk/remote/Client.zig index 1d93f469e..09d9bc1f7 100644 --- a/sdk/remote/Client.zig +++ b/sdk/remote/Client.zig @@ -135,17 +135,28 @@ pub fn deinit(self: *Client) void { if (self.socket_path) |p| self.gpa.free(p); } -/// Where fizzy would be installed on `env.os`, likeliest first; not `$PATH` (`onPath`). Touches nothing. +/// Where fizzy would be installed on `env.os`, likeliest first; not `$PATH` (`onPath`). Touches +/// nothing. pub fn candidates(arena: std.mem.Allocator, identity: Identity, env: Env) error{OutOfMemory}![]const Install { var list: std.ArrayList(Install) = .empty; const sep: []const u8 = if (env.os == .windows) "\\" else "/"; switch (env.os) { .macos => { - const roots = [_]?[]const u8{ "/Applications", if (env.home) |h| try join(arena, "/", &.{ h, "Applications" }) else null }; + const roots = [_]?[]const u8{ + "/Applications", + if (env.home) |h| try join( + arena, + "/", + &.{ h, "Applications" }, + ) else null, + }; for (roots) |root_opt| { const root = root_opt orelse continue; const app = try std.fmt.allocPrint(arena, "{s}/{s}.app", .{ root, identity.display_name }); - try list.append(arena, .{ .exe = try join(arena, "/", &.{ app, "Contents/MacOS", identity.name }), .bundle = app }); + try list.append( + arena, + .{ .exe = try join(arena, "/", &.{ app, "Contents/MacOS", identity.name }), .bundle = app }, + ); } }, .windows => if (env.local_app_data) |base| { @@ -209,7 +220,11 @@ fn lookupSystem(io: Io, arena: std.mem.Allocator, identity: Identity, env: Env) switch (env.os) { .macos => { if (std.mem.findAny(u8, identity.bundle_id, "\"\\") != null) return null; - const query = std.fmt.allocPrint(arena, "kMDItemCFBundleIdentifier == \"{s}\"", .{identity.bundle_id}) catch return null; + const query = std.fmt.allocPrint( + arena, + "kMDItemCFBundleIdentifier == \"{s}\"", + .{identity.bundle_id}, + ) catch return null; const out = runCapture(io, arena, &.{ "/usr/bin/mdfind", query }) orelse return null; const bundle = std.mem.trim(u8, firstLine(out), " \t\r"); if (bundle.len == 0) return null; @@ -217,7 +232,11 @@ fn lookupSystem(io: Io, arena: std.mem.Allocator, identity: Identity, env: Env) return if (exists(io, exe)) .{ .exe = exe, .bundle = bundle } else null; }, .windows => for ([_][]const u8{ "HKCU", "HKLM" }) |hive| { - const key = std.fmt.allocPrint(arena, "{s}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{s}.exe", .{ hive, identity.name }) catch return null; + const key = std.fmt.allocPrint( + arena, + "{s}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{s}.exe", + .{ hive, identity.name }, + ) catch return null; const out = runCapture(io, arena, &.{ "reg", "query", key, "/ve" }) orelse continue; const exe = parseRegValue(out) orelse continue; if (exists(io, exe)) return .{ .exe = exe }; @@ -241,7 +260,11 @@ fn firstLine(text: []const u8) []const u8 { /// Stdout of a run that exited 0, or null. fn runCapture(io: Io, arena: std.mem.Allocator, argv: []const []const u8) ?[]const u8 { - const result = std.process.run(arena, io, .{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(5), .clock = .awake } } }) catch return null; + const result = std.process.run( + arena, + io, + .{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(5), .clock = .awake } } }, + ) catch return null; return switch (result.term) { .exited => |code| if (code == 0) result.stdout else null, else => null, @@ -254,7 +277,11 @@ pub fn controlPath(io: Io, arena: std.mem.Allocator, install: Install, profile: &.{ install.exe, "--control-path", "--profile", p } else &.{ install.exe, "--control-path" }; - const result = std.process.run(arena, io, .{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(10), .clock = .awake } } }) catch |err| return spawnError(err); + const result = std.process.run( + arena, + io, + .{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(10), .clock = .awake } } }, + ) catch |err| return spawnError(err); // A fizzy that predates `--control-path` starts as an editor instead: no path to read. const ok = switch (result.term) { .exited => |code| code == 0, @@ -358,7 +385,10 @@ fn timeoutMs(ms: u32) Io.Timeout { /// `timeout` passed first. pub fn readSome(io: Io, stream: Io.net.Stream, buf: []u8, timeout: Io.Timeout) error{Closed}!?usize { var data: [1][]u8 = .{buf}; - const result = io.operateTimeout(.{ .net_read = .{ .socket_handle = stream.socket.handle, .data = &data } }, timeout) catch |err| switch (err) { + const result = io.operateTimeout( + .{ .net_read = .{ .socket_handle = stream.socket.handle, .data = &data } }, + timeout, + ) catch |err| switch (err) { error.Timeout => return null, else => return error.Closed, }; @@ -428,14 +458,23 @@ pub fn connect(self: *Client) Error!void { /// `GET /remote` on `stream`, then the hellos. Takes ownership of `stream`. fn upgrade(self: *Client, stream: Io.net.Stream, deadline: Deadline) Error!void { errdefer stream.close(self.io); - try writeAll(self.io, stream, "GET /remote HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: " ++ upgrade_token ++ "\r\n\r\n"); + try writeAll( + self.io, + stream, + "GET /remote HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: " ++ upgrade_token ++ "\r\n\r\n", + ); var head: [2048]u8 = undefined; var len: usize = 0; const body_at = while (true) { if (std.mem.find(u8, head[0..len], "\r\n\r\n")) |at| break at + 4; if (len == head.len) return error.Protocol; - const n = try readSome(self.io, stream, head[len..], timeoutMs(deadline.remainingMs())) orelse return error.TimedOut; + const n = try readSome( + self.io, + stream, + head[len..], + timeoutMs(deadline.remainingMs()), + ) orelse return error.TimedOut; if (n == 0) return error.Closed; len += n; }; @@ -509,7 +548,8 @@ pub fn connected(self: *const Client) bool { } /// Check `msg` against the conversation's rules and send it. A message the rules refuse is the -/// caller's mistake: it is not sent and the stream stays open. A write that fails closes the client. +/// caller's mistake: it is not sent and the stream stays open. A write that fails closes the +/// client. pub fn send(self: *Client, msg: message.Message) Error!void { const ep = &(self.endpoint orelse return error.Closed); if (self.stream == null) return self.closeStale(); @@ -598,7 +638,11 @@ test candidates { const mac_path = try onPath(arena, id, .{ .os = .macos, .path = "/usr/bin:/opt/bin" }); try std.testing.expectEqualStrings("/opt/bin/fizzy", mac_path[1].exe); - const win = try candidates(arena, id, .{ .os = .windows, .local_app_data = "C:\\Users\\a\\AppData\\Local", .path = "C:\\bin;D:\\tools" }); + const win = try candidates( + arena, + id, + .{ .os = .windows, .local_app_data = "C:\\Users\\a\\AppData\\Local", .path = "C:\\bin;D:\\tools" }, + ); try std.testing.expectEqualStrings("C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe", win[0].exe); const win_path = try onPath(arena, id, .{ .os = .windows, .path = "C:\\bin;D:\\tools" }); try std.testing.expectEqualStrings("D:\\tools\\fizzy.exe", win_path[1].exe); @@ -607,11 +651,16 @@ test candidates { try std.testing.expectEqualStrings("/home/a/.local/share/paint/current/paint", linux[0].exe); try std.testing.expectEqualStrings("/home/a/Applications/paint.AppImage", linux[2].exe); try std.testing.expectEqual(@as(usize, 3), linux.len); - try std.testing.expectEqualStrings("/usr/bin/paint", (try onPath(arena, .{ .name = "paint" }, .{ .os = .linux, .path = "/usr/bin" }))[0].exe); + try std.testing.expectEqualStrings( + "/usr/bin/paint", + (try onPath(arena, .{ .name = "paint" }, .{ .os = .linux, .path = "/usr/bin" }))[0].exe, + ); } test parseRegValue { - const out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\fizzy.exe\r\n (Default) REG_SZ C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe\r\n"; + const out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion" ++ + "\\App Paths\\fizzy.exe\r\n (Default) REG_SZ " ++ + "C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe\r\n"; try std.testing.expectEqualStrings("C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe", parseRegValue(out).?); try std.testing.expect(parseRegValue("ERROR: The system was unable to find the specified registry key") == null); } diff --git a/sdk/remote/Connection.zig b/sdk/remote/Connection.zig index 9ac3b3d4e..05a6fffd9 100644 --- a/sdk/remote/Connection.zig +++ b/sdk/remote/Connection.zig @@ -153,7 +153,11 @@ fn apply(self: *Connection, msg: Message, from: Role) Error!void { .create_view => |m| { if (m.view == 0) return error.UnknownView; if (self.views.contains(m.view)) return error.DuplicateView; - try self.views.put(self.gpa, m.view, .{ .tier = m.tier, .width = m.width, .height = m.height, .scale = m.scale }); + try self.views.put( + self.gpa, + m.view, + .{ .tier = m.tier, .width = m.width, .height = m.height, .scale = m.scale }, + ); }, .view_created => |m| { const v = self.views.getPtr(m.view) orelse return error.UnknownView; @@ -254,14 +258,23 @@ test "a view's life, and the frames that answer requests" { try say(&p.editor, &p.host, .{ .view_created = .{ .view = 7, .tier = .draw_list, .width = 100, .height = 50 } }); try t.expect(p.host.view(7).?.focused); - try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 7, .seq = 1, .tier = .draw_list } })); + try t.expectError( + error.UnexpectedFrame, + p.editor.sending(.{ .frame = .{ .view = 7, .seq = 1, .tier = .draw_list } }), + ); try say(&p.host, &p.editor, .{ .frame_request = .{ .view = 7, .seq = 1 } }); try t.expectError(error.SeqNotIncreasing, p.host.sending(.{ .frame_request = .{ .view = 7, .seq = 1 } })); try say(&p.host, &p.editor, .{ .frame_request = .{ .view = 7, .seq = 3 } }); // Fizzy may skip seq 1 and answer 3 only; 2 was never asked, and 3 cannot be answered twice. try say(&p.editor, &p.host, .{ .frame = .{ .view = 7, .seq = 3, .tier = .draw_list } }); - try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 7, .seq = 3, .tier = .draw_list } })); - try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 7, .seq = 4, .tier = .draw_list } })); + try t.expectError( + error.UnexpectedFrame, + p.editor.sending(.{ .frame = .{ .view = 7, .seq = 3, .tier = .draw_list } }), + ); + try t.expectError( + error.UnexpectedFrame, + p.editor.sending(.{ .frame = .{ .view = 7, .seq = 4, .tier = .draw_list } }), + ); try say(&p.host, &p.editor, .{ .close_view = .{ .view = 7 } }); try t.expectError(error.ViewClosing, p.host.sending(.{ .resize = .{ .view = 7, .width = 1, .height = 1 } })); @@ -287,7 +300,10 @@ test "each side may send only its own messages" { defer p.editor.deinit(); try open(&p); try t.expectError(error.WrongDirection, p.host.sending(.{ .cursor = .{ .view = 1, .shape = .arrow } })); - try t.expectError(error.WrongDirection, p.host.received(.{ .create_view = .{ .view = 1, .width = 1, .height = 1 } })); + try t.expectError( + error.WrongDirection, + p.host.received(.{ .create_view = .{ .view = 1, .width = 1, .height = 1 } }), + ); } test "no common version closes, naming who updates" { @@ -332,7 +348,13 @@ test "view 0 is no view, and a frame must match its view's tier" { try t.expectError(error.UnknownView, p.host.sending(.{ .create_view = .{ .view = 0, .width = 1, .height = 1 } })); try say(&p.host, &p.editor, .{ .create_view = .{ .view = 1, .width = 1, .height = 1 } }); try say(&p.host, &p.editor, .{ .frame_request = .{ .view = 1, .seq = 1 } }); - try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .draw_list } })); + try t.expectError( + error.UnexpectedFrame, + p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .draw_list } }), + ); try say(&p.editor, &p.host, .{ .view_created = .{ .view = 1, .tier = .draw_list, .width = 1, .height = 1 } }); - try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .shared_texture } })); + try t.expectError( + error.UnexpectedFrame, + p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .shared_texture } }), + ); } diff --git a/sdk/remote/capi.zig b/sdk/remote/capi.zig index 66a25cb20..0b9134a33 100644 --- a/sdk/remote/capi.zig +++ b/sdk/remote/capi.zig @@ -92,7 +92,8 @@ export fn fizzy_remote_open(options: ?*const Options, out: ?*?*Handle) callconv( const o = options orelse return @backingInt(Status.invalid_argument); const result = out orelse return @backingInt(Status.invalid_argument); result.* = null; - if (o.size < @offsetOf(Options, "socket_path") + @sizeOf(?[*:0]const u8)) return @backingInt(Status.invalid_argument); + if (o.size < @offsetOf(Options, "socket_path") + @sizeOf(?[*:0]const u8)) + return @backingInt(Status.invalid_argument); const handle = gpa.create(Handle) catch return @backingInt(Status.out_of_memory); handle.* = .{ .arena = .init(gpa), .threaded = .init(gpa, .{ .environ = processEnviron() }), .client = undefined }; @@ -156,8 +157,10 @@ export fn fizzy_remote_send(handle: ?*Handle, frame: ?[*]const u8, len: usize) c reader.push(bytes) catch return @backingInt(Status.out_of_memory); var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); - const frame_in = (reader.next() catch return @backingInt(Status.protocol)) orelse return @backingInt(Status.protocol); - const msg = (codec.decode(arena.allocator(), frame_in) catch return @backingInt(Status.protocol)) orelse return @backingInt(Status.protocol); + const frame_in = (reader.next() catch return @backingInt(Status.protocol)) orelse + return @backingInt(Status.protocol); + const msg = (codec.decode(arena.allocator(), frame_in) catch return @backingInt(Status.protocol)) orelse + return @backingInt(Status.protocol); h.client.send(msg) catch |err| return status(err); return 0; } @@ -165,7 +168,13 @@ export fn fizzy_remote_send(handle: ?*Handle, frame: ?[*]const u8, len: usize) c /// The next frame from fizzy into `buf`, waiting up to `timeout_ms` (negative: forever). /// `*len` is its length; 0 with `ok` means the wait passed. A frame larger than `cap` is kept: /// `buffer_too_small` with `*len` the size needed, then call again with room. -export fn fizzy_remote_recv(handle: ?*Handle, buf: ?[*]u8, cap: usize, len: ?*usize, timeout_ms: i32) callconv(.c) c_int { +export fn fizzy_remote_recv( + handle: ?*Handle, + buf: ?[*]u8, + cap: usize, + len: ?*usize, + timeout_ms: i32, +) callconv(.c) c_int { const h = handle orelse return @backingInt(Status.invalid_argument); const out_len = len orelse return @backingInt(Status.invalid_argument); out_len.* = 0; @@ -248,7 +257,12 @@ test "the C ABI opens, round trips a frame and closes" { var out: std.ArrayList(u8) = .empty; defer out.deinit(t.allocator); - try codec.encode(t.allocator, &out, .{ .create_view = .{ .view = 3, .width = 64, .height = 48 } }, codec.default_max_frame); + try codec.encode( + t.allocator, + &out, + .{ .create_view = .{ .view = 3, .width = 64, .height = 48 } }, + codec.default_max_frame, + ); try t.expectEqual(@as(c_int, 0), fizzy_remote_send(handle, out.items.ptr, out.items.len)); var small: [4]u8 = undefined; diff --git a/sdk/remote/fake_fizzy.zig b/sdk/remote/fake_fizzy.zig index 6324717b4..19974bda7 100644 --- a/sdk/remote/fake_fizzy.zig +++ b/sdk/remote/fake_fizzy.zig @@ -48,7 +48,12 @@ pub fn main(init: std.process.Init) !void { } if (quiet) try appendLaunch(gpa, io, dir); - const text = try Io.Dir.cwd().readFileAlloc(io, try std.fmt.allocPrint(gpa, "{s}/mode", .{dir}), gpa, .limited(256)); + const text = try Io.Dir.cwd().readFileAlloc( + io, + try std.fmt.allocPrint(gpa, "{s}/mode", .{dir}), + gpa, + .limited(256), + ); var words = std.mem.tokenizeAny(u8, text, " \r\n"); const mode = std.meta.stringToEnum(Mode, words.next() orelse "serve") orelse .serve; switch (mode) { @@ -91,9 +96,23 @@ fn talk(gpa: std.mem.Allocator, io: Io, stream: Io.net.Stream, mode: Mode, n: us len += got; }; if (mode == .noremote) return Client.writeAll(io, stream, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"); - try Client.writeAll(io, stream, "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: " ++ Client.upgrade_token ++ "\r\n\r\n"); + try Client.writeAll( + io, + stream, + "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: " ++ Client.upgrade_token ++ "\r\n\r\n", + ); - var ep: Endpoint = .init(gpa, .editor, .{ .app_id = "fizzy-fake", .mode = .shared, .tiers_offered = .{ .draw_list = true }, .version_min = if (mode == .old) 9 else message.min_version, .version_max = if (mode == .old) 9 else message.max_version }); + var ep: Endpoint = .init( + gpa, + .editor, + .{ + .app_id = "fizzy-fake", + .mode = .shared, + .tiers_offered = .{ .draw_list = true }, + .version_min = if (mode == .old) 9 else message.min_version, + .version_max = if (mode == .old) 9 else message.max_version, + }, + ); defer ep.deinit(); try ep.start(); try ep.feed(head[body_at..len]); @@ -107,7 +126,15 @@ fn talk(gpa: std.mem.Allocator, io: Io, stream: Io.net.Stream, mode: Mode, n: us try Client.writeAll(io, stream, ep.pending()); return; }, - .create_view => |cv| if (mode == .bye) try ep.send(.{ .bye = .{ .reason = .shutdown, .text = "going away" } }) else try ep.send(.{ .view_created = .{ .view = cv.view, .tier = .draw_list, .width = cv.width, .height = cv.height } }), + .create_view => |cv| if (mode == .bye) + try ep.send(.{ .bye = .{ .reason = .shutdown, .text = "going away" } }) + else + try ep.send(.{ .view_created = .{ + .view = cv.view, + .tier = .draw_list, + .width = cv.width, + .height = cv.height, + } }), .bye => return, else => {}, }; diff --git a/sdk/remote/message.zig b/sdk/remote/message.zig index 9b9212802..fa90b8dd4 100644 --- a/sdk/remote/message.zig +++ b/sdk/remote/message.zig @@ -47,7 +47,16 @@ pub const Type = enum(u16) { /// know an optional type skips it by its length. Sent in the frame header. pub fn required(t: Type) bool { return switch (t) { - .hello, .bye, .create_view, .view_created, .close_view, .view_closed, .resize, .frame_request, .frame => true, + .hello, + .bye, + .create_view, + .view_created, + .close_view, + .view_closed, + .resize, + .frame_request, + .frame, + => true, else => false, }; } @@ -56,7 +65,18 @@ pub const Type = enum(u16) { pub fn sender(t: Type) Sender { return switch (t) { .hello, .bye => .either, - .view_created, .view_closed, .cursor, .ime_rect, .text_input_active, .clipboard_request, .wants_keys, .open_url, .drag_begin, .frame, .frame_ready => .editor, + .view_created, + .view_closed, + .cursor, + .ime_rect, + .text_input_active, + .clipboard_request, + .wants_keys, + .open_url, + .drag_begin, + .frame, + .frame_ready, + => .editor, else => .host, }; } diff --git a/sdk/remote/root.zig b/sdk/remote/root.zig index 0214b791a..2b9ffce6b 100644 --- a/sdk/remote/root.zig +++ b/sdk/remote/root.zig @@ -10,7 +10,8 @@ //! * `FrameReader` — cuts a byte stream into frames, in whatever chunks it arrives. //! * `Connection` — the rules of the conversation, with no bytes in it. //! * `Endpoint` — a `Connection` with the bytes around it, and a loopback `pump` for tests. -//! * `Client` — finds, quietly starts and connects to the installed fizzy; `capi.zig` is its C ABI. +//! * `Client` — finds, quietly starts and connects to the installed fizzy; `capi.zig` is its +//! C ABI. pub const message = @import("message.zig"); pub const codec = @import("codec.zig"); pub const FrameReader = @import("FrameReader.zig"); diff --git a/sdk/remote/tests.zig b/sdk/remote/tests.zig index e2d46f5ed..5eb83da72 100644 --- a/sdk/remote/tests.zig +++ b/sdk/remote/tests.zig @@ -11,15 +11,40 @@ fn sample(comptime tag: message.Type) Message { const rect: message.Rect = .{ .x = 1, .y = 2.5, .w = 3, .h = -4 }; const mods: message.Mods = .{ .shift = true, .super = true }; return switch (tag) { - .hello => .{ .hello = .{ .version_min = 1, .version_max = 3, .app_id = "it.fizzy", .app_version = "0.9.1", .tiers_offered = .{ .draw_list = true, .shared_texture = true }, .tiers_wanted = .{ .draw_list = true }, .features = 0x8000_0000_0000_0001, .mode = .standalone } }, + .hello => .{ .hello = .{ + .version_min = 1, + .version_max = 3, + .app_id = "it.fizzy", + .app_version = "0.9.1", + .tiers_offered = .{ .draw_list = true, .shared_texture = true }, + .tiers_wanted = .{ .draw_list = true }, + .features = 0x8000_0000_0000_0001, + .mode = .standalone, + } }, .bye => .{ .bye = .{ .reason = .shutdown, .text = "going away" } }, - .create_view => .{ .create_view = .{ .view = 9, .kind = .document, .target = "/a/b.md", .width = 1920, .height = 1080, .scale = 2, .tier = .native_surface, .native_handle = 0xdead_beef_cafe } }, + .create_view => .{ .create_view = .{ + .view = 9, + .kind = .document, + .target = "/a/b.md", + .width = 1920, + .height = 1080, + .scale = 2, + .tier = .native_surface, + .native_handle = 0xdead_beef_cafe, + } }, .view_created => .{ .view_created = .{ .view = 9, .tier = .shared_texture, .width = 10, .height = 20 } }, .close_view => .{ .close_view = .{ .view = 9 } }, .view_closed => .{ .view_closed = .{ .view = 9, .reason = .create_failed, .text = "no such region" } }, .resize => .{ .resize = .{ .view = 9, .width = 800, .height = 600, .scale = 1.5, .rect = rect } }, .pointer_motion => .{ .pointer_motion = .{ .view = 9, .x = 10.5, .y = -3, .mods = mods } }, - .pointer_button => .{ .pointer_button = .{ .view = 9, .button = 2, .pressed = true, .x = 1, .y = 2, .mods = mods } }, + .pointer_button => .{ .pointer_button = .{ + .view = 9, + .button = 2, + .pressed = true, + .x = 1, + .y = 2, + .mods = mods, + } }, .wheel => .{ .wheel = .{ .view = 9, .dx = 0, .dy = -120, .mods = mods } }, .touch => .{ .touch = .{ .view = 9, .id = 4, .phase = .move, .kind = .pen, .x = 5, .y = 6, .pressure = 0.25 } }, .key => .{ .key = .{ .view = 9, .down = true, .repeat = true, .host_code = 65, .code = 0x1e, .mods = mods } }, @@ -164,7 +189,10 @@ test "unknown types: optional skipped, required refused" { defer arena.deinit(); const unknown: message.Type = @fromBackingInt(@intCast(5000)); try t.expect(try codec.decode(arena.allocator(), .{ .type = unknown, .flags = 0, .payload = "abc" }) == null); - try t.expectError(error.UnknownRequired, codec.decode(arena.allocator(), .{ .type = unknown, .flags = codec.flag_required, .payload = "abc" })); + try t.expectError( + error.UnknownRequired, + codec.decode(arena.allocator(), .{ .type = unknown, .flags = codec.flag_required, .payload = "abc" }), + ); // Through a stream, the skipped frame does not disturb its neighbours. var bytes: std.ArrayList(u8) = .empty; @@ -208,7 +236,10 @@ test "oversize messages are refused and leave the buffer alone" { try codec.encode(t.allocator, &out, sample(.focus), 1024); const before = out.items.len; const big: [2000]u8 = @splat(0); - try t.expectError(error.FrameTooLarge, codec.encode(t.allocator, &out, .{ .text_input = .{ .view = 1, .text = &big } }, 1024)); + try t.expectError( + error.FrameTooLarge, + codec.encode(t.allocator, &out, .{ .text_input = .{ .view = 1, .text = &big } }, 1024), + ); try t.expectEqual(before, out.items.len); } @@ -225,7 +256,8 @@ test "fuzz: random and mutated frames never crash, only error" { for (0..300) |_| { const copy = try t.allocator.dupe(u8, bytes.items); defer t.allocator.free(copy); - for (0..rnd.intRangeAtMost(usize, 1, 4)) |_| copy[8 + rnd.uintLessThan(usize, copy.len - 8 + @intFromBool(copy.len == 8))] ^= rnd.int(u8); + for (0..rnd.intRangeAtMost(usize, 1, 4)) |_| + copy[8 + rnd.uintLessThan(usize, copy.len - 8 + @intFromBool(copy.len == 8))] ^= rnd.int(u8); _ = arena.reset(.retain_capacity); _ = codec.decode(arena.allocator(), frameOf(copy)) catch {}; } diff --git a/sdk/tape/Script.zig b/sdk/tape/Script.zig index 1f2cd865e..13188b602 100644 --- a/sdk/tape/Script.zig +++ b/sdk/tape/Script.zig @@ -142,9 +142,16 @@ pub fn keyframe(self: *Script, kf: Tape.Keyframe) !void { const open = try a.alloc([]const u8, kf.open.len); for (kf.open, open) |p, *out| out.* = try self.dupe(p); const settings = try a.alloc(Tape.Setting, kf.settings.len); - for (kf.settings, settings) |st, *out| out.* = .{ .owner = try self.dupe(st.owner), .key = try self.dupe(st.key), .value = try self.dupe(st.value) }; + for (kf.settings, settings) |st, *out| out.* = .{ + .owner = try self.dupe(st.owner), + .key = try self.dupe(st.key), + .value = try self.dupe(st.value), + }; const root = try self.dupe(kf.root); - try self.keyframes.append(a, .{ .root = root, .files = files, .open = open, .layout = kf.layout, .settings = settings }); + try self.keyframes.append( + a, + .{ .root = root, .files = files, .open = open, .layout = kf.layout, .settings = settings }, + ); try self.push(.{ .at = self.t, .do = .{ .keyframe = @intCast(self.keyframes.items.len - 1) } }); try self.push(.{ .at = self.t, .do = .{ .wait = .{ .until = .idle } } }); self.root = root; @@ -220,12 +227,18 @@ pub const WaitOptions = struct { /// Hold the demo until the tagged widget is drawn. pub fn waitFor(self: *Script, tag: []const u8, opts: WaitOptions) !void { - try self.push(.{ .at = self.t, .do = .{ .wait = .{ .until = .{ .shown = try self.dupe(tag) }, .timeout = opts.timeout } } }); + try self.push(.{ + .at = self.t, + .do = .{ .wait = .{ .until = .{ .shown = try self.dupe(tag) }, .timeout = opts.timeout } }, + }); } /// Hold the demo until the tagged widget is gone. pub fn waitGone(self: *Script, tag: []const u8, opts: WaitOptions) !void { - try self.push(.{ .at = self.t, .do = .{ .wait = .{ .until = .{ .gone = try self.dupe(tag) }, .timeout = opts.timeout } } }); + try self.push(.{ + .at = self.t, + .do = .{ .wait = .{ .until = .{ .gone = try self.dupe(tag) }, .timeout = opts.timeout } }, + }); } /// Hold the demo until the stage has nothing in flight. @@ -559,7 +572,13 @@ test "a caption shows for a time per word, never under the floor" { try testing.expectEqual(@as(u32, 2000), readingMs("", "a b c d e f")); try testing.expectEqual(@as(u32, 2100), readingMs("", "a b c d e f g")); // Title and text both count, and a long caption is not cut short. - try testing.expectEqual(@as(u32, 300 * 30), readingMs("one two three four five", "a b c d e f g h i j k l m n o p q r s t u v w x y")); + try testing.expectEqual( + @as(u32, 300 * 30), + readingMs("one two three four five", "a b c d e f g h i j k l m n o p q r s t u v w x y"), + ); // Past the ceiling a caption stops growing. - try testing.expectEqual(reading_max_ms, readingMs("", "w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w")); + try testing.expectEqual( + reading_max_ms, + readingMs("", "w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w"), + ); } diff --git a/sdk/tape/Sequencer.zig b/sdk/tape/Sequencer.zig index bfd190090..fabf38ddb 100644 --- a/sdk/tape/Sequencer.zig +++ b/sdk/tape/Sequencer.zig @@ -76,10 +76,10 @@ const Glide = struct { /// Demo time the pointer was last put down along the path. at: f64, /// Begun with a button held: carrying something, the pointer goes where the target was seen - /// to settle, and does not follow it after (`locked`). What it is carried over moves for it — a tab strip - /// makes room for the tab, a drop opens under it — and chasing that moved the room it made: - /// the tab carried at a tab went back and forth between two places every other frame. A person - /// aims where they saw it, too. + /// to settle, and does not follow it after (`locked`). What it is carried over moves for it — a + /// tab strip makes room for the tab, a drop opens under it — and chasing that moved the room + /// it made: the tab carried at a tab went back and forth between two places every other frame. + /// A person aims where they saw it, too. carried: bool = false, /// Carried, and the target seen in the same place on two frames: `to` is where it goes. locked: bool = false, @@ -312,7 +312,12 @@ pub fn advance(self: *Sequencer, until: f64, wall_ms: f64, sink: Sink) Progress // A move only: a stroke's points are on a canvas that is not going anywhere, and a // drawing has hundreds of them. if (op.do == .move) if (sink.vtable.still) |still| if (!still(sink.ctx, target)) return .holding; - self.glide = .{ .op = self.cursor, .from = self.pointer, .at = self.now, .carried = self.pressed.count() > 0 }; + self.glide = .{ + .op = self.cursor, + .from = self.pointer, + .at = self.now, + .carried = self.pressed.count() > 0, + }; self.cursor += 1; }, .type => { @@ -507,7 +512,8 @@ fn pathPoint(op: Tape.Op, from: Point, to: Point, f: f32) Point { } /// The pointer `f` of the way along a glide: eased in and out, on a slight bow rather than a ruled -/// line — a hand moving a mouse swings a little, and a dead-straight constant-speed pointer reads as +/// line — a hand moving a mouse swings a little, and a dead-straight constant-speed pointer reads +/// as /// a machine. pub fn glidePoint(from: Point, to: Point, f: f32) Point { const e = easeInOut(std.math.clamp(f, 0, 1)); @@ -966,7 +972,10 @@ test "a timed-out wait still releases a button held across it, but not a press i defer log.deinit(); var seq: Sequencer = .init(&tape); while (!seq.done()) _ = seq.advance(std.math.inf(f64), 30, log.sink()); - try testing.expectEqualStrings("keyframe demo://a\npress left\ntimeout 2 skipping 2\nrelease left\n", log.out.items); + try testing.expectEqualStrings( + "keyframe demo://a\npress left\ntimeout 2 skipping 2\nrelease left\n", + log.out.items, + ); } test "a timed-out wait skips a keyframe and the commands after it, up to the next chapter" { @@ -1000,7 +1009,10 @@ test "a timed-out wait releases the buttons held across it, whichever order the defer log.deinit(); var seq: Sequencer = .init(&tape); while (!seq.done()) _ = seq.advance(std.math.inf(f64), 30, log.sink()); - try testing.expectEqualStrings("keyframe demo://a\npress left\ntimeout 2 skipping 2\nrelease left\n", log.out.items); + try testing.expectEqualStrings( + "keyframe demo://a\npress left\ntimeout 2 skipping 2\nrelease left\n", + log.out.items, + ); try testing.expectEqual(@as(usize, 0), seq.pressed.count()); } @@ -1018,7 +1030,10 @@ test "a move to a target never drawn skips the chapter, so no press lands off ta defer log.deinit(); var seq: Sequencer = .init(&tape); while (!seq.done()) _ = seq.advance(std.math.inf(f64), 30, log.sink()); - try testing.expectEqualStrings("keyframe demo://a\nmove 0,0\nmissing missing 1 skipping 2\nkey plays\n", log.out.items); + try testing.expectEqualStrings( + "keyframe demo://a\nmove 0,0\nmissing missing 1 skipping 2\nkey plays\n", + log.out.items, + ); } test "rewinding to a keyframe and replaying fast lands where live play did" { diff --git a/sdk/tape/Tape.zig b/sdk/tape/Tape.zig index 2ff612cde..8ff0f51f0 100644 --- a/sdk/tape/Tape.zig +++ b/sdk/tape/Tape.zig @@ -22,8 +22,8 @@ //! **Steps** carry a document's own history rather than input: the effect of one change (the //! pixels a stroke set, the text a transaction inserted) in its owner's encoding, applied to a //! file the keyframe declared. Input replays only in a window laid out as it was recorded; an -//! effect replays exactly anywhere. What a step's bytes mean is the owner's, versioned by the file's -//! `format`, and whether the app can apply them is the app's to say (`Check.steps`). +//! effect replays exactly anywhere. What a step's bytes mean is the owner's, versioned by the +//! file's `format`, and whether the app can apply them is the app's to say (`Check.steps`). //! //! Everything here is plain data with no pointers into anything but its own strings, so a tape //! round-trips through ZON (`parse`, `write`): the same thing a script builds (`Script`) is what @@ -484,7 +484,8 @@ pub const Check = struct { /// Check what `Sequencer` relies on, once, at load — a tape that fails here would misplay rather /// than crash, which is worse to debug. pub fn validate(self: Tape, check: Check) Error!void { - if (!check.live and (self.ops.len == 0 or self.ops[0].at != 0 or self.ops[0].do != .keyframe)) return error.NoKeyframeAtStart; + if (!check.live and (self.ops.len == 0 or self.ops[0].at != 0 or self.ops[0].do != .keyframe)) + return error.NoKeyframeAtStart; var prev: u32 = 0; // The keyframe the ops are under: a step's file is one of its. var scene: ?*const Keyframe = null; @@ -542,7 +543,10 @@ pub fn parse(gpa: std.mem.Allocator, source: [:0]const u8, check: Check) !Owned var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); var diag: std.zon.parse.Diagnostics = undefined; - const tape = try std.zon.parse.fromSlice(Tape, .{ .gpa = gpa, .arena = arena.allocator(), .source = source, .diagnostics = &diag }); + const tape = try std.zon.parse.fromSlice( + Tape, + .{ .gpa = gpa, .arena = arena.allocator(), .source = source, .diagnostics = &diag }, + ); try tape.validate(check); return .{ .arena = arena, .tape = tape }; } @@ -573,7 +577,14 @@ fn sample() Tape { const chapters = [_]Chapter{ .{ .at = 0, .title = "one" }, .{ .at = 1000, .title = "two" } }; const captions = [_]Caption{.{ .at = 200, .ms = 2000, .text = "hi", .near = "button", .clear = &.{"field"} }}; }; - return .{ .name = "t", .ops = &S.ops, .keyframes = &S.keyframes, .chapters = &S.chapters, .captions = &S.captions, .home = "pane" }; + return .{ + .name = "t", + .ops = &S.ops, + .keyframes = &S.keyframes, + .chapters = &S.chapters, + .captions = &S.captions, + .home = "pane", + }; } test "a seek replays from the last keyframe at or before it" { @@ -693,8 +704,18 @@ test "a tape must open on a keyframe, in order, with chords the app can spell" { try testing.expectError(error.NoKeyframeAtStart, (Tape{ .name = "x", .ops = &bad_start }).validate(.{})); const kf = [_]Keyframe{.{ .root = "demo://x" }}; - const backwards = [_]Op{ .{ .at = 0, .do = .{ .keyframe = 0 } }, .{ .at = 50, .do = .{ .key = "a" } }, .{ .at = 10, .do = .{ .key = "b" } } }; - try testing.expectError(error.OpsOutOfOrder, (Tape{ .name = "x", .ops = &backwards, .keyframes = &kf }).validate(.{})); + const backwards = [_]Op{ + .{ + .at = 0, + .do = .{ .keyframe = 0 }, + }, + .{ .at = 50, .do = .{ .key = "a" } }, + .{ .at = 10, .do = .{ .key = "b" } }, + }; + try testing.expectError( + error.OpsOutOfOrder, + (Tape{ .name = "x", .ops = &backwards, .keyframes = &kf }).validate(.{}), + ); // Whether a chord is spelled right is the app's to say; unasked, it is not checked here. const bad_chord = [_]Op{ .{ .at = 0, .do = .{ .keyframe = 0 } }, .{ .at = 1, .do = .{ .key = "mod+nosuchkey" } } }; @@ -703,7 +724,10 @@ test "a tape must open on a keyframe, in order, with chords the app can spell" { try testing.expectError(error.BadChord, chords.validate(test_check)); const bad_index = [_]Op{.{ .at = 0, .do = .{ .keyframe = 3 } }}; - try testing.expectError(error.BadKeyframeIndex, (Tape{ .name = "x", .ops = &bad_index, .keyframes = &kf }).validate(.{})); + try testing.expectError( + error.BadKeyframeIndex, + (Tape{ .name = "x", .ops = &bad_index, .keyframes = &kf }).validate(.{}), + ); } test "a live tape plays on the app as it is: no keyframe, anywhere" { @@ -715,7 +739,10 @@ test "a live tape plays on the app as it is: no keyframe, anywhere" { try (Tape{ .name = "empty", .ops = &.{} }).validate(.{ .live = true }); const cut = [_]Op{ .{ .at = 0, .do = .{ .key = "a" } }, .{ .at = 10, .do = .{ .keyframe = 0 } } }; - try testing.expectError(error.KeyframeInLiveTape, (Tape{ .name = "x", .ops = &cut, .keyframes = &kf }).validate(.{ .live = true })); + try testing.expectError( + error.KeyframeInLiveTape, + (Tape{ .name = "x", .ops = &cut, .keyframes = &kf }).validate(.{ .live = true }), + ); } /// A spelling for tests: any chord whose last key is a single letter. @@ -808,7 +835,15 @@ pub const test_steps: Check = .{ .steps = struct { test "a step changes a file of the keyframe before it, whose owner the app can apply" { const sprite: [5]u8 = .{ 0x89, 'P', 0, 0xff, '\n' }; - const files = [_]File{ .{ .path = "a.pixi", .text = &sprite, .owner = "pixi", .format = 1 }, .{ .path = "notes.md", .text = "# hi" } }; + const files = [_]File{ + .{ + .path = "a.pixi", + .text = &sprite, + .owner = "pixi", + .format = 1, + }, + .{ .path = "notes.md", .text = "# hi" }, + }; const kf = [_]Keyframe{ .{ .root = "demo://s", .files = &files, .open = &.{"a.pixi"} }, .{ .root = "demo://s" } }; const ops = [_]Op{ .{ .at = 0, .do = .{ .keyframe = 0 } }, @@ -820,17 +855,37 @@ test "a step changes a file of the keyframe before it, whose owner the app can a // An app that says nothing of steps, or no to this owner's format, refuses the tape. try testing.expectError(error.StepsUnsupported, tape.validate(.{})); const other_format = [_]File{.{ .path = "a.pixi", .text = &sprite, .owner = "pixi", .format = 2 }}; - try testing.expectError(error.StepsUnsupported, (Tape{ .name = "s", .ops = &ops, .keyframes = &.{.{ .root = "demo://s", .files = &other_format }} }).validate(test_steps)); + try testing.expectError( + error.StepsUnsupported, + (Tape{ + .name = "s", + .ops = &ops, + .keyframes = &.{.{ .root = "demo://s", .files = &other_format }}, + }).validate(test_steps), + ); // A file that names its owner and takes no steps needs nothing of the app. - try (Tape{ .name = "s", .ops = ops[0..1], .keyframes = &.{.{ .root = "demo://s", .files = &other_format }} }).validate(.{}); + try (Tape{ + .name = "s", + .ops = ops[0..1], + .keyframes = &.{.{ .root = "demo://s", .files = &other_format }}, + }).validate(.{}); // Its file must be the keyframe's, and one with an owner to apply it. const elsewhere = [_]Op{ ops[0], .{ .at = 50, .do = .{ .keyframe = 1 } }, ops[1] }; - try testing.expectError(error.StepWithoutFile, (Tape{ .name = "s", .ops = &elsewhere, .keyframes = &kf }).validate(test_steps)); + try testing.expectError( + error.StepWithoutFile, + (Tape{ .name = "s", .ops = &elsewhere, .keyframes = &kf }).validate(test_steps), + ); const ownerless = [_]Op{ ops[0], .{ .at = 50, .do = .{ .step = .{ .file = "notes.md", .effect = "x" } } } }; - try testing.expectError(error.StepWithoutFile, (Tape{ .name = "s", .ops = &ownerless, .keyframes = &kf }).validate(test_steps)); + try testing.expectError( + error.StepWithoutFile, + (Tape{ .name = "s", .ops = &ownerless, .keyframes = &kf }).validate(test_steps), + ); // A live tape has no keyframe, so nothing to step. - try testing.expectError(error.StepWithoutFile, (Tape{ .name = "s", .ops = ops[1..] }).validate(.{ .live = true, .steps = test_steps.steps })); + try testing.expectError( + error.StepWithoutFile, + (Tape{ .name = "s", .ops = ops[1..] }).validate(.{ .live = true, .steps = test_steps.steps }), + ); // Through ZON, the binary file and the steps come back byte for byte. var out: std.Io.Writer.Allocating = .init(testing.allocator); diff --git a/sdk/tape/binary.zig b/sdk/tape/binary.zig index 5f940d88d..28daabb9a 100644 --- a/sdk/tape/binary.zig +++ b/sdk/tape/binary.zig @@ -230,7 +230,11 @@ fn writeBody(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { .blobs = count(blobs.items.len), .blob_bytes = count(blob_bytes), }; - inline for (@typeInfo(Header).@"struct".field_names, @typeInfo(Header).@"struct".field_types, @typeInfo(Header).@"struct".field_attrs) |f_name, _, _| try w.writeInt(u32, @field(h, f_name), .little); + inline for ( + @typeInfo(Header).@"struct".field_names, + @typeInfo(Header).@"struct".field_types, + @typeInfo(Header).@"struct".field_attrs, + ) |f_name, _, _| try w.writeInt(u32, @field(h, f_name), .little); try writeTable(w, table.list.items, table.bytes); try writeTable(w, blobs.items, blob_bytes); @@ -310,7 +314,14 @@ fn writeBody(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { try w.writeInt(u32, table.index(kf.root), .little); try w.writeByte(@backingInt(kf.layout)); try w.splatByteAll(0, 3); - for ([_]u32{ files_at, count(kf.files.len), open_at, count(kf.open.len), settings_at, count(kf.settings.len) }) |v| + for ([_]u32{ + files_at, + count(kf.files.len), + open_at, + count(kf.open.len), + settings_at, + count(kf.settings.len), + }) |v| try w.writeInt(u32, v, .little); files_at += count(kf.files.len); open_at += count(kf.open.len); @@ -502,7 +513,11 @@ pub fn read(gpa: std.mem.Allocator, bytes: []const u8, check: Tape.Check) ReadEr } }, .wait => .{ .wait = .{ .timeout = ib, .until = try untilOf(strings, small, ia) } }, .skip => .{ .skip = .{ .count = ib, .when = try untilOf(strings, small, ia) } }, - .step => .{ .step = .{ .file = try strings.get(ia), .effect = try blobs.get(ib), .motion = try blobs.get(words[0]) } }, + .step => .{ .step = .{ + .file = try strings.get(ia), + .effect = try blobs.get(ib), + .motion = try blobs.get(words[0]), + } }, } }; } @@ -523,7 +538,11 @@ pub fn read(gpa: std.mem.Allocator, bytes: []const u8, check: Tape.Check) ReadEr const settings = try a.alloc(Tape.Setting, h.settings); for (settings, 0..) |*s, i| { var f: Reader = .{ .bytes = setting_bytes[i * setting_size ..][0..setting_size] }; - s.* = .{ .owner = try strings.get(try f.int(u32)), .key = try strings.get(try f.int(u32)), .value = try strings.get(try f.int(u32)) }; + s.* = .{ + .owner = try strings.get(try f.int(u32)), + .key = try strings.get(try f.int(u32)), + .value = try strings.get(try f.int(u32)), + }; } const keyframes = try a.alloc(Tape.Keyframe, h.keyframes); for (keyframes, 0..) |*kf, i| { @@ -674,14 +693,29 @@ const testing = std.testing; const Everything = struct { const files = [_]Tape.File{ .{ .path = "a.md", .text = "# A\n" }, .{ .path = "b.md", .text = "" } }; const settings = [_]Tape.Setting{.{ .owner = "markdown", .key = "default_md_view", .value = ".split" }}; - const keyframe = Tape.Keyframe{ .root = "demo://t", .files = &files, .open = &.{ "a.md", "b.md" }, .settings = &settings, .layout = .reset }; + const keyframe = Tape.Keyframe{ + .root = "demo://t", + .files = &files, + .open = &.{ "a.md", "b.md" }, + .settings = &settings, + .layout = .reset, + }; /// A binary document, by its owner, and its steps. - const sprite = [_]Tape.File{.{ .path = "a.pixi", .text = &.{ 0x89, 'P', 0, 0xff, '\n', 0 }, .owner = "pixi", .format = 1 }}; + const sprite = [_]Tape.File{.{ + .path = "a.pixi", + .text = &.{ 0x89, 'P', 0, 0xff, '\n', 0 }, + .owner = "pixi", + .format = 1, + }}; const ops_v2 = [_]Tape.Op{ .{ .at = 0, .do = .{ .keyframe = 0 } }, .{ .at = 0, .do = .{ .wait = .{ .until = .idle } } }, .{ .at = 0, .do = .{ .wait = .{ .until = .{ .shown = "text.end:demo://t/a.md" }, .timeout = 2500 } } }, - .{ .at = 100, .ms = 400, .do = .{ .move = .{ .tag = "text.end:demo://t/a.md", .x = 0.25, .y = 0.75, .dx = -3.5, .dy = 8 } } }, + .{ + .at = 100, + .ms = 400, + .do = .{ .move = .{ .tag = "text.end:demo://t/a.md", .x = 0.25, .y = 0.75, .dx = -3.5, .dy = 8 } }, + }, .{ .at = 500, .do = .{ .press = .right } }, .{ .at = 560, .do = .{ .release = .right } }, .{ .at = 600, .do = .{ .scroll = .{ .x = 1, .y = -3 } } }, @@ -695,7 +729,11 @@ const Everything = struct { .{ .at = 1700, .ms = 80, .do = .{ .trace = .{ .tag = "pixi.canvas:demo://t/a.pixi", .x = 0.4, .y = 0.6 } } }, }; const ops = ops_v2 ++ [_]Tape.Op{ - .{ .at = 1800, .ms = 250, .do = .{ .step = .{ .file = "a.pixi", .effect = "\x00\x01\xff", .motion = "\x02\x00" } } }, + .{ + .at = 1800, + .ms = 250, + .do = .{ .step = .{ .file = "a.pixi", .effect = "\x00\x01\xff", .motion = "\x02\x00" } }, + }, // The same effect again is stored again; one with no motion stores an empty one. .{ .at = 2100, .do = .{ .step = .{ .file = "a.pixi", .effect = "\x00\x01\xff" } } }, .{ .at = 2200, .do = .{ .skip = .{ .when = .{ .shown = "fizzy.rail:workbench.files" }, .count = 3 } } }, @@ -703,12 +741,26 @@ const Everything = struct { }; const captions = [_]Tape.Caption{ .{ .at = 0, .ms = 900, .title = "Hi", .text = "Welcome.", .place = .middle }, - .{ .at = 900, .ms = 600, .text = "Beside it.", .near = "text.end:demo://t/a.md", .clear = &.{ "text.body:demo://t/a.md", "text.preview:demo://t/a.md" } }, + .{ + .at = 900, + .ms = 600, + .text = "Beside it.", + .near = "text.end:demo://t/a.md", + .clear = &.{ "text.body:demo://t/a.md", "text.preview:demo://t/a.md" }, + }, }; const chapters = [_]Tape.Chapter{ .{ .at = 0, .title = "One" }, .{ .at = 1400, .title = "Two" } }; fn tape(ops_: []const Tape.Op, keyframes: []const Tape.Keyframe) Tape { - return .{ .name = "t", .title = "Everything", .home = "region:Main", .ops = ops_, .keyframes = keyframes, .captions = &captions, .chapters = &chapters }; + return .{ + .name = "t", + .title = "Everything", + .home = "region:Main", + .ops = ops_, + .keyframes = keyframes, + .captions = &captions, + .chapters = &chapters, + }; } }; @@ -907,10 +959,13 @@ test "a version-1 tape still reads, its commands without arguments" { test "a recording's key chords and steps are checked against the app, as in ZON" { const bytes = try encode(testing.allocator, everything(), .{}); defer testing.allocator.free(bytes); - try testing.expectError(error.BadChord, read(testing.allocator, bytes, .{ .steps = steps_check.steps, .key = struct { - fn ok(c: []const u8) bool { - return std.mem.findScalar(u8, c, ' ') == null; - } - }.ok })); + try testing.expectError(error.BadChord, read(testing.allocator, bytes, .{ + .steps = steps_check.steps, + .key = struct { + fn ok(c: []const u8) bool { + return std.mem.findScalar(u8, c, ' ') == null; + } + }.ok, + })); try testing.expectError(error.StepsUnsupported, read(testing.allocator, bytes, .{})); } diff --git a/sdk/tape/events.zig b/sdk/tape/events.zig index 2f67e62a8..7923b24d0 100644 --- a/sdk/tape/events.zig +++ b/sdk/tape/events.zig @@ -83,7 +83,10 @@ pub fn selectionBytes(start: u64, end: u64) [16]u8 { pub fn selection(payload: []const u8) ?struct { start: u64, end: u64 } { if (payload.len != 16) return null; - return .{ .start = std.mem.readInt(u64, payload[0..8], .little), .end = std.mem.readInt(u64, payload[8..16], .little) }; + return .{ + .start = std.mem.readInt(u64, payload[0..8], .little), + .end = std.mem.readInt(u64, payload[8..16], .little), + }; } fn testFrame(t: i64) Frame { diff --git a/sdk/tape/key.zig b/sdk/tape/key.zig index 467d5dd75..07e68ef46 100644 --- a/sdk/tape/key.zig +++ b/sdk/tape/key.zig @@ -136,7 +136,15 @@ pub const Key = enum { /// ever fire together with itself, which is never what anyone means. pub fn isModifier(k: Key) bool { return switch (k) { - .left_shift, .right_shift, .left_control, .right_control, .left_alt, .right_alt, .left_command, .right_command => true, + .left_shift, + .right_shift, + .left_control, + .right_control, + .left_alt, + .right_alt, + .left_command, + .right_command, + => true, else => false, }; } diff --git a/sdk/tape/ring.zig b/sdk/tape/ring.zig index e423ac0f4..bebfe6bc4 100644 --- a/sdk/tape/ring.zig +++ b/sdk/tape/ring.zig @@ -77,8 +77,10 @@ pub fn header(bytes: []const u8) ReadError!Header { pub fn Ring(comptime name: []const u8, comptime Frame: type, comptime Record: type, comptime caps: Caps) type { comptime { - for (.{ "t_ns", "first", "count" }) |f| if (!@hasField(Frame, f)) @compileError(@typeName(Frame) ++ " has no `" ++ f ++ "`"); - for (.{ "bytes_at", "bytes_len" }) |f| if (!@hasField(Record, f)) @compileError(@typeName(Record) ++ " has no `" ++ f ++ "`"); + for (.{ "t_ns", "first", "count" }) |f| + if (!@hasField(Frame, f)) @compileError(@typeName(Frame) ++ " has no `" ++ f ++ "`"); + for (.{ "bytes_at", "bytes_len" }) |f| + if (!@hasField(Record, f)) @compileError(@typeName(Record) ++ " has no `" ++ f ++ "`"); } return struct { const Self = @This(); @@ -208,7 +210,8 @@ pub fn Ring(comptime name: []const u8, comptime Frame: type, comptime Record: ty for (0..fr.count) |k| { const r = self.records[@intCast((fr.first + k) % caps.records)]; var b: u64 = 0; - while (b < r.bytes_len) : (b += 1) try w.writeByte(self.bytes[@intCast((r.bytes_at + b) % caps.bytes)]); + while (b < r.bytes_len) : (b += 1) + try w.writeByte(self.bytes[@intCast((r.bytes_at + b) % caps.bytes)]); } } } @@ -226,7 +229,18 @@ pub fn Ring(comptime name: []const u8, comptime Frame: type, comptime Record: ty /// A stretch of nothing: what a capture holds when nothing was recorded. pub fn writeEmpty(w: *std.Io.Writer) !void { - try w.writeStruct(Header{ .stream = stream, .frame_size = @sizeOf(Frame), .record_size = @sizeOf(Record), .frames = 0, .records = 0, .bytes = 0, .first_frame = 0 }, .little); + try w.writeStruct( + Header{ + .stream = stream, + .frame_size = @sizeOf(Frame), + .record_size = @sizeOf(Record), + .frames = 0, + .records = 0, + .bytes = 0, + .first_frame = 0, + }, + .little, + ); } /// A file `write` made, read in place: its arrays are views into the bytes, checked to fit. @@ -250,7 +264,9 @@ pub fn Ring(comptime name: []const u8, comptime Frame: type, comptime Record: ty pub fn read(bytes: []const u8) ReadError!Stretch { const h = try header(bytes); - if (!std.mem.eql(u8, &h.stream, &stream) or h.frame_size != @sizeOf(Frame) or h.record_size != @sizeOf(Record)) return error.OtherStream; + if (!std.mem.eql(u8, &h.stream, &stream) or + h.frame_size != @sizeOf(Frame) or h.record_size != @sizeOf(Record)) + return error.OtherStream; const frames_bytes = @as(usize, h.frames) * @sizeOf(Frame); const records_bytes = @as(usize, h.records) * @sizeOf(Record); if (bytes.len < @sizeOf(Header) + frames_bytes + records_bytes + h.bytes) return error.Truncated; @@ -312,7 +328,11 @@ test "the rings keep the newest, and a stretch starts at the oldest frame still defer std.testing.allocator.destroy(ring); ring.* = .{}; // More frames than the ring holds, each with one record. - for (0..TestRing.frames_cap + 10) |i| ring.appendFrame(.{ .t_ns = @intCast(i) }, &.{.{ .button = @intCast(i % 16) }}, &.{}); + for (0..TestRing.frames_cap + 10) |i| ring.appendFrame( + .{ .t_ns = @intCast(i) }, + &.{.{ .button = @intCast(i % 16) }}, + &.{}, + ); try std.testing.expectEqual(@as(u64, 10), ring.oldestWhole()); // Payloads outrunning their ring: a frame whose bytes were overwritten is not whole. for (0..8) |_| ring.appendFrame(.{ .t_ns = 100 }, &.{.{ .button = 1 }}, &.{&@as([64]u8, @splat('x'))}); @@ -326,7 +346,15 @@ test "the rings keep the newest, and a stretch starts at the oldest frame still test "a file that lies about its sizes is refused" { try std.testing.expectError(error.Truncated, TestRing.read("FZR")); - var bad: [@sizeOf(Header)]u8 = std.mem.toBytes(Header{ .stream = TestRing.stream, .frame_size = @sizeOf(TestFrame), .record_size = @sizeOf(TestRecord), .frames = 5, .records = 0, .bytes = 0, .first_frame = 0 }); + var bad: [@sizeOf(Header)]u8 = std.mem.toBytes(Header{ + .stream = TestRing.stream, + .frame_size = @sizeOf(TestFrame), + .record_size = @sizeOf(TestRecord), + .frames = 5, + .records = 0, + .bytes = 0, + .first_frame = 0, + }); try std.testing.expectError(error.Truncated, TestRing.read(&bad)); bad[0] = 'X'; try std.testing.expectError(error.NotARecording, TestRing.read(&bad));