You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
ndg/src/ui/settings.zig

381 lines
15 KiB
Zig

//! settings main tab.
//! all functions assume LVGL is init'ed and ui mutex is locked on entry.
//!
//! TODO: at the moment, most of the code is still in C; need to port to zig from src/ui/c/ui.c
const std = @import("std");
const comm = @import("../comm.zig");
const types = @import("../types.zig");
const lvgl = @import("lvgl.zig");
const symbol = @import("symbol.zig");
const widget = @import("widget.zig");
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
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.
pub var allocator: std.mem.Allocator = undefined;
/// the settings tab alive for the whole duration of the process.
var tab: struct {
nodename: struct {
card: lvgl.Card,
currname: lvgl.Label,
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,
switchbtn: lvgl.TextButton,
currchan: lvgl.Label,
},
} = undefined;
/// 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_sysupdates_chan: ?comm.Message.SysupdatesChan = null,
} = .{};
/// creates a settings panel allowing to change hostname and lnd alias,
/// aka nodename.
pub fn initNodenamePanel(cont: lvgl.Container) !lvgl.Card {
tab.nodename.card = try lvgl.Card.new(cont, symbol.Edit ++ " NODE NAME", .{ .spinner = true });
const row = try lvgl.FlexLayout.new(tab.nodename.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.nodename.currname = try lvgl.Label.new(left, cmark ++ "CURRENT NAME:# unknown", .{ .recolor = true });
tab.nodename.currname.setHeightToContent();
const lab = try lvgl.Label.new(left, "the name is visible on a local network as well as lightning.", .{});
lab.setWidth(lvgl.sizePercent(100));
lab.setHeightToContent();
lab.setPad(0, .right, .{});
// right column
const right = try lvgl.FlexLayout.new(row, .column, .{ .height = .content });
right.flexGrow(1);
right.setPad(10, .row, .{});
right.setPad(0, .column, .{});
tab.nodename.textarea = try lvgl.TextArea.new(right, .{
.maxlen = std.posix.HOST_NAME_MAX,
.oneline = true,
});
tab.nodename.textarea.setWidth(lvgl.sizePercent(100));
_ = tab.nodename.textarea.on(.all, nm_nodename_textarea_input, null);
tab.nodename.changebtn = try lvgl.TextButton.new(right, textChange);
tab.nodename.changebtn.setWidth(lvgl.sizePercent(100));
tab.nodename.changebtn.setPad(0, .left, .{});
// disable name change 'till data received from the daemon.
tab.nodename.textarea.disable();
tab.nodename.changebtn.disable();
_ = tab.nodename.changebtn.on(.click, nm_nodename_change_btn_click, null);
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 {
tab.sysupdates.card = try lvgl.Card.new(cont, symbol.Loop ++ " SYSUPDATES", .{ .spinner = true });
const l1 = try lvgl.Label.new(tab.sysupdates.card, "" //
++ "https://git.qcode.ch/nakamochi/sysupdates " // TODO: make this configurable?
++ "is the source of system updates.", .{});
l1.setPad(15, .top, .{});
l1.setWidth(lvgl.sizePercent(100));
l1.setHeightToContent();
const row = try lvgl.FlexLayout.new(tab.sysupdates.card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.flexGrow(1);
left.setPad(10, .row, .{});
left.setHeightToContent();
tab.sysupdates.currchan = try lvgl.Label.new(left, cmark ++ "CURRENT CHANNEL:# unknown", .{ .recolor = true });
tab.sysupdates.currchan.setHeightToContent();
const lab = try lvgl.Label.new(left, "edge channel may contain some experimental and unstable features.", .{});
lab.setWidth(lvgl.sizePercent(100));
lab.setHeightToContent();
// right column
const right = try lvgl.FlexLayout.new(row, .column, .{});
right.flexGrow(1);
right.setPad(10, .row, .{});
right.setHeightToContent();
tab.sysupdates.chansel = try lvgl.Dropdown.newStatic(right, blk: {
// items order must match that of the switch in update fn.
break :blk @tagName(comm.Message.SysupdatesChan.stable) // index 0
++ "\n" ++ @tagName(comm.Message.SysupdatesChan.edge); // index 1
});
tab.sysupdates.chansel.setWidth(lvgl.sizePercent(100));
tab.sysupdates.chansel.setText(""); // show no pre-selected value
_ = tab.sysupdates.chansel.on(.value_changed, nm_sysupdates_chansel_changed, null);
tab.sysupdates.switchbtn = try lvgl.TextButton.new(right, textSwitch);
tab.sysupdates.switchbtn.setWidth(lvgl.sizePercent(100));
// disable channel switch button 'till data received from the daemon
// or user-selected value.
tab.sysupdates.switchbtn.disable();
_ = tab.sysupdates.switchbtn.on(.click, nm_sysupdates_switch_click, null);
return tab.sysupdates.card;
}
/// updates the UI with the data from the provided settings arg.
pub fn update(sett: comm.Message.Settings) !void {
// sysupdates channel
var buf: [512]u8 = undefined;
try tab.sysupdates.currchan.setTextFmt(&buf, cmark ++ "CURRENT CHANNEL:# {s}", .{@tagName(sett.sysupdates.channel)});
state.curr_sysupdates_chan = sett.sysupdates.channel;
// nodename
state.curr_nodename.set(sett.hostname);
try tab.nodename.currname.setTextFmt(&buf, cmark ++ "CURRENT NAME:# {s}", .{state.curr_nodename.val()});
if (state.nodename_change_inprogress) {
const currname = tab.nodename.textarea.text();
if (std.mem.eql(u8, sett.hostname, currname)) {
state.nodename_change_inprogress = false;
tab.nodename.textarea.setText("");
tab.nodename.textarea.enable();
tab.nodename.card.spin(.off);
}
} else {
tab.nodename.textarea.enable();
const currname = state.curr_nodename.val();
const newname = tab.nodename.textarea.text();
if (newname.len > 0 and !std.mem.eql(u8, newname, currname)) {
tab.nodename.changebtn.enable();
} else {
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 {
switch (e.code()) {
.focus => widget.keyboardOn(tab.nodename.textarea),
.defocus, .ready, .cancel => widget.keyboardOff(),
.value_changed => {
const currname = state.curr_nodename.val();
const newname = tab.nodename.textarea.text();
if (currname.len > 0 and newname.len > 0 and !std.mem.eql(u8, newname, currname)) {
tab.nodename.changebtn.enable();
} else {
tab.nodename.changebtn.disable();
}
},
else => {},
}
}
export fn nm_nodename_change_btn_click(_: *lvgl.LvEvent) void {
const newname = tab.nodename.textarea.text();
comm.pipeWrite(.{ .set_nodename = newname }) catch |err| {
logger.err("nodename change pipe write: {!}", .{err});
return;
};
state.nodename_change_inprogress = true;
tab.nodename.changebtn.disable();
tab.nodename.textarea.disable();
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);
const chan = std.meta.stringToEnum(comm.Message.SysupdatesChan, name) orelse return;
if (state.curr_sysupdates_chan) |curr_chan| {
if (chan != curr_chan) {
tab.sysupdates.switchbtn.enable();
tab.sysupdates.chansel.clearText(); // show selected value
} else {
tab.sysupdates.switchbtn.disable();
tab.sysupdates.chansel.setText(""); // hide selected value
}
} else {
tab.sysupdates.switchbtn.enable();
tab.sysupdates.chansel.clearText(); // show selected value
}
}
export fn nm_sysupdates_switch_click(_: *lvgl.LvEvent) void {
var buf = [_]u8{0} ** 32;
const name = tab.sysupdates.chansel.getSelectedStr(&buf);
switchSysupdates(name) catch |err| logger.err("switchSysupdates: {any}", .{err});
}
fn switchSysupdates(name: []const u8) !void {
const chan = std.meta.stringToEnum(comm.Message.SysupdatesChan, name) orelse return error.InvalidSysupdateChannel;
logger.debug("switching sysupdates to channel {}", .{chan});
tab.sysupdates.switchbtn.disable();
tab.sysupdates.switchbtn.label.setTextStatic("UPDATING ...");
tab.sysupdates.chansel.disable();
tab.sysupdates.card.spin(.on);
errdefer {
tab.sysupdates.card.spin(.off);
tab.sysupdates.chansel.enable();
tab.sysupdates.switchbtn.enable();
tab.sysupdates.switchbtn.label.setTextStatic(textSwitch);
}
try comm.pipeWrite(.{ .switch_sysupdates = chan });
}