#include "ui.h" #include #include #include #include "display.h" #include "esp_err.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "ili9341.h" #include "lvgl.h" #include "mnemonic.h" #include "secure_mem.h" #include "touch.h" #define UI_TICK_MS 2 #define UI_DRAW_LINES 20 #define UI_MNEMONIC_WORDS 12 #define UI_BIP39_WORD_MAX 9 #define UI_SUGGESTION_STORE_MAX 64 #define UI_SUGGESTION_VISIBLE_MAX 32 static const char *TAG = "ui_lvgl"; static bool s_lvgl_inited = false; static lv_disp_draw_buf_t s_draw_buf; static lv_color_t s_lv_draw_buf[DISPLAY_WIDTH * UI_DRAW_LINES]; static uint8_t s_flush_bytes[DISPLAY_WIDTH * UI_DRAW_LINES * 2]; static esp_timer_handle_t s_lv_tick_timer; static bool s_done = false; static bool s_have_mnemonic = false; static char s_selected_mnemonic[256]; static lv_obj_t *s_status_label = NULL; static char s_committed_words[UI_MNEMONIC_WORDS][UI_BIP39_WORD_MAX]; static size_t s_word_count = 0; static char s_prefix[UI_BIP39_WORD_MAX]; static size_t s_prefix_len = 0; static bool s_checksum_override = false; static lv_obj_t *s_entry_words_label = NULL; static lv_obj_t *s_entry_prefix_label = NULL; static lv_obj_t *s_suggestion_row = NULL; static lv_obj_t *s_key_matrix = NULL; static lv_obj_t *s_checksum_panel = NULL; static uint16_t s_match_indices[UI_SUGGESTION_STORE_MAX]; static size_t s_match_total = 0; static ui_approval_decision_t s_approval_decision = UI_APPROVAL_TIMEOUT; static lv_obj_t *s_approval_countdown = NULL; static int64_t s_approval_deadline_us = 0; #define UI_EVENT_LOG_MAX 8 #define UI_EVENT_LINE_MAX 96 static char s_event_lines[UI_EVENT_LOG_MAX][UI_EVENT_LINE_MAX]; static int s_event_count = 0; static void reset_screen_widget_refs(void) { s_status_label = NULL; s_entry_words_label = NULL; s_entry_prefix_label = NULL; s_suggestion_row = NULL; s_key_matrix = NULL; s_checksum_panel = NULL; s_approval_countdown = NULL; s_match_total = 0; } static void style_screen(lv_obj_t *scr) { lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), 0); } static void style_button(lv_obj_t *btn) { lv_obj_set_style_radius(btn, 6, 0); lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(btn, lv_color_hex(0x000000), 0); lv_obj_set_style_border_width(btn, 2, 0); lv_obj_set_style_border_color(btn, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_color(btn, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_border_color(btn, lv_color_hex(0xFF0000), LV_STATE_PRESSED); lv_obj_set_style_text_color(btn, lv_color_hex(0xFF0000), LV_STATE_PRESSED); lv_obj_set_style_border_color(btn, lv_color_hex(0xFF0000), LV_STATE_FOCUSED); lv_obj_set_style_text_color(btn, lv_color_hex(0xFF0000), LV_STATE_FOCUSED); } static void style_key_matrix(lv_obj_t *km) { lv_obj_set_style_bg_opa(km, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(km, lv_color_hex(0x000000), 0); lv_obj_set_style_border_width(km, 1, 0); lv_obj_set_style_border_color(km, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_bg_opa(km, LV_OPA_COVER, LV_PART_ITEMS); lv_obj_set_style_bg_color(km, lv_color_hex(0x000000), LV_PART_ITEMS); lv_obj_set_style_border_color(km, lv_color_hex(0xFFFFFF), LV_PART_ITEMS); lv_obj_set_style_border_width(km, 1, LV_PART_ITEMS); lv_obj_set_style_text_color(km, lv_color_hex(0xFFFFFF), LV_PART_ITEMS); lv_obj_set_style_border_color(km, lv_color_hex(0xFF0000), LV_PART_ITEMS | LV_STATE_PRESSED); lv_obj_set_style_text_color(km, lv_color_hex(0xFF0000), LV_PART_ITEMS | LV_STATE_PRESSED); lv_obj_set_style_text_color(km, lv_color_hex(0x505050), LV_PART_ITEMS | LV_STATE_DISABLED); lv_obj_set_style_border_color(km, lv_color_hex(0x303030), LV_PART_ITEMS | LV_STATE_DISABLED); } static void lvgl_tick_cb(void *arg) { (void)arg; lv_tick_inc(UI_TICK_MS); } static void lvgl_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { const uint16_t w = (uint16_t)(area->x2 - area->x1 + 1); const uint16_t h = (uint16_t)(area->y2 - area->y1 + 1); const uint32_t px_count = (uint32_t)w * h; if (px_count > (uint32_t)(DISPLAY_WIDTH * UI_DRAW_LINES)) { lv_disp_flush_ready(disp_drv); return; } for (uint32_t i = 0; i < px_count; ++i) { uint16_t c = color_p[i].full; s_flush_bytes[i * 2 + 0] = (uint8_t)(c >> 8); s_flush_bytes[i * 2 + 1] = (uint8_t)(c & 0xFF); } (void)ili9341_blit_rgb565((uint16_t)area->x1, (uint16_t)area->y1, w, h, s_flush_bytes); lv_disp_flush_ready(disp_drv); } static void lvgl_touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { (void)drv; int sx = 0; int sy = 0; uint16_t pressure = 0; if (!touch_read_screen(&sx, &sy, &pressure)) { data->state = LV_INDEV_STATE_RELEASED; return; } data->state = LV_INDEV_STATE_PRESSED; data->point.x = sx; data->point.y = sy; } static esp_err_t ui_lvgl_init_once(void) { if (s_lvgl_inited) { return ESP_OK; } ESP_ERROR_CHECK(touch_init()); lv_init(); lv_disp_draw_buf_init(&s_draw_buf, s_lv_draw_buf, NULL, DISPLAY_WIDTH * UI_DRAW_LINES); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = DISPLAY_WIDTH; disp_drv.ver_res = DISPLAY_HEIGHT; disp_drv.flush_cb = lvgl_flush_cb; disp_drv.draw_buf = &s_draw_buf; lv_disp_drv_register(&disp_drv); static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = lvgl_touch_read_cb; lv_indev_drv_register(&indev_drv); const esp_timer_create_args_t lv_tick_timer_args = { .callback = lvgl_tick_cb, .name = "lv_tick", }; ESP_ERROR_CHECK(esp_timer_create(&lv_tick_timer_args, &s_lv_tick_timer)); ESP_ERROR_CHECK(esp_timer_start_periodic(s_lv_tick_timer, UI_TICK_MS * 1000)); s_lvgl_inited = true; return ESP_OK; } static void set_status(const char *text) { if (s_status_label != NULL) { lv_label_set_text(s_status_label, text); } } static void on_generate(lv_event_t *e) { (void)e; memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic)); if (generate_mnemonic_12(s_selected_mnemonic, sizeof(s_selected_mnemonic)) != 0) { set_status("Failed to generate mnemonic"); return; } s_have_mnemonic = true; s_done = true; } static void build_main_menu(void); static void refresh_entry_ui(void); static bool verify_or_prompt_checksum(void); static const char *s_key_map[] = { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n", "a", "s", "d", "f", "g", "h", "j", "k", "l", "\n", "z", "x", "c", "v", "b", "n", "m", "<", "", }; static void clear_entry_state(void) { secure_memzero(s_committed_words, sizeof(s_committed_words)); secure_memzero(s_prefix, sizeof(s_prefix)); s_word_count = 0; s_prefix_len = 0; s_checksum_override = false; s_match_total = 0; } static bool join_committed_words(char *out, size_t out_len) { size_t cursor = 0; if (out == NULL || out_len == 0) { return false; } out[0] = '\0'; for (size_t i = 0; i < s_word_count; ++i) { size_t wl = strlen(s_committed_words[i]); if (cursor != 0) { if (cursor + 1 >= out_len) { out[0] = '\0'; return false; } out[cursor++] = ' '; } if (cursor + wl >= out_len) { out[0] = '\0'; return false; } memcpy(out + cursor, s_committed_words[i], wl); cursor += wl; out[cursor] = '\0'; } return true; } static void commit_word(const char *word) { if (word == NULL || s_word_count >= UI_MNEMONIC_WORDS) { return; } memset(s_committed_words[s_word_count], 0, UI_BIP39_WORD_MAX); strncpy(s_committed_words[s_word_count], word, UI_BIP39_WORD_MAX - 1); s_word_count++; secure_memzero(s_prefix, sizeof(s_prefix)); s_prefix_len = 0; s_checksum_override = false; } static void on_checksum_use_anyway(lv_event_t *e) { (void)e; char joined[256]; memset(joined, 0, sizeof(joined)); if (!join_committed_words(joined, sizeof(joined))) { set_status("Mnemonic buffer error"); secure_memzero(joined, sizeof(joined)); return; } memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic)); strncpy(s_selected_mnemonic, joined, sizeof(s_selected_mnemonic) - 1); s_have_mnemonic = true; s_done = true; s_checksum_override = true; secure_memzero(joined, sizeof(joined)); } static void on_checksum_edit_words(lv_event_t *e) { (void)e; if (s_checksum_panel != NULL) { lv_obj_del(s_checksum_panel); s_checksum_panel = NULL; } set_status("Checksum mismatch: edit or use anyway"); refresh_entry_ui(); } static bool verify_or_prompt_checksum(void) { char joined[256]; if (s_key_matrix == NULL || s_entry_words_label == NULL || s_entry_prefix_label == NULL) { return false; } if (s_word_count != UI_MNEMONIC_WORDS || s_prefix_len != 0) { set_status("Need exactly 12 complete words"); return false; } memset(joined, 0, sizeof(joined)); if (!join_committed_words(joined, sizeof(joined))) { set_status("Mnemonic buffer error"); secure_memzero(joined, sizeof(joined)); return false; } if (mnemonic_validate(joined) == 0) { if (s_checksum_panel != NULL) { lv_obj_del(s_checksum_panel); s_checksum_panel = NULL; } set_status("Checksum valid"); memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic)); strncpy(s_selected_mnemonic, joined, sizeof(s_selected_mnemonic) - 1); s_have_mnemonic = true; s_done = true; secure_memzero(joined, sizeof(joined)); return true; } if (s_checksum_panel != NULL) { lv_obj_del(s_checksum_panel); s_checksum_panel = NULL; } s_checksum_panel = lv_obj_create(lv_scr_act()); lv_obj_set_size(s_checksum_panel, DISPLAY_WIDTH - 20, 82); lv_obj_align(s_checksum_panel, LV_ALIGN_TOP_MID, 0, 78); lv_obj_set_style_bg_color(s_checksum_panel, lv_color_hex(0x000000), 0); lv_obj_set_style_border_color(s_checksum_panel, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_border_width(s_checksum_panel, 2, 0); lv_obj_set_style_radius(s_checksum_panel, 6, 0); lv_obj_set_style_pad_all(s_checksum_panel, 6, 0); lv_obj_t *txt = lv_label_create(s_checksum_panel); lv_label_set_text(txt, "Checksum invalid. Use anyway?"); lv_obj_set_style_text_color(txt, lv_color_hex(0xFF0000), 0); lv_obj_align(txt, LV_ALIGN_TOP_MID, 0, 0); lv_obj_t *use_btn = lv_btn_create(s_checksum_panel); lv_obj_set_size(use_btn, 128, 30); lv_obj_align(use_btn, LV_ALIGN_BOTTOM_LEFT, 4, -2); style_button(use_btn); lv_obj_set_style_border_color(use_btn, lv_color_hex(0xFF0000), 0); lv_obj_add_event_cb(use_btn, on_checksum_use_anyway, LV_EVENT_RELEASED, NULL); lv_obj_t *use_lbl = lv_label_create(use_btn); lv_label_set_text(use_lbl, "Use Anyway"); lv_obj_center(use_lbl); lv_obj_t *edit_btn = lv_btn_create(s_checksum_panel); lv_obj_set_size(edit_btn, 128, 30); lv_obj_align(edit_btn, LV_ALIGN_BOTTOM_RIGHT, -4, -2); style_button(edit_btn); lv_obj_add_event_cb(edit_btn, on_checksum_edit_words, LV_EVENT_RELEASED, NULL); lv_obj_t *edit_lbl = lv_label_create(edit_btn); lv_label_set_text(edit_lbl, "Edit Words"); lv_obj_center(edit_lbl); set_status("Checksum invalid"); secure_memzero(joined, sizeof(joined)); return false; } static void on_back_to_menu(lv_event_t *e) { (void)e; if (s_checksum_panel != NULL) { lv_obj_del(s_checksum_panel); s_checksum_panel = NULL; } clear_entry_state(); build_main_menu(); } static void on_suggestion_tap(lv_event_t *e) { const char *word = (const char *)lv_event_get_user_data(e); if (s_suggestion_row == NULL || word == NULL || s_word_count >= UI_MNEMONIC_WORDS) { return; } commit_word(word); refresh_entry_ui(); if (s_word_count == UI_MNEMONIC_WORDS) { (void)verify_or_prompt_checksum(); } } static void entry_backspace_step(void) { if (s_prefix_len > 0) { s_prefix_len--; s_prefix[s_prefix_len] = '\0'; } else if (s_word_count > 0) { s_word_count--; memset(s_prefix, 0, sizeof(s_prefix)); strncpy(s_prefix, s_committed_words[s_word_count], sizeof(s_prefix) - 1); s_prefix_len = strlen(s_prefix); secure_memzero(s_committed_words[s_word_count], sizeof(s_committed_words[s_word_count])); } s_checksum_override = false; if (s_checksum_panel != NULL) { lv_obj_del(s_checksum_panel); s_checksum_panel = NULL; } refresh_entry_ui(); } static void on_key_matrix(lv_event_t *e) { lv_obj_t *obj = lv_event_get_target(e); if (s_key_matrix == NULL || obj != s_key_matrix) { return; } uint16_t idx = lv_btnmatrix_get_selected_btn(obj); const char *txt = lv_btnmatrix_get_btn_text(obj, idx); if (txt == NULL || txt[0] == '\0' || txt[1] != '\0') { return; } if (txt[0] == '<') { entry_backspace_step(); return; } if (s_word_count >= UI_MNEMONIC_WORDS) { set_status("Already have 12 words"); return; } if (s_prefix_len + 1 >= sizeof(s_prefix)) { set_status("Word too long"); return; } s_prefix[s_prefix_len++] = txt[0]; s_prefix[s_prefix_len] = '\0'; s_checksum_override = false; refresh_entry_ui(); if (s_match_total == 1) { const char *word = mnemonic_word_at(s_match_indices[0]); if (word != NULL) { commit_word(word); refresh_entry_ui(); if (s_word_count == UI_MNEMONIC_WORDS) { (void)verify_or_prompt_checksum(); } } } } static void refresh_entry_ui(void) { size_t shown = 0; char words_line[256]; char prefix_line[64]; if (s_checksum_panel != NULL) { lv_obj_del(s_checksum_panel); s_checksum_panel = NULL; } memset(words_line, 0, sizeof(words_line)); (void)join_committed_words(words_line, sizeof(words_line)); if (s_entry_words_label != NULL) { if (s_word_count == 0) { lv_label_set_text(s_entry_words_label, "(no words selected)"); } else { lv_label_set_text(s_entry_words_label, words_line); } } snprintf(prefix_line, sizeof(prefix_line), "Word %u/12 prefix: %s", (unsigned)(s_word_count + 1), (s_prefix_len > 0) ? s_prefix : "-"); if (s_entry_prefix_label != NULL) { lv_label_set_text(s_entry_prefix_label, prefix_line); } if (s_suggestion_row != NULL) { lv_obj_clean(s_suggestion_row); if (s_prefix_len > 0 && s_word_count < UI_MNEMONIC_WORDS) { s_match_total = mnemonic_words_with_prefix(s_prefix, s_match_indices, UI_SUGGESTION_STORE_MAX); shown = (s_match_total > UI_SUGGESTION_VISIBLE_MAX) ? UI_SUGGESTION_VISIBLE_MAX : s_match_total; for (size_t i = 0; i < shown; ++i) { const char *word = mnemonic_word_at(s_match_indices[i]); lv_obj_t *btn; lv_obj_t *lbl; if (word == NULL) { continue; } btn = lv_btn_create(s_suggestion_row); lv_obj_set_size(btn, 84, 26); style_button(btn); lv_obj_add_event_cb(btn, on_suggestion_tap, LV_EVENT_RELEASED, (void *)word); lbl = lv_label_create(btn); lv_label_set_text(lbl, word); lv_obj_center(lbl); } if (s_match_total > shown) { char more[24]; lv_obj_t *m = lv_label_create(s_suggestion_row); snprintf(more, sizeof(more), "+%u more", (unsigned)(s_match_total - shown)); lv_label_set_text(m, more); lv_obj_set_style_text_color(m, lv_color_hex(0xFFFFFF), 0); } if (s_match_total == 0) { set_status("No matches"); } else { set_status(""); } } else { s_match_total = 0; } } if (s_key_matrix != NULL) { const uint16_t matrix_btn_count = 27; for (uint16_t i = 0; i < matrix_btn_count; ++i) { const char *txt = lv_btnmatrix_get_btn_text(s_key_matrix, i); bool enabled = false; if (txt == NULL || txt[0] == '\0' || txt[1] != '\0') { continue; } if (txt[0] == '<') { enabled = (s_prefix_len > 0 || s_word_count > 0); } else if (s_word_count >= UI_MNEMONIC_WORDS) { enabled = false; } else if (s_prefix_len == 0) { size_t start = 0; enabled = (mnemonic_words_for_letter(txt[0], &start) > 0); } else { enabled = mnemonic_letter_extends_prefix(s_prefix, s_prefix_len, txt[0]); } if (enabled) { lv_btnmatrix_clear_btn_ctrl(s_key_matrix, i, LV_BTNMATRIX_CTRL_DISABLED); } else { lv_btnmatrix_set_btn_ctrl(s_key_matrix, i, LV_BTNMATRIX_CTRL_DISABLED); } } } } static void build_entry_screen(void) { lv_obj_t *scr = lv_scr_act(); lv_obj_t *title; lv_obj_t *close_btn; lv_obj_t *close_lbl; reset_screen_widget_refs(); lv_obj_clean(scr); style_screen(scr); clear_entry_state(); title = lv_label_create(scr); lv_label_set_text(title, "Enter Mnemonic"); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 4); s_entry_words_label = lv_label_create(scr); lv_obj_set_width(s_entry_words_label, DISPLAY_WIDTH - 16); lv_label_set_long_mode(s_entry_words_label, LV_LABEL_LONG_WRAP); lv_label_set_text(s_entry_words_label, "(no words selected)"); lv_obj_set_style_text_color(s_entry_words_label, lv_color_hex(0xFFFFFF), 0); lv_obj_align(s_entry_words_label, LV_ALIGN_TOP_LEFT, 8, 26); s_entry_prefix_label = lv_label_create(scr); lv_label_set_text(s_entry_prefix_label, "Word 1/12 prefix: -"); lv_obj_set_style_text_color(s_entry_prefix_label, lv_color_hex(0xFFFFFF), 0); lv_obj_align(s_entry_prefix_label, LV_ALIGN_TOP_LEFT, 8, 66); s_status_label = lv_label_create(scr); lv_label_set_text(s_status_label, ""); lv_obj_set_style_text_color(s_status_label, lv_color_hex(0xFF0000), 0); lv_obj_align(s_status_label, LV_ALIGN_TOP_LEFT, 8, 82); s_suggestion_row = lv_obj_create(scr); lv_obj_set_size(s_suggestion_row, DISPLAY_WIDTH - 12, 30); lv_obj_align(s_suggestion_row, LV_ALIGN_TOP_MID, 0, 96); lv_obj_set_style_bg_color(s_suggestion_row, lv_color_hex(0x000000), 0); lv_obj_set_style_border_color(s_suggestion_row, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_border_width(s_suggestion_row, 1, 0); lv_obj_set_style_pad_all(s_suggestion_row, 2, 0); lv_obj_set_scroll_dir(s_suggestion_row, LV_DIR_HOR); lv_obj_set_flex_flow(s_suggestion_row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(s_suggestion_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); close_btn = lv_btn_create(scr); lv_obj_set_size(close_btn, 42, 30); lv_obj_align(close_btn, LV_ALIGN_TOP_RIGHT, -4, 2); style_button(close_btn); lv_obj_add_event_cb(close_btn, on_back_to_menu, LV_EVENT_CLICKED, NULL); close_lbl = lv_label_create(close_btn); lv_label_set_text(close_lbl, "X"); lv_obj_center(close_lbl); s_key_matrix = lv_btnmatrix_create(scr); lv_obj_set_size(s_key_matrix, DISPLAY_WIDTH - 16, 100); lv_obj_align(s_key_matrix, LV_ALIGN_TOP_MID, 0, 132); lv_btnmatrix_set_map(s_key_matrix, s_key_map); lv_obj_add_event_cb(s_key_matrix, on_key_matrix, LV_EVENT_VALUE_CHANGED, NULL); style_key_matrix(s_key_matrix); lv_obj_set_style_pad_all(s_key_matrix, 0, 0); lv_obj_set_style_pad_gap(s_key_matrix, 0, LV_PART_ITEMS); lv_obj_set_style_border_width(s_key_matrix, 1, LV_PART_ITEMS); refresh_entry_ui(); } static void on_enter(lv_event_t *e) { (void)e; build_entry_screen(); } static void on_approve_deny(lv_event_t *e) { (void)e; s_approval_decision = UI_APPROVAL_DENY; } static void on_approve_once(lv_event_t *e) { (void)e; s_approval_decision = UI_APPROVAL_ONCE; } static void on_approve_always(lv_event_t *e) { (void)e; s_approval_decision = UI_APPROVAL_ALWAYS; } static void on_use_viewed(lv_event_t *e) { (void)e; if (!s_have_mnemonic) { set_status("No generated mnemonic yet"); return; } s_done = true; } static void build_view_screen(void) { lv_obj_t *scr = lv_scr_act(); reset_screen_widget_refs(); lv_obj_clean(scr); style_screen(scr); lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "Mnemonic Preview"); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 6); lv_obj_t *mn = lv_label_create(scr); lv_obj_set_width(mn, DISPLAY_WIDTH - 20); lv_label_set_long_mode(mn, LV_LABEL_LONG_WRAP); lv_label_set_text(mn, s_have_mnemonic ? s_selected_mnemonic : "No mnemonic generated yet"); lv_obj_align(mn, LV_ALIGN_TOP_LEFT, 10, 40); lv_obj_set_style_text_color(mn, lv_color_hex(0xFFFFFF), 0); lv_obj_t *use_btn = lv_btn_create(scr); lv_obj_set_size(use_btn, 150, 36); lv_obj_align(use_btn, LV_ALIGN_BOTTOM_LEFT, 8, -8); lv_obj_add_event_cb(use_btn, on_use_viewed, LV_EVENT_RELEASED, NULL); style_button(use_btn); lv_obj_t *use_lbl = lv_label_create(use_btn); lv_label_set_text(use_lbl, "Use This"); lv_obj_center(use_lbl); lv_obj_t *back_btn = lv_btn_create(scr); lv_obj_set_size(back_btn, 150, 36); lv_obj_align(back_btn, LV_ALIGN_BOTTOM_RIGHT, -8, -8); lv_obj_add_event_cb(back_btn, on_back_to_menu, LV_EVENT_RELEASED, NULL); style_button(back_btn); lv_obj_t *back_lbl = lv_label_create(back_btn); lv_label_set_text(back_lbl, "Back"); lv_obj_center(back_lbl); s_status_label = NULL; } static void on_view(lv_event_t *e) { (void)e; if (!s_have_mnemonic) { memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic)); if (generate_mnemonic_12(s_selected_mnemonic, sizeof(s_selected_mnemonic)) == 0) { s_have_mnemonic = true; } } build_view_screen(); } static void build_main_menu(void) { lv_obj_t *scr = lv_scr_act(); reset_screen_widget_refs(); lv_obj_clean(scr); style_screen(scr); lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "n_signer CYD"); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 8); lv_obj_t *subtitle = lv_label_create(scr); lv_label_set_text(subtitle, "Select startup mode"); lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 36); lv_obj_set_style_text_color(subtitle, lv_color_hex(0xFFFFFF), 0); lv_obj_t *btn_gen = lv_btn_create(scr); lv_obj_set_size(btn_gen, DISPLAY_WIDTH - 40, 42); lv_obj_align(btn_gen, LV_ALIGN_TOP_MID, 0, 64); lv_obj_add_event_cb(btn_gen, on_generate, LV_EVENT_RELEASED, NULL); style_button(btn_gen); lv_obj_t *lbl_gen = lv_label_create(btn_gen); lv_label_set_text(lbl_gen, "Generate New Mnemonic"); lv_obj_center(lbl_gen); lv_obj_t *btn_enter = lv_btn_create(scr); lv_obj_set_size(btn_enter, DISPLAY_WIDTH - 40, 42); lv_obj_align(btn_enter, LV_ALIGN_TOP_MID, 0, 112); lv_obj_add_event_cb(btn_enter, on_enter, LV_EVENT_RELEASED, NULL); style_button(btn_enter); lv_obj_t *lbl_enter = lv_label_create(btn_enter); lv_label_set_text(lbl_enter, "Enter Existing Mnemonic"); lv_obj_center(lbl_enter); lv_obj_t *btn_view = lv_btn_create(scr); lv_obj_set_size(btn_view, DISPLAY_WIDTH - 40, 42); lv_obj_align(btn_view, LV_ALIGN_TOP_MID, 0, 160); lv_obj_add_event_cb(btn_view, on_view, LV_EVENT_RELEASED, NULL); style_button(btn_view); lv_obj_t *lbl_view = lv_label_create(btn_view); lv_label_set_text(lbl_view, "View Current Mnemonic"); lv_obj_center(lbl_view); s_status_label = lv_label_create(scr); lv_label_set_text(s_status_label, ""); lv_obj_set_style_text_color(s_status_label, lv_color_hex(0xFF0000), 0); lv_obj_align(s_status_label, LV_ALIGN_BOTTOM_MID, 0, -10); } void ui_show_approval_prompt(const char *caller_pubkey_hex, int kind, const char *content, uint32_t timeout_ms) { if (!s_lvgl_inited) { return; } lv_obj_t *scr = lv_scr_act(); reset_screen_widget_refs(); lv_obj_clean(scr); style_screen(scr); char caller_line[64]; char kind_line[32]; char content_line[96]; if (caller_pubkey_hex != NULL && strlen(caller_pubkey_hex) >= 12) { snprintf(caller_line, sizeof(caller_line), "from %.6s..%.4s", caller_pubkey_hex, caller_pubkey_hex + strlen(caller_pubkey_hex) - 4); } else { snprintf(caller_line, sizeof(caller_line), "from %s", caller_pubkey_hex ? caller_pubkey_hex : "anon"); } snprintf(kind_line, sizeof(kind_line), "kind: %d", kind); snprintf(content_line, sizeof(content_line), "%.80s", content ? content : ""); lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "Approve sign_event?"); lv_obj_set_style_text_color(title, lv_color_hex(0xFF0000), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 8); lv_obj_t *caller_lbl = lv_label_create(scr); lv_label_set_text(caller_lbl, caller_line); lv_obj_set_style_text_color(caller_lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_align(caller_lbl, LV_ALIGN_TOP_LEFT, 8, 34); lv_obj_t *kind_lbl = lv_label_create(scr); lv_label_set_text(kind_lbl, kind_line); lv_obj_set_style_text_color(kind_lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_align(kind_lbl, LV_ALIGN_TOP_LEFT, 8, 52); lv_obj_t *content_lbl = lv_label_create(scr); lv_obj_set_width(content_lbl, DISPLAY_WIDTH - 16); lv_label_set_long_mode(content_lbl, LV_LABEL_LONG_WRAP); lv_label_set_text(content_lbl, content_line); lv_obj_set_style_text_color(content_lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_align(content_lbl, LV_ALIGN_TOP_LEFT, 8, 72); lv_obj_t *deny_btn = lv_btn_create(scr); lv_obj_set_size(deny_btn, 98, 34); lv_obj_align(deny_btn, LV_ALIGN_BOTTOM_LEFT, 8, -8); lv_obj_add_event_cb(deny_btn, on_approve_deny, LV_EVENT_RELEASED, NULL); style_button(deny_btn); lv_obj_t *deny_lbl = lv_label_create(deny_btn); lv_label_set_text(deny_lbl, "Deny"); lv_obj_center(deny_lbl); lv_obj_t *once_btn = lv_btn_create(scr); lv_obj_set_size(once_btn, 98, 34); lv_obj_align(once_btn, LV_ALIGN_BOTTOM_MID, 0, -8); lv_obj_add_event_cb(once_btn, on_approve_once, LV_EVENT_RELEASED, NULL); style_button(once_btn); lv_obj_t *once_lbl = lv_label_create(once_btn); lv_label_set_text(once_lbl, "Approve"); lv_obj_center(once_lbl); lv_obj_t *always_btn = lv_btn_create(scr); lv_obj_set_size(always_btn, 98, 34); lv_obj_align(always_btn, LV_ALIGN_BOTTOM_RIGHT, -8, -8); lv_obj_add_event_cb(always_btn, on_approve_always, LV_EVENT_RELEASED, NULL); style_button(always_btn); lv_obj_t *always_lbl = lv_label_create(always_btn); lv_label_set_text(always_lbl, "Always"); lv_obj_center(always_lbl); s_approval_countdown = lv_label_create(scr); lv_obj_set_style_text_color(s_approval_countdown, lv_color_hex(0xFF0000), 0); lv_obj_align(s_approval_countdown, LV_ALIGN_BOTTOM_MID, 0, -46); s_approval_decision = UI_APPROVAL_TIMEOUT; s_approval_deadline_us = esp_timer_get_time() + ((int64_t)timeout_ms * 1000); } ui_approval_decision_t ui_wait_for_approval(uint32_t timeout_ms) { if (!s_lvgl_inited) { return UI_APPROVAL_TIMEOUT; } int64_t deadline_us = esp_timer_get_time() + ((int64_t)timeout_ms * 1000); while (1) { int64_t now = esp_timer_get_time(); if (s_approval_decision != UI_APPROVAL_TIMEOUT) { return s_approval_decision; } if (now >= deadline_us) { return UI_APPROVAL_TIMEOUT; } if (s_approval_countdown != NULL) { int remain_s = (int)((deadline_us - now + 999999) / 1000000); if (remain_s < 0) { remain_s = 0; } char buf[32]; snprintf(buf, sizeof(buf), "timeout: %ds", remain_s); lv_label_set_text(s_approval_countdown, buf); } lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } } static void show_event_log_screen(void) { lv_obj_t *scr = lv_scr_act(); reset_screen_widget_refs(); lv_obj_clean(scr); style_screen(scr); lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "recent events"); lv_obj_set_style_text_color(title, lv_color_hex(0xFF0000), 0); lv_obj_align(title, LV_ALIGN_TOP_LEFT, 8, 6); lv_obj_t *sub = lv_label_create(scr); lv_label_set_text(sub, "newest first"); lv_obj_set_style_text_color(sub, lv_color_hex(0xFFFFFF), 0); lv_obj_align(sub, LV_ALIGN_TOP_RIGHT, -8, 8); int y = 30; for (int i = 0; i < s_event_count; ++i) { lv_obj_t *line = lv_label_create(scr); lv_obj_set_width(line, DISPLAY_WIDTH - 16); lv_label_set_long_mode(line, LV_LABEL_LONG_DOT); lv_label_set_text(line, s_event_lines[i]); lv_obj_set_style_text_color(line, lv_color_hex(0xFFFFFF), 0); lv_obj_align(line, LV_ALIGN_TOP_LEFT, 8, y); y += 24; } if (s_event_count == 0) { lv_obj_t *empty = lv_label_create(scr); lv_label_set_text(empty, "No events yet"); lv_obj_set_style_text_color(empty, lv_color_hex(0xFFFFFF), 0); lv_obj_align(empty, LV_ALIGN_TOP_LEFT, 8, 36); } lv_obj_t *ready = lv_label_create(scr); lv_label_set_text(ready, "ready"); lv_obj_set_style_text_color(ready, lv_color_hex(0xFF0000), 0); lv_obj_align(ready, LV_ALIGN_BOTTOM_RIGHT, -8, -8); } void ui_show_idle(const char *npub_full, const char *mnemonic_full, const char *version_text) { if (!s_lvgl_inited) { return; } lv_obj_t *scr = lv_scr_act(); reset_screen_widget_refs(); lv_obj_clean(scr); style_screen(scr); lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "nostr npub"); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_align(title, LV_ALIGN_TOP_LEFT, 8, 6); lv_obj_t *npub = lv_label_create(scr); lv_obj_set_width(npub, DISPLAY_WIDTH - 16); lv_label_set_long_mode(npub, LV_LABEL_LONG_WRAP); lv_label_set_text(npub, npub_full ? npub_full : ""); lv_obj_set_style_text_color(npub, lv_color_hex(0xFFFFFF), 0); lv_obj_align(npub, LV_ALIGN_TOP_LEFT, 8, 24); lv_obj_t *mn_title = lv_label_create(scr); lv_label_set_text(mn_title, "mnemonic"); lv_obj_set_style_text_color(mn_title, lv_color_hex(0xFFFFFF), 0); lv_obj_align_to(mn_title, npub, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8); lv_obj_t *mn = lv_label_create(scr); lv_obj_set_width(mn, DISPLAY_WIDTH - 16); lv_label_set_long_mode(mn, LV_LABEL_LONG_WRAP); lv_label_set_text(mn, mnemonic_full ? mnemonic_full : ""); lv_obj_set_style_text_color(mn, lv_color_hex(0xFFFFFF), 0); lv_obj_align_to(mn, mn_title, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 4); lv_obj_t *ver = lv_label_create(scr); lv_label_set_text(ver, version_text ? version_text : ""); lv_obj_set_style_text_color(ver, lv_color_hex(0xFFFFFF), 0); lv_obj_align(ver, LV_ALIGN_BOTTOM_LEFT, 8, -8); lv_obj_t *ready = lv_label_create(scr); lv_label_set_text(ready, "ready"); lv_obj_set_style_text_color(ready, lv_color_hex(0xFF0000), 0); lv_obj_align(ready, LV_ALIGN_BOTTOM_RIGHT, -8, -8); } void ui_add_event(const char *action, const char *caller_pubkey_hex, const char *extra) { char caller_short[20] = "anon"; char line[UI_EVENT_LINE_MAX]; if (!s_lvgl_inited) { return; } if (caller_pubkey_hex != NULL && strlen(caller_pubkey_hex) >= 12) { snprintf(caller_short, sizeof(caller_short), "%.6s..%.4s", caller_pubkey_hex, caller_pubkey_hex + strlen(caller_pubkey_hex) - 4); } else if (caller_pubkey_hex != NULL && caller_pubkey_hex[0] != '\0') { snprintf(caller_short, sizeof(caller_short), "%.16s", caller_pubkey_hex); } snprintf(line, sizeof(line), "%s %s %s", action ? action : "event", caller_short, extra ? extra : ""); for (int i = (s_event_count < UI_EVENT_LOG_MAX ? s_event_count : UI_EVENT_LOG_MAX - 1); i > 0; --i) { strncpy(s_event_lines[i], s_event_lines[i - 1], UI_EVENT_LINE_MAX - 1); s_event_lines[i][UI_EVENT_LINE_MAX - 1] = '\0'; } strncpy(s_event_lines[0], line, UI_EVENT_LINE_MAX - 1); s_event_lines[0][UI_EVENT_LINE_MAX - 1] = '\0'; if (s_event_count < UI_EVENT_LOG_MAX) { s_event_count++; } show_event_log_screen(); } void ui_pump(void) { if (!s_lvgl_inited) { return; } lv_timer_handler(); } int ui_init_runtime(void) { return (ui_lvgl_init_once() == ESP_OK) ? 0 : -1; } int ui_run_until_working(char out_mnemonic[256]) { if (out_mnemonic == NULL) { return -1; } if (ui_lvgl_init_once() != ESP_OK) { return -1; } s_done = false; s_have_mnemonic = false; memset(s_selected_mnemonic, 0, sizeof(s_selected_mnemonic)); build_main_menu(); while (!s_done) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } memset(out_mnemonic, 0, 256); strncpy(out_mnemonic, s_selected_mnemonic, 255); ESP_LOGI(TAG, "Mnemonic selected via LVGL UI"); return 0; }