zig: upgrade from 0.10.x to 0.11.0
ci/woodpecker/push/woodpecker Pipeline was successful Details

while there's lots of changes and new features in zig v0.11.0, the most
important for this project at the moment is the HTTP client.
the client is most likely what will connect to lnd lightning node to
fetch stats and info for the UI to then visualize it on "lightning" tab,
similar to the bitcoind RPC client.

see all zig 0.11 release notes here:
https://ziglang.org/download/0.11.0/release-notes.html
pull/26/head
alex 1 year ago
parent e84489c345
commit 328df67c5d
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -9,19 +9,23 @@ clone:
recursive: false recursive: false
pipeline: pipeline:
lint: lint:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands: commands:
- ./tools/fmt-check.sh - ./tools/fmt-check.sh
test: test:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands: commands:
- zig build test - zig build test
sdl2: sdl2:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands: commands:
- zig build -Ddriver=sdl2 - zig build -Ddriver=sdl2
aarch64: aarch64:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands: commands:
- zig build -Ddriver=fbev -Dtarget=aarch64-linux-musl -Drelease-safe -Dstrip - zig build -Ddriver=fbev -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe -Dstrip
- sha256sum zig-out/bin/nd zig-out/bin/ngui - sha256sum zig-out/bin/nd zig-out/bin/ngui
playground:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands:
- zig build guiplay btcrpc

@ -2,13 +2,13 @@
build for rpi: build for rpi:
zig build -Dtarget=aarch64-linux-musl -Ddriver=fbev -Drelease-safe -Dstrip zig build -Dtarget=aarch64-linux-musl -Ddriver=fbev -Doptimize=ReleaseSafe -Dstrip
otherwise just `zig build` on dev host otherwise just `zig build` on dev host
## local development ## local development
you'll need [zig v0.10.x](https://ziglang.org/download/). you'll need [zig v0.11.x](https://ziglang.org/download/).
if working on the gui, also [SDL2](https://www.libsdl.org/). if working on the gui, also [SDL2](https://www.libsdl.org/).
note that compiling the daemon on macOS is currently unsupported since note that compiling the daemon on macOS is currently unsupported since
@ -58,17 +58,17 @@ to make a new image and switch the CI to use it, first modify the
[ci-containerfile](tools/ci-containerfile) and produce the image locally: [ci-containerfile](tools/ci-containerfile) and produce the image locally:
podman build --rm -t ndg-ci -f ./tools/ci-containerfile \ podman build --rm -t ndg-ci -f ./tools/ci-containerfile \
--build-arg ZIGURL=https://ziglang.org/download/0.10.1/zig-linux-x86_64-0.10.1.tar.xz --build-arg ZIGURL=https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
then tag it with the target URL, for example: then tag it with the target URL, for example:
podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.10.1:v1 podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.11.0:v2
generate an [access token](https://git.qcode.ch/user/settings/applications), generate an [access token](https://git.qcode.ch/user/settings/applications),
login to the container registry and push the image to remote: login to the container registry and push the image to remote:
podman login git.qcode.ch podman login git.qcode.ch
podman push git.qcode.ch/nakamochi/ci-zig0.10.1:v1 podman push git.qcode.ch/nakamochi/ci-zig0.11.0:v2
the image will be available at the image will be available at
https://git.qcode.ch/nakamochi/-/packages/ https://git.qcode.ch/nakamochi/-/packages/

@ -1,20 +1,26 @@
const std = @import("std"); const std = @import("std");
const nifbuild = @import("lib/nif/build.zig");
pub fn build(b: *std.build.Builder) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const mode = b.standardReleaseOptions(); const optimize = b.standardOptimizeOption(.{});
const strip = b.option(bool, "strip", "strip output binary; default: false") orelse false; const strip = b.option(bool, "strip", "strip output binary; default: false") orelse false;
const drv = b.option(DriverTarget, "driver", "display and input drivers combo; default: sdl2") orelse .sdl2; const drv = b.option(DriverTarget, "driver", "display and input drivers combo; default: sdl2") orelse .sdl2;
const disp_horiz = b.option(u32, "horiz", "display horizontal pixels count; default: 800") orelse 800; const disp_horiz = b.option(u32, "horiz", "display horizontal pixels count; default: 800") orelse 800;
const disp_vert = b.option(u32, "vert", "display vertical pixels count; default: 480") orelse 480; const disp_vert = b.option(u32, "vert", "display vertical pixels count; default: 480") orelse 480;
const lvgl_loglevel = b.option(LVGLLogLevel, "lvgl_loglevel", "LVGL lib logging level") orelse LVGLLogLevel.default(mode); const lvgl_loglevel = b.option(LVGLLogLevel, "lvgl_loglevel", "LVGL lib logging level") orelse LVGLLogLevel.default(optimize);
const inver = b.option([]const u8, "version", "semantic version of the build; must match git tag when available"); const inver = b.option([]const u8, "version", "semantic version of the build; must match git tag when available");
const buildopts = b.addOptions(); const buildopts = b.addOptions();
buildopts.addOption(DriverTarget, "driver", drv); buildopts.addOption(DriverTarget, "driver", drv);
const semver_step = VersionStep.create(b, buildopts, inver); const semver_step = VersionStep.create(b, buildopts, inver);
buildopts.step.dependOn(semver_step);
// network interface (nif) standalone library used by the daemon and tests.
const libnif_dep = b.anonymousDependency("lib/nif", @import("lib/nif/build.zig"), .{
.target = target,
.optimize = optimize,
});
const libnif = libnif_dep.artifact("nif");
const common_cflags = .{ const common_cflags = .{
"-Wall", "-Wall",
@ -25,17 +31,18 @@ pub fn build(b: *std.build.Builder) void {
}; };
// gui build // gui build
const ngui = b.addExecutable("ngui", "src/ngui.zig"); const ngui = b.addExecutable(.{
ngui.setTarget(target); .name = "ngui",
ngui.setBuildMode(mode); .root_source_file = .{ .path = "src/ngui.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
});
ngui.pie = true; ngui.pie = true;
ngui.strip = strip; ngui.strip = strip;
ngui.step.dependOn(semver_step); ngui.addOptions("build_options", buildopts);
ngui.addIncludePath(.{ .path = "lib" });
ngui.addPackage(buildopts.getPackage("build_options")); ngui.addIncludePath(.{ .path = "src/ui/c" });
ngui.addIncludePath("lib");
ngui.addIncludePath("src/ui/c");
ngui.linkLibC();
const lvgl_flags = .{ const lvgl_flags = .{
"-std=c11", "-std=c11",
@ -68,14 +75,14 @@ pub fn build(b: *std.build.Builder) void {
switch (drv) { switch (drv) {
.sdl2 => { .sdl2 => {
ngui.addCSourceFiles(lvgl_sdl2_src, &lvgl_flags); ngui.addCSourceFiles(lvgl_sdl2_src, &lvgl_flags);
ngui.addCSourceFile("src/ui/c/drv_sdl2.c", &ngui_cflags); ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_sdl2.c" }, .flags = &ngui_cflags });
ngui.defineCMacro("NM_DRV_SDL2", null); ngui.defineCMacro("NM_DRV_SDL2", null);
ngui.defineCMacro("USE_SDL", null); ngui.defineCMacro("USE_SDL", null);
ngui.linkSystemLibrary("SDL2"); ngui.linkSystemLibrary("SDL2");
}, },
.fbev => { .fbev => {
ngui.addCSourceFiles(lvgl_fbev_src, &lvgl_flags); ngui.addCSourceFiles(lvgl_fbev_src, &lvgl_flags);
ngui.addCSourceFile("src/ui/c/drv_fbev.c", &ngui_cflags); ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_fbev.c" }, .flags = &ngui_cflags });
ngui.defineCMacro("NM_DRV_FBEV", null); ngui.defineCMacro("NM_DRV_FBEV", null);
ngui.defineCMacro("USE_FBDEV", null); ngui.defineCMacro("USE_FBDEV", null);
ngui.defineCMacro("USE_EVDEV", null); ngui.defineCMacro("USE_EVDEV", null);
@ -83,68 +90,77 @@ pub fn build(b: *std.build.Builder) void {
} }
const ngui_build_step = b.step("ngui", "build ngui (nakamochi gui)"); const ngui_build_step = b.step("ngui", "build ngui (nakamochi gui)");
ngui_build_step.dependOn(&b.addInstallArtifact(ngui).step); ngui_build_step.dependOn(&b.addInstallArtifact(ngui, .{}).step);
// daemon build // daemon build
const nd = b.addExecutable("nd", "src/nd.zig"); const nd = b.addExecutable(.{
nd.setTarget(target); .name = "nd",
nd.setBuildMode(mode); .root_source_file = .{ .path = "src/nd.zig" },
.target = target,
.optimize = optimize,
});
nd.pie = true; nd.pie = true;
nd.strip = strip; nd.strip = strip;
nd.step.dependOn(semver_step); nd.addOptions("build_options", buildopts);
nd.addModule("nif", libnif_dep.module("nif"));
nd.addPackage(buildopts.getPackage("build_options")); nd.linkLibrary(libnif);
nifbuild.addPkg(b, nd, "lib/nif");
const niflib = nifbuild.library(b, "lib/nif");
niflib.setTarget(target);
niflib.setBuildMode(mode);
nd.linkLibrary(niflib);
const nd_build_step = b.step("nd", "build nd (nakamochi daemon)"); const nd_build_step = b.step("nd", "build nd (nakamochi daemon)");
nd_build_step.dependOn(&b.addInstallArtifact(nd).step); nd_build_step.dependOn(&b.addInstallArtifact(nd, .{}).step);
// default build
const build_all_step = b.step("all", "build nd and ngui");
build_all_step.dependOn(ngui_build_step);
build_all_step.dependOn(nd_build_step);
b.default_step.dependOn(build_all_step);
// automated tests
{ {
const tests = b.addTest("src/test.zig"); const tests = b.addTest(.{
tests.setTarget(target); .root_source_file = .{ .path = "src/test.zig" },
tests.setBuildMode(mode); .target = target,
tests.linkLibC(); .optimize = optimize,
tests.addPackage(buildopts.getPackage("build_options")); .link_libc = true,
nifbuild.addPkg(b, tests, "lib/nif"); .filter = b.option([]const u8, "test-filter", "run tests matching the filter"),
});
const f = b.option([]const u8, "test-filter", "run tests matching the filter"); tests.addOptions("build_options", buildopts);
tests.setFilter(f); tests.addModule("nif", libnif_dep.module("nif"));
tests.linkLibrary(libnif);
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "run tests"); const test_step = b.step("test", "run tests");
test_step.dependOn(&tests.step); test_step.dependOn(&run_tests.step);
} }
// GUI playground
{ {
const guiplay = b.addExecutable("guiplay", "src/test/guiplay.zig"); const guiplay = b.addExecutable(.{
guiplay.setTarget(target); .name = "guiplay",
guiplay.setBuildMode(mode); .root_source_file = .{ .path = "src/test/guiplay.zig" },
guiplay.step.dependOn(semver_step); .target = target,
guiplay.addPackagePath("comm", "src/comm.zig"); .optimize = optimize,
});
guiplay.addModule("comm", b.createModule(.{ .source_file = .{ .path = "src/comm.zig" } }));
const guiplay_build_step = b.step("guiplay", "build GUI playground"); const guiplay_build_step = b.step("guiplay", "build GUI playground");
guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay).step); guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay, .{}).step);
guiplay_build_step.dependOn(ngui_build_step); guiplay_build_step.dependOn(ngui_build_step);
} }
// bitcoind RPC client playground
{ {
const btcrpc = b.addExecutable("btcrpc", "src/test/btcrpc.zig"); const btcrpc = b.addExecutable(.{
btcrpc.setTarget(target); .name = "btcrpc",
btcrpc.setBuildMode(mode); .root_source_file = .{ .path = "src/test/btcrpc.zig" },
.target = target,
.optimize = optimize,
});
btcrpc.strip = strip; btcrpc.strip = strip;
btcrpc.addPackagePath("bitcoindrpc", "src/nd/bitcoindrpc.zig"); btcrpc.addModule("bitcoindrpc", b.createModule(.{ .source_file = .{ .path = "src/nd/bitcoindrpc.zig" } }));
const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground"); const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc).step); btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step);
} }
// default build step
const build_all_step = b.step("all", "build nd and ngui (default step)");
build_all_step.dependOn(ngui_build_step);
build_all_step.dependOn(nd_build_step);
b.default_step.dependOn(build_all_step);
} }
const DriverTarget = enum { const DriverTarget = enum {
@ -354,23 +370,28 @@ const LVGLLogLevel = enum {
/// git tag is found. /// git tag is found.
const VersionStep = struct { const VersionStep = struct {
inver: ?[]const u8, // input version in std.SemanticVersion.parse format inver: ?[]const u8, // input version in std.SemanticVersion.parse format
buildopts: *std.build.OptionsStep, // where to store the build version buildopts: *std.Build.Step.Options, // where to store the build version
b: *std.build.Builder, b: *std.Build,
step: std.build.Step, step: std.Build.Step,
fn create(b: *std.build.Builder, o: *std.build.OptionsStep, inver: ?[]const u8) *std.build.Step { fn create(b: *std.Build, o: *std.Build.Step.Options, inver: ?[]const u8) *std.Build.Step {
const vstep = b.allocator.create(VersionStep) catch unreachable; const vstep = b.allocator.create(VersionStep) catch unreachable;
vstep.* = VersionStep{ vstep.* = VersionStep{
.inver = inver, .inver = inver,
.buildopts = o, .buildopts = o,
.b = b, .b = b,
.step = std.build.Step.init(.custom, "VersionStep: ndg semver", b.allocator, make), .step = std.Build.Step.init(.{
.id = .custom,
.name = "VersionStep: ndg semver",
.owner = b,
.makeFn = make,
}),
}; };
return &vstep.step; return &vstep.step;
} }
fn make(step: *std.build.Step) anyerror!void { fn make(step: *std.Build.Step, _: *std.Progress.Node) anyerror!void {
const self = @fieldParentPtr(VersionStep, "step", step); const self = @fieldParentPtr(VersionStep, "step", step);
const semver = try self.eval(); const semver = try self.eval();
std.log.info("build version: {any}", .{semver}); std.log.info("build version: {any}", .{semver});

@ -1,21 +1,23 @@
const build = @import("std").build; const std = @import("std");
pub fn addPkg(b: *build.Builder, obj: *build.LibExeObjStep, prefix: []const u8) void { pub fn build(b: *std.Build) void {
obj.addPackagePath("nif", pkgPath(b, prefix)); _ = b.addModule("nif", .{ .source_file = .{ .path = "nif.zig" } });
}
pub fn pkgPath(b: *build.Builder, prefix: []const u8) []const u8 {
return b.pathJoin(&.{ prefix, "nif.zig" });
}
pub fn library(b: *build.Builder, prefix: []const u8) *build.LibExeObjStep { const target = b.standardTargetOptions(.{});
const lib = b.addStaticLibrary("nif", b.pathJoin(&.{ prefix, "nif.zig" })); const optimize = b.standardOptimizeOption(.{});
lib.addIncludePath(b.pathJoin(&.{ prefix, "wpa_supplicant" })); const lib = b.addStaticLibrary(.{
.name = "nif",
.root_source_file = .{ .path = "nif.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
});
lib.defineCMacro("CONFIG_CTRL_IFACE", null); lib.defineCMacro("CONFIG_CTRL_IFACE", null);
lib.defineCMacro("CONFIG_CTRL_IFACE_UNIX", null); lib.defineCMacro("CONFIG_CTRL_IFACE_UNIX", null);
lib.addIncludePath(.{ .path = "wpa_supplicant" });
lib.addCSourceFiles(&.{ lib.addCSourceFiles(&.{
b.pathJoin(&.{ prefix, "wpa_supplicant/wpa_ctrl.c" }), "wpa_supplicant/wpa_ctrl.c",
b.pathJoin(&.{ prefix, "wpa_supplicant/os_unix.c" }), "wpa_supplicant/os_unix.c",
}, &.{ }, &.{
"-Wall", "-Wall",
"-Wextra", "-Wextra",
@ -24,6 +26,5 @@ pub fn library(b: *build.Builder, prefix: []const u8) *build.LibExeObjStep {
"-Wunused-parameter", "-Wunused-parameter",
"-Werror", "-Werror",
}); });
lib.linkLibC(); b.installArtifact(lib);
return lib;
} }

@ -46,7 +46,7 @@ pub fn pubAddresses(allocator: mem.Allocator, ifname: ?[]const u8) ![]net.Addres
// skip loopbacks and those which are not "up" // skip loopbacks and those which are not "up"
continue; continue;
} }
const ipaddr = net.Address.initPosix(@alignCast(4, sa)); // initPosix makes a copy const ipaddr = net.Address.initPosix(@alignCast(sa)); // initPosix makes a copy
if (ipaddr.any.family == os.AF.INET6 and ipaddr.in6.sa.scope_id > 0) { if (ipaddr.any.family == os.AF.INET6 and ipaddr.in6.sa.scope_id > 0) {
// want only global, with 0 scope // want only global, with 0 scope
// non-zero scopes make sense for link-local addr only. // non-zero scopes make sense for link-local addr only.

@ -103,22 +103,37 @@ pub const MessageTag = enum(u16) {
// next: 0x0b // next: 0x0b
}; };
/// the return value type from `read` fn.
pub const ParsedMessage = struct {
value: Message,
arena: ?*std.heap.ArenaAllocator = null, // null for void message tags
/// releases all resources used by the message.
pub fn deinit(self: @This()) void {
if (self.arena) |a| {
const allocator = a.child_allocator;
a.deinit();
allocator.destroy(a);
}
}
};
/// reads and parses a single message from the input stream reader. /// reads and parses a single message from the input stream reader.
/// propagates reader errors as is. for example, a closed reader returns /// propagates reader errors as is. for example, a closed reader returns
/// error.EndOfStream. /// error.EndOfStream.
/// ///
/// callers must deallocate resources with Message.free when done. /// callers must deallocate resources with ParsedMessage.deinit when done.
pub fn read(allocator: mem.Allocator, reader: anytype) !Message { pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic. // alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
const tag = try reader.readEnum(MessageTag, .Little); const tag = try reader.readEnum(MessageTag, .Little);
const len = try reader.readIntLittle(u64); const len = try reader.readIntLittle(u64);
if (len == 0) { if (len == 0) {
return switch (tag) { return switch (tag) {
.ping => Message{ .ping = {} }, .ping => .{ .value = .{ .ping = {} } },
.pong => Message{ .pong = {} }, .pong => .{ .value = .{ .pong = {} } },
.poweroff => Message{ .poweroff = {} }, .poweroff => .{ .value = .{ .poweroff = {} } },
.standby => Message{ .standby = {} }, .standby => .{ .value = .{ .standby = {} } },
.wakeup => Message{ .wakeup = {} }, .wakeup => .{ .value = .{ .wakeup = {} } },
else => Error.CommReadZeroLenInNonVoidTag, else => Error.CommReadZeroLenInNonVoidTag,
}; };
} }
@ -126,24 +141,22 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
var bytes = try allocator.alloc(u8, len); var bytes = try allocator.alloc(u8, len);
defer allocator.free(bytes); defer allocator.free(bytes);
try reader.readNoEof(bytes); try reader.readNoEof(bytes);
const jopt = json.ParseOptions{ .allocator = allocator, .ignore_unknown_fields = true };
var jstream = json.TokenStream.init(bytes);
return switch (tag) { return switch (tag) {
.ping, .pong, .poweroff, .standby, .wakeup => unreachable, // handled above .ping, .pong, .poweroff, .standby, .wakeup => unreachable, // handled above
.wifi_connect => Message{ inline else => |t| {
.wifi_connect = try json.parse(Message.WifiConnect, &jstream, jopt), var arena = try allocator.create(std.heap.ArenaAllocator);
}, arena.* = std.heap.ArenaAllocator.init(allocator);
.network_report => Message{ errdefer {
.network_report = try json.parse(Message.NetworkReport, &jstream, jopt), arena.deinit();
}, allocator.destroy(arena);
.get_network_report => Message{ }
.get_network_report = try json.parse(Message.GetNetworkReport, &jstream, jopt), const jopt = std.json.ParseOptions{ .ignore_unknown_fields = true, .allocate = .alloc_always };
}, const v = try json.parseFromSliceLeaky(std.meta.TagPayload(Message, t), arena.allocator(), bytes, jopt);
.poweroff_progress => Message{ const parsed = ParsedMessage{
.poweroff_progress = try json.parse(Message.PoweroffProgress, &jstream, jopt), .arena = arena,
}, .value = @unionInit(Message, @tagName(t), v),
.bitcoind_report => Message{ };
.bitcoind_report = try json.parse(Message.BitcoindReport, &jstream, jopt), return parsed;
}, },
}; };
} }
@ -151,35 +164,25 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
/// outputs the message msg using writer. /// outputs the message msg using writer.
/// all allocated resources are freed upon return. /// all allocated resources are freed upon return.
pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void { pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
const jopt = .{ .whitespace = null };
var data = ByteArrayList.init(allocator); var data = ByteArrayList.init(allocator);
defer data.deinit(); defer data.deinit();
switch (msg) { switch (msg) {
.ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload .ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload
.wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()), .wifi_connect => try json.stringify(msg.wifi_connect, .{}, data.writer()),
.network_report => try json.stringify(msg.network_report, jopt, data.writer()), .network_report => try json.stringify(msg.network_report, .{}, data.writer()),
.get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()), .get_network_report => try json.stringify(msg.get_network_report, .{}, data.writer()),
.poweroff_progress => try json.stringify(msg.poweroff_progress, jopt, data.writer()), .poweroff_progress => try json.stringify(msg.poweroff_progress, .{}, data.writer()),
.bitcoind_report => try json.stringify(msg.bitcoind_report, jopt, data.writer()), .bitcoind_report => try json.stringify(msg.bitcoind_report, .{}, data.writer()),
} }
if (data.items.len > std.math.maxInt(u64)) { if (data.items.len > std.math.maxInt(u64)) {
return Error.CommWriteTooLarge; return Error.CommWriteTooLarge;
} }
try writer.writeIntLittle(u16, @enumToInt(msg)); try writer.writeIntLittle(u16, @intFromEnum(msg));
try writer.writeIntLittle(u64, data.items.len); try writer.writeIntLittle(u64, data.items.len);
try writer.writeAll(data.items); try writer.writeAll(data.items);
} }
pub fn free(allocator: mem.Allocator, m: Message) void {
switch (m) {
.ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload
else => |v| {
json.parseFree(@TypeOf(v), v, .{ .allocator = allocator });
},
}
}
// TODO: use fifo // TODO: use fifo
// //
// var buf = std.fifo.LinearFifo(u8, .Dynamic).init(t.allocator); // var buf = std.fifo.LinearFifo(u8, .Dynamic).init(t.allocator);
@ -206,16 +209,16 @@ test "read" {
var buf = std.ArrayList(u8).init(t.allocator); var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit(); defer buf.deinit();
try buf.writer().writeIntLittle(u16, @enumToInt(msg)); try buf.writer().writeIntLittle(u16, @intFromEnum(msg));
try buf.writer().writeIntLittle(u64, data.items.len); try buf.writer().writeIntLittle(u64, data.items.len);
try buf.writer().writeAll(data.items); try buf.writer().writeAll(data.items);
var bs = std.io.fixedBufferStream(buf.items); var bs = std.io.fixedBufferStream(buf.items);
const res = try read(t.allocator, bs.reader()); const res = try read(t.allocator, bs.reader());
defer free(t.allocator, res); defer res.deinit();
try t.expectEqualStrings(msg.wifi_connect.ssid, res.wifi_connect.ssid); try t.expectEqualStrings(msg.wifi_connect.ssid, res.value.wifi_connect.ssid);
try t.expectEqualStrings(msg.wifi_connect.password, res.wifi_connect.password); try t.expectEqualStrings(msg.wifi_connect.password, res.value.wifi_connect.password);
} }
test "write" { test "write" {
@ -229,7 +232,7 @@ test "write" {
const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}"; const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}";
var js = std.ArrayList(u8).init(t.allocator); var js = std.ArrayList(u8).init(t.allocator);
defer js.deinit(); defer js.deinit();
try js.writer().writeIntLittle(u16, @enumToInt(msg)); try js.writer().writeIntLittle(u16, @intFromEnum(msg));
try js.writer().writeIntLittle(u64, payload.len); try js.writer().writeIntLittle(u64, payload.len);
try js.appendSlice(payload); try js.appendSlice(payload);
@ -255,8 +258,8 @@ test "write/read void tags" {
try write(t.allocator, buf.writer(), m); try write(t.allocator, buf.writer(), m);
var bs = std.io.fixedBufferStream(buf.items); var bs = std.io.fixedBufferStream(buf.items);
const res = try read(t.allocator, bs.reader()); const res = try read(t.allocator, bs.reader());
free(t.allocator, res); // noop res.deinit(); // noop due to void type
try t.expectEqual(m, res); try t.expectEqual(m, res.value);
} }
} }
@ -283,7 +286,7 @@ test "msg sequence" {
var bs = std.io.fixedBufferStream(buf.items); var bs = std.io.fixedBufferStream(buf.items);
for (msgs) |m| { for (msgs) |m| {
const res = try read(t.allocator, bs.reader()); const res = try read(t.allocator, bs.reader());
defer free(t.allocator, res); defer res.deinit();
try t.expectEqual(@as(MessageTag, m), @as(MessageTag, res)); try t.expectEqual(@as(MessageTag, m), @as(MessageTag, res.value));
} }
} }

@ -126,7 +126,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
pub fn main() !void { pub fn main() !void {
// main heap allocator used throughout the lifetime of nd // main heap allocator used throughout the lifetime of nd
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) { defer if (gpa_state.deinit() == .leak) {
logger.err("memory leaks detected", .{}); logger.err("memory leaks detected", .{});
}; };
const gpa = gpa_state.allocator(); const gpa = gpa_state.allocator();

@ -82,7 +82,7 @@ pub fn init(a: std.mem.Allocator, r: std.fs.File.Reader, w: std.fs.File.Writer,
.uiwriter = w, .uiwriter = w,
.wpa_ctrl = try types.WpaControl.open(wpa), .wpa_ctrl = try types.WpaControl.open(wpa),
.state = .stopped, .state = .stopped,
.services = svlist.toOwnedSlice(), .services = try svlist.toOwnedSlice(),
// send a network report right at start without wifi scan to make it faster. // send a network report right at start without wifi scan to make it faster.
.want_network_report = true, .want_network_report = true,
.want_wifi_scan = false, .want_wifi_scan = false,
@ -297,7 +297,7 @@ fn commThreadLoop(self: *Daemon) void {
std.atomic.spinLoopHint(); std.atomic.spinLoopHint();
time.sleep(100 * time.ns_per_ms); time.sleep(100 * time.ns_per_ms);
const msg = comm.read(self.allocator, self.uireader) catch |err| { const res = comm.read(self.allocator, self.uireader) catch |err| {
self.mu.lock(); self.mu.lock();
defer self.mu.unlock(); defer self.mu.unlock();
if (self.want_stop) { if (self.want_stop) {
@ -316,7 +316,9 @@ fn commThreadLoop(self: *Daemon) void {
}, },
} }
}; };
defer res.deinit();
const msg = res.value;
logger.debug("got msg: {s}", .{@tagName(msg)}); logger.debug("got msg: {s}", .{@tagName(msg)});
switch (msg) { switch (msg) {
.pong => { .pong => {
@ -343,12 +345,12 @@ fn commThreadLoop(self: *Daemon) void {
}, },
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}), else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
} }
comm.free(self.allocator, msg);
self.mu.lock(); self.mu.lock();
quit = self.want_stop; quit = self.want_stop;
self.mu.unlock(); self.mu.unlock();
} }
logger.info("exiting comm thread loop", .{}); logger.info("exiting comm thread loop", .{});
} }
@ -356,8 +358,8 @@ fn commThreadLoop(self: *Daemon) void {
fn sendPoweroffReport(self: *Daemon) !void { fn sendPoweroffReport(self: *Daemon) !void {
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len); var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
defer self.allocator.free(svstat); defer self.allocator.free(svstat);
for (self.services) |*sv, i| { for (self.services, svstat) |*sv, *stat| {
svstat[i] = .{ stat.* = .{
.name = sv.name, .name = sv.name,
.stopped = sv.status() == .stopped, .stopped = sv.status() == .stopped,
.err = if (sv.lastStopError()) |err| @errorName(err) else null, .err = if (sv.lastStopError()) |err| @errorName(err) else null,
@ -616,19 +618,19 @@ test "start-poweroff" {
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{ try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
.{ .name = "lnd", .stopped = false, .err = null }, .{ .name = "lnd", .stopped = false, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null }, .{ .name = "bitcoind", .stopped = false, .err = null },
} } }, msg1); } } }, msg1.value);
const msg2 = try comm.read(arena, gui_reader); const msg2 = try comm.read(arena, gui_reader);
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{ try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null }, .{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null }, .{ .name = "bitcoind", .stopped = false, .err = null },
} } }, msg2); } } }, msg2.value);
const msg3 = try comm.read(arena, gui_reader); const msg3 = try comm.read(arena, gui_reader);
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{ try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null }, .{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = true, .err = null }, .{ .name = "bitcoind", .stopped = true, .err = null },
} } }, msg3); } } }, msg3.value);
// TODO: ensure "poweroff" was executed once custom runner is in a zig release; // TODO: ensure "poweroff" was executed once custom runner is in a zig release;
// need custom runner to set up a global registry for child processes. // need custom runner to set up a global registry for child processes.

@ -55,16 +55,7 @@ pub const Client = struct {
}; };
pub fn Result(comptime m: Method) type { pub fn Result(comptime m: Method) type {
return struct { return std.json.Parsed(ResultValue(m));
value: ResultValue(m),
arena: *ArenaAllocator,
pub fn deinit(self: @This()) void {
const allocator = self.arena.child_allocator;
self.arena.deinit();
allocator.destroy(self.arena);
}
};
} }
pub fn ResultValue(comptime m: Method) type { pub fn ResultValue(comptime m: Method) type {
@ -142,27 +133,16 @@ pub const Client = struct {
} }
fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) { fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) {
var result = Result(m){ const jopt = std.json.ParseOptions{ .ignore_unknown_fields = true, .allocate = .alloc_always };
.value = undefined, const resp = try std.json.parseFromSlice(RpcResponse(m), self.allocator, b, jopt);
.arena = try self.allocator.create(ArenaAllocator), errdefer resp.deinit();
}; if (resp.value.@"error") |errfield| {
errdefer self.allocator.destroy(result.arena);
result.arena.* = ArenaAllocator.init(self.allocator);
var jstream = std.json.TokenStream.init(b);
const jopt = std.json.ParseOptions{ .allocator = result.arena.allocator(), .ignore_unknown_fields = true };
const resp = try std.json.parse(RpcResponse(m), &jstream, jopt);
errdefer result.arena.deinit();
if (resp.@"error") |errfield| {
return rpcErrorFromCode(errfield.code) orelse error.UnknownError; return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
} }
if (resp.result == null) { if (resp.value.result == null) {
return error.NullResult; return error.NullResult;
} }
return .{ .value = resp.value.result.?, .arena = resp.arena };
result.value = resp.result.?;
return result;
} }
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 { fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
@ -189,7 +169,7 @@ pub const Client = struct {
try w.print("Content-Length: {d}\r\n", .{jreq.items.len}); try w.print("Content-Length: {d}\r\n", .{jreq.items.len});
try w.writeAll("\r\n"); try w.writeAll("\r\n");
try w.writeAll(jreq.items); try w.writeAll(jreq.items);
return bytes.toOwnedSlice(); return try bytes.toOwnedSlice();
} }
fn getAuthBase64(self: Client) ![]const u8 { fn getAuthBase64(self: Client) ![]const u8 {

@ -62,40 +62,34 @@ pub fn addWifi(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, ssid: []const u8
/// reports network status to the writer w in `comm.Message.NetworkReport` format. /// reports network status to the writer w in `comm.Message.NetworkReport` format.
pub fn sendReport(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, w: anytype) !void { pub fn sendReport(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, w: anytype) !void {
var arena_state = std.heap.ArenaAllocator.init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
var report = comm.Message.NetworkReport{ var report = comm.Message.NetworkReport{
.ipaddrs = undefined, .ipaddrs = undefined,
.wifi_ssid = null, .wifi_ssid = null,
.wifi_scan_networks = undefined, .wifi_scan_networks = &.{},
}; };
// fetch all public IP addresses using getifaddrs // fetch all public IP addresses using getifaddrs
const pubaddr = try nif.pubAddresses(gpa, null); const pubaddr = try nif.pubAddresses(arena, null);
defer gpa.free(pubaddr); var ipaddr = try std.ArrayList([]const u8).initCapacity(arena, pubaddr.len);
//var addrs = std.ArrayList([]).init(t.allocator); for (pubaddr) |apub| {
var ipaddrs = try gpa.alloc([]const u8, pubaddr.len); try ipaddr.append(try std.fmt.allocPrint(arena, "{}", .{apub}));
for (pubaddr) |a, i| {
ipaddrs[i] = try std.fmt.allocPrint(gpa, "{s}", .{a});
} }
defer { report.ipaddrs = try ipaddr.toOwnedSlice();
for (ipaddrs) |a| gpa.free(a);
gpa.free(ipaddrs);
}
report.ipaddrs = ipaddrs;
// get currently connected SSID, if any, from WPA ctrl // get currently connected SSID, if any, from WPA ctrl
const ssid = queryWifiSSID(gpa, wpa_ctrl) catch |err| blk: { report.wifi_ssid = queryWifiSSID(arena, wpa_ctrl) catch |err| blk: {
logger.err("queryWifiSsid: {any}", .{err}); logger.err("queryWifiSsid: {any}", .{err});
break :blk null; break :blk null;
}; };
defer if (ssid) |v| gpa.free(v);
report.wifi_ssid = ssid;
// fetch available wifi networks from scan results using WPA ctrl // fetch available wifi networks from scan results using WPA ctrl
var wifi_networks: ?types.StringList = if (queryWifiScanResults(gpa, wpa_ctrl)) |v| v else |err| blk: { var wifi_networks: ?types.StringList = if (queryWifiScanResults(arena, wpa_ctrl)) |v| v else |err| blk: {
logger.err("queryWifiScanResults: {any}", .{err}); logger.err("queryWifiScanResults: {any}", .{err});
break :blk null; break :blk null;
}; };
defer if (wifi_networks) |*list| list.deinit();
if (wifi_networks) |list| { if (wifi_networks) |list| {
report.wifi_scan_networks = list.items(); report.wifi_scan_networks = list.items();
} }
@ -179,5 +173,5 @@ fn queryWifiNetworksList(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, filter
} }
list.append(id) catch {}; // grab anything we can list.append(id) catch {}; // grab anything we can
} }
return list.toOwnedSlice(); return try list.toOwnedSlice();
} }

@ -10,12 +10,6 @@ const lvgl = @import("ui/lvgl.zig");
const screen = @import("ui/screen.zig"); const screen = @import("ui/screen.zig");
const symbol = @import("ui/symbol.zig"); const symbol = @import("ui/symbol.zig");
/// SIGPIPE is triggered when a process attempts to write to a broken pipe.
/// by default, SIGPIPE terminates the process without invoking a panic handler.
/// this declaration makes such writes result in EPIPE (error.BrokenPipe) to let
/// the program can handle it.
pub const keep_sigpipe = true;
const logger = std.log.scoped(.ngui); const logger = std.log.scoped(.ngui);
// these are auto-closed as soon as main fn terminates. // these are auto-closed as soon as main fn terminates.
@ -46,39 +40,40 @@ var state: enum {
/// while deinit and replace handle concurrency, field access requires holding mu. /// while deinit and replace handle concurrency, field access requires holding mu.
var last_report: struct { var last_report: struct {
mu: std.Thread.Mutex = .{}, mu: std.Thread.Mutex = .{},
network: ?comm.Message.NetworkReport = null, network: ?comm.ParsedMessage = null, // NetworkReport
bitcoind: ?comm.Message.BitcoindReport = null, bitcoind: ?comm.ParsedMessage = null, // BitcoinReport
fn deinit(self: *@This()) void { fn deinit(self: *@This()) void {
self.mu.lock(); self.mu.lock();
defer self.mu.unlock(); defer self.mu.unlock();
if (self.network) |v| { if (self.network) |v| {
comm.free(gpa, .{ .network_report = v }); v.deinit();
self.network = null; self.network = null;
} }
if (self.bitcoind) |v| { if (self.bitcoind) |v| {
comm.free(gpa, .{ .bitcoind_report = v }); v.deinit();
self.bitcoind = null; self.bitcoind = null;
} }
} }
fn replace(self: *@This(), new: anytype) void { fn replace(self: *@This(), new: comm.ParsedMessage) void {
self.mu.lock(); self.mu.lock();
defer self.mu.unlock(); defer self.mu.unlock();
switch (@TypeOf(new)) { const tag: comm.MessageTag = new.value;
comm.Message.NetworkReport => { switch (tag) {
.network_report => {
if (self.network) |old| { if (self.network) |old| {
comm.free(gpa, .{ .network_report = old }); old.deinit();
} }
self.network = new; self.network = new;
}, },
comm.Message.BitcoindReport => { .bitcoind_report => {
if (self.bitcoind) |old| { if (self.bitcoind) |old| {
comm.free(gpa, .{ .bitcoind_report = old }); old.deinit();
} }
self.bitcoind = new; self.bitcoind = new;
}, },
else => @compileError("unhandled type: " ++ @typeName(@TypeOf(new))), else => |t| logger.err("last_report: replace: unhandled tag {}", .{t}),
} }
} }
} = .{}; } = .{};
@ -103,9 +98,9 @@ export fn nm_get_curr_tick() u32 {
const ms = tick_timer.read() / time.ns_per_ms; const ms = tick_timer.read() / time.ns_per_ms;
const over = ms >> 32; const over = ms >> 32;
if (over > 0) { if (over > 0) {
return @truncate(u32, over); // LVGL deals with overflow correctly return @truncate(over); // LVGL deals with overflow correctly
} }
return @truncate(u32, ms); return @truncate(ms);
} }
export fn nm_check_idle_time(_: *lvgl.LvTimer) void { export fn nm_check_idle_time(_: *lvgl.LvTimer) void {
@ -223,27 +218,27 @@ fn commThreadLoopCycle() !void {
ui_mutex.lock(); // guards the state and all UI calls below ui_mutex.lock(); // guards the state and all UI calls below
defer ui_mutex.unlock(); defer ui_mutex.unlock();
switch (state) { switch (state) {
.standby => switch (msg) { .standby => switch (msg.value) {
.ping => try comm.write(gpa, stdout, comm.Message.pong), .ping => try comm.write(gpa, stdout, comm.Message.pong),
.network_report => |v| last_report.replace(v), .network_report => last_report.replace(msg),
.bitcoind_report => |v| last_report.replace(v), .bitcoind_report => last_report.replace(msg),
else => logger.debug("ignoring {s}: in standby", .{@tagName(msg)}), else => logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)}),
}, },
.active, .alert => switch (msg) { .active, .alert => switch (msg.value) {
.ping => try comm.write(gpa, stdout, comm.Message.pong), .ping => try comm.write(gpa, stdout, comm.Message.pong),
.poweroff_progress => |rep| { .poweroff_progress => |rep| {
ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err}); ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
comm.free(gpa, msg); msg.deinit();
}, },
.network_report => |rep| { .network_report => |rep| {
updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err}); updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
last_report.replace(rep); last_report.replace(msg);
}, },
.bitcoind_report => |rep| { .bitcoind_report => |rep| {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err}); ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
last_report.replace(rep); last_report.replace(msg);
}, },
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}), else => logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)}),
}, },
} }
} }
@ -281,11 +276,15 @@ fn uiThreadLoop() void {
last_report.mu.lock(); last_report.mu.lock();
defer last_report.mu.unlock(); defer last_report.mu.unlock();
if (last_report.network) |rep| { if (last_report.network) |msg| {
updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err}); updateNetworkStatus(msg.value.network_report) catch |err| {
logger.err("updateNetworkStatus: {any}", .{err});
};
} }
if (last_report.bitcoind) |rep| { if (last_report.bitcoind) |msg| {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err}); ui.bitcoin.updateTabPanel(msg.value.bitcoind_report) catch |err| {
logger.err("bitcoin.updateTabPanel: {any}", .{err});
};
} }
} }
continue; continue;
@ -342,7 +341,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
pub fn main() anyerror!void { pub fn main() anyerror!void {
// main heap allocator used through the lifetime of nd // main heap allocator used through the lifetime of nd
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) { defer if (gpa_state.deinit() == .leak) {
logger.err("memory leaks detected", .{}); logger.err("memory leaks detected", .{});
}; };
gpa = gpa_state.allocator(); gpa = gpa_state.allocator();

@ -55,8 +55,8 @@ pub const TestChildProcess = struct {
pub fn init(argv: []const []const u8, allocator: std.mem.Allocator) TestChildProcess { pub fn init(argv: []const []const u8, allocator: std.mem.Allocator) TestChildProcess {
var adup = allocator.alloc([]u8, argv.len) catch unreachable; var adup = allocator.alloc([]u8, argv.len) catch unreachable;
for (argv) |v, i| { for (argv, adup) |v, *dup| {
adup[i] = allocator.dupe(u8, v) catch unreachable; dup.* = allocator.dupe(u8, v) catch unreachable;
} }
return .{ return .{
.allocator = allocator, .allocator = allocator,
@ -167,10 +167,10 @@ pub const TestWpaControl = struct {
} }
}; };
/// similar to std.testing.expectEqual but compares slices with expectEqualSlices /// similar to std.testing.expectEqualDeep but compares slices with expectEqualSlices
/// or expectEqualStrings where slice element is a u8. /// or expectEqualStrings where slice element is a u8.
/// unhandled types are passed to std.testing.expectEqualDeep.
pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void { pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
const t = std.testing;
switch (@typeInfo(@TypeOf(actual))) { switch (@typeInfo(@TypeOf(actual))) {
.Pointer => |p| { .Pointer => |p| {
switch (p.size) { switch (p.size) {
@ -185,7 +185,7 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
} }
break :blk null; break :blk null;
}; };
const n = std.math.min(expected.len, actual.len); const n = @min(expected.len, actual.len);
var i: usize = 0; var i: usize = 0;
while (i < n) : (i += 1) { while (i < n) : (i += 1) {
expectDeepEqual(expected[i], actual[i]) catch |e| { expectDeepEqual(expected[i], actual[i]) catch |e| {
@ -199,14 +199,14 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
}, },
else => { else => {
if (p.child == u8) { if (p.child == u8) {
try t.expectEqualStrings(expected, actual); try std.testing.expectEqualStrings(expected, actual);
} else { } else {
try t.expectEqualSlices(p.child, expected, actual); try std.testing.expectEqualSlices(p.child, expected, actual);
} }
}, },
} }
}, },
else => try t.expectEqual(expected, actual), else => try std.testing.expectEqualDeep(expected, actual),
} }
}, },
.Struct => |st| { .Struct => |st| {
@ -238,17 +238,15 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
} }
const Tag = std.meta.Tag(@TypeOf(expected)); const Tag = std.meta.Tag(@TypeOf(expected));
const atag = @as(Tag, actual); const atag = @as(Tag, actual);
try t.expectEqual(@as(Tag, expected), atag); try std.testing.expectEqual(@as(Tag, expected), atag);
inline for (u.fields) |f| { switch (expected) {
if (std.mem.eql(u8, f.name, @tagName(atag))) { inline else => |x, tag| {
try expectDeepEqual(@field(expected, f.name), @field(actual, f.name)); try expectDeepEqual(x, @field(actual, @tagName(tag)));
return; },
}
} }
unreachable;
}, },
else => { else => {
try t.expectEqual(expected, actual); try std.testing.expectEqualDeep(expected, actual);
}, },
} }
} }

@ -4,7 +4,7 @@ const bitcoinrpc = @import("bitcoindrpc");
pub fn main() !void { pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) { defer if (gpa_state.deinit() == .leak) {
std.debug.print("memory leaks detected!", .{}); std.debug.print("memory leaks detected!", .{});
}; };
const gpa = gpa_state.allocator(); const gpa = gpa_state.allocator();

@ -87,9 +87,10 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
logger.err("comm.read: {any}", .{err}); logger.err("comm.read: {any}", .{err});
continue; continue;
}; };
defer msg.deinit();
logger.debug("got ui msg tagged {s}", .{@tagName(msg)}); logger.debug("got msg: {s}", .{@tagName(msg.value)});
switch (msg) { switch (msg.value) {
.pong => { .pong => {
logger.info("received pong from ngui", .{}); logger.info("received pong from ngui", .{});
}, },
@ -131,18 +132,18 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
while (true) { while (true) {
time.sleep(time.ns_per_s); time.sleep(time.ns_per_s);
if (sectimer.read() < time.ns_per_s) { if (sectimer.read() < 3 * time.ns_per_s) {
continue; continue;
} }
sectimer.reset(); sectimer.reset();
block_count += 1; block_count += 1;
const now = time.timestamp(); const now = time.timestamp();
const btcrep: comm.Message.BitcoindReport = .{ const btcrep: comm.Message.BitcoindReport = .{
.blocks = block_count, .blocks = block_count,
.headers = block_count, .headers = block_count,
.timestamp = @intCast(u64, now), .timestamp = @intCast(now),
.hash = "00000000000000000002bf8029f6be4e40b4a3e0e161b6a1044ddaf9eb126504", .hash = "00000000000000000002bf8029f6be4e40b4a3e0e161b6a1044ddaf9eb126504",
.ibd = false, .ibd = false,
.verifyprogress = 100, .verifyprogress = 100,
@ -155,7 +156,7 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
.mempool = .{ .mempool = .{
.loaded = true, .loaded = true,
.txcount = 100000 + block_count, .txcount = 100000 + block_count,
.usage = std.math.min(200123456 + block_count * 10, 300000000), .usage = @min(200123456 + block_count * 10, 300000000),
.max = 300000000, .max = 300000000,
.totalfee = 2.23049932, .totalfee = 2.23049932,
.minfee = 0.00004155, .minfee = 0.00004155,
@ -168,7 +169,7 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
pub fn main() !void { pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) { defer if (gpa_state.deinit() == .leak) {
logger.err("memory leaks detected", .{}); logger.err("memory leaks detected", .{});
}; };
const gpa = gpa_state.allocator(); const gpa = gpa_state.allocator();

@ -106,10 +106,10 @@ pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void {
if (rep.mempool.max == 0) { if (rep.mempool.max == 0) {
break :pct 0; break :pct 0;
} }
const v = @intToFloat(f64, rep.mempool.usage) / @intToFloat(f64, rep.mempool.max); const v = @as(f64, @floatFromInt(rep.mempool.usage)) / @as(f64, @floatFromInt(rep.mempool.max));
break :pct @floatCast(f32, v * 100); break :pct @floatCast(v * 100);
}; };
tab.mempool.usage_bar.setValue(@floatToInt(i32, @round(mempool_pct))); tab.mempool.usage_bar.setValue(@as(i32, @intFromFloat(@round(mempool_pct))));
try tab.mempool.usage_lab.setTextFmt(&buf, "{:.1} " ++ cmark ++ "out of# {:.1} ({d:.1}%)", .{ try tab.mempool.usage_lab.setTextFmt(&buf, "{:.1} " ++ cmark ++ "out of# {:.1} ({d:.1}%)", .{
fmt.fmtIntSizeBin(rep.mempool.usage), fmt.fmtIntSizeBin(rep.mempool.usage),
fmt.fmtIntSizeBin(rep.mempool.max), fmt.fmtIntSizeBin(rep.mempool.max),

@ -203,7 +203,7 @@ pub const LvStyle = opaque {
/// produces an int value suitable for lv_xxx functions. /// produces an int value suitable for lv_xxx functions.
fn value(self: Selector) c.lv_style_selector_t { fn value(self: Selector) c.lv_style_selector_t {
return @enumToInt(self.part) | @enumToInt(self.state); return @intFromEnum(self.part) | @intFromEnum(self.state);
} }
}; };
}; };
@ -249,11 +249,11 @@ const RGB16 = packed struct {
/// rgb produces a Color value base on the red, green and blue components. /// rgb produces a Color value base on the red, green and blue components.
pub inline fn rgb(r: u8, g: u8, b: u8) Color { pub inline fn rgb(r: u8, g: u8, b: u8) Color {
const c16 = RGB16{ const c16 = RGB16{
.b = @truncate(u5, b >> 3), .b = @truncate(b >> 3),
.g = @truncate(u6, g >> 2), .g = @truncate(g >> 2),
.r = @truncate(u5, r >> 3), .r = @truncate(r >> 3),
}; };
return @bitCast(Color, c16); return @bitCast(c16);
} }
/// black color /// black color
@ -295,19 +295,19 @@ pub const Palette = enum(c.lv_palette_t) {
/// returns main color from the predefined palette. /// returns main color from the predefined palette.
pub inline fn main(p: Palette) Color { pub inline fn main(p: Palette) Color {
return lv_palette_main(@enumToInt(p)); return lv_palette_main(@intFromEnum(p));
} }
/// makes the main color from the predefined palette lighter according to the /// makes the main color from the predefined palette lighter according to the
/// specified level. /// specified level.
pub inline fn lighten(p: Palette, l: ModLevel) Color { pub inline fn lighten(p: Palette, l: ModLevel) Color {
return lv_palette_lighten(@enumToInt(p), @enumToInt(l)); return lv_palette_lighten(@intFromEnum(p), @intFromEnum(l));
} }
/// makes the main color from the predefined palette darker according to the /// makes the main color from the predefined palette darker according to the
/// specified level. /// specified level.
pub inline fn darken(p: Palette, l: ModLevel) Color { pub inline fn darken(p: Palette, l: ModLevel) Color {
return lv_palette_darken(@enumToInt(p), @enumToInt(l)); return lv_palette_darken(@intFromEnum(p), @intFromEnum(l));
} }
}; };
@ -326,16 +326,16 @@ pub const BaseObjMethods = struct {
/// sets or clears an object flag. /// sets or clears an object flag.
pub fn setFlag(self: anytype, v: LvObj.Flag) void { pub fn setFlag(self: anytype, v: LvObj.Flag) void {
lv_obj_add_flag(self.lvobj, @enumToInt(v)); lv_obj_add_flag(self.lvobj, @intFromEnum(v));
} }
pub fn clearFlag(self: anytype, v: LvObj.Flag) void { pub fn clearFlag(self: anytype, v: LvObj.Flag) void {
lv_obj_clear_flag(self.lvobj, @enumToInt(v)); lv_obj_clear_flag(self.lvobj, @intFromEnum(v));
} }
/// reports whether the object has v flag set. /// reports whether the object has v flag set.
pub fn hasFlag(self: anytype, v: LvObj.Flag) bool { pub fn hasFlag(self: anytype, v: LvObj.Flag) bool {
return lv_obj_has_flag(self.lvobj, @enumToInt(v)); return lv_obj_has_flag(self.lvobj, @intFromEnum(v));
} }
/// returns a user data pointer associated with the object. /// returns a user data pointer associated with the object.
@ -409,7 +409,7 @@ pub const WidgetMethods = struct {
/// aligns object position. the offset is relative to the specified alignment a. /// aligns object position. the offset is relative to the specified alignment a.
pub fn posAlign(self: anytype, a: PosAlign, xoffset: Coord, yoffset: Coord) void { pub fn posAlign(self: anytype, a: PosAlign, xoffset: Coord, yoffset: Coord) void {
lv_obj_align(self.lvobj, @enumToInt(a), xoffset, yoffset); lv_obj_align(self.lvobj, @intFromEnum(a), xoffset, yoffset);
} }
/// sets flex layout growth property; same meaning as in CSS flex. /// sets flex layout growth property; same meaning as in CSS flex.
@ -548,24 +548,24 @@ pub const FlexLayout = struct {
} }
fn adopt(obj: *LvObj, flow: Flow, opt: AlignOpt) FlexLayout { fn adopt(obj: *LvObj, flow: Flow, opt: AlignOpt) FlexLayout {
lv_obj_set_flex_flow(obj, @enumToInt(flow)); lv_obj_set_flex_flow(obj, @intFromEnum(flow));
if (opt.all) |a| { if (opt.all) |a| {
const v = @enumToInt(a); const v = @intFromEnum(a);
lv_obj_set_flex_align(obj, v, v, v); lv_obj_set_flex_align(obj, v, v, v);
} else { } else {
lv_obj_set_flex_align(obj, @enumToInt(opt.main), @enumToInt(opt.cross), @enumToInt(opt.track)); lv_obj_set_flex_align(obj, @intFromEnum(opt.main), @intFromEnum(opt.cross), @intFromEnum(opt.track));
} }
return .{ .lvobj = obj }; return .{ .lvobj = obj };
} }
/// sets flex layout flow on the object. /// sets flex layout flow on the object.
pub fn setFlow(self: FlexLayout, ff: Flow) void { pub fn setFlow(self: FlexLayout, ff: Flow) void {
lv_obj_set_flex_flow(self.lvobj, @enumToInt(ff)); lv_obj_set_flex_flow(self.lvobj, @intFromEnum(ff));
} }
/// sets flex layout alignments. /// sets flex layout alignments.
pub fn setAlign(self: FlexLayout, main: Align, cross: AlignCross, track: Align) void { pub fn setAlign(self: FlexLayout, main: Align, cross: AlignCross, track: Align) void {
lv_obj_set_flex_align(self.lvobj, @enumToInt(main), @enumToInt(cross), @enumToInt(track)); lv_obj_set_flex_align(self.lvobj, @intFromEnum(main), @intFromEnum(cross), @intFromEnum(track));
} }
/// same as setPad .column but using a default constant to make flex layouts consistent. /// same as setPad .column but using a default constant to make flex layouts consistent.
@ -645,10 +645,10 @@ pub const Label = struct {
lv_label_set_text(lv_label, text); lv_label_set_text(lv_label, text);
//lv_obj_set_height(lb, sizeContent); // default //lv_obj_set_height(lb, sizeContent); // default
if (opt.long_mode) |m| { if (opt.long_mode) |m| {
lv_label_set_long_mode(lv_label, @enumToInt(m)); lv_label_set_long_mode(lv_label, @intFromEnum(m));
} }
if (opt.pos) |p| { if (opt.pos) |p| {
lv_obj_align(lv_label, @enumToInt(p), 0, 0); lv_obj_align(lv_label, @intFromEnum(p), 0, 0);
} }
if (opt.recolor) { if (opt.recolor) {
lv_label_set_recolor(lv_label, true); lv_label_set_recolor(lv_label, true);

@ -67,12 +67,12 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
btncont.setHeightToContent(); btncont.setHeightToContent();
// leave 5% as an extra spacing. // leave 5% as an extra spacing.
const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @truncate(u8, btns.len))); const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @as(u8, @truncate(btns.len))));
for (btns) |btext, i| { for (btns, 0..) |btext, i| {
const btn = try lvgl.TextButton.new(btncont, btext); const btn = try lvgl.TextButton.new(btncont, btext);
btn.setFlag(.event_bubble); btn.setFlag(.event_bubble);
btn.setFlag(.user1); // .user1 indicates actionable button in callback btn.setFlag(.user1); // .user1 indicates actionable button in callback
btn.setUserdata(@intToPtr(?*anyopaque, i)); // button index in callback btn.setUserdata(@ptrFromInt(i)); // button index in callback
btn.setWidth(btnwidth); btn.setWidth(btnwidth);
if (i == 0) { if (i == 0) {
btn.addStyle(lvgl.nm_style_btn_red(), .{}); btn.addStyle(lvgl.nm_style_btn_red(), .{});
@ -88,9 +88,9 @@ export fn nm_modal_callback(e: *lvgl.LvEvent) void {
return; return;
} }
const btn_index = @ptrToInt(target.userdata()); const btn_index = @intFromPtr(target.userdata());
const win = lvgl.Window{ .lvobj = @ptrCast(*lvgl.LvObj, edata) }; const win = lvgl.Window{ .lvobj = @ptrCast(edata) };
const cb = @ptrCast(ModalButtonCallbackFn, @alignCast(@alignOf(ModalButtonCallbackFn), win.userdata())); const cb: ModalButtonCallbackFn = @alignCast(@ptrCast(win.userdata()));
win.destroy(); win.destroy();
cb(btn_index); cb(btn_index);
} }

@ -14,7 +14,7 @@ fn formatUnix(sec: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w
if (sec > std.math.maxInt(u47)) { if (sec > std.math.maxInt(u47)) {
// EpochSeconds.getEpochDay trucates to u47 which results in a "truncated bits" // EpochSeconds.getEpochDay trucates to u47 which results in a "truncated bits"
// panic for too big numbers. so, just print raw digits. // panic for too big numbers. so, just print raw digits.
return std.fmt.format(w, "{d}", .{sec}); return std.fmt.formatInt(sec, 10, .lower, .{}, w);
} }
const epoch: std.time.epoch.EpochSeconds = .{ .secs = sec }; const epoch: std.time.epoch.EpochSeconds = .{ .secs = sec };
const daysec = epoch.getDaySeconds(); const daysec = epoch.getDaySeconds();

@ -1,12 +1,12 @@
# ci container file for compiling and testing zig projects. # ci container file for compiling and testing zig projects.
# requires a ZIGURL build arg. for instance: # requires a ZIGURL build arg. for instance:
# podman build --rm -t ci-zig0.10.1 -f ci-containerfile \ # podman build --rm -t ci-zig0.11.0 -f ci-containerfile \
# --build-arg ZIGURL=https://ziglang.org/download/0.10.1/zig-linux-x86_64-0.10.1.tar.xz # --build-arg ZIGURL=https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
FROM alpine:3.17.1 FROM alpine:3.18.3
ARG ZIGURL ARG ZIGURL
RUN apk add --no-cache git curl xz sdl2-dev clang15-extra-tools && \ RUN apk add --no-cache git curl xz libc-dev sdl2-dev clang16-extra-tools && \
mkdir -p /tools/zig && \ mkdir -p /tools/zig && \
cd /tools/zig && \ cd /tools/zig && \
curl -o zig.tar.xz $ZIGURL && \ curl -o zig.tar.xz $ZIGURL && \