Compare commits

..

No commits in common. 'master' and 'v0.7.0' have entirely different histories.

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

@ -61,17 +61,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:
podman build --rm -t ndg-ci -f ./tools/ci-containerfile \
--build-arg ZIGURL=https://ziglang.org/download/0.12.0/zig-linux-x86_64-0.12.0.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:
podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.12.0: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),
login to the container registry and push the image to remote:
podman login git.qcode.ch
podman push git.qcode.ch/nakamochi/ci-zig0.12.0:v1
podman push git.qcode.ch/nakamochi/ci-zig0.11.0:v2
the image will be available at
https://git.qcode.ch/nakamochi/-/packages/

@ -11,16 +11,18 @@ pub fn build(b: *std.Build) void {
const inver = b.option([]const u8, "version", "semantic version of the build; must match git tag when available");
const buildopts = b.addOptions();
const buildopts_mod = buildopts.createModule();
buildopts.addOption(DriverTarget, "driver", drv);
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.lazyDependency("nif", .{ .target = target, .optimize = optimize }) orelse return;
const libnif_dep = b.anonymousDependency("lib/nif", @import("lib/nif/build.zig"), .{
.target = target,
.optimize = optimize,
});
const libnif = libnif_dep.artifact("nif");
// ini file format parser
const libini_dep = b.lazyDependency("ini", .{ .target = target, .optimize = optimize }) orelse return;
const libini = b.addModule("ini", .{ .source_file = .{ .path = "lib/ini/src/ini.zig" } });
const common_cflags = .{
"-Wall",
@ -33,16 +35,16 @@ pub fn build(b: *std.Build) void {
// gui build
const ngui = b.addExecutable(.{
.name = "ngui",
.root_source_file = b.path("src/ngui.zig"),
.root_source_file = .{ .path = "src/ngui.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
.strip = strip,
});
ngui.pie = true;
ngui.root_module.addImport("build_options", buildopts_mod);
ngui.addIncludePath(b.path("lib"));
ngui.addIncludePath(b.path("src/ui/c"));
ngui.strip = strip;
ngui.addOptions("build_options", buildopts);
ngui.addIncludePath(.{ .path = "lib" });
ngui.addIncludePath(.{ .path = "src/ui/c" });
const lvgl_flags = .{
"-std=c11",
@ -50,7 +52,7 @@ pub fn build(b: *std.Build) void {
"-Wformat",
"-Wformat-security",
} ++ common_cflags;
ngui.addCSourceFiles(.{ .files = lvgl_generic_src, .flags = &lvgl_flags });
ngui.addCSourceFiles(lvgl_generic_src, &lvgl_flags);
const ngui_cflags = .{
"-std=c11",
@ -58,46 +60,39 @@ pub fn build(b: *std.Build) void {
"-Wunused-parameter",
"-Werror",
} ++ common_cflags;
ngui.addCSourceFiles(.{
.root = b.path("src/ui/c"),
.files = &.{
"ui.c",
"lv_font_courierprimecode_14.c",
"lv_font_courierprimecode_16.c",
"lv_font_courierprimecode_24.c",
},
.flags = &ngui_cflags,
});
ngui.root_module.addCMacro("NM_DISP_HOR", b.fmt("{d}", .{disp_horiz}));
ngui.root_module.addCMacro("NM_DISP_VER", b.fmt("{d}", .{disp_vert}));
ngui.defineCMacro("LV_CONF_INCLUDE_SIMPLE", "1");
ngui.addCSourceFiles(&.{
"src/ui/c/ui.c",
"src/ui/c/lv_font_courierprimecode_14.c",
"src/ui/c/lv_font_courierprimecode_16.c",
"src/ui/c/lv_font_courierprimecode_24.c",
}, &ngui_cflags);
ngui.defineCMacroRaw(b.fmt("NM_DISP_HOR={}", .{disp_horiz}));
ngui.defineCMacroRaw(b.fmt("NM_DISP_VER={}", .{disp_vert}));
ngui.defineCMacro("LV_CONF_INCLUDE_SIMPLE", null);
ngui.defineCMacro("LV_LOG_LEVEL", lvgl_loglevel.text());
ngui.defineCMacro("LV_TICK_CUSTOM", "1");
ngui.defineCMacro("LV_TICK_CUSTOM_INCLUDE", "\"lv_custom_tick.h\"");
ngui.defineCMacro("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(nm_get_curr_tick())");
switch (drv) {
.sdl2 => {
ngui.addCSourceFiles(.{ .files = lvgl_sdl2_src, .flags = &lvgl_flags });
ngui.addCSourceFile(.{ .file = b.path("src/ui/c/drv_sdl2.c"), .flags = &ngui_cflags });
ngui.addCSourceFiles(lvgl_sdl2_src, &lvgl_flags);
ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_sdl2.c" }, .flags = &ngui_cflags });
ngui.defineCMacro("USE_SDL", "1");
ngui.linkSystemLibrary("SDL2");
},
.x11 => {
ngui.addCSourceFiles(.{ .files = lvgl_x11_src, .flags = &lvgl_flags });
ngui.addCSourceFiles(.{
.files = &.{
ngui.addCSourceFiles(lvgl_x11_src, &lvgl_flags);
ngui.addCSourceFiles(&.{
"src/ui/c/drv_x11.c",
"src/ui/c/mouse_cursor_icon.c",
},
.flags = &ngui_cflags,
});
}, &ngui_cflags);
ngui.defineCMacro("USE_X11", "1");
ngui.linkSystemLibrary("X11");
},
.fbev => {
ngui.addCSourceFiles(.{ .files = lvgl_fbev_src, .flags = &lvgl_flags });
ngui.addCSourceFile(.{ .file = b.path("src/ui/c/drv_fbev.c"), .flags = &ngui_cflags });
ngui.addCSourceFiles(lvgl_fbev_src, &lvgl_flags);
ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_fbev.c" }, .flags = &ngui_cflags });
ngui.defineCMacro("USE_FBDEV", "1");
ngui.defineCMacro("USE_EVDEV", "1");
},
@ -109,15 +104,15 @@ pub fn build(b: *std.Build) void {
// daemon build
const nd = b.addExecutable(.{
.name = "nd",
.root_source_file = b.path("src/nd.zig"),
.root_source_file = .{ .path = "src/nd.zig" },
.target = target,
.optimize = optimize,
.strip = strip,
});
nd.pie = true;
nd.root_module.addImport("build_options", buildopts_mod);
nd.root_module.addImport("nif", libnif_dep.module("nif"));
nd.root_module.addImport("ini", libini_dep.module("ini"));
nd.strip = strip;
nd.addOptions("build_options", buildopts);
nd.addModule("nif", libnif_dep.module("nif"));
nd.addModule("ini", libini);
nd.linkLibrary(libnif);
const nd_build_step = b.step("nd", "build nd (nakamochi daemon)");
@ -126,15 +121,15 @@ pub fn build(b: *std.Build) void {
// automated tests
{
const tests = b.addTest(.{
.root_source_file = b.path("src/test.zig"),
.root_source_file = .{ .path = "src/test.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
.filter = b.option([]const u8, "test-filter", "run tests matching the filter"),
});
tests.root_module.addImport("build_options", buildopts_mod);
tests.root_module.addImport("nif", libnif_dep.module("nif"));
tests.root_module.addImport("ini", libini_dep.module("ini"));
tests.addOptions("build_options", buildopts);
tests.addModule("nif", libnif_dep.module("nif"));
tests.addModule("ini", libini);
tests.linkLibrary(libnif);
const run_tests = b.addRunArtifact(tests);
@ -146,11 +141,11 @@ pub fn build(b: *std.Build) void {
{
const guiplay = b.addExecutable(.{
.name = "guiplay",
.root_source_file = b.path("src/test/guiplay.zig"),
.root_source_file = .{ .path = "src/test/guiplay.zig" },
.target = target,
.optimize = optimize,
});
guiplay.root_module.addImport("comm", b.createModule(.{ .root_source_file = b.path("src/comm.zig") }));
guiplay.addModule("comm", b.createModule(.{ .source_file = .{ .path = "src/comm.zig" } }));
const guiplay_build_step = b.step("guiplay", "build GUI playground");
guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay, .{}).step);
@ -161,12 +156,12 @@ pub fn build(b: *std.Build) void {
{
const btcrpc = b.addExecutable(.{
.name = "btcrpc",
.root_source_file = b.path("src/test/btcrpc.zig"),
.root_source_file = .{ .path = "src/test/btcrpc.zig" },
.target = target,
.optimize = optimize,
.strip = strip,
});
btcrpc.root_module.addImport("bitcoindrpc", b.createModule(.{ .root_source_file = b.path("src/bitcoindrpc.zig") }));
btcrpc.strip = strip;
btcrpc.addModule("bitcoindrpc", b.createModule(.{ .source_file = .{ .path = "src/bitcoindrpc.zig" } }));
const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step);
@ -176,12 +171,12 @@ pub fn build(b: *std.Build) void {
{
const lndhc = b.addExecutable(.{
.name = "lndhc",
.root_source_file = b.path("src/test/lndhc.zig"),
.root_source_file = .{ .path = "src/test/lndhc.zig" },
.target = target,
.optimize = optimize,
.strip = strip,
});
lndhc.root_module.addImport("lightning", b.createModule(.{ .root_source_file = b.path("src/lightning.zig") }));
lndhc.strip = strip;
lndhc.addModule("lightning", b.createModule(.{ .source_file = .{ .path = "src/lightning.zig" } }));
const lndhc_build_step = b.step("lndhc", "lnd HTTP API client playground");
lndhc_build_step.dependOn(&b.addInstallArtifact(lndhc, .{}).step);
@ -428,7 +423,7 @@ const VersionStep = struct {
}
fn make(step: *std.Build.Step, _: *std.Progress.Node) anyerror!void {
const self: *@This() = @fieldParentPtr("step", step);
const self = @fieldParentPtr(VersionStep, "step", step);
const semver = try self.eval();
std.log.info("build version: {any}", .{semver});
self.buildopts.addOption(std.SemanticVersion, "semver", semver);
@ -465,7 +460,7 @@ const VersionStep = struct {
const matchTag = self.b.fmt("{s}*.*.*", .{prefix});
const cmd = [_][]const u8{ git, "-C", self.b.pathFromRoot("."), "describe", "--match", matchTag, "--tags", "--abbrev=8" };
var code: u8 = undefined;
const git_describe = self.b.runAllowFail(&cmd, &code, .Ignore) catch return null;
const git_describe = self.b.execAllowFail(&cmd, &code, .Ignore) catch return null;
const repotag = std.mem.trim(u8, git_describe, " \n\r")[prefix.len..];
return std.SemanticVersion.parse(repotag) catch |err| ret: {
std.log.err("unparsable git tag semver '{s}': {any}", .{ repotag, err });

@ -1,17 +0,0 @@
.{
.name = "ndg",
.version = "0.8.1",
.dependencies = .{
.nif = .{
.path = "lib/nif",
},
.ini = .{
.path = "lib/ini",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

@ -2,10 +2,9 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOptions(.{});
_ = b.addModule("ini", .{
.root_source_file = .{
.source_file = .{
.path = "src/ini.zig",
},
});
@ -13,10 +12,9 @@ pub fn build(b: *std.Build) void {
const lib = b.addStaticLibrary(.{
.name = "ini",
.root_source_file = .{ .path = "src/lib.zig" },
.target = target,
.target = b.standardTargetOptions(.{}),
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.addIncludePath(.{ .path = "src" });
lib.linkLibC();
@ -26,7 +24,6 @@ pub fn build(b: *std.Build) void {
const example_c = b.addExecutable(.{
.name = "example-c",
.optimize = optimize,
.target = target,
});
example_c.addCSourceFile(.{
.file = .{
@ -48,9 +45,8 @@ pub fn build(b: *std.Build) void {
.name = "example-zig",
.root_source_file = .{ .path = "example/example.zig" },
.optimize = optimize,
.target = target,
});
example_zig.root_module.addImport("ini", b.modules.get("ini").?);
example_zig.addModule("ini", b.modules.get("ini").?);
b.installArtifact(example_zig);

@ -1,10 +0,0 @@
.{
.name = "libini",
.version = "0.0.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

@ -78,7 +78,7 @@ test "buffer parser" {
}
test "file parser" {
const file = c.fopen("example/example.ini", "rb") orelse unreachable;
var file = c.fopen("example/example.ini", "rb") orelse unreachable;
defer _ = c.fclose(file);
var parser: c.ini_Parser = undefined;

@ -1,33 +1,30 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
_ = b.addModule("nif", .{ .root_source_file = b.path("nif.zig") });
_ = b.addModule("nif", .{ .source_file = .{ .path = "nif.zig" } });
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
.name = "nif",
.root_source_file = b.path("nif.zig"),
.root_source_file = .{ .path = "nif.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
});
lib.defineCMacro("CONFIG_CTRL_IFACE", null);
lib.defineCMacro("CONFIG_CTRL_IFACE_UNIX", null);
lib.addIncludePath(b.path("wpa_supplicant"));
lib.addCSourceFiles(.{
.files = &.{
lib.addIncludePath(.{ .path = "wpa_supplicant" });
lib.addCSourceFiles(&.{
"wpa_supplicant/wpa_ctrl.c",
"wpa_supplicant/os_unix.c",
},
.flags = &.{
}, &.{
"-Wall",
"-Wextra",
"-Wshadow",
"-Wundef",
"-Wunused-parameter",
"-Werror",
},
});
b.installArtifact(lib);
}

@ -1,10 +0,0 @@
.{
.name = "libnif",
.version = "0.0.1",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

@ -1,7 +1,7 @@
const std = @import("std");
const mem = std.mem;
const net = std.net;
const posix = std.posix;
const os = std.os;
pub const wpa = @import("wpa.zig");
@ -12,11 +12,11 @@ const ifaddrs = extern struct {
next: ?*ifaddrs,
name: [*:0]const u8,
flags: c_uint, // see IFF_xxx SIOCGIFFLAGS in netdevice(7)
addr: ?*std.posix.sockaddr,
netmask: ?*std.posix.sockaddr,
addr: ?*std.os.sockaddr,
netmask: ?*std.os.sockaddr,
ifu: extern union {
broad: *posix.sockaddr, // flags & IFF_BROADCAST
dst: *posix.sockaddr, // flags & IFF_POINTOPOINT
broad: *os.sockaddr, // flags & IFF_BROADCAST
dst: *os.sockaddr, // flags & IFF_POINTOPOINT
},
data: ?*anyopaque,
};
@ -37,8 +37,8 @@ pub fn pubAddresses(allocator: mem.Allocator, ifname: ?[]const u8) ![]net.Addres
var list = std.ArrayList(net.Address).init(allocator);
var it: ?*ifaddrs = res;
while (it) |ifa| : (it = ifa.next) {
const sa: *posix.sockaddr = ifa.addr orelse continue;
if (sa.family != posix.AF.INET and sa.family != posix.AF.INET6) {
const sa: *os.sockaddr = ifa.addr orelse continue;
if (sa.family != os.AF.INET and sa.family != os.AF.INET6) {
// not an IP address
continue;
}
@ -47,7 +47,7 @@ pub fn pubAddresses(allocator: mem.Allocator, ifname: ?[]const u8) ![]net.Addres
continue;
}
const ipaddr = net.Address.initPosix(@alignCast(sa)); // initPosix makes a copy
if (ipaddr.any.family == posix.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
// non-zero scopes make sense for link-local addr only.
continue;

@ -2,7 +2,7 @@
const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
const Atomic = std.atomic.Value;
const Atomic = std.atomic.Atomic;
const base64enc = std.base64.standard.Encoder;
const types = @import("types.zig");
@ -13,7 +13,7 @@ pub const Client = struct {
addr: []const u8 = "127.0.0.1",
port: u16 = 8332,
// each request gets a new ID with a value of reqid.fetchAdd(1, .monotonic)
// each request gets a new ID with a value of reqid.fetchAdd(1, .Monotonic)
reqid: Atomic(u64) = Atomic(u64).init(1),
pub const Method = enum {
@ -153,7 +153,7 @@ pub const Client = struct {
/// callers own returned value.
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
const req = RpcRequest(m){
.id = self.reqid.fetchAdd(1, .monotonic),
.id = self.reqid.fetchAdd(1, .Monotonic),
.method = @tagName(m),
.params = args,
};
@ -183,7 +183,7 @@ pub const Client = struct {
defer file.close();
const cookie = try file.readToEndAlloc(self.allocator, 1024);
defer self.allocator.free(cookie);
const auth = try self.allocator.alloc(u8, base64enc.calcSize(cookie.len));
var auth = try self.allocator.alloc(u8, base64enc.calcSize(cookie.len));
return base64enc.encode(auth, cookie);
}

@ -56,15 +56,15 @@ pub const MessageTag = enum(u16) {
ping = 0x01,
pong = 0x02,
poweroff = 0x03,
// nd -> ngui: reports poweroff progress
poweroff_progress = 0x09,
wifi_connect = 0x04,
network_report = 0x05,
get_network_report = 0x06,
// ngui -> nd: screen timeout, no user activity; no reply
standby = 0x07,
// ngui -> nd: resume screen due to user touch; no reply
wakeup = 0x08,
wifi_connect = 0x04,
network_report = 0x05,
get_network_report = 0x06,
// nd -> ngui: reports poweroff progress
poweroff_progress = 0x09,
// nd -> ngui: bitcoin core daemon status report
onchain_report = 0x0a,
// nd -> ngui: lnd status and stats report
@ -89,13 +89,7 @@ pub const MessageTag = enum(u16) {
set_nodename = 0x15,
// nd -> ngui: all ndg settings
settings = 0x0d,
// ngui -> nd: verify pincode
unlock_screen = 0x17,
// nd -> ngui: result of try_unlock
screen_unlock_result = 0x18,
// ngui -> nd: set or disable screenlock pin code
slock_set_pincode = 0x19,
// next: 0x1a
// next: 0x16
};
/// daemon and gui exchange messages of this type.
@ -103,12 +97,12 @@ pub const Message = union(MessageTag) {
ping: void,
pong: void,
poweroff: void,
poweroff_progress: PoweroffProgress,
standby: void,
wakeup: void,
wifi_connect: WifiConnect,
network_report: NetworkReport,
get_network_report: GetNetworkReport,
poweroff_progress: PoweroffProgress,
onchain_report: OnchainReport,
lightning_report: LightningReport,
lightning_error: LightningError,
@ -121,9 +115,6 @@ pub const Message = union(MessageTag) {
switch_sysupdates: SysupdatesChan,
set_nodename: []const u8,
settings: Settings,
unlock_screen: []const u8, // pincode
screen_unlock_result: ScreenUnlockResult,
slock_set_pincode: ?[]const u8,
pub const WifiConnect = struct {
ssid: []const u8,
@ -259,17 +250,11 @@ pub const Message = union(MessageTag) {
};
pub const Settings = struct {
slock_enabled: bool,
hostname: []const u8, // see .set_nodename
sysupdates: struct {
channel: SysupdatesChan,
},
};
pub const ScreenUnlockResult = struct {
ok: bool,
err: ?[]const u8 = null, // error message when !ok
};
};
/// the return value type from `read` fn.
@ -294,8 +279,8 @@ pub const ParsedMessage = struct {
/// callers must deallocate resources with ParsedMessage.deinit when done.
pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
const tag = try reader.readEnum(MessageTag, .little);
const len = try reader.readInt(u64, .little);
const tag = try reader.readEnum(MessageTag, .Little);
const len = try reader.readIntLittle(u64);
if (len == 0) {
return switch (tag) {
.lightning_get_ctrlconn => .{ .value = .lightning_get_ctrlconn },
@ -318,7 +303,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
.wakeup,
=> unreachable, // handled above
inline else => |t| {
const bytes = try allocator.alloc(u8, len);
var bytes = try allocator.alloc(u8, len);
defer allocator.free(bytes);
try reader.readNoEof(bytes);
@ -362,16 +347,13 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
.switch_sysupdates => try json.stringify(msg.switch_sysupdates, .{}, data.writer()),
.set_nodename => try json.stringify(msg.set_nodename, .{}, data.writer()),
.settings => try json.stringify(msg.settings, .{}, data.writer()),
.unlock_screen => try json.stringify(msg.unlock_screen, .{}, data.writer()),
.screen_unlock_result => try json.stringify(msg.screen_unlock_result, .{}, data.writer()),
.slock_set_pincode => try json.stringify(msg.slock_set_pincode, .{}, data.writer()),
}
if (data.items.len > std.math.maxInt(u64)) {
return Error.CommWriteTooLarge;
}
try writer.writeInt(u16, @intFromEnum(msg), .little);
try writer.writeInt(u64, data.items.len, .little);
try writer.writeIntLittle(u16, @intFromEnum(msg));
try writer.writeIntLittle(u64, data.items.len);
try writer.writeAll(data.items);
}
@ -401,8 +383,8 @@ test "read" {
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
try buf.writer().writeInt(u16, @intFromEnum(msg), .little);
try buf.writer().writeInt(u64, data.items.len, .little);
try buf.writer().writeIntLittle(u16, @intFromEnum(msg));
try buf.writer().writeIntLittle(u64, data.items.len);
try buf.writer().writeAll(data.items);
var bs = std.io.fixedBufferStream(buf.items);
@ -424,8 +406,8 @@ test "write" {
const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}";
var js = std.ArrayList(u8).init(t.allocator);
defer js.deinit();
try js.writer().writeInt(u16, @intFromEnum(msg), .little);
try js.writer().writeInt(u64, payload.len, .little);
try js.writer().writeIntLittle(u16, @intFromEnum(msg));
try js.writer().writeIntLittle(u64, payload.len);
try js.appendSlice(payload);
try t.expectEqualStrings(js.items, buf.items);
@ -442,8 +424,8 @@ test "write enum" {
const payload = "\"edge\"";
var js = std.ArrayList(u8).init(t.allocator);
defer js.deinit();
try js.writer().writeInt(u16, @intFromEnum(msg), .little);
try js.writer().writeInt(u64, payload.len, .little);
try js.writer().writeIntLittle(u16, @intFromEnum(msg));
try js.writer().writeIntLittle(u64, payload.len);
try js.appendSlice(payload);
try t.expectEqualStrings(js.items, buf.items);

@ -36,7 +36,7 @@ pub const Section = struct {
const vdup = try self.alloc.dupe(u8, value);
errdefer self.alloc.free(vdup);
const res = try self.props.getOrPut(try self.alloc.dupe(u8, key));
var res = try self.props.getOrPut(try self.alloc.dupe(u8, key));
if (!res.found_existing) {
res.value_ptr.* = .{ .str = vdup };
return;
@ -180,7 +180,7 @@ pub fn appendDefaultSection(self: *LndConf) !*Section {
/// the section name ascii is converted to lower case.
pub fn appendSection(self: *LndConf, name: []const u8) !*Section {
const alloc = self.arena.allocator();
const low_name = try std.ascii.allocLowerString(alloc, name);
var low_name = try std.ascii.allocLowerString(alloc, name);
try self.sections.append(.{
.name = low_name,
.props = std.StringArrayHashMap(PropValue).init(alloc),

@ -143,29 +143,22 @@ pub const Client = struct {
pub fn call(self: *Client, comptime apimethod: ApiMethod, args: MethodArgs(apimethod)) !Result(apimethod) {
const formatted = try self.formatreq(apimethod, args);
defer formatted.deinit();
var headersbuf: [8 * 1024]u8 = undefined;
const reqinfo = formatted.value;
const opt = std.http.Client.RequestOptions{
.redirect_behavior = .not_allowed, // no redirects in REST API
.headers = reqinfo.stdheaders,
.privileged_headers = reqinfo.xheaders,
.server_header_buffer = &headersbuf,
};
var req = try self.httpClient.open(reqinfo.httpmethod, reqinfo.url, opt);
const opt = std.http.Client.Options{ .handle_redirects = false }; // no redirects in REST API
var req = try self.httpClient.request(reqinfo.httpmethod, reqinfo.url, reqinfo.headers, opt);
defer req.deinit();
if (reqinfo.payload) |p| {
req.transfer_encoding = .{ .content_length = p.len };
}
try req.send();
try req.start();
if (reqinfo.payload) |p| {
req.writeAll(p) catch return Error.LndPayloadWriteFail;
req.writer().writeAll(p) catch return Error.LndPayloadWriteFail;
try req.finish();
}
try req.wait();
if (req.response.status.class() != .success) {
// a structured error reporting in lnd is unclear:
// a structured error reporting in lnd is in a less than desirable state.
// https://github.com/lightningnetwork/lnd/issues/5586
// TODO: return a more detailed error when the upstream improves.
return Error.LndHttpBadStatusCode;
@ -188,9 +181,8 @@ pub const Client = struct {
const HttpReqInfo = struct {
httpmethod: std.http.Method,
url: std.Uri,
stdheaders: std.http.Client.Request.Headers = .{}, // overridable standard headers
xheaders: []const std.http.Header = &.{}, // any extra headers
payload: ?[]const u8 = null,
headers: std.http.Headers,
payload: ?[]const u8,
};
fn formatreq(self: Client, comptime apimethod: ApiMethod, args: MethodArgs(apimethod)) !types.Deinitable(HttpReqInfo) {
@ -202,10 +194,12 @@ pub const Client = struct {
.genseed, .walletstatus => |m| .{
.httpmethod = .GET,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = std.http.Headers{ .allocator = arena },
.payload = null,
},
.initwallet => |m| blk: {
const payload = p: {
const params: struct {
var params: struct {
wallet_password: []const u8, // base64
cipher_seed_mnemonic: []const []const u8,
aezeed_passphrase: ?[]const u8 = null, // base64
@ -221,12 +215,13 @@ pub const Client = struct {
break :blk .{
.httpmethod = .POST,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = std.http.Headers{ .allocator = arena },
.payload = payload,
};
},
.unlockwallet => |m| blk: {
const payload = p: {
const params: struct {
var params: struct {
wallet_password: []const u8, // base64
} = .{
.wallet_password = try base64EncodeAlloc(arena, args.unlock_password),
@ -238,19 +233,20 @@ pub const Client = struct {
break :blk .{
.httpmethod = .POST,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = std.http.Headers{ .allocator = arena },
.payload = payload,
};
},
.feereport, .getinfo, .getnetworkinfo, .pendingchannels, .walletbalance => |m| .{
.httpmethod = .GET,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.xheaders = blk: {
.headers = blk: {
if (self.macaroon.readonly == null) {
return Error.LndHttpMissingMacaroon;
}
var h = std.ArrayList(std.http.Header).init(arena);
try h.append(.{ .name = authHeaderName, .value = self.macaroon.readonly.? });
break :blk try h.toOwnedSlice();
var h = std.http.Headers{ .allocator = arena };
try h.append(authHeaderName, self.macaroon.readonly.?);
break :blk h;
},
.payload = null,
},
@ -274,13 +270,13 @@ pub const Client = struct {
}
break :blk try std.Uri.parse(buf.items); // uri point to the original buf
},
.xheaders = blk: {
.headers = blk: {
if (self.macaroon.readonly == null) {
return Error.LndHttpMissingMacaroon;
}
var h = std.ArrayList(std.http.Header).init(arena);
try h.append(.{ .name = authHeaderName, .value = self.macaroon.readonly.? });
break :blk try h.toOwnedSlice();
var h = std.http.Headers{ .allocator = arena };
try h.append(authHeaderName, self.macaroon.readonly.?);
break :blk h;
},
.payload = null,
},
@ -303,7 +299,7 @@ pub const Client = struct {
}
fn base64EncodeAlloc(gpa: std.mem.Allocator, v: []const u8) ![]const u8 {
const buf = try gpa.alloc(u8, base64enc.calcSize(v.len));
var buf = try gpa.alloc(u8, base64enc.calcSize(v.len));
return base64enc.encode(buf, v); // always returns a slice of buf.len
}
};

@ -1,13 +1,13 @@
const buildopts = @import("build_options");
const std = @import("std");
const posix = std.posix;
const os = std.os;
const sys = os.system;
const time = std.time;
const Address = std.net.Address;
const nif = @import("nif");
const comm = @import("comm.zig");
const Config = @import("nd/Config.zig");
const Daemon = @import("nd/Daemon.zig");
const screen = @import("ui/screen.zig");
@ -136,7 +136,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
return;
}
switch (sig) {
posix.SIG.INT, posix.SIG.TERM => sigquit.set(),
os.SIG.INT, os.SIG.TERM => sigquit.set(),
else => {},
}
}
@ -158,19 +158,9 @@ pub fn main() !void {
// of its previous state.
screen.backlight(.on) catch |err| logger.err("backlight: {any}", .{err});
// load config file to figure out whether to start ngui in screenlocked mode.
const conf = try Config.init(gpa, args.conf.?);
defer conf.deinit();
// start ngui, unless -nogui mode
const gui_path = args.gui.?; // guaranteed to be non-null
var ngui_args = std.ArrayList([]const u8).init(gpa);
defer ngui_args.deinit();
try ngui_args.append(gui_path);
if (conf.data.slock != null) {
try ngui_args.append("-slock");
}
var ngui = std.ChildProcess.init(ngui_args.items, gpa);
var ngui = std.ChildProcess.init(&.{gui_path}, gpa);
ngui.stdin_behavior = .Pipe;
ngui.stdout_behavior = .Pipe;
ngui.stderr_behavior = .Inherit;
@ -210,7 +200,7 @@ pub fn main() !void {
var nd = try Daemon.init(.{
.allocator = gpa,
.conf = conf,
.confpath = args.conf.?,
.uir = uireader,
.uiw = uiwriter,
.wpa = args.wpa.?,
@ -219,13 +209,13 @@ pub fn main() !void {
try nd.start();
// graceful shutdown; see sigaction(2)
const sa = posix.Sigaction{
const sa = os.Sigaction{
.handler = .{ .handler = sighandler },
.mask = posix.empty_sigset,
.mask = os.empty_sigset,
.flags = 0,
};
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
try os.sigaction(os.SIG.INT, &sa, null);
try os.sigaction(os.SIG.TERM, &sa, null);
sigquit.wait();
logger.info("sigquit: terminating ...", .{});

@ -40,13 +40,7 @@ data: Data,
/// top struct stored on disk.
/// access with `safeReadOnly` or lock/unlock `mu`.
///
/// for backwards compatibility, all newly introduced fields must have default values.
pub const Data = struct {
slock: ?struct { // null indicates screenlock is disabled
bcrypt_hash: []const u8, // std.crypto.bcrypt .phc format
incorrect_attempts: u8, // reset after each successful unlock
} = null,
syschannel: SysupdatesChannel,
syscronscript: []const u8,
sysrunscript: []const u8,
@ -145,7 +139,7 @@ fn inferStaticData(allocator: std.mem.Allocator) !StaticData {
}
fn inferLndTorHostname(allocator: std.mem.Allocator) ![]const u8 {
const raw = try std.fs.cwd().readFileAlloc(allocator, TOR_DATA_DIR ++ "/lnd/hostname", 1024);
var raw = try std.fs.cwd().readFileAlloc(allocator, TOR_DATA_DIR ++ "/lnd/hostname", 1024);
const hostname = std.mem.trim(u8, raw, &std.ascii.whitespace);
logger.info("inferred lnd tor hostname: [{s}]", .{hostname});
return hostname;
@ -156,7 +150,7 @@ fn inferBitcoindRpcPass(allocator: std.mem.Allocator) ![]const u8 {
// the password was placed on a separate comment line, preceding another comment
// line containing "rpcauth.py".
// TODO: get rid of the hack; do something more robust
const conf = try std.fs.cwd().readFileAlloc(allocator, BITCOIND_CONFIG_PATH, 1024 * 1024);
var conf = try std.fs.cwd().readFileAlloc(allocator, BITCOIND_CONFIG_PATH, 1024 * 1024);
var it = std.mem.tokenizeScalar(u8, conf, '\n');
var next_is_pass = false;
while (it.next()) |line| {
@ -181,50 +175,6 @@ pub fn safeReadOnly(self: *Config, comptime F: anytype) @typeInfo(@TypeOf(F)).Fn
return F(self.data, self.static);
}
/// matches the `input` against the hash in `Data.slock.bcrypt_hash` previously set with `setSlockPin`.
/// incrementing `Data.slock.incorrect_attempts` each unsuccessful result.
/// the number of attemps are persisted at `Config.confpath` upon function return.
pub fn verifySlockPin(self: *Config, input: []const u8) !void {
self.mu.lock();
defer self.mu.unlock();
const slock = self.data.slock orelse return;
defer self.dumpUnguarded() catch |errdump| logger.err("dumpUnguarded: {!}", .{errdump});
std.crypto.pwhash.bcrypt.strVerify(slock.bcrypt_hash, input, .{}) catch |err| {
if (err == error.PasswordVerificationFailed) {
self.data.slock.?.incorrect_attempts += 1;
return error.IncorrectSlockPin;
}
logger.err("bcrypt.strVerify: {!}", .{err});
return err;
};
self.data.slock.?.incorrect_attempts = 0;
}
/// enables or disables screenlock, persistently. null `code` indicates disabled.
/// safe for concurrent use.
pub fn setSlockPin(self: *Config, code: ?[]const u8) !void {
self.mu.lock();
defer self.mu.unlock();
// TODO: free existing slock.bcrypt_hash? it is in arena but still
if (code) |s| {
const bcrypt = std.crypto.pwhash.bcrypt;
const opt: bcrypt.HashOptions = .{
.params = .{ .rounds_log = 12 },
.encoding = .phc,
.silently_truncate_password = false,
};
var buf: [bcrypt.hash_length * 2]u8 = undefined;
const hash = try bcrypt.strHash(s, opt, &buf);
self.data.slock = .{
.bcrypt_hash = try self.arena.allocator().dupe(u8, hash),
.incorrect_attempts = 0,
};
} else {
self.data.slock = null;
}
try self.dumpUnguarded();
}
/// used by mutateLndConf to guard concurrent access.
var lndconf_mu: std.Thread.Mutex = .{};
@ -346,7 +296,7 @@ fn genSysupdatesCronScript(self: Config) !void {
///
/// the caller must serialize this function calls.
fn runSysupdates(allocator: std.mem.Allocator, scriptpath: []const u8) !void {
const res = try std.ChildProcess.run(.{ .allocator = allocator, .argv = &.{scriptpath} });
const res = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &.{scriptpath} });
defer {
allocator.free(res.stdout);
allocator.free(res.stderr);
@ -383,7 +333,7 @@ pub fn lndConnectWaitMacaroonFile(self: Config, allocator: std.mem.Allocator, ty
defer allocator.free(macaroon);
const base64enc = std.base64.url_safe_no_pad.Encoder;
const buf = try allocator.alloc(u8, base64enc.calcSize(macaroon.len));
var buf = try allocator.alloc(u8, base64enc.calcSize(macaroon.len));
defer allocator.free(buf);
const macaroon_b64 = base64enc.encode(buf, macaroon);
const port: u16 = switch (typ) {
@ -599,7 +549,7 @@ test "ndconfig: switch sysupdates with .run=true" {
const tt = @import("../test.zig");
// no arena deinit here: expecting Config to auto-deinit.
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(std.testing.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
@ -644,7 +594,7 @@ test "ndconfig: genLndConfig" {
const tt = @import("../test.zig");
// Config auto-deinits the arena.
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(std.testing.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
@ -695,7 +645,7 @@ test "ndconfig: mutate LndConf" {
const tt = @import("../test.zig");
// Config auto-deinits the arena.
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(t.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
@ -731,92 +681,3 @@ test "ndconfig: mutate LndConf" {
\\
, cont);
}
test "ndconfig: screen lock" {
const t = std.testing;
const tt = @import("../test.zig");
// Config auto-deinits the arena.
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(t.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
// nonexistent config file
{
var conf = try init(t.allocator, "/nonexistent.json");
defer conf.deinit();
try t.expect(conf.data.slock == null);
try conf.verifySlockPin("");
try conf.verifySlockPin("any");
}
// conf file without slock field
{
const confpath = try tmp.join(&.{"conf.json"});
try tmp.dir.writeFile(confpath,
\\{
\\"syschannel": "dev",
\\"syscronscript": "/cron/sysupdates.sh",
\\"sysrunscript": "/sysupdates/run.sh"
\\}
);
var conf = try init(t.allocator, confpath);
defer conf.deinit();
try t.expect(conf.data.slock == null);
try conf.verifySlockPin("");
try conf.verifySlockPin("0000");
}
// conf file with null slock
{
const confpath = try tmp.join(&.{"conf.json"});
try tmp.dir.writeFile(confpath,
\\{
\\"slock": null,
\\"syschannel": "dev",
\\"syscronscript": "/cron/sysupdates.sh",
\\"sysrunscript": "/sysupdates/run.sh"
\\}
);
var conf = try init(t.allocator, confpath);
defer conf.deinit();
try t.expect(conf.data.slock == null);
try conf.verifySlockPin("");
try conf.verifySlockPin("1111");
}
const newpinconf = try tmp.join(&.{"newconf.json"});
{
var conf = Config{
.arena = conf_arena,
.confpath = newpinconf,
.data = .{
.slock = null,
.syschannel = .master, // unused
.syscronscript = undefined, // unused
.sysrunscript = undefined, // unused
},
.static = undefined, // unused
};
defer conf.deinit();
// any pin should workd because slock is null
try conf.verifySlockPin("");
try conf.verifySlockPin("any");
// set a new pin code
try conf.setSlockPin("1357");
try conf.verifySlockPin("1357");
try t.expectError(error.IncorrectSlockPin, conf.verifySlockPin(""));
try t.expectError(error.IncorrectSlockPin, conf.verifySlockPin("any"));
}
// load conf from file and check
{
var conf = try init(t.allocator, newpinconf);
defer conf.deinit();
try t.expect(conf.data.slock != null);
try conf.setSlockPin("1357");
try conf.verifySlockPin("1357");
try t.expectError(error.IncorrectSlockPin, conf.verifySlockPin("any2"));
}
}

@ -32,9 +32,6 @@ uireader: std.fs.File.Reader, // ngui stdout
uiwriter: std.fs.File.Writer, // ngui stdin
wpa_ctrl: types.WpaControl, // guarded by mu once start'ed
/// used only in comm thread; move under mu when no longer the case
screenstate: enum { locked, unlocked },
/// guards all the fields below to sync between pub fns and main/poweroff threads.
mu: std.Thread.Mutex = .{},
@ -121,7 +118,7 @@ const Error = error{
const InitOpt = struct {
allocator: std.mem.Allocator,
conf: Config,
confpath: []const u8,
uir: std.fs.File.Reader,
uiw: std.fs.File.Writer,
wpa: [:0]const u8,
@ -141,14 +138,15 @@ pub fn init(opt: InitOpt) !Daemon {
try svlist.append(sys.Service.init(opt.allocator, sys.Service.LND, .{ .stop_wait_sec = 600 }));
try svlist.append(sys.Service.init(opt.allocator, sys.Service.BITCOIND, .{ .stop_wait_sec = 600 }));
const conf = try Config.init(opt.allocator, opt.confpath);
errdefer conf.deinit();
return .{
.allocator = opt.allocator,
.conf = opt.conf,
.conf = conf,
.uireader = opt.uir,
.uiwriter = opt.uiw,
.wpa_ctrl = try types.WpaControl.open(opt.wpa),
.state = .stopped,
.screenstate = if (opt.conf.data.slock != null) .locked else .unlocked,
.services = .{ .list = try svlist.toOwnedSlice() },
// send persisted settings immediately on start
.want_settings = true,
@ -170,6 +168,7 @@ pub fn init(opt: InitOpt) !Daemon {
pub fn deinit(self: *Daemon) void {
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
self.services.deinit(self.allocator);
self.conf.deinit();
}
/// start launches daemon threads and returns immediately.
@ -185,7 +184,6 @@ pub fn start(self: *Daemon) !void {
}
try self.wpa_ctrl.attach();
self.want_stop = false;
errdefer {
self.wpa_ctrl.detach() catch {};
self.want_stop = true;
@ -226,6 +224,7 @@ pub fn wait(self: *Daemon) void {
}
self.wpa_ctrl.detach() catch |err| logger.err("wait: wpa_ctrl.detach: {any}", .{err});
self.want_stop = false;
self.state = .stopped;
}
@ -238,16 +237,8 @@ fn standby(self: *Daemon) !void {
.stopped, .poweroff => return Error.InvalidState,
.wallet_reset => return Error.WalletResetActive,
.running => {
const has_lock = self.conf.safeReadOnly(struct {
fn f(data: Config.Data, _: Config.StaticData) bool {
return data.slock != null;
}
}.f);
if (has_lock) {
self.screenstate = .locked;
}
self.state = .standby;
try screen.backlight(.off);
self.state = .standby;
},
}
}
@ -347,7 +338,6 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
fn f(conf: Config.Data, static: Config.StaticData) bool {
const msg: comm.Message.Settings = .{
.hostname = static.hostname,
.slock_enabled = conf.slock != null,
.sysupdates = .{
.channel = switch (conf.syschannel) {
.dev => .edge,
@ -384,7 +374,6 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
// onchain bitcoin stats
if (self.want_onchain_report or self.bitcoin_timer.read() > self.onchain_report_interval) {
// TODO: this takes too long; run in a separate thread
if (self.sendOnchainReport()) {
self.bitcoin_timer.reset();
self.want_onchain_report = false;
@ -396,7 +385,6 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
// lightning stats
if (self.state != .wallet_reset) {
if (self.want_lnd_report or self.lnd_timer.read() > self.lnd_report_interval) {
// TODO: this takes too long; run in a separate thread
if (self.sendLightningReport()) {
self.lnd_timer.reset();
self.want_lnd_report = false;
@ -451,13 +439,9 @@ fn commThreadLoop(self: *Daemon) void {
self.reportNetworkStatus(.{ .scan = req.scan });
},
.wifi_connect => |req| {
if (self.screenstate != .locked) {
self.startConnectWifi(req.ssid, req.password) catch |err| {
logger.err("startConnectWifi: {any}", .{err});
};
} else {
logger.warn("refusing wifi connect: screen is locked", .{});
}
},
.standby => {
logger.info("entering standby mode", .{});
@ -468,68 +452,38 @@ fn commThreadLoop(self: *Daemon) void {
self.wakeup() catch |err| logger.err("nd.wakeup: {any}", .{err});
},
.switch_sysupdates => |chan| {
if (self.screenstate != .locked) {
logger.info("switching sysupdates channel to {s}", .{@tagName(chan)});
self.switchSysupdates(chan) catch |err| {
logger.err("switchSysupdates: {any}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("ignoring sysupdates switch: screen is locked", .{});
}
},
.set_nodename => |newname| {
if (self.screenstate != .locked) {
self.setNodename(newname) catch |err| {
logger.err("setNodename: {!}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("ignoring nodename change: screen is locked", .{});
}
},
.lightning_genseed => {
// non commital: ok even if the screen is locked
self.generateWalletSeed() catch |err| {
logger.err("generateWalletSeed: {!}", .{err});
// TODO: send err back to ngui
};
},
.lightning_init_wallet => |req| {
if (self.screenstate != .locked) {
self.initWallet(req) catch |err| {
logger.err("initWallet: {!}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("ignoring lnd wallet init: screen is locked", .{});
}
},
.lightning_get_ctrlconn => {
if (self.screenstate != .locked) {
self.sendLightningPairingConn() catch |err| {
logger.err("sendLightningPairingConn: {!}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("refusing to give out lnd pairing: screen is locked", .{});
}
},
.lightning_reset => {
if (self.screenstate != .locked) {
self.resetLndNode() catch |err| logger.err("resetLndNode: {!}", .{err});
} else {
logger.warn("refusing lnd reset: screen is locked", .{});
}
},
.slock_set_pincode => |pincode_or_null| {
self.conf.setSlockPin(pincode_or_null) catch |err| logger.err("conf.setSlockPin: {!}", .{err});
self.mu.lock();
self.want_settings = true;
self.mu.unlock();
},
.unlock_screen => |pincode| {
self.unlockScreen(pincode) catch |err| logger.err("unlockScreen: {!}", .{err});
},
else => |v| logger.warn("unhandled msg tag {s}", .{@tagName(v)}),
}
@ -542,29 +496,9 @@ fn commThreadLoop(self: *Daemon) void {
logger.info("exiting comm thread loop", .{});
}
/// all callers must belong to comm thread due to self.screenstate access.
fn unlockScreen(self: *Daemon, pincode: []const u8) !void {
const pindup = try self.allocator.dupe(u8, pincode);
defer self.allocator.free(pindup);
// TODO: slow down
self.conf.verifySlockPin(pindup) catch |err| {
if (!builtin.is_test) { // logging err makes some tests fail
logger.err("verifySlockPin: {!}", .{err});
}
const errmsg: comm.Message = .{ .screen_unlock_result = .{
.ok = false,
.err = if (err == error.IncorrectSlockPin) "incorrect pin code" else "unlock failed",
} };
return comm.write(self.allocator, self.uiwriter, errmsg);
};
const ok: comm.Message = .{ .screen_unlock_result = .{ .ok = true } };
comm.write(self.allocator, self.uiwriter, ok) catch |err| logger.err("{!}", .{err});
self.screenstate = .unlocked;
}
/// sends poweroff progress to uiwriter in comm.Message.PoweroffProgress format.
fn sendPoweroffReport(self: *Daemon) !void {
const svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.list.len);
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.list.len);
defer self.allocator.free(svstat);
for (self.services.list, svstat) |*sv, *stat| {
stat.* = .{
@ -964,7 +898,7 @@ fn processLndReportError(self: *Daemon, err: anyerror) !void {
error.FileNotFound, // tls cert file missing, not re-generated by lnd yet
=> return comm.write(self.allocator, self.uiwriter, msg_starting),
// old tls cert, refused by our http client
std.http.Client.ConnectTcpError.TlsInitializationFailed => {
std.http.Client.ConnectUnproxiedError.TlsInitializationFailed => {
try self.resetLndTlsUnguarded();
return error.LndReportRetryLater;
},
@ -1008,7 +942,7 @@ fn sendLightningPairingConn(self: *Daemon) !void {
defer self.allocator.free(tor_rpc);
const tor_http = try self.conf.lndConnectWaitMacaroonFile(self.allocator, .tor_http);
defer self.allocator.free(tor_http);
const conn: comm.Message.LightningCtrlConn = &.{
var conn: comm.Message.LightningCtrlConn = &.{
.{ .url = tor_rpc, .typ = .lnd_rpc, .perm = .admin },
.{ .url = tor_http, .typ = .lnd_http, .perm = .admin },
};
@ -1259,7 +1193,7 @@ fn setNodenameInternal(self: *Daemon, newname: []const u8) !void {
/// replaces whitespace with space literal and ignores ascii control chars.
/// caller owns returned value.
fn allocSanitizeNodename(allocator: std.mem.Allocator, name: []const u8) ![]const u8 {
if (name.len == 0 or try std.unicode.utf8CountCodepoints(name) > std.posix.HOST_NAME_MAX) {
if (name.len == 0 or try std.unicode.utf8CountCodepoints(name) > std.os.HOST_NAME_MAX) {
return error.InvalidNodenameLength;
}
var sanitized = try std.ArrayList(u8).initCapacity(allocator, name.len);
@ -1283,18 +1217,17 @@ fn allocSanitizeNodename(allocator: std.mem.Allocator, name: []const u8) ![]cons
return allocator.dupe(u8, trimmed);
}
test "daemon: start-stop" {
test "start-stop" {
const t = std.testing;
const pipe = try types.IoPipe.create();
var daemon = try Daemon.init(.{
.allocator = t.allocator,
.conf = try dummyTestConfig(),
.confpath = "/unused.json",
.uir = pipe.reader(),
.uiw = pipe.writer(),
.wpa = "/dev/null",
});
defer daemon.conf.deinit();
daemon.want_settings = false;
daemon.want_network_report = false;
daemon.want_onchain_report = false;
@ -1329,7 +1262,7 @@ test "daemon: start-stop" {
try t.expect(!daemon.wpa_ctrl.opened);
}
test "daemon: start-poweroff" {
test "start-poweroff" {
const t = std.testing;
const tt = @import("../test.zig");
@ -1342,7 +1275,7 @@ test "daemon: start-poweroff" {
const gui_reader = gui_stdin.reader();
var daemon = try Daemon.init(.{
.allocator = arena,
.conf = try dummyTestConfig(),
.confpath = "/unused.json",
.uir = gui_stdout.reader(),
.uiw = gui_stdin.writer(),
.wpa = "/dev/null",
@ -1353,7 +1286,6 @@ test "daemon: start-poweroff" {
daemon.want_lnd_report = false;
defer {
daemon.deinit();
daemon.conf.deinit();
gui_stdin.close();
}
@ -1394,78 +1326,3 @@ test "daemon: start-poweroff" {
// need custom runner to set up a global registry for child processes.
// https://github.com/ziglang/zig/pull/13411
}
test "daemon: screen unlock" {
const t = std.testing;
const tt = @import("../test.zig");
var arena_alloc = std.heap.ArenaAllocator.init(t.allocator);
defer arena_alloc.deinit();
const arena = arena_alloc.allocator();
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
const correct_pin = "12345";
var ndconf = try Config.init(t.allocator, try tmp.join(&.{"ndconf.json"}));
defer ndconf.deinit();
try ndconf.setSlockPin(correct_pin);
const gui_stdin = try types.IoPipe.create();
const gui_stdout = try types.IoPipe.create();
const gui_reader = gui_stdin.reader();
var daemon = try Daemon.init(.{
.allocator = arena,
.conf = ndconf,
.uir = gui_stdout.reader(),
.uiw = gui_stdin.writer(),
.wpa = "/dev/null",
});
defer {
daemon.deinit();
gui_stdin.close();
}
daemon.want_settings = false;
daemon.want_network_report = false;
daemon.want_onchain_report = false;
daemon.want_lnd_report = false;
try daemon.start();
try t.expect(daemon.screenstate == .locked);
try comm.write(arena, gui_stdout.writer(), .{ .unlock_screen = "000" });
try comm.write(arena, gui_stdout.writer(), .{ .unlock_screen = correct_pin });
{
const msg = try comm.read(arena, gui_reader);
try t.expect(!msg.value.screen_unlock_result.ok);
}
{
const msg = try comm.read(arena, gui_reader);
try t.expect(msg.value.screen_unlock_result.ok);
}
daemon.stop();
gui_stdout.close();
daemon.wait();
try t.expect(daemon.screenstate == .unlocked);
}
fn dummyTestConfig() !Config {
const talloc = std.testing.allocator;
const arena = try talloc.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(talloc);
return Config{
.arena = arena,
.confpath = "/dummy.conf",
.data = .{
.slock = null,
.syschannel = .master,
.syscronscript = "",
.sysrunscript = "",
},
.static = .{
.hostname = "testhost",
.lnd_user = null,
.lnd_tor_hostname = null,
.bitcoind_rpc_pass = null,
},
};
}

@ -86,7 +86,7 @@ pub fn sendReport(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, w: anytype) !
};
// fetch available wifi networks from scan results using WPA ctrl
const wifi_networks: ?types.StringList = if (queryWifiScanResults(arena, 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});
break :blk null;
};

@ -1,6 +1,6 @@
const buildopts = @import("build_options");
const std = @import("std");
const posix = std.posix;
const os = std.os;
const time = std.time;
const comm = @import("comm.zig");
@ -8,7 +8,6 @@ const types = @import("types.zig");
const ui = @import("ui/ui.zig");
const lvgl = @import("ui/lvgl.zig");
const screen = @import("ui/screen.zig");
const screenlock = @import("ui/screenlock.zig");
const symbol = @import("ui/symbol.zig");
const logger = std.log.scoped(.ngui);
@ -27,18 +26,13 @@ var gpa: std.mem.Allocator = undefined;
var ui_mutex: std.Thread.Mutex = .{};
/// current state of the GUI.
/// guarded by `ui_mutex` since some `nm_xxx` funcs branch based off of the state.
/// guarded by ui_mutex since some nm_xxx funcs branch based off of the state.
var state: enum {
active, // normal operational mode
standby, // idling
alert, // draw user attention; never go standby
} = .active;
var slock_status: enum {
enabled,
disabled,
} = undefined; // set in main after parsing cmd line flags
/// last report received from comm.
/// deinit'ed at program exit.
/// while deinit and replace handle concurrency, field access requires holding mu.
@ -230,7 +224,34 @@ fn commThreadLoopCycle() !void {
const msg = try comm.pipeRead(); // blocking
ui_mutex.lock(); // guards the state and all UI calls below
defer ui_mutex.unlock();
switch (msg.value) {
switch (state) {
.standby => switch (msg.value) {
.ping => {
defer msg.deinit();
try comm.pipeWrite(comm.Message.pong);
},
.network_report,
.onchain_report,
.lightning_report,
.lightning_error,
=> last_report.replace(msg),
.lightning_genseed_result,
.lightning_ctrlconn,
// TODO: merge standby vs active switch branches
=> {
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
msg.deinit();
},
.settings => |sett| {
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
msg.deinit();
},
else => {
logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)});
msg.deinit();
},
},
.active, .alert => switch (msg.value) {
.ping => {
defer msg.deinit();
try comm.pipeWrite(comm.Message.pong);
@ -240,21 +261,15 @@ fn commThreadLoopCycle() !void {
msg.deinit();
},
.network_report => |rep| {
if (state != .standby) {
updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
}
last_report.replace(msg);
},
.onchain_report => |rep| {
if (state != .standby) {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
}
last_report.replace(msg);
},
.lightning_report, .lightning_error => {
if (state != .standby) {
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
}
last_report.replace(msg);
},
.lightning_genseed_result,
@ -265,24 +280,13 @@ fn commThreadLoopCycle() !void {
},
.settings => |sett| {
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
slock_status = if (sett.slock_enabled) .enabled else .disabled;
msg.deinit();
},
.screen_unlock_result => |unlock| {
if (unlock.ok) {
ui.screenlock.unlockSuccess();
} else {
var errmsg: [:0]const u8 = "invalid pin code";
if (unlock.err) |s| {
errmsg = gpa.dupeZ(u8, s) catch errmsg;
}
ui.screenlock.unlockFailure(errmsg);
}
},
else => {
logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)});
msg.deinit();
},
},
}
}
@ -291,7 +295,7 @@ fn commThreadLoopCycle() !void {
fn uiThreadLoop() void {
while (true) {
ui_mutex.lock();
const till_next_ms = lvgl.loopCycle(); // UI loop
var till_next_ms = lvgl.loopCycle(); // UI loop
const do_state = state;
ui_mutex.unlock();
@ -302,9 +306,6 @@ fn uiThreadLoop() void {
// go into a screen sleep mode due to no user activity
wakeup.reset();
comm.pipeWrite(comm.Message.standby) catch |err| logger.err("standby: {any}", .{err});
if (slock_status == .enabled) {
screenlock.activate();
}
screen.sleep(&ui_mutex, &wakeup); // blocking
// wake up due to touch screen activity or wakeup event is set
@ -345,25 +346,7 @@ fn uiThreadLoop() void {
logger.info("exiting UI thread loop", .{});
}
/// prints usage help text to stderr.
fn usage(prog: []const u8) !void {
try stderr.print(
\\usage: {s} [-v] [-slock]
\\
\\ngui is nakamochi GUI interface. it communicates with nd, nakamochi daemon,
\\via stdio and is typically launched by the daemon as a child process.
\\
\\-slock makes the interface start up in a screenlocked mode.
, .{prog});
}
const CmdFlags = struct {
slock: bool, // whether to start the UI in screen locked mode
};
fn parseArgs(alloc: std.mem.Allocator) !CmdFlags {
var flags = CmdFlags{ .slock = false };
fn parseArgs(alloc: std.mem.Allocator) !void {
var args = try std.process.ArgIterator.initWithAllocator(alloc);
defer args.deinit();
const prog = args.next() orelse return error.NoProgName;
@ -375,14 +358,22 @@ fn parseArgs(alloc: std.mem.Allocator) !CmdFlags {
} else if (std.mem.eql(u8, a, "-v")) {
try stderr.print("{any}\n", .{buildopts.semver});
std.process.exit(0);
} else if (std.mem.eql(u8, a, "-slock")) {
flags.slock = true;
} else {
logger.err("unknown arg name {s}", .{a});
return error.UnknownArgName;
}
}
return flags;
}
/// prints usage help text to stderr.
fn usage(prog: []const u8) !void {
try stderr.print(
\\usage: {s} [-v]
\\
\\ngui is nakamochi GUI interface. it communicates with nd, nakamochi daemon,
\\via stdio and is typically launched by the daemon as a child process.
\\
, .{prog});
}
/// handles sig TERM and INT: makes the program exit.
@ -391,7 +382,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
return;
}
switch (sig) {
posix.SIG.INT, posix.SIG.TERM => sigquit.set(),
os.SIG.INT, os.SIG.TERM => sigquit.set(),
else => {},
}
}
@ -404,9 +395,8 @@ pub fn main() anyerror!void {
logger.err("memory leaks detected", .{});
};
gpa = gpa_state.allocator();
const flags = try parseArgs(gpa);
try parseArgs(gpa);
logger.info("ndg version {any}", .{buildopts.semver});
slock_status = if (flags.slock) .enabled else .disabled;
// ensure timer is available on this platform before doing anything else;
// the UI is unusable otherwise.
@ -416,7 +406,7 @@ pub fn main() anyerror!void {
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
// initalizes display, input driver and finally creates the user interface.
ui.init(.{ .allocator = gpa, .slock = flags.slock }) catch |err| {
ui.init(gpa) catch |err| {
logger.err("ui.init: {any}", .{err});
return err;
};
@ -432,7 +422,6 @@ pub fn main() anyerror!void {
const th = try std.Thread.spawn(.{}, uiThreadLoop, .{});
th.detach();
}
{
// start comms with daemon in a seaparate thread.
const th = try std.Thread.spawn(.{}, commThreadLoop, .{});
@ -440,13 +429,13 @@ pub fn main() anyerror!void {
}
// set up a sigterm handler for clean exit.
const sa = posix.Sigaction{
const sa = os.Sigaction{
.handler = .{ .handler = sighandler },
.mask = posix.empty_sigset,
.mask = os.empty_sigset,
.flags = 0,
};
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
try os.sigaction(os.SIG.INT, &sa, null);
try os.sigaction(os.SIG.TERM, &sa, null);
sigquit.wait();
logger.info("sigquit: terminating ...", .{});

@ -5,8 +5,8 @@ const types = @import("../types.zig");
/// caller owns memory; must dealloc using `allocator`.
pub fn hostname(allocator: std.mem.Allocator) ![]const u8 {
var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
const name = try std.posix.gethostname(&buf);
var buf: [std.os.HOST_NAME_MAX]u8 = undefined;
const name = try std.os.gethostname(&buf);
return allocator.dupe(u8, name);
}
@ -40,8 +40,8 @@ pub fn setHostname(allocator: std.mem.Allocator, name: []const u8) !void {
const newname = sanitized.items;
// need not continue if current name matches the new one.
var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
const currname = try std.posix.gethostname(&buf);
var buf: [std.os.HOST_NAME_MAX]u8 = undefined;
const currname = try std.os.gethostname(&buf);
if (std.mem.eql(u8, currname, newname)) {
return;
}

@ -37,7 +37,7 @@ pub fn initGlobal() void {
fn initGlobalFn() void {
global_gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
global_gpa = global_gpa_state.allocator();
const pipe = types.IoPipe.create() catch |err| {
var pipe = types.IoPipe.create() catch |err| {
std.debug.panic("IoPipe.create: {any}", .{err});
};
comm.initPipe(global_gpa, pipe);
@ -118,7 +118,7 @@ pub const TestChildProcess = struct {
argv: []const []const u8,
pub fn init(argv: []const []const u8, allocator: std.mem.Allocator) TestChildProcess {
const adup = allocator.alloc([]u8, argv.len) catch unreachable;
var adup = allocator.alloc([]u8, argv.len) catch unreachable;
for (argv, adup) |v, *dup| {
dup.* = allocator.dupe(u8, v) catch unreachable;
}
@ -242,7 +242,7 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
.Slice => {
switch (@typeInfo(p.child)) {
.Pointer, .Struct, .Optional, .Union => {
const err: ?anyerror = blk: {
var err: ?anyerror = blk: {
if (expected.len != actual.len) {
std.debug.print("expected.len = {d}, actual.len = {d}\n", .{ expected.len, actual.len });
break :blk error.ExpectDeepEqual;
@ -331,7 +331,6 @@ test {
_ = @import("ngui.zig");
_ = @import("lightning.zig");
_ = @import("sys.zig");
_ = @import("xfmt.zig");
std.testing.refAllDecls(@This());
}

@ -1,6 +1,6 @@
const std = @import("std");
const time = std.time;
const posix = std.posix;
const os = std.os;
const comm = @import("comm");
const types = @import("../types.zig");
@ -12,9 +12,9 @@ var ngui_proc: std.ChildProcess = undefined;
var sigquit: std.Thread.ResetEvent = .{};
fn sighandler(sig: c_int) callconv(.C) void {
logger.info("received signal {} (TERM={} INT={})", .{ sig, posix.SIG.TERM, posix.SIG.INT });
logger.info("received signal {} (TERM={} INT={})", .{ sig, os.SIG.TERM, os.SIG.INT });
switch (sig) {
posix.SIG.INT, posix.SIG.TERM => sigquit.set(),
os.SIG.INT, os.SIG.TERM => sigquit.set(),
else => {},
}
}
@ -29,7 +29,6 @@ fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
const Flags = struct {
ngui_path: ?[:0]const u8 = null,
slock: bool = false, // gui screen lock
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
if (self.ngui_path) |p| allocator.free(p);
@ -58,8 +57,6 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
}
if (std.mem.eql(u8, a, "-ngui")) {
lastarg = .ngui_path;
} else if (std.mem.eql(u8, a, "-slock")) {
flags.slock = true;
} else {
fatal("unknown arg name {s}", .{a});
}
@ -77,18 +74,9 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
}
/// global vars for comm read/write threads
var state: struct {
mu: std.Thread.Mutex = .{},
nodename: types.BufTrimString(std.posix.HOST_NAME_MAX) = .{},
slock_pincode: ?[]const u8 = null, // disabled when null
settings_sent: bool = false,
fn deinit(self: @This(), gpa: std.mem.Allocator) void {
if (self.slock_pincode) |s| {
gpa.free(s);
}
}
} = .{};
var mu: std.Thread.Mutex = .{};
var nodename: types.BufTrimString(std.os.HOST_NAME_MAX) = .{};
var settings_sent = false;
fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
comm.write(gpa, w, .ping) catch |err| logger.err("comm.write ping: {any}", .{err});
@ -114,7 +102,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
},
.poweroff => {
logger.info("sending poweroff status1", .{});
const s1: comm.Message.PoweroffProgress = .{ .services = &.{
var s1: comm.Message.PoweroffProgress = .{ .services = &.{
.{ .name = "lnd", .stopped = false, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null },
} };
@ -122,7 +110,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
time.sleep(2 * time.ns_per_s);
logger.info("sending poweroff status2", .{});
const s2: comm.Message.PoweroffProgress = .{ .services = &.{
var s2: comm.Message.PoweroffProgress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null },
} };
@ -130,7 +118,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
time.sleep(3 * time.ns_per_s);
logger.info("sending poweroff status3", .{});
const s3: comm.Message.PoweroffProgress = .{ .services = &.{
var s3: comm.Message.PoweroffProgress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = true, .err = null },
} };
@ -149,46 +137,17 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
time.sleep(3 * time.ns_per_s);
},
.lightning_get_ctrlconn => {
const conn: comm.Message.LightningCtrlConn = &.{
var conn: comm.Message.LightningCtrlConn = &.{
.{ .url = "lndconnect://adfkjhadwaepoijsadflkjtrpoijawokjafulkjsadfkjhgjfdskjszd.onion:10009?macaroon=Adasjsadkfljhfjhasdpiuhfiuhawfffoihgpoiadsfjharpoiuhfdsgpoihafdsgpoiheafoiuhasdfhisdufhiuhfewiuhfiuhrfl6prrx", .typ = .lnd_rpc, .perm = .admin },
.{ .url = "lndconnect://adfkjhadwaepoijsadflkjtrpoijawokjafulkjsadfkjhgjfdskjszd.onion:10010?macaroon=Adasjsadkfljhfjhasdpiuhfiuhawfffoihgpoiadsfjharpoiuhfdsgpoihafdsgpoiheafoiuhasdfhisdufhiuhfewiuhfiuhrfl6prrx", .typ = .lnd_http, .perm = .admin },
};
comm.write(gpa, w, .{ .lightning_ctrlconn = conn }) catch |err| logger.err("{!}", .{err});
},
.set_nodename => |s| {
state.mu.lock();
defer state.mu.unlock();
state.nodename.set(s);
state.settings_sent = false;
},
.unlock_screen => |pin| {
logger.info("unlock pincode: {s}", .{pin});
time.sleep(1 * time.ns_per_s);
state.mu.lock();
defer state.mu.unlock();
if (state.slock_pincode == null or std.mem.eql(u8, pin, state.slock_pincode.?)) {
const res: comm.Message.ScreenUnlockResult = .{
.ok = true,
.err = null,
};
comm.write(gpa, w, .{ .screen_unlock_result = res }) catch |err| logger.err("{!}", .{err});
} else {
comm.write(gpa, w, .{ .screen_unlock_result = .{
.ok = false,
.err = "incorrect pin code",
} }) catch |err| logger.err("{!}", .{err});
}
},
.slock_set_pincode => |newpin| {
logger.info("slock_set_pincode: {?s}", .{newpin});
time.sleep(1 * time.ns_per_s);
state.mu.lock();
defer state.mu.unlock();
if (state.slock_pincode) |s| {
gpa.free(s);
}
state.slock_pincode = if (newpin) |pin| gpa.dupe(u8, pin) catch unreachable else null;
state.settings_sent = false;
mu.lock();
defer mu.unlock();
nodename.set(s);
settings_sent = false;
},
else => {},
}
@ -210,19 +169,18 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
}
sectimer.reset();
state.mu.lock();
defer state.mu.unlock();
mu.lock();
defer mu.unlock();
if (!state.settings_sent) {
state.settings_sent = true;
if (!settings_sent) {
settings_sent = true;
const sett: comm.Message.Settings = .{
.slock_enabled = state.slock_pincode != null,
.hostname = state.nodename.val(),
.hostname = nodename.val(),
.sysupdates = .{ .channel = .edge },
};
comm.write(gpa, w, .{ .settings = sett }) catch |err| {
logger.err("{}", .{err});
state.settings_sent = false;
settings_sent = false;
};
}
@ -348,17 +306,9 @@ pub fn main() !void {
const flags = try parseArgs(gpa);
defer flags.deinit(gpa);
state.slock_pincode = if (flags.slock) try gpa.dupe(u8, "0000") else null;
state.nodename.set("guiplayhost");
defer state.deinit(gpa);
nodename.set("guiplayhost");
var a = std.ArrayList([]const u8).init(gpa);
defer a.deinit();
try a.append(flags.ngui_path.?);
if (flags.slock) {
try a.append("-slock");
}
ngui_proc = std.ChildProcess.init(a.items, gpa);
ngui_proc = std.ChildProcess.init(&.{flags.ngui_path.?}, gpa);
ngui_proc.stdin_behavior = .Pipe;
ngui_proc.stdout_behavior = .Pipe;
ngui_proc.stderr_behavior = .Inherit;
@ -374,13 +324,13 @@ pub fn main() !void {
const th2 = try std.Thread.spawn(.{}, commWriteThread, .{ gpa, uiwriter });
th2.detach();
const sa = posix.Sigaction{
const sa = os.Sigaction{
.handler = .{ .handler = sighandler },
.mask = posix.empty_sigset,
.mask = os.empty_sigset,
.flags = 0,
};
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
try os.sigaction(os.SIG.INT, &sa, null);
try os.sigaction(os.SIG.TERM, &sa, null);
sigquit.wait();
logger.info("killing ngui", .{});

@ -40,8 +40,8 @@ pub const IoPipe = struct {
w: std.fs.File,
/// a pipe must be close'ed when done.
pub fn create() std.posix.PipeError!IoPipe {
const fds = try std.posix.pipe();
pub fn create() std.os.PipeError!IoPipe {
const fds = try std.os.pipe();
return .{
.r = std.fs.File{ .handle = fds[0] },
.w = std.fs.File{ .handle = fds[1] },
@ -138,7 +138,7 @@ pub fn Deinitable(comptime T: type) type {
const Self = @This();
pub fn init(allocator: std.mem.Allocator) !Self {
const res = Self{
var res = Self{
.arena = try allocator.create(std.heap.ArenaAllocator),
.value = undefined,
};

@ -10,13 +10,6 @@
#include <stdlib.h>
#include <unistd.h>
static lv_style_t style_title;
static lv_style_t style_text_muted;
static lv_style_t style_btn_red;
static const lv_font_t *font_large;
static lv_obj_t *virt_keyboard;
static lv_obj_t *tabview; /* main tabs content parent; lv_tabview_create */
/**
* initiates system shutdown leading to poweroff.
*/
@ -42,11 +35,6 @@ int nm_create_lightning_panel(lv_obj_t *parent);
*/
lv_obj_t *nm_create_settings_nodename(lv_obj_t *parent);
/**
* creates screenlock card of the settings panel.
*/
lv_obj_t *nm_create_settings_screenlock(lv_obj_t *parent);
/**
* creates the sysupdates section of the settings panel.
*/
@ -68,6 +56,13 @@ int nm_wifi_start_connect(const char *ssid, const char *password);
*/
void nm_poweroff_btn_callback(lv_event_t *e);
static lv_style_t style_title;
static lv_style_t style_text_muted;
static lv_style_t style_btn_red;
static const lv_font_t *font_large;
static lv_obj_t *virt_keyboard;
static lv_obj_t *tabview; /* main tabs content parent; lv_tabview_create */
/**
* returns user-managed data previously set on an object with nm_obj_set_userdata.
* the returned value may be NULL.
@ -102,11 +97,6 @@ extern lv_style_t *nm_style_title()
return &style_title;
}
extern const lv_font_t *nm_font_large()
{
return font_large;
}
/**
* a hack to prevent tabview from switching to the next tab
* on a scroll event, for example coming from a top layer window.
@ -264,7 +254,6 @@ static int create_settings_panel(lv_obj_t *parent)
********************/
// ported to zig;
lv_obj_t *nodename_panel = nm_create_settings_nodename(parent);
lv_obj_t *screenlock_panel = nm_create_settings_screenlock(parent);
lv_obj_t *sysupdates_panel = nm_create_settings_sysupdates(parent);
/********************
@ -274,16 +263,14 @@ static int create_settings_panel(lv_obj_t *parent)
static lv_coord_t parent_grid_rows[] = {/**/
LV_GRID_CONTENT, /* wifi panel */
LV_GRID_CONTENT, /* nodename panel */
LV_GRID_CONTENT, /* screenlock panel */
LV_GRID_CONTENT, /* power panel */
LV_GRID_CONTENT, /* sysupdates panel */
LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(parent, parent_grid_cols, parent_grid_rows);
lv_obj_set_grid_cell(wifi_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_grid_cell(nodename_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_grid_cell(screenlock_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 2, 1);
lv_obj_set_grid_cell(power_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 3, 1);
lv_obj_set_grid_cell(sysupdates_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 4, 1);
lv_obj_set_grid_cell(power_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 2, 1);
lv_obj_set_grid_cell(sysupdates_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 3, 1);
static lv_coord_t wifi_grid_cols[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t wifi_grid_rows[] = {/**/
@ -338,7 +325,7 @@ static void tab_changed_event_cb(lv_event_t *e)
}
}
extern void nm_ui_init_theme(lv_disp_t *disp)
extern int nm_ui_init(lv_disp_t *disp)
{
/* default theme is static */
lv_theme_t *theme = lv_theme_default_init(disp, /**/
@ -357,22 +344,18 @@ extern void nm_ui_init_theme(lv_disp_t *disp)
lv_style_init(&style_btn_red);
lv_style_set_bg_color(&style_btn_red, lv_palette_main(LV_PALETTE_RED));
}
extern int nm_ui_init_main_tabview(lv_obj_t *scr)
{
/* global virtual keyboard */
virt_keyboard = lv_keyboard_create(scr);
virt_keyboard = lv_keyboard_create(lv_scr_act());
if (virt_keyboard == NULL) {
/* TODO: or continue without keyboard? */
return -1;
}
lv_obj_set_style_text_font(virt_keyboard, font_large, LV_PART_ITEMS);
lv_obj_set_style_max_height(virt_keyboard, NM_DISP_HOR * 2 / 3, 0);
lv_obj_add_flag(virt_keyboard, LV_OBJ_FLAG_HIDDEN);
/* the paren of all main tabs */
const lv_coord_t tabh = 60;
tabview = lv_tabview_create(scr, LV_DIR_TOP, tabh);
tabview = lv_tabview_create(lv_scr_act(), LV_DIR_TOP, tabh);
if (tabview == NULL) {
return -1;
}

@ -46,9 +46,9 @@ pub usingnamespace switch (buildopts.driver) {
}
},
.fbev => struct {
extern "c" fn nm_open_evdev_nonblock() std.posix.fd_t;
extern "c" fn nm_close_evdev(fd: std.posix.fd_t) void;
extern "c" fn nm_consume_input_events(fd: std.posix.fd_t) bool;
extern "c" fn nm_open_evdev_nonblock() std.os.fd_t;
extern "c" fn nm_close_evdev(fd: std.os.fd_t) void;
extern "c" fn nm_consume_input_events(fd: std.os.fd_t) bool;
pub fn InputWatcher() !EvdevWatcher {
const fd = nm_open_evdev_nonblock();
@ -59,7 +59,7 @@ pub usingnamespace switch (buildopts.driver) {
}
pub const EvdevWatcher = struct {
evdev_fd: std.posix.fd_t,
evdev_fd: std.os.fd_t,
pub fn consume(self: @This()) bool {
return nm_consume_input_events(self.evdev_fd);

@ -99,7 +99,7 @@ var tab: struct {
} = null,
fn initSetup(self: *@This(), topwin: lvgl.Window) !void {
const arena = try self.allocator.create(std.heap.ArenaAllocator);
var arena = try self.allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(tab.allocator);
self.seed_setup = .{ .arena = arena, .topwin = topwin };
}

@ -213,9 +213,6 @@ pub const LvStyle = opaque {
};
};
/// represents lv_font_t in C.
pub const LvFont = opaque {};
/// a simplified color type compatible with LVGL which defines lv_color_t
/// as a union containing an bit-fields struct unsupported in zig cImport.
pub const Color = u16; // originally c.lv_color_t; TODO: comptime switch for u32
@ -430,11 +427,6 @@ pub const WidgetMethods = struct {
lv_obj_align(self.lvobj, @intFromEnum(a), xoffset, yoffset);
}
/// similar to `posAlign` but the alignment is in relation to another object `rel`.
pub fn posAlignTo(self: anytype, rel: anytype, a: PosAlign, xoffset: Coord, yoffset: Coord) void {
lv_obj_align_to(self.lvobj, rel.lvobj, @intFromEnum(a), xoffset, yoffset);
}
/// sets flex layout growth property; same meaning as in CSS flex.
pub fn flexGrow(self: anytype, val: u8) void {
lv_obj_set_flex_grow(self.lvobj, val);
@ -503,7 +495,7 @@ pub const Screen = struct {
/// makes a screen active.
pub fn load(scr: Screen) void {
lv_disp_load_scr(scr.lvobj);
lv_disp_load_scr(scr.obj);
}
};
@ -718,11 +710,10 @@ pub const Label = struct {
};
/// the text value is copied into a heap-allocated alloc.
pub fn new(parent: anytype, text: ?[*:0]const u8, opt: Opt) !Label {
const lv_label = lv_label_create(parent.lvobj) orelse return error.OutOfMemory;
if (text) |s| {
lv_label_set_text(lv_label, s);
}
pub fn new(parent: anytype, text: [*:0]const u8, opt: Opt) !Label {
var lv_label = lv_label_create(parent.lvobj) orelse return error.OutOfMemory;
//lv_label_set_text_static(lb, text); // static doesn't work with .dot
lv_label_set_text(lv_label, text);
//lv_obj_set_height(lb, sizeContent); // default
if (opt.long_mode) |m| {
lv_label_set_long_mode(lv_label, @intFromEnum(m));
@ -745,8 +736,8 @@ pub const Label = struct {
/// sets label text to a new value.
/// previous value is dealloc'ed.
pub fn setText(self: Label, text: [:0]const u8) void {
lv_label_set_text(self.lvobj, text.ptr);
pub fn setText(self: Label, text: [*:0]const u8) void {
lv_label_set_text(self.lvobj, text);
}
/// sets label text without heap alloc but assumes text outlives the label obj.
@ -757,7 +748,7 @@ pub const Label = struct {
/// formats a new label text and passes it on to `setText`.
/// the buffer can be dropped once the function returns.
pub fn setTextFmt(self: Label, buf: []u8, comptime format: []const u8, args: anytype) !void {
const s = try std.fmt.bufPrintZ(buf, format, args);
var s = try std.fmt.bufPrintZ(buf, format, args);
self.setText(s);
}
@ -964,40 +955,6 @@ pub const QrCode = struct {
}
};
pub const Keyboard = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
const Mode = enum(c.lv_keyboard_mode_t) {
lower = c.LV_KEYBOARD_MODE_TEXT_LOWER,
upper = c.LV_KEYBOARD_MODE_TEXT_UPPER,
special = c.LV_KEYBOARD_MODE_SPECIAL,
number = c.LV_KEYBOARD_MODE_NUMBER,
user1 = c.LV_KEYBOARD_MODE_USER_1,
user2 = c.LV_KEYBOARD_MODE_USER_2,
user3 = c.LV_KEYBOARD_MODE_USER_3,
user4 = c.LV_KEYBOARD_MODE_USER_4,
};
pub fn new(parent: anytype, mode: Mode) !Keyboard {
const kb = lv_keyboard_create(parent.lvobj) orelse return error.OutOfMemory;
lv_keyboard_set_mode(kb, @intFromEnum(mode));
const sel = LvStyle.Selector{ .part = .item };
lv_obj_set_style_text_font(kb, nm_font_large(), sel.value());
return .{ .lvobj = kb };
}
pub fn attach(self: Keyboard, ta: TextArea) void {
lv_keyboard_set_textarea(self.lvobj, ta.lvobj);
}
pub fn setMode(self: Keyboard, m: Mode) void {
lv_keyboard_set_mode(self.lvobj, m);
}
};
/// represents lv_obj_t type in C.
pub const LvObj = opaque {
/// feature-flags controlling object's behavior.
@ -1131,8 +1088,6 @@ pub const PosAlign = enum(c.lv_align_t) {
pub extern fn nm_style_btn_red() *LvStyle; // TODO: make it private
/// returns a title style with a larger font.
pub extern fn nm_style_title() *LvStyle; // TODO: make it private
/// returns default font of large size.
pub extern fn nm_font_large() *const LvFont; // TODO: make it private
// the "native" lv_obj_set/get user_data are static inline, so make our own funcs.
extern "c" fn nm_obj_userdata(obj: *LvObj) ?*anyopaque;
@ -1205,7 +1160,6 @@ extern fn lv_obj_remove_style(obj: *LvObj, style: ?*LvStyle, sel: c.lv_style_sel
extern fn lv_obj_remove_style_all(obj: *LvObj) void;
extern fn lv_obj_set_style_bg_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void;
extern fn lv_obj_set_style_text_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void;
extern fn lv_obj_set_style_text_font(obj: *LvObj, font: *const LvFont, sel: c.lv_style_selector_t) void;
extern fn lv_obj_set_style_pad_left(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
extern fn lv_obj_set_style_pad_right(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
extern fn lv_obj_set_style_pad_top(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
@ -1237,7 +1191,6 @@ extern fn lv_obj_clear_flag(obj: *LvObj, v: c.lv_obj_flag_t) void;
extern fn lv_obj_has_flag(obj: *LvObj, v: c.lv_obj_flag_t) bool;
extern fn lv_obj_align(obj: *LvObj, a: c.lv_align_t, x: c.lv_coord_t, y: c.lv_coord_t) void;
extern fn lv_obj_align_to(obj: *LvObj, rel: *LvObj, a: c.lv_align_t, x: c.lv_coord_t, y: c.lv_coord_t) void;
extern fn lv_obj_set_height(obj: *LvObj, h: c.lv_coord_t) void;
extern fn lv_obj_set_width(obj: *LvObj, w: c.lv_coord_t) void;
extern fn lv_obj_set_size(obj: *LvObj, w: c.lv_coord_t, h: c.lv_coord_t) void;
@ -1290,7 +1243,3 @@ extern fn lv_win_get_content(win: *LvObj) *LvObj;
extern fn lv_qrcode_create(parent: *LvObj, size: c.lv_coord_t, dark: Color, light: Color) ?*LvObj;
extern fn lv_qrcode_update(qrcode: *LvObj, data: *const anyopaque, data_len: u32) c.lv_res_t;
extern fn lv_keyboard_create(parent: *LvObj) ?*LvObj;
extern fn lv_keyboard_set_textarea(kb: *LvObj, ta: *LvObj) void;
extern fn lv_keyboard_set_mode(kb: *LvObj, mode: c.lv_keyboard_mode_t) void;

@ -1,90 +0,0 @@
const std = @import("std");
const comm = @import("../comm.zig");
const lvgl = @import("lvgl.zig");
const logger = std.log.scoped(.ui_screenlock);
const infoTextInit = "please enter pin code to unlock the screen";
var main_screen: lvgl.Screen = undefined;
var locked_screen: lvgl.Screen = undefined;
var pincode: lvgl.TextArea = undefined;
var info: lvgl.Label = undefined;
var spinner: lvgl.Spinner = undefined;
var keyboard: lvgl.Keyboard = undefined;
var is_active: bool = false;
pub fn init(main_scr: lvgl.Screen) !void {
main_screen = main_scr;
locked_screen = try lvgl.Screen.new();
pincode = try lvgl.TextArea.new(locked_screen, .{ .password_mode = true });
pincode.posAlign(.top_mid, 0, 20);
_ = pincode.on(.ready, nm_pincode_input, null);
info = try lvgl.Label.new(locked_screen, null, .{});
info.setTextStatic(infoTextInit);
info.posAlignTo(pincode, .out_bottom_mid, 0, 10);
spinner = try lvgl.Spinner.new(locked_screen);
spinner.center();
spinner.hide();
keyboard = try lvgl.Keyboard.new(locked_screen, .number);
keyboard.attach(pincode);
}
pub fn activate() void {
if (is_active) {
logger.info("screenlock already active", .{});
return;
}
is_active = true;
locked_screen.load();
spinner.hide();
pincode.enable();
pincode.setText("");
info.setTextStatic(infoTextInit);
info.posAlignTo(pincode, .out_bottom_mid, 0, 10);
keyboard.show();
logger.info("screenlock active", .{});
}
/// msg lifetime is only until function return.
pub fn unlockFailure(msg: [:0]const u8) void {
info.setText(msg);
info.posAlignTo(pincode, .out_bottom_mid, 0, 10);
spinner.hide();
pincode.setText("");
pincode.enable();
keyboard.show();
}
pub fn unlockSuccess() void {
logger.info("deactivating screenlock", .{});
pincode.setText("");
main_screen.load();
is_active = false;
}
export fn nm_pincode_input(e: *lvgl.LvEvent) void {
switch (e.code()) {
.ready => {
keyboard.hide();
pincode.disable();
spinner.show();
comm.pipeWrite(.{ .unlock_screen = pincode.text() }) catch |err| {
logger.err("unlock_screen pipe write: {!}", .{err});
var buf: [256]u8 = undefined;
const msg = std.fmt.bufPrintZ(&buf, "internal error: {!}", .{err}) catch {
unlockFailure("internal error");
return;
};
unlockFailure(msg);
};
},
else => {},
}
}

@ -16,13 +16,9 @@ const logger = std.log.scoped(.ui);
/// label color mark start to make "label:" part of a "label: value"
/// in a different color.
const cmark = "#bbbbbb ";
/// button labels and other text
/// buttons text
const textSwitch = "SWITCH";
const textChange = "CHANGE";
const textDisable = "DISABLE";
const textSlockBtnEnable = "SET PIN CODE";
const textSlockDisabled = "screenlock is disabled\nset a pin code to activate";
const textSlockEnabled = "screenlock is enabled\nit activates once in standby mode";
// global allocator set in init.
// must be set before any call into pub funcs in this module.
@ -36,33 +32,6 @@ var tab: struct {
textarea: lvgl.TextArea,
changebtn: lvgl.TextButton,
},
screenlock: struct {
card: lvgl.Card,
textlabel: lvgl.Label,
enbtn: lvgl.TextButton,
disbtn: lvgl.TextButton,
setpin_win: lvgl.Window,
setpin_label: lvgl.Label,
setpin_input: lvgl.TextArea,
fn beginSetPin(self: *@This()) !void {
self.setpin_win = try lvgl.Window.newTop(60, "SET SCREENLOCK PIN CODE");
const wincont = self.setpin_win.content().flex(.column, .{ .cross = .center, .track = .center });
self.setpin_input = try lvgl.TextArea.new(wincont, .{ .password_mode = true });
self.setpin_input.posAlign(.top_mid, 0, 20);
_ = self.setpin_input.on(.ready, nm_screenlock_pincode_input, null);
self.setpin_label = try lvgl.Label.new(wincont, null, .{});
self.setpin_label.posAlignTo(self.setpin_input, .out_bottom_mid, 0, 10);
self.setpin_label.setTextStatic("please enter a new pin code");
const kb = try lvgl.Keyboard.new(self.setpin_win, .number);
kb.attach(self.setpin_input);
}
fn endSetPin(self: @This()) void {
self.setpin_win.destroy();
}
},
sysupdates: struct {
card: lvgl.Card,
chansel: lvgl.Dropdown,
@ -73,12 +42,8 @@ var tab: struct {
/// holds last values received from the daemon.
var state: struct {
// node name
nodename_change_inprogress: bool = false,
curr_nodename: types.BufTrimString(std.posix.HOST_NAME_MAX) = .{},
// screenlock
slock_pin_input1: ?[]const u8 = null, // verified against a second time input
// sysupdates channel
curr_nodename: types.BufTrimString(std.os.HOST_NAME_MAX) = .{},
curr_sysupdates_chan: ?comm.Message.SysupdatesChan = null,
} = .{};
@ -111,7 +76,7 @@ pub fn initNodenamePanel(cont: lvgl.Container) !lvgl.Card {
right.setPad(0, .column, .{});
tab.nodename.textarea = try lvgl.TextArea.new(right, .{
.maxlen = std.posix.HOST_NAME_MAX,
.maxlen = std.os.HOST_NAME_MAX,
.oneline = true,
});
tab.nodename.textarea.setWidth(lvgl.sizePercent(100));
@ -129,38 +94,6 @@ pub fn initNodenamePanel(cont: lvgl.Container) !lvgl.Card {
return tab.nodename.card;
}
/// creates a settings panel for setting screenlock pin code.
pub fn initScreenlockPanel(cont: lvgl.Container) !lvgl.Card {
tab.screenlock.card = try lvgl.Card.new(cont, symbol.EyeClose ++ " SCREENLOCK", .{ .spinner = true });
const row = try lvgl.FlexLayout.new(tab.screenlock.card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{ .height = .content });
left.flexGrow(1);
left.setPad(10, .row, .{});
tab.screenlock.textlabel = try lvgl.Label.new(left, null, .{});
tab.screenlock.textlabel.setTextStatic("no info available yet");
// right column
const right = try lvgl.FlexLayout.new(row, .column, .{ .height = .content });
right.flexGrow(1);
right.setPad(10, .row, .{});
tab.screenlock.enbtn = try lvgl.TextButton.new(right, textSlockBtnEnable);
tab.screenlock.enbtn.setWidth(lvgl.sizePercent(100));
tab.screenlock.enbtn.hide();
_ = tab.screenlock.enbtn.on(.click, nm_screenlock_enbtn_click, null);
tab.screenlock.disbtn = try lvgl.TextButton.new(right, textDisable);
tab.screenlock.disbtn.setWidth(lvgl.sizePercent(100));
tab.screenlock.disbtn.addStyle(lvgl.nm_style_btn_red(), .{});
tab.screenlock.disbtn.hide();
_ = tab.screenlock.disbtn.on(.click, nm_screenlock_disbtn_click, null);
return tab.screenlock.card;
}
/// creates a settings panel UI to control system updates channel.
/// must be called only once at program startup.
pub fn initSysupdatesPanel(cont: lvgl.Container) !lvgl.Card {
@ -238,20 +171,6 @@ pub fn update(sett: comm.Message.Settings) !void {
tab.nodename.changebtn.disable();
}
}
// screenlock
tab.screenlock.card.spin(.off);
if (sett.slock_enabled) {
tab.screenlock.textlabel.setTextStatic(textSlockEnabled);
tab.screenlock.enbtn.hide();
tab.screenlock.disbtn.enable();
tab.screenlock.disbtn.show();
} else {
tab.screenlock.textlabel.setTextStatic(textSlockDisabled);
tab.screenlock.enbtn.enable();
tab.screenlock.enbtn.show();
tab.screenlock.disbtn.hide();
}
}
export fn nm_nodename_textarea_input(e: *lvgl.LvEvent) void {
@ -283,60 +202,6 @@ export fn nm_nodename_change_btn_click(_: *lvgl.LvEvent) void {
tab.nodename.card.spin(.on);
}
export fn nm_screenlock_enbtn_click(_: *lvgl.LvEvent) void {
tab.screenlock.beginSetPin() catch |err| {
logger.err("screenlock.beginSetPin: {!}", .{err});
};
}
export fn nm_screenlock_pincode_input(_: *lvgl.LvEvent) void {
// first time input; prompt user for second input to verify
if (state.slock_pin_input1 == null) {
state.slock_pin_input1 = allocator.dupe(u8, tab.screenlock.setpin_input.text()) catch |err| {
logger.err("unable to continue setting screenlock pin code: {!}", .{err});
tab.screenlock.endSetPin();
return;
};
tab.screenlock.setpin_label.setTextStatic("please enter the pin once more to verify");
tab.screenlock.setpin_input.setText("");
return;
}
// ensure first and second time inputs match
const pininput2 = tab.screenlock.setpin_input.text();
if (!std.mem.eql(u8, pininput2, state.slock_pin_input1.?)) {
allocator.free(state.slock_pin_input1.?);
state.slock_pin_input1 = null;
tab.screenlock.setpin_label.setTextStatic("pin codes mismatch, please try again");
tab.screenlock.setpin_input.setText("");
return;
}
// send the pin code to nd and return to the main settings screen
defer {
tab.screenlock.endSetPin();
allocator.free(state.slock_pin_input1.?);
state.slock_pin_input1 = null;
}
tab.screenlock.card.spin(.on);
tab.screenlock.enbtn.disable();
comm.pipeWrite(.{ .slock_set_pincode = pininput2 }) catch |err| {
logger.err("comm slock_set_pincode: {!}", .{err});
tab.screenlock.card.spin(.off);
tab.screenlock.enbtn.enable();
};
}
export fn nm_screenlock_disbtn_click(_: *lvgl.LvEvent) void {
tab.screenlock.card.spin(.on);
tab.screenlock.disbtn.disable();
comm.pipeWrite(.{ .slock_set_pincode = null }) catch |err| {
logger.err("comm slock_set_pincode(null): {!}", .{err});
tab.screenlock.card.spin(.off);
tab.screenlock.disbtn.enable();
};
}
export fn nm_sysupdates_chansel_changed(_: *lvgl.LvEvent) void {
var buf = [_]u8{0} ** 32;
const name = tab.sysupdates.chansel.getSelectedStr(&buf);

@ -1,6 +1,5 @@
///! see lv_symbols_def.h
pub const Edit = &[_]u8{ 0xef, 0x8c, 0x84 };
pub const EyeClose = &[_]u8{ 0xef, 0x81, 0xb0 };
pub const LightningBolt = &[_]u8{ 0xef, 0x83, 0xa7 };
pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 };
pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c };

@ -10,28 +10,21 @@ const widget = @import("widget.zig");
pub const bitcoin = @import("bitcoin.zig");
pub const lightning = @import("lightning.zig");
pub const poweroff = @import("poweroff.zig");
pub const screenlock = @import("screenlock.zig");
pub const settings = @import("settings.zig");
const logger = std.log.scoped(.ui);
// defined in src/ui/c/ui.c
extern "c" fn nm_ui_init_theme(disp: *lvgl.LvDisp) void;
// calls back into nm_create_xxx_panel functions defined here during init.
extern "c" fn nm_ui_init_main_tabview(screen: *lvgl.LvObj) c_int;
extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int;
// global allocator set on init.
// must be set before a call to nm_ui_init.
var allocator: std.mem.Allocator = undefined;
pub const InitOpt = struct {
allocator: std.mem.Allocator,
slock: bool, // whether to start the UI in screen locked mode
};
pub fn init(opt: InitOpt) !void {
allocator = opt.allocator;
settings.allocator = opt.allocator;
pub fn init(gpa: std.mem.Allocator) !void {
allocator = gpa;
settings.allocator = gpa;
lvgl.init();
const disp = try drv.initDisplay();
drv.initInput() catch |err| {
@ -40,16 +33,8 @@ pub fn init(opt: InitOpt) !void {
// otherwise, impossible to wake up the screen. */
return err;
};
nm_ui_init_theme(disp);
const main_scr = try lvgl.Screen.active();
if (nm_ui_init_main_tabview(main_scr.lvobj) != 0) {
return error.UiInitMainTabview;
}
try screenlock.init(main_scr);
if (opt.slock) {
screenlock.activate();
if (nm_ui_init(disp) != 0) {
return error.UiInitFailure;
}
}
@ -85,14 +70,6 @@ export fn nm_create_settings_nodename(parent: *lvgl.LvObj) ?*lvgl.LvObj {
return card.lvobj;
}
export fn nm_create_settings_screenlock(parent: *lvgl.LvObj) ?*lvgl.LvObj {
const card = settings.initScreenlockPanel(lvgl.Container{ .lvobj = parent }) catch |err| {
logger.err("initScreenlockPanel: {any}", .{err});
return null;
};
return card.lvobj;
}
export fn nm_create_settings_sysupdates(parent: *lvgl.LvObj) ?*lvgl.LvObj {
const card = settings.initSysupdatesPanel(lvgl.Container{ .lvobj = parent }) catch |err| {
logger.err("initSysupdatesPanel: {any}", .{err});

@ -41,11 +41,12 @@ fn formatUnix(sec: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w
}
fn formatMetricI(value: i64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w: anytype) !void {
const uval: u64 = @abs(value);
const uval: u64 = std.math.absCast(value);
const base: u64 = 1000;
if (uval < base) {
return std.fmt.formatIntValue(value, fmt, opts, w);
}
if (value < 0) {
try w.writeByte('-');
}
@ -63,60 +64,8 @@ fn formatMetricU(value: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptio
const mags_si = " kMGTPEZY";
const log2 = std.math.log2(value);
const m = @min(log2 / comptime std.math.log2(base), mags_si.len - 1);
const newval = lossyCast(f64, value) / std.math.pow(f64, lossyCast(f64, base), lossyCast(f64, m));
const suffix = mags_si[m];
const newval: f64 = lossyCast(f64, value) / std.math.pow(f64, lossyCast(f64, base), lossyCast(f64, m));
try std.fmt.formatType(newval, "d", opts, w, 0);
try std.fmt.formatFloatDecimal(newval, opts, w);
try w.writeByte(suffix);
}
test "unix" {
const t = std.testing;
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
try std.fmt.format(fbs.writer(), "{}", .{unix(1136239445)});
try t.expectEqualStrings("2006-01-02 22:04:05 UTC", fbs.getWritten());
}
test "imetric" {
const t = std.testing;
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
const table: []const struct { val: i64, str: []const u8 } = &.{
.{ .val = 0, .str = "0" },
.{ .val = -13, .str = "-13" },
.{ .val = 1000, .str = "1k" },
.{ .val = -1234, .str = "-1.234k" },
.{ .val = 12340, .str = "12.34k" },
.{ .val = -123400, .str = "-123.4k" },
.{ .val = 1234000, .str = "1.234M" },
.{ .val = -1234000000, .str = "-1.234G" },
};
for (table) |item| {
fbs.reset();
try std.fmt.format(fbs.writer(), "{}", .{imetric(item.val)});
try t.expectEqualStrings(item.str, fbs.getWritten());
}
}
test "umetric" {
const t = std.testing;
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
const table: []const struct { val: u64, str: []const u8 } = &.{
.{ .val = 0, .str = "0" },
.{ .val = 13, .str = "13" },
.{ .val = 1000, .str = "1k" },
.{ .val = 1234, .str = "1.234k" },
.{ .val = 12340, .str = "12.34k" },
.{ .val = 123400, .str = "123.4k" },
.{ .val = 1234000, .str = "1.234M" },
.{ .val = 1234000000, .str = "1.234G" },
};
for (table) |item| {
fbs.reset();
try std.fmt.format(fbs.writer(), "{}", .{umetric(item.val)});
try t.expectEqualStrings(item.str, fbs.getWritten());
}
}

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