22//! all functions assume LVGL is init'ed and ui mutex is locked on entry.
33
44const std = @import ("std" );
5+ const ascii = std .ascii ;
56
67const comm = @import ("../comm.zig" );
78const lvgl = @import ("lvgl.zig" );
@@ -88,6 +89,7 @@ var tab: struct {
8889 topwin : lvgl.Window ,
8990 arena : * std.heap.ArenaAllocator , // all non-UI elements are alloc'ed here
9091 mnemonic : ? types.StringList = null , // 24 words genseed result
92+ restore_input : ? lvgl.TextArea = null , // restore-from-seed UI bits (optional)
9193 pairing : ? struct {
9294 // app_description key to connection URL.
9395 // keys are static, values are heap-alloc'ed in `setupPairing`.
@@ -183,9 +185,13 @@ pub fn initTabPanel(allocator: std.mem.Allocator, cont: lvgl.Container) !void {
183185 tab .nowallet .resizeToMax ();
184186 tab .nowallet .setPad (10 , .row , .{});
185187 _ = try lvgl .Label .new (tab .nowallet , "lightning wallet is uninitialized.\n tap the button to start the setup process." , .{});
186- const btn = try lvgl .TextButton .new (tab .nowallet , "SETUP NEW WALLET" );
187- btn .setWidth (lvgl .sizePercent (50 ));
188- _ = btn .on (.click , nm_lnd_setup_click , null );
188+ const btn_new = try lvgl .TextButton .new (tab .nowallet , "SETUP NEW WALLET" );
189+ btn_new .setWidth (lvgl .sizePercent (60 ));
190+ _ = btn_new .on (.click , nm_lnd_setup_click , null );
191+
192+ const btn_restore = try lvgl .TextButton .new (tab .nowallet , "RESTORE FROM SEED" );
193+ btn_restore .setWidth (lvgl .sizePercent (60 ));
194+ _ = btn_restore .on (.click , nm_lnd_restore_click , null );
189195 }
190196
191197 // locked wallet state
@@ -305,6 +311,10 @@ export fn nm_lnd_setup_click(_: *lvgl.LvEvent) void {
305311 startSeedSetup () catch | err | logger .err ("startSeedSetup: {any}" , .{err });
306312}
307313
314+ export fn nm_lnd_restore_click (_ : * lvgl.LvEvent ) void {
315+ startRestoreSetup () catch | err | logger .err ("startRestoreSetup: {any}" , .{err });
316+ }
317+
308318export fn nm_lnd_pair_click (_ : * lvgl.LvEvent ) void {
309319 startPairing () catch | err | logger .err ("startPairing: {any}" , .{err });
310320}
@@ -327,6 +337,55 @@ fn startSeedSetup() !void {
327337 try comm .pipeWrite (.lightning_genseed );
328338}
329339
340+ fn startRestoreSetup () ! void {
341+ const win = try lvgl .Window .newTop (60 , " " ++ symbol .LightningBolt ++ " RESTORE FROM SEED" );
342+ try tab .initSetup (win );
343+ errdefer tab .destroySetup (); // TODO: display an error instead
344+
345+ const wincont = win .content ().flex (.column , .{});
346+
347+ _ = try lvgl .Label .new (wincont ,
348+ \\enter the 24-word mnemonic seed below, separated by spaces.
349+ \\the seed is case-insensitive, and words may be separated
350+ \\by multiple spaces/newlines.
351+ , .{});
352+
353+ var ta = try lvgl .TextArea .new (wincont , .{ .oneline = false });
354+ ta .setWidth (lvgl .sizePercent (100 ));
355+ ta .setHeight (lvgl .sizePercent (55 ));
356+ ta .setPlaceholderText ("word1 word2 ... word24" );
357+ _ = ta .on (.all , nm_lnd_restore_input_event , null );
358+ tab .seed_setup .? .restore_input = ta ;
359+
360+ const btnrow = try lvgl .FlexLayout .new (wincont , .row , .{
361+ .width = lvgl .sizePercent (100 ),
362+ .height = .content ,
363+ .main = .space_between ,
364+ });
365+
366+ const cancel_btn = try lvgl .TextButton .new (btnrow , "CANCEL" );
367+ cancel_btn .setWidth (lvgl .sizePercent (30 ));
368+ cancel_btn .addStyle (lvgl .nm_style_btn_red (), .{});
369+ _ = cancel_btn .on (.click , nm_lnd_setup_finish , null );
370+
371+ const proceed_btn = try lvgl .TextButton .new (btnrow , "PROCEED " ++ symbol .Right );
372+ proceed_btn .setWidth (lvgl .sizePercent (30 ));
373+ _ = proceed_btn .on (.click , nm_lnd_restore_proceed , null );
374+ }
375+
376+ export fn nm_lnd_restore_input_event (e : * lvgl.LvEvent ) callconv (.C ) void {
377+ const code = e .code ();
378+ if (code == .click or code == .press or code == .focus ) {
379+ if (tab .seed_setup ) | ss | if (ss .restore_input ) | ta | widget .keyboardOn (ta );
380+ } else if (code == .defocus or code == .ready or code == .cancel ) {
381+ widget .keyboardOff ();
382+ }
383+ }
384+
385+ export fn nm_lnd_restore_proceed (_ : * lvgl.LvEvent ) void {
386+ restoreProceed () catch | err | logger .err ("restoreProceed: {any}" , .{err });
387+ }
388+
330389/// similar to `startSeedSetup` but used when seed is already setup,
331390/// at any time later.
332391/// reuses tab.seed_setup elements for the same purpose.
@@ -396,6 +455,60 @@ export fn nm_lnd_setup_commit_seed(_: *lvgl.LvEvent) void {
396455 setupCommitSeed () catch | err | logger .err ("setupCommitSeed: {any}" , .{err });
397456}
398457
458+ fn restoreProceed () ! void {
459+ errdefer tab .destroySetup (); // TODO: display an error instead
460+ if (tab .seed_setup == null ) return error .LightningSetupInactive ;
461+ const ss = & tab .seed_setup .? ;
462+ if (ss .restore_input == null ) return error .LightningSetupInactive ;
463+
464+ const raw = ss .restore_input .? .text (); // expected: []const u8
465+
466+ // Tokenize by any whitespace; ignore multiple spaces/newlines/tabs.
467+ var it = std .mem .tokenizeAny (u8 , raw , " \t \r \n " );
468+
469+ // Collect up to 24 words.
470+ var words : [24 ][]const u8 = undefined ;
471+ var n : usize = 0 ;
472+ while (it .next ()) | w | {
473+ if (n >= 24 ) {
474+ try showRestoreError ("too many words: expected 24." );
475+ return ;
476+ }
477+ words [n ] = w ;
478+ n += 1 ;
479+ }
480+ if (n != 24 ) {
481+ try showRestoreError ("invalid seed length: expected 24 words." );
482+ return ;
483+ }
484+
485+ const alloc = ss .arena .allocator ();
486+ var normalized = try alloc .alloc ([]const u8 , 24 );
487+ for (words , 0.. ) | w , i | {
488+ var tmp = try alloc .alloc (u8 , w .len );
489+ for (w , 0.. ) | ch , j | tmp [j ] = ascii .toLower (ch );
490+ normalized [i ] = tmp ;
491+ }
492+
493+ ss .mnemonic = try types .StringList .fromUnowned (alloc , normalized );
494+
495+ widget .keyboardOff ();
496+ try setupCommitSeed ();
497+ }
498+
499+ fn showRestoreError (msg : [* :0 ]const u8 ) ! void {
500+ // Reuse modal helper for a simple “OK”.
501+ const ok : [* :0 ]const u8 = "OK" ;
502+ widget .modal (" " ++ symbol .Warning ++ " INVALID SEED" , msg , &.{ok }, restoreErrModalCallback ) catch | err | {
503+ logger .err ("showRestoreError: modal: {any}" , .{err });
504+ };
505+ }
506+
507+ fn restoreErrModalCallback (_ : usize ) align (@alignOf (widget .ModalButtonCallbackFn )) void {
508+ // nothing; modal close already handled in widget.modal callback path
509+ preserve_main_active_tab ();
510+ }
511+
399512fn setupCommitSeed () ! void {
400513 errdefer tab .destroySetup (); // TODO: display an error instead
401514 if (tab .seed_setup == null ) {
0 commit comments