v0.0.13 - continuous TUI: anchored-frame layout, breadcrumbs, responsive rows, destructive confirmations, splash screen, SIGWINCH handling

This commit is contained in:
Laan Tungir
2026-05-05 14:01:16 -04:00
parent b9911dd4a6
commit 918cc618a9
17 changed files with 1036 additions and 328 deletions

View File

@@ -1,18 +1,58 @@
#ifndef TUI_H #ifndef TUI_H
#define TUI_H #define TUI_H
#define TUI_KEY_RESIZE -2
typedef struct {
const char *breadcrumb;
const char *title_suffix;
} tui_view_t;
/* Initialize terminal input mode (non-canonical, no echo). */ /* Initialize terminal input mode (non-canonical, no echo). */
void tui_init(void); void tui_init(void);
/* Restore terminal settings to their original state. */ /* Restore terminal settings to their original state. */
void tui_cleanup(void); void tui_cleanup(void);
/* Install SIGWINCH handler for resize-aware redraw loops. */
void tui_install_resize_handler(void);
/* Consume pending resize flag (returns 1 if a resize was pending). */
int tui_consume_resize_flag(void);
/* Set app title used by anchored frame renderers. */
void tui_set_app_title(const char *title);
/* Clear the screen area by printing terminal-height blank lines. */ /* Clear the screen area by printing terminal-height blank lines. */
void tui_clear_screen(void); void tui_clear_screen(void);
/* Print formatted output and render ^_...^: hotkey highlights. */ /* Print formatted output and render ^_...^: hotkey highlights. */
void tui_print(const char *fmt, ...); void tui_print(const char *fmt, ...);
/* Print full-width horizontal separator using '='. */
void tui_print_hr(void);
/* Print centered text in the current terminal width. */
void tui_print_centered(const char *text);
/* Render anchored top frame (separator/title/separator/breadcrumb + blank lines). */
void tui_render_top_frame(const tui_view_t *view);
/* Column where centered menu block should start. */
int tui_centered_menu_start_col(void);
/* Print one menu line aligned to centered menu start. */
void tui_print_menu_item(const char *fmt, ...);
/* Begin canonical continuous frame and reset line counting. */
void tui_begin_frame(const tui_view_t *view);
/* End frame with anchored prompt near bottom, then read line. */
int tui_end_frame_with_prompt(const char *prompt, char *buf, int bufsize);
/* Render startup splash screen and wait for Enter. */
void tui_show_splash(void);
/* Prompt and read a blocking input line into buf, returns length. */ /* Prompt and read a blocking input line into buf, returns length. */
int tui_get_line(const char *prompt, char *buf, int bufsize); int tui_get_line(const char *prompt, char *buf, int bufsize);

View File

@@ -11,8 +11,8 @@
*/ */
#define NT_VERSION_MAJOR 0 #define NT_VERSION_MAJOR 0
#define NT_VERSION_MINOR 0 #define NT_VERSION_MINOR 0
#define NT_VERSION_PATCH 12 #define NT_VERSION_PATCH 13
#define NT_VERSION "v0.0.12" #define NT_VERSION "v0.0.13"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -34,8 +34,13 @@ void menu_ai(void);
void menu_ecash(void); void menu_ecash(void);
static void menu_show_loaded_events(void) { static void menu_show_loaded_events(void) {
tui_clear_screen(); static const tui_view_t view = {
tui_print("LOADED EVENTS\n"); "> Main Menu > Loaded Events",
NULL,
};
char input[8];
tui_begin_frame(&view);
tui_print("KIND 0 (metadata content):"); tui_print("KIND 0 (metadata content):");
tui_print("%s", g_state.kind0_json ? g_state.kind0_json : "(not loaded)"); tui_print("%s", g_state.kind0_json ? g_state.kind0_json : "(not loaded)");
@@ -61,42 +66,44 @@ static void menu_show_loaded_events(void) {
tui_print("%s", g_state.user_settings_json ? g_state.user_settings_json : "(not loaded)"); tui_print("%s", g_state.user_settings_json ? g_state.user_settings_json : "(not loaded)");
tui_print(""); tui_print("");
tui_print("\nPress Enter to return."); (void)tui_end_frame_with_prompt("Press Enter to return >", input, (int)sizeof(input));
{
char input[8];
tui_get_line(">", input, (int)sizeof(input));
}
} }
void menu_main(void) { void menu_main(void) {
static const tui_view_t main_view = {
"> Main Menu",
NULL,
};
char input[64]; char input[64];
while (1) { while (1) {
tui_clear_screen(); (void)tui_consume_resize_flag();
tui_print("NOSTR TERMINAL %s - %s\n", g_state.version, g_state.user_name); tui_begin_frame(&main_view);
tui_print("User: %s", g_state.user_name);
tui_print("");
if (!g_state.logged_in) { if (!g_state.logged_in) {
tui_print("^_L^:og in"); tui_print_menu_item("^_L^:og in");
tui_print("^_Q^:uit."); tui_print_menu_item("^_Q^:uit");
} else { } else {
tui_print("^_W^:rite"); tui_print_menu_item("^_W^:rite");
tui_print("^_T^:weet"); tui_print_menu_item("^_T^:weet");
tui_print("^_P^:rofile"); tui_print_menu_item("^_P^:rofile");
tui_print("^_R^:elays"); tui_print_menu_item("^_R^:elays");
tui_print("^_F^:ollows"); tui_print_menu_item("^_F^:ollows");
tui_print("^_K^:ind/event dump"); tui_print_menu_item("^_K^:ind/event dump");
tui_print("^_N^:otifications"); tui_print_menu_item("^_N^:otifications");
tui_print("^_B^:logs/posts"); tui_print_menu_item("^_B^:logs/posts");
tui_print("^_L^:ive feeds"); tui_print_menu_item("^_L^:ive feeds");
tui_print("^_M^:essage"); tui_print_menu_item("^_M^:essage");
tui_print("To^_d^:o"); tui_print_menu_item("To^_d^:o");
tui_print("^_J^:ournal"); tui_print_menu_item("^_J^:ournal");
tui_print("^_A^:i"); tui_print_menu_item("^_A^:i");
tui_print("^_E^:cash"); tui_print_menu_item("^_E^:cash");
tui_print("^_Q^:uit."); tui_print_menu_item("^_Q^:uit");
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'q' || input[0] == 'Q' || input[0] == 'x' || input[0] == 'X') { if (input[0] == 'q' || input[0] == 'Q' || input[0] == 'x' || input[0] == 'X') {
tui_print("\nHasta luego.\n"); tui_print("\nHasta luego.\n");
@@ -194,9 +201,15 @@ int main(int argc, char *argv[]) {
} }
tui_init(); tui_init();
tui_install_resize_handler();
tui_set_window_title("Nostr Terminal"); tui_set_window_title("Nostr Terminal");
state_init(); state_init();
snprintf(g_state.version, sizeof(g_state.version), "%s", NT_VERSION); snprintf(g_state.version, sizeof(g_state.version), "%s", NT_VERSION);
{
char app_title[64];
snprintf(app_title, sizeof(app_title), "NOSTR TERMINAL %s", g_state.version);
tui_set_app_title(app_title);
}
if (nostr_init() != 0) { if (nostr_init() != 0) {
fprintf(stderr, "Failed to initialize nostr library.\n"); fprintf(stderr, "Failed to initialize nostr library.\n");
@@ -206,6 +219,7 @@ int main(int argc, char *argv[]) {
return 1; return 1;
} }
tui_show_splash();
menu_main(); menu_main();
nostr_cleanup(); nostr_cleanup();

View File

@@ -492,6 +492,14 @@ static char *ai_extract_response(const char *resp_json) {
} }
static void ai_chat(const ai_prefs_t *prefs) { static void ai_chat(const ai_prefs_t *prefs) {
static const tui_view_t ai_chat_view = {
"> Main Menu > AI > Chat",
NULL,
};
static const tui_view_t ai_response_view = {
"> Main Menu > AI > Chat > Response",
NULL,
};
char prompt[4096]; char prompt[4096];
cJSON *root; cJSON *root;
cJSON *messages; cJSON *messages;
@@ -506,14 +514,17 @@ static void ai_chat(const ai_prefs_t *prefs) {
return; return;
} }
tui_get_line("prompt >", prompt, (int)sizeof(prompt)); tui_begin_frame(&ai_chat_view);
tui_print("Model: %s", prefs->model ? prefs->model : "");
(void)tui_end_frame_with_prompt("prompt >", prompt, (int)sizeof(prompt));
if (prompt[0] == '\0') { if (prompt[0] == '\0') {
return; return;
} }
if (!prefs->api_key || prefs->api_key[0] == '\0') { if (!prefs->api_key || prefs->api_key[0] == '\0') {
tui_begin_frame(&ai_chat_view);
tui_print("AI API key is empty. Configure settings first."); tui_print("AI API key is empty. Configure settings first.");
tui_get_line("Press Enter to continue >", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -547,23 +558,23 @@ static void ai_chat(const ai_prefs_t *prefs) {
free(body); free(body);
if (!resp_json) { if (!resp_json) {
tui_begin_frame(&ai_chat_view);
tui_print("AI request failed."); tui_print("AI request failed.");
tui_get_line("Press Enter to continue >", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
answer = ai_extract_response(resp_json); answer = ai_extract_response(resp_json);
free(resp_json); free(resp_json);
tui_clear_screen(); tui_begin_frame(&ai_response_view);
tui_print("AI RESPONSE\n");
if (answer) { if (answer) {
tui_print("%s", answer); tui_print("%s", answer);
} else { } else {
tui_print("Failed to parse AI response."); tui_print("Failed to parse AI response.");
} }
free(answer); free(answer);
tui_get_line("Press Enter to continue >", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
static void ai_choose_model(ai_prefs_t *prefs) { static void ai_choose_model(ai_prefs_t *prefs) {
@@ -582,6 +593,10 @@ static void ai_choose_model(ai_prefs_t *prefs) {
} }
static void ai_settings(ai_prefs_t *prefs) { static void ai_settings(ai_prefs_t *prefs) {
static const tui_view_t ai_settings_view = {
"> Main Menu > AI > Settings",
NULL,
};
char endpoint[512]; char endpoint[512];
char key[1024]; char key[1024];
char model[256]; char model[256];
@@ -591,36 +606,43 @@ static void ai_settings(ai_prefs_t *prefs) {
return; return;
} }
tui_begin_frame(&ai_settings_view);
tui_print("Leave empty to keep current value."); tui_print("Leave empty to keep current value.");
tui_print("Current endpoint: %s", prefs->endpoint ? prefs->endpoint : ""); tui_print("Current endpoint: %s", prefs->endpoint ? prefs->endpoint : "");
tui_get_line("new endpoint >", endpoint, (int)sizeof(endpoint)); (void)tui_end_frame_with_prompt("new endpoint >", endpoint, (int)sizeof(endpoint));
if (endpoint[0] != '\0') { if (endpoint[0] != '\0') {
ai_set_string(&prefs->endpoint, endpoint); ai_set_string(&prefs->endpoint, endpoint);
} }
tui_begin_frame(&ai_settings_view);
tui_print("Current API key: %s", (prefs->api_key && prefs->api_key[0] != '\0') ? "(set)" : "(empty)"); tui_print("Current API key: %s", (prefs->api_key && prefs->api_key[0] != '\0') ? "(set)" : "(empty)");
tui_get_line("new api key >", key, (int)sizeof(key)); (void)tui_end_frame_with_prompt("new api key >", key, (int)sizeof(key));
if (key[0] != '\0') { if (key[0] != '\0') {
ai_set_string(&prefs->api_key, key); ai_set_string(&prefs->api_key, key);
} }
tui_begin_frame(&ai_settings_view);
tui_print("Current model: %s", prefs->model ? prefs->model : ""); tui_print("Current model: %s", prefs->model ? prefs->model : "");
tui_get_line("new model >", model, (int)sizeof(model)); (void)tui_end_frame_with_prompt("new model >", model, (int)sizeof(model));
if (model[0] != '\0') { if (model[0] != '\0') {
ai_set_string(&prefs->model, model); ai_set_string(&prefs->model, model);
} }
tui_begin_frame(&ai_settings_view);
if (ai_save_prefs(prefs) != 0) { if (ai_save_prefs(prefs) != 0) {
tui_print("Failed to save prefs."); tui_print("Failed to save prefs.");
} else { } else {
tui_print("Prefs saved."); tui_print("Prefs saved.");
} }
tui_get_line("Press Enter to continue >", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
void menu_ai(void) { void menu_ai(void) {
static const tui_view_t ai_view = {
"> Main Menu > AI",
NULL,
};
ai_prefs_t prefs; ai_prefs_t prefs;
char input[512]; char input[512];
@@ -628,16 +650,15 @@ void menu_ai(void) {
(void)ai_load_prefs(&prefs); (void)ai_load_prefs(&prefs);
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&ai_view);
tui_print("AI\n");
tui_print("Model: %s", prefs.model ? prefs.model : ""); tui_print("Model: %s", prefs.model ? prefs.model : "");
tui_print("Endpoint: %s", prefs.endpoint ? prefs.endpoint : ""); tui_print("Endpoint: %s", prefs.endpoint ? prefs.endpoint : "");
tui_print("^_C^:hat"); tui_print_menu_item("^_C^:hat");
tui_print("^_M^:odel"); tui_print_menu_item("^_M^:odel");
tui_print("^_S^:ettings"); tui_print_menu_item("^_S^:ettings");
tui_print("^_B^:ack"); tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'b' || input[0] == 'B' || if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' || input[0] == 'x' || input[0] == 'X' ||

View File

@@ -250,6 +250,14 @@ static int diary_cmp_desc_date(const void *a, const void *b) {
} }
static void diary_browse_past(void) { static void diary_browse_past(void) {
static const tui_view_t diary_past_view = {
"> Main Menu > Diary > Past Entries",
NULL,
};
static const tui_view_t diary_entry_view = {
"> Main Menu > Diary > Past Entries > View",
NULL,
};
const char **read_relays; const char **read_relays;
int relay_count = 0; int relay_count = 0;
char filter[256]; char filter[256];
@@ -267,8 +275,9 @@ static void diary_browse_past(void) {
g_state.npub_hex); g_state.npub_hex);
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) { if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
tui_begin_frame(&diary_past_view);
tui_print("Failed to query diary entries."); tui_print("Failed to query diary entries.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
@@ -326,17 +335,17 @@ static void diary_browse_past(void) {
} }
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&diary_past_view);
tui_print("PAST DIARY ENTRIES\n");
for (i = 0; i < entries_count; i++) { for (i = 0; i < entries_count; i++) {
tui_print("%2d - %s", i + 1, entries[i].date_d ? entries[i].date_d : "(no d-tag)"); tui_print("%2d - %s", i + 1, entries[i].date_d ? entries[i].date_d : "(no d-tag)");
} }
if (entries_count == 0) { if (entries_count == 0) {
tui_print("No diary entries found."); tui_print("No diary entries found.");
} }
tui_print("\nEnter number to view, x to back."); tui_print("");
tui_print("Enter number to view, x to back.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') { if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
break; break;
} }
@@ -346,10 +355,11 @@ static void diary_browse_past(void) {
if (idx < 0 || idx >= entries_count) { if (idx < 0 || idx >= entries_count) {
continue; continue;
} }
tui_clear_screen(); tui_begin_frame(&diary_entry_view);
tui_print("DIARY %s\n", entries[idx].date_d ? entries[idx].date_d : ""); tui_print("Date: %s", entries[idx].date_d ? entries[idx].date_d : "");
tui_print("");
tui_print("%s", entries[idx].content ? entries[idx].content : ""); tui_print("%s", entries[idx].content ? entries[idx].content : "");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
} }
} }
@@ -357,29 +367,33 @@ static void diary_browse_past(void) {
} }
void menu_diary(void) { void menu_diary(void) {
static const tui_view_t diary_view = {
"> Main Menu > Diary",
NULL,
};
char today_d[16]; char today_d[16];
char *existing = NULL; char *existing = NULL;
char *edited = NULL; char *edited = NULL;
char input[16]; char input[16];
if (diary_get_today_d(today_d) != 0) { if (diary_get_today_d(today_d) != 0) {
tui_begin_frame(&diary_view);
tui_print("Failed to compute today's date."); tui_print("Failed to compute today's date.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
existing = diary_load_entry_plain(today_d); existing = diary_load_entry_plain(today_d);
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&diary_view);
tui_print("DIARY\n");
tui_print("Today: %s", today_d); tui_print("Today: %s", today_d);
tui_print("Existing entry: %s", existing ? "yes" : "no"); tui_print("Existing entry: %s", existing ? "yes" : "no");
tui_print("^_E^:dit today's entry"); tui_print_menu_item("^_E^:dit today's entry");
tui_print("^_B^:rowse past entries"); tui_print_menu_item("^_B^:rowse past entries");
tui_print("^_Q^:uit"); tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'q' || input[0] == 'Q' || if (input[0] == 'q' || input[0] == 'Q' ||
input[0] == 'x' || input[0] == 'X') { input[0] == 'x' || input[0] == 'X') {
@@ -389,8 +403,9 @@ void menu_diary(void) {
if (input[0] == 'e' || input[0] == 'E') { if (input[0] == 'e' || input[0] == 'E') {
edited = editor_launch(existing ? existing : ""); edited = editor_launch(existing ? existing : "");
if (!edited) { if (!edited) {
tui_begin_frame(&diary_view);
tui_print("Edit canceled."); tui_print("Edit canceled.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -399,7 +414,9 @@ void menu_diary(void) {
existing = diary_strdup(edited); existing = diary_strdup(edited);
} }
free(edited); free(edited);
tui_get_line(">", input, (int)sizeof(input)); tui_begin_frame(&diary_view);
tui_print("Diary save attempted.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} else if (input[0] == 'b' || input[0] == 'B') { } else if (input[0] == 'b' || input[0] == 'B') {
diary_browse_past(); diary_browse_past();
} }

View File

@@ -133,6 +133,10 @@ static int dm_parse_recipient_hex(const char *input, char out_hex[65]) {
} }
static void menu_dm_send(void) { static void menu_dm_send(void) {
static const tui_view_t dm_send_view = {
"> Main Menu > DM > Send",
NULL,
};
char who[256]; char who[256];
char msg[4096]; char msg[4096];
char recip_hex[65] = {0}; char recip_hex[65] = {0};
@@ -147,28 +151,28 @@ static void menu_dm_send(void) {
int any_ok = 0; int any_ok = 0;
char hold[16]; char hold[16];
tui_clear_screen(); tui_begin_frame(&dm_send_view);
tui_print("SEND DM\n"); (void)tui_end_frame_with_prompt("recipient (npub or hex) >", who, (int)sizeof(who));
tui_get_line("recipient (npub or hex) >", who, (int)sizeof(who));
if (who[0] == '\0') { if (who[0] == '\0') {
return; return;
} }
if (dm_parse_recipient_hex(who, recip_hex) != 0) { if (dm_parse_recipient_hex(who, recip_hex) != 0) {
tui_begin_frame(&dm_send_view);
tui_print("Invalid recipient. Use npub or 64-char hex pubkey."); tui_print("Invalid recipient. Use npub or 64-char hex pubkey.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
tui_get_line("message >", msg, (int)sizeof(msg)); (void)tui_end_frame_with_prompt("message >", msg, (int)sizeof(msg));
if (msg[0] == '\0') { if (msg[0] == '\0') {
return; return;
} }
if (signer_get_local_private_key(priv) != 0) { if (signer_get_local_private_key(priv) != 0) {
tui_begin_frame(&dm_send_view);
tui_print("Private key unavailable for DM send."); tui_print("Private key unavailable for DM send.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -181,20 +185,23 @@ static void menu_dm_send(void) {
NULL, NULL,
g_state.npub_hex); g_state.npub_hex);
if (!rumor) { if (!rumor) {
tui_begin_frame(&dm_send_view);
tui_print("Failed to build NIP-17 DM rumor."); tui_print("Failed to build NIP-17 DM rumor.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
wrap_count = nostr_nip17_send_dm(rumor, recips, 1, priv, gift_wraps, 8, 0); wrap_count = nostr_nip17_send_dm(rumor, recips, 1, priv, gift_wraps, 8, 0);
cJSON_Delete(rumor); cJSON_Delete(rumor);
if (wrap_count <= 0) { if (wrap_count <= 0) {
tui_begin_frame(&dm_send_view);
tui_print("Failed to create NIP-17 gift wrap event(s)."); tui_print("Failed to create NIP-17 gift wrap event(s).");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
write_relays = state_get_write_relays(&relay_count); write_relays = state_get_write_relays(&relay_count);
tui_begin_frame(&dm_send_view);
tui_print("Publishing %d wrapped DM event(s) to %d relay(s)...", wrap_count, relay_count); tui_print("Publishing %d wrapped DM event(s) to %d relay(s)...", wrap_count, relay_count);
for (i = 0; i < wrap_count; i++) { for (i = 0; i < wrap_count; i++) {
@@ -225,7 +232,7 @@ static void menu_dm_send(void) {
tui_print("DM publish failed."); tui_print("DM publish failed.");
} }
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
static char *dm_try_decrypt_kind4(const char *sender_pub_hex, const char *ciphertext) { static char *dm_try_decrypt_kind4(const char *sender_pub_hex, const char *ciphertext) {
@@ -397,13 +404,16 @@ static void dm_collect_nip17_kind1059(dm_item_t **items, int *items_count) {
} }
static void menu_dm_read_inbox(void) { static void menu_dm_read_inbox(void) {
static const tui_view_t dm_inbox_view = {
"> Main Menu > DM > Inbox",
NULL,
};
dm_item_t *items = NULL; dm_item_t *items = NULL;
int items_count = 0; int items_count = 0;
int i; int i;
char hold[16]; char hold[16];
tui_clear_screen(); tui_begin_frame(&dm_inbox_view);
tui_print("READ INBOX\n");
tui_print("Querying kind 1059 + kind 4..."); tui_print("Querying kind 1059 + kind 4...");
dm_collect_nip17_kind1059(&items, &items_count); dm_collect_nip17_kind1059(&items, &items_count);
@@ -413,8 +423,7 @@ static void menu_dm_read_inbox(void) {
qsort(items, (size_t)items_count, sizeof(dm_item_t), dm_cmp_desc_created); qsort(items, (size_t)items_count, sizeof(dm_item_t), dm_cmp_desc_created);
} }
tui_clear_screen(); tui_begin_frame(&dm_inbox_view);
tui_print("INBOX\n");
for (i = 0; i < items_count; i++) { for (i = 0; i < items_count; i++) {
char sender_short[32]; char sender_short[32];
@@ -427,20 +436,23 @@ static void menu_dm_read_inbox(void) {
} }
dm_free_items(items, items_count); dm_free_items(items, items_count);
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to exit >", hold, (int)sizeof(hold));
} }
void menu_dm(void) { void menu_dm(void) {
static const tui_view_t dm_view = {
"> Main Menu > DM",
NULL,
};
char input[16]; char input[16];
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&dm_view);
tui_print("DIRECT MESSAGE\n"); tui_print_menu_item("^_S^:end DM (NIP-17)");
tui_print("^_S^:end DM (NIP-17)"); tui_print_menu_item("^_R^:ead inbox (kind 1059 + kind 4)");
tui_print("^_R^:ead inbox (kind 1059 + kind 4)"); tui_print_menu_item("^_X^:Exit");
tui_print("^_B^:ack");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'b' || input[0] == 'B' || if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' || input[0] == 'x' || input[0] == 'X' ||

View File

@@ -406,12 +406,30 @@ static uint64_t ecash_wallet_total(const ecash_wallet_t *w) {
return total; return total;
} }
static void ecash_print_mint_row_responsive(int term_width, int index, const char *mint_url, uint64_t mint_total) {
if (term_width < 90) {
tui_print("%2d. %s", index, mint_url ? mint_url : "");
tui_print(" %llu sats", (unsigned long long)mint_total);
return;
}
tui_print("%2d. %-48s %12llu sats",
index,
mint_url ? mint_url : "",
(unsigned long long)mint_total);
}
static void ecash_show_balance(const ecash_wallet_t *w) { static void ecash_show_balance(const ecash_wallet_t *w) {
static const tui_view_t ecash_balance_view = {
"> Main Menu > Ecash > Balance",
NULL,
};
int i; int i;
int term_width = 0;
char hold[16]; char hold[16];
tui_clear_screen(); tui_begin_frame(&ecash_balance_view);
tui_print("ECASH BALANCE\n"); tui_get_terminal_size(&term_width, NULL);
tui_print("Total: %llu sats", (unsigned long long)ecash_wallet_total(w)); tui_print("Total: %llu sats", (unsigned long long)ecash_wallet_total(w));
tui_print(""); tui_print("");
@@ -425,30 +443,36 @@ static void ecash_show_balance(const ecash_wallet_t *w) {
} }
} }
tui_print("%2d. %s", i + 1, w->mints[i]); ecash_print_mint_row_responsive(term_width, i + 1, w->mints[i], mint_total);
tui_print(" balance: %llu sats", (unsigned long long)mint_total);
} }
if (w->mint_count == 0) { if (w->mint_count == 0) {
tui_print("No mints configured."); tui_print("No mints configured.");
} }
tui_print("\nProof count: %d", w->proof_count); tui_print("");
tui_get_line(">", hold, (int)sizeof(hold)); tui_print("Proof count: %d", w->proof_count);
(void)tui_end_frame_with_prompt("Press Enter to exit >", hold, (int)sizeof(hold));
} }
static void ecash_add_mint_menu(ecash_wallet_t *w) { static void ecash_add_mint_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_add_mint_view = {
"> Main Menu > Ecash > Add Mint",
NULL,
};
char mint[512]; char mint[512];
char hold[16]; char hold[16];
tui_get_line("mint url >", mint, (int)sizeof(mint)); tui_begin_frame(&ecash_add_mint_view);
(void)tui_end_frame_with_prompt("mint url >", mint, (int)sizeof(mint));
if (mint[0] == '\0') { if (mint[0] == '\0') {
return; return;
} }
tui_begin_frame(&ecash_add_mint_view);
if (ecash_add_mint(w, mint) != 0) { if (ecash_add_mint(w, mint) != 0) {
tui_print("Failed to add mint."); tui_print("Failed to add mint.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -457,30 +481,48 @@ static void ecash_add_mint_menu(ecash_wallet_t *w) {
} else { } else {
tui_print("Mint added."); tui_print("Mint added.");
} }
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
static void ecash_remove_mint_menu(ecash_wallet_t *w) { static void ecash_remove_mint_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_remove_mint_view = {
"> Main Menu > Ecash > Remove Mint",
NULL,
};
char input[64]; char input[64];
char yn[8];
int idx; int idx;
int i; int i;
char hold[16]; char hold[16];
if (!w || w->mint_count <= 0) { if (!w || w->mint_count <= 0) {
tui_begin_frame(&ecash_remove_mint_view);
tui_print("No mints to remove."); tui_print("No mints to remove.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
tui_begin_frame(&ecash_remove_mint_view);
for (i = 0; i < w->mint_count; i++) { for (i = 0; i < w->mint_count; i++) {
tui_print("%2d. %s", i + 1, w->mints[i]); tui_print("%2d. %s", i + 1, w->mints[i]);
} }
tui_get_line("remove number >", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("remove number >", input, (int)sizeof(input));
idx = atoi(input) - 1; idx = atoi(input) - 1;
if (idx < 0 || idx >= w->mint_count) { if (idx < 0 || idx >= w->mint_count) {
tui_begin_frame(&ecash_remove_mint_view);
tui_print("Invalid mint number."); tui_print("Invalid mint number.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
tui_begin_frame(&ecash_remove_mint_view);
tui_print("Remove mint: %s", w->mints[idx] ? w->mints[idx] : "");
(void)tui_end_frame_with_prompt("Confirm remove [y/n] >", yn, (int)sizeof(yn));
if (!(yn[0] == 'y' || yn[0] == 'Y')) {
tui_begin_frame(&ecash_remove_mint_view);
tui_print("Remove canceled.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -490,15 +532,20 @@ static void ecash_remove_mint_menu(ecash_wallet_t *w) {
} }
w->mint_count--; w->mint_count--;
tui_begin_frame(&ecash_remove_mint_view);
if (ecash_publish_wallet(w) != 0) { if (ecash_publish_wallet(w) != 0) {
tui_print("Failed to publish wallet metadata."); tui_print("Failed to publish wallet metadata.");
} else { } else {
tui_print("Mint removed."); tui_print("Mint removed.");
} }
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
static void ecash_receive_token_menu(ecash_wallet_t *w) { static void ecash_receive_token_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_import_view = {
"> Main Menu > Ecash > Import Token",
NULL,
};
char token[8192]; char token[8192];
cashu_decoded_token_t decoded; cashu_decoded_token_t decoded;
int i; int i;
@@ -509,21 +556,24 @@ static void ecash_receive_token_menu(ecash_wallet_t *w) {
} }
memset(&decoded, 0, sizeof(decoded)); memset(&decoded, 0, sizeof(decoded));
tui_get_line("paste cashu token >", token, (int)sizeof(token)); tui_begin_frame(&ecash_import_view);
(void)tui_end_frame_with_prompt("paste cashu token >", token, (int)sizeof(token));
if (token[0] == '\0') { if (token[0] == '\0') {
return; return;
} }
if (cashu_decode_token(token, &decoded) != 0) { if (cashu_decode_token(token, &decoded) != 0) {
tui_begin_frame(&ecash_import_view);
tui_print("Failed to decode token."); tui_print("Failed to decode token.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
if (!decoded.mint_url || decoded.proof_count <= 0 || !decoded.proofs) { if (!decoded.mint_url || decoded.proof_count <= 0 || !decoded.proofs) {
cashu_free_decoded_token(&decoded); cashu_free_decoded_token(&decoded);
tui_begin_frame(&ecash_import_view);
tui_print("Token did not contain usable proofs."); tui_print("Token did not contain usable proofs.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -534,12 +584,13 @@ static void ecash_receive_token_menu(ecash_wallet_t *w) {
cashu_free_decoded_token(&decoded); cashu_free_decoded_token(&decoded);
tui_begin_frame(&ecash_import_view);
if (ecash_publish_wallet(w) != 0) { if (ecash_publish_wallet(w) != 0) {
tui_print("Token imported locally, but publish failed."); tui_print("Token imported locally, but publish failed.");
} else { } else {
tui_print("Token received and wallet updated."); tui_print("Token received and wallet updated.");
} }
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *indices, int nidx) { static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *indices, int nidx) {
@@ -582,6 +633,14 @@ static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *
} }
static void ecash_send_token_menu(ecash_wallet_t *w) { static void ecash_send_token_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_send_view = {
"> Main Menu > Ecash > Send Token",
NULL,
};
static const tui_view_t ecash_send_result_view = {
"> Main Menu > Ecash > Send Token > Result",
NULL,
};
char input[128]; char input[128];
int mint_idx; int mint_idx;
uint64_t target_amount; uint64_t target_amount;
@@ -602,28 +661,34 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
} }
if (w->mint_count <= 0) { if (w->mint_count <= 0) {
tui_begin_frame(&ecash_send_view);
tui_print("No mints configured."); tui_print("No mints configured.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
tui_begin_frame(&ecash_send_view);
for (i = 0; i < w->mint_count; i++) { for (i = 0; i < w->mint_count; i++) {
tui_print("%2d. %s", i + 1, w->mints[i]); tui_print("%2d. %s", i + 1, w->mints[i]);
} }
tui_get_line("mint number >", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("mint number >", input, (int)sizeof(input));
mint_idx = atoi(input) - 1; mint_idx = atoi(input) - 1;
if (mint_idx < 0 || mint_idx >= w->mint_count) { if (mint_idx < 0 || mint_idx >= w->mint_count) {
tui_begin_frame(&ecash_send_view);
tui_print("Invalid mint number."); tui_print("Invalid mint number.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
tui_get_line("amount (sats) >", input, (int)sizeof(input)); tui_begin_frame(&ecash_send_view);
tui_print("Mint: %s", w->mints[mint_idx]);
(void)tui_end_frame_with_prompt("amount (sats) >", input, (int)sizeof(input));
target_amount = (uint64_t)strtoull(input, NULL, 10); target_amount = (uint64_t)strtoull(input, NULL, 10);
if (target_amount == 0) { if (target_amount == 0) {
tui_begin_frame(&ecash_send_view);
tui_print("Invalid amount."); tui_print("Invalid amount.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -636,8 +701,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (!next_proofs) { if (!next_proofs) {
free(mint_proofs); free(mint_proofs);
free(mint_global_indices); free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error."); tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -647,8 +713,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (!next_idx) { if (!next_idx) {
free(mint_proofs); free(mint_proofs);
free(mint_global_indices); free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error."); tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -662,8 +729,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (mint_proof_count <= 0) { if (mint_proof_count <= 0) {
free(mint_proofs); free(mint_proofs);
free(mint_global_indices); free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("No proofs available for selected mint."); tui_print("No proofs available for selected mint.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -671,8 +739,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (!selected_local) { if (!selected_local) {
free(mint_proofs); free(mint_proofs);
free(mint_global_indices); free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error."); tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -686,8 +755,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(selected_local); free(selected_local);
free(mint_proofs); free(mint_proofs);
free(mint_global_indices); free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Could not select proofs for that amount."); tui_print("Could not select proofs for that amount.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -703,8 +773,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(mint_global_indices); free(mint_global_indices);
free(selected_global); free(selected_global);
cashu_free_decoded_token(&token); cashu_free_decoded_token(&token);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error."); tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -719,8 +790,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(mint_global_indices); free(mint_global_indices);
free(selected_global); free(selected_global);
cashu_free_decoded_token(&token); cashu_free_decoded_token(&token);
tui_begin_frame(&ecash_send_view);
tui_print("Failed to assemble token proofs."); tui_print("Failed to assemble token proofs.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
@@ -734,23 +806,23 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(mint_global_indices); free(mint_global_indices);
free(selected_global); free(selected_global);
cashu_free_decoded_token(&token); cashu_free_decoded_token(&token);
tui_begin_frame(&ecash_send_view);
tui_print("Failed to encode token."); tui_print("Failed to encode token.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return; return;
} }
ecash_remove_proofs_by_global_indices(w, selected_global, selected_count); ecash_remove_proofs_by_global_indices(w, selected_global, selected_count);
(void)ecash_publish_wallet(w); (void)ecash_publish_wallet(w);
tui_clear_screen(); tui_begin_frame(&ecash_send_result_view);
tui_print("SEND TOKEN\n");
tui_print("Requested: %llu sats", (unsigned long long)target_amount); tui_print("Requested: %llu sats", (unsigned long long)target_amount);
tui_print("Selected: %llu sats", (unsigned long long)selected_total); tui_print("Selected: %llu sats", (unsigned long long)selected_total);
tui_print("Token:"); tui_print("Token:");
tui_print("%s", encoded); tui_print("%s", encoded);
tui_print(""); tui_print("");
tui_print("Note: Mint HTTP swap/melt flows are not yet implemented in this MVP."); tui_print("Note: Mint HTTP swap/melt flows are not yet implemented in this MVP.");
tui_get_line(">", hold, (int)sizeof(hold)); (void)tui_end_frame_with_prompt("Press Enter to exit >", hold, (int)sizeof(hold));
free(encoded); free(encoded);
free(selected_local); free(selected_local);
@@ -761,6 +833,10 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
} }
void menu_ecash(void) { void menu_ecash(void) {
static const tui_view_t ecash_view = {
"> Main Menu > Ecash",
NULL,
};
ecash_wallet_t wallet; ecash_wallet_t wallet;
char input[16]; char input[16];
@@ -769,18 +845,17 @@ void menu_ecash(void) {
} }
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&ecash_view);
tui_print("ECASH WALLET\n");
tui_print("Mints: %d", wallet.mint_count); tui_print("Mints: %d", wallet.mint_count);
tui_print("Proofs: %d", wallet.proof_count); tui_print("Proofs: %d", wallet.proof_count);
tui_print("^_B^:alance"); tui_print_menu_item("^_B^:alance");
tui_print("^_A^:dd mint"); tui_print_menu_item("^_A^:dd mint");
tui_print("^_R^:emove mint"); tui_print_menu_item("^_R^:emove mint");
tui_print("^_I^:mport token"); tui_print_menu_item("^_I^:mport token");
tui_print("^_S^:end token"); tui_print_menu_item("^_S^:end token");
tui_print("E^_x^:it"); tui_print_menu_item("E^_x^:it");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') { input[0] == 'q' || input[0] == 'Q') {

View File

@@ -30,7 +30,23 @@ static int follows_publish_kind3(cJSON *event_obj) {
8000); 8000);
} }
static void follows_print_row_responsive(int term_width, int index, const char *npub, const char *name) {
if (term_width < 90) {
tui_print("[%2d] %s", index, npub ? npub : "");
if (name && name[0] != '\0') {
tui_print(" %s", name);
}
return;
}
tui_print("[%2d] %s %s", index, npub ? npub : "", name ? name : "");
}
void menu_follows(void) { void menu_follows(void) {
static const tui_view_t follows_view = {
"> Main Menu > Follows",
NULL,
};
cJSON *working = NULL; cJSON *working = NULL;
cJSON *tags = NULL; cJSON *tags = NULL;
char input[256]; char input[256];
@@ -53,10 +69,10 @@ void menu_follows(void) {
while (1) { while (1) {
int i; int i;
int n; int n;
int term_width = 0;
tui_clear_screen(); tui_begin_frame(&follows_view);
tui_print("FOLLOWS"); tui_get_terminal_size(&term_width, NULL);
tui_print("\n\n");
n = cJSON_GetArraySize(tags); n = cJSON_GetArraySize(tags);
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
@@ -81,20 +97,20 @@ void menu_follows(void) {
} }
} }
tui_print("[%2d] %s %s", follows_print_row_responsive(term_width,
i + 1, i + 1,
npub, npub,
(cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : ""); (cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : "");
} }
tui_print("\n\n"); tui_print("");
tui_print("^_R^:efresh profile"); tui_print_menu_item("^_R^:efresh profile");
tui_print("^_A^:dd follow"); tui_print_menu_item("^_A^:dd follow");
tui_print("^_D^:elete follow"); tui_print_menu_item("^_D^:elete follow");
tui_print("^_P^:ost changes and exit."); tui_print_menu_item("^_P^:ost changes and exit.");
tui_print("E^_x^:it without saving"); tui_print_menu_item("E^_x^:it without saving");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'a' || input[0] == 'A') { if (input[0] == 'a' || input[0] == 'A') {
char who[256]; char who[256];
@@ -107,7 +123,8 @@ void menu_follows(void) {
cJSON *meta; cJSON *meta;
cJSON *name; cJSON *name;
tui_get_line("npub to add >", who, (int)sizeof(who)); tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt("npub to add >", who, (int)sizeof(who));
if (who[0] == '\0') { if (who[0] == '\0') {
continue; continue;
} }
@@ -115,23 +132,26 @@ void menu_follows(void) {
if (strncmp(who, "npub", 4) == 0) { if (strncmp(who, "npub", 4) == 0) {
unsigned char pub[32]; unsigned char pub[32];
if (nostr_decode_npub(who, pub) != 0) { if (nostr_decode_npub(who, pub) != 0) {
tui_begin_frame(&follows_view);
tui_print("Invalid npub."); tui_print("Invalid npub.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
nostr_bytes_to_hex(pub, 32, follow_hex); nostr_bytes_to_hex(pub, 32, follow_hex);
} else if (strlen(who) == 64) { } else if (strlen(who) == 64) {
unsigned char pub[32]; unsigned char pub[32];
if (nostr_hex_to_bytes(who, pub, 32) != 0) { if (nostr_hex_to_bytes(who, pub, 32) != 0) {
tui_begin_frame(&follows_view);
tui_print("Invalid hex pubkey."); tui_print("Invalid hex pubkey.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
strncpy(follow_hex, who, sizeof(follow_hex) - 1U); strncpy(follow_hex, who, sizeof(follow_hex) - 1U);
follow_hex[sizeof(follow_hex) - 1U] = '\0'; follow_hex[sizeof(follow_hex) - 1U] = '\0';
} else { } else {
tui_begin_frame(&follows_view);
tui_print("Enter npub or 64-char hex pubkey."); tui_print("Enter npub or 64-char hex pubkey.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -141,8 +161,9 @@ void menu_follows(void) {
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count, g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
7000, 7000,
&follow_kind0) != 0 || !follow_kind0) { &follow_kind0) != 0 || !follow_kind0) {
tui_begin_frame(&follows_view);
tui_print("Can't find user. Try adding more relays."); tui_print("Can't find user. Try adding more relays.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -159,11 +180,12 @@ void menu_follows(void) {
} }
} }
tui_get_line((cJSON_IsString(name) && name->valuestring) tui_begin_frame(&follows_view);
? "Add follow [y/n] >" (void)tui_end_frame_with_prompt((cJSON_IsString(name) && name->valuestring)
: "Add this follow [y/n] >", ? "Add follow [y/n] >"
yn, : "Add this follow [y/n] >",
(int)sizeof(yn)); yn,
(int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') { if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON *tag = cJSON_CreateArray(); cJSON *tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("p")); cJSON_AddItemToArray(tag, cJSON_CreateString("p"));
@@ -181,13 +203,14 @@ void menu_follows(void) {
char yn[32]; char yn[32];
int idx; int idx;
tui_get_line("# to delete >", numbuf, (int)sizeof(numbuf)); tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt("# to delete >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1; idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) { if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue; continue;
} }
tui_get_line("Delete follow [y/n] >", yn, (int)sizeof(yn)); (void)tui_end_frame_with_prompt("Delete follow [y/n] >", yn, (int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') { if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON_DeleteItemFromArray(tags, idx); cJSON_DeleteItemFromArray(tags, idx);
} }
@@ -202,7 +225,8 @@ void menu_follows(void) {
cJSON *content; cJSON *content;
cJSON *meta; cJSON *meta;
tui_get_line("#>", numbuf, (int)sizeof(numbuf)); tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt("# >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1; idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) { if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue; continue;
@@ -220,8 +244,9 @@ void menu_follows(void) {
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count, g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
7000, 7000,
&follow_kind0) != 0 || !follow_kind0) { &follow_kind0) != 0 || !follow_kind0) {
tui_begin_frame(&follows_view);
tui_print("Unable to fetch profile."); tui_print("Unable to fetch profile.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -234,7 +259,8 @@ void menu_follows(void) {
} }
} }
tui_print("PROFILE"); tui_begin_frame(&follows_view);
tui_print("Profile");
if (meta) { if (meta) {
cJSON *it = NULL; cJSON *it = NULL;
cJSON_ArrayForEach(it, meta) { cJSON_ArrayForEach(it, meta) {
@@ -246,7 +272,7 @@ void menu_follows(void) {
tui_print("No metadata content."); tui_print("No metadata content.");
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
cJSON_Delete(meta); cJSON_Delete(meta);
cJSON_Delete(ev); cJSON_Delete(ev);
free(follow_kind0); free(follow_kind0);
@@ -257,10 +283,14 @@ void menu_follows(void) {
free(g_state.kind3_json); free(g_state.kind3_json);
g_state.kind3_json = new_json; g_state.kind3_json = new_json;
} }
tui_begin_frame(&follows_view);
if (posted < 0) { if (posted < 0) {
tui_print("Follows publish failed."); tui_print("Follows publish failed.");
} else {
tui_print("Follows published.");
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
break; break;
} else if (input[0] == 'x' || input[0] == 'X' || } else if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') { input[0] == 'q' || input[0] == 'Q') {

View File

@@ -218,6 +218,10 @@ static void live_free_authors(char **authors, int count) {
} }
static void menu_live_run_filter(const char *title, const char *filter_json) { static void menu_live_run_filter(const char *title, const char *filter_json) {
static const tui_view_t live_feed_view = {
"> Main Menu > Live Feeds > Stream",
NULL,
};
const char **read_relays; const char **read_relays;
int relay_count = 0; int relay_count = 0;
live_feed_ctx_t ctx; live_feed_ctx_t ctx;
@@ -225,8 +229,8 @@ static void menu_live_run_filter(const char *title, const char *filter_json) {
char input[8]; char input[8];
read_relays = state_get_read_relays(&relay_count); read_relays = state_get_read_relays(&relay_count);
tui_clear_screen(); tui_begin_frame(&live_feed_view);
tui_print("%s\n", title); tui_print("%s", title ? title : "Live Feed");
tui_print("Press any key to stop..."); tui_print("Press any key to stop...");
memset(&ctx, 0, sizeof(ctx)); memset(&ctx, 0, sizeof(ctx));
@@ -238,27 +242,30 @@ static void menu_live_run_filter(const char *title, const char *filter_json) {
live_event_print_cb, live_event_print_cb,
&ctx); &ctx);
tui_print(""); tui_begin_frame(&live_feed_view);
if (events_received < 0) { if (events_received < 0) {
tui_print("Feed stopped (failed to connect to all relays)."); tui_print("Feed stopped (failed to connect to all relays).");
} else { } else {
tui_print("Feed stopped. %d events received.", events_received); tui_print("Feed stopped. %d events received.", events_received);
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} }
void menu_live(void) { void menu_live(void) {
static const tui_view_t live_menu_view = {
"> Main Menu > Live Feeds",
NULL,
};
char input[16]; char input[16];
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&live_menu_view);
tui_print("LIVE FEEDS\n"); tui_print_menu_item("^_F^:ollows feed");
tui_print("^_F^:ollows feed"); tui_print_menu_item("^_M^:entions");
tui_print("^_M^:entions"); tui_print_menu_item("^_G^:lobal firehose");
tui_print("^_G^:lobal firehose"); tui_print_menu_item("^_X^:Exit");
tui_print("^_B^:ack");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'b' || input[0] == 'B' || if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' || input[0] == 'x' || input[0] == 'X' ||
@@ -277,8 +284,9 @@ void menu_live(void) {
int i; int i;
if (live_extract_follow_authors(&authors, &authors_count) != 0 || authors_count <= 0) { if (live_extract_follow_authors(&authors, &authors_count) != 0 || authors_count <= 0) {
tui_begin_frame(&live_menu_view);
tui_print("No follows found in kind3 list."); tui_print("No follows found in kind3 list.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
live_free_authors(authors, authors_count); live_free_authors(authors, authors_count);
continue; continue;
} }
@@ -293,8 +301,9 @@ void menu_live(void) {
cJSON_Delete(authors_json); cJSON_Delete(authors_json);
cJSON_Delete(kinds_json); cJSON_Delete(kinds_json);
live_free_authors(authors, authors_count); live_free_authors(authors, authors_count);
tui_begin_frame(&live_menu_view);
tui_print("Failed to build follows filter."); tui_print("Failed to build follows filter.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }

View File

@@ -94,6 +94,10 @@ static int menu_publish_signed_event(int kind, const char *content, cJSON *tags)
} }
static void menu_add_new_user(void) { static void menu_add_new_user(void) {
static const tui_view_t login_new_profile_view = {
"> Login > New Account > Profile Setup",
NULL,
};
char buf[512]; char buf[512];
char username[128]; char username[128];
cJSON *meta = cJSON_CreateObject(); cJSON *meta = cJSON_CreateObject();
@@ -103,28 +107,35 @@ static void menu_add_new_user(void) {
username[0] = '\0'; username[0] = '\0';
do { do {
tui_get_line("Enter a username for this account (required) >", username, (int)sizeof(username)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter a username for this account (required) >", username, (int)sizeof(username));
} while (username[0] == '\0'); } while (username[0] == '\0');
cJSON_AddStringToObject(meta, "name", username); cJSON_AddStringToObject(meta, "name", username);
cJSON_AddStringToObject(meta, "displayname", username); cJSON_AddStringToObject(meta, "displayname", username);
tui_get_line("Enter \"about\" info: >", buf, (int)sizeof(buf)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter \"about\" info: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "about", buf); cJSON_AddStringToObject(meta, "about", buf);
tui_get_line("Enter profile picture url: >", buf, (int)sizeof(buf)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter profile picture url: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "picture", buf); cJSON_AddStringToObject(meta, "picture", buf);
tui_get_line("Enter banner picture url: >", buf, (int)sizeof(buf)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter banner picture url: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "banner", buf); cJSON_AddStringToObject(meta, "banner", buf);
tui_get_line("Enter website url: >", buf, (int)sizeof(buf)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter website url: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "website", buf); cJSON_AddStringToObject(meta, "website", buf);
tui_get_line("Enter lud16: >", buf, (int)sizeof(buf)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter lud16: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "lud16", buf); cJSON_AddStringToObject(meta, "lud16", buf);
tui_get_line("Enter nip05: >", buf, (int)sizeof(buf)); tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter nip05: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "nip05", buf); cJSON_AddStringToObject(meta, "nip05", buf);
free(g_state.kind0_json); free(g_state.kind0_json);
@@ -153,6 +164,34 @@ static void menu_add_new_user(void) {
} }
void menu_login(void) { void menu_login(void) {
static const tui_view_t login_view = {
"> Login",
NULL,
};
static const tui_view_t login_privkey_view = {
"> Login > Private Key",
NULL,
};
static const tui_view_t login_mnemonic_view = {
"> Login > Mnemonic",
NULL,
};
static const tui_view_t login_signer_local_view = {
"> Login > Signer Local",
NULL,
};
static const tui_view_t login_signer_url_view = {
"> Login > URL Signer",
NULL,
};
static const tui_view_t login_signer_qrexec_view = {
"> Login > Qrexec Signer",
NULL,
};
static const tui_view_t login_new_account_view = {
"> Login > New Account",
NULL,
};
char input[512]; char input[512];
char pending_test_mnemonic[512] = {0}; char pending_test_mnemonic[512] = {0};
int pending_test_mnemonic_ready = 0; int pending_test_mnemonic_ready = 0;
@@ -161,17 +200,17 @@ void menu_login(void) {
unsigned char priv[32]; unsigned char priv[32];
unsigned char pub[32]; unsigned char pub[32];
tui_clear_screen(); (void)tui_consume_resize_flag();
tui_print("LOGIN\n"); tui_begin_frame(&login_view);
tui_print("^_E^:nter test/dev seed fallback"); tui_print_menu_item("^_E^:nter test/dev seed fallback");
tui_print("^_P^:rivate key (nsec or 64-hex)"); tui_print_menu_item("^_P^:rivate key (nsec or 64-hex)");
tui_print("^_M^:nemonic (12 words)"); tui_print_menu_item("^_M^:nemonic (12 words)");
tui_print("^_S^:igner local (same qube)"); tui_print_menu_item("^_S^:igner local (same qube)");
tui_print("^_U^:RL signer (FIPS/web address)"); tui_print_menu_item("^_U^:RL signer (FIPS/web address)");
tui_print("^_N^:ew account"); tui_print_menu_item("^_N^:ew account");
tui_print("^_Q^:uit"); tui_print_menu_item("^_Q^:uit");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (strcmp(input, "q") == 0 || strcmp(input, "x") == 0) { if (strcmp(input, "q") == 0 || strcmp(input, "x") == 0) {
tui_print("\nHasta luego.\n"); tui_print("\nHasta luego.\n");
@@ -182,12 +221,14 @@ void menu_login(void) {
if (input[0] == 'e' || input[0] == 'E') { if (input[0] == 'e' || input[0] == 'E') {
input[0] = '\0'; input[0] = '\0';
} else if (input[0] == 'p' || input[0] == 'P') { } else if (input[0] == 'p' || input[0] == 'P') {
tui_get_line("Private key (nsec or 64-hex) >", input, (int)sizeof(input)); tui_begin_frame(&login_privkey_view);
(void)tui_end_frame_with_prompt("Private key (nsec or 64-hex) >", input, (int)sizeof(input));
if (input[0] == '\0') { if (input[0] == '\0') {
continue; continue;
} }
} else if (input[0] == 'm' || input[0] == 'M') { } else if (input[0] == 'm' || input[0] == 'M') {
tui_get_line("Seed phrase (12 words) >", input, (int)sizeof(input)); tui_begin_frame(&login_mnemonic_view);
(void)tui_end_frame_with_prompt("Seed phrase (12 words) >", input, (int)sizeof(input));
if (input[0] == '\0') { if (input[0] == '\0') {
continue; continue;
} }
@@ -202,11 +243,12 @@ void menu_login(void) {
char idxbuf[32]; char idxbuf[32];
int nostr_index = 0; int nostr_index = 0;
tui_begin_frame(&login_signer_local_view);
tui_print("Searching for running n_signer instances..."); tui_print("Searching for running n_signer instances...");
if (nsigner_list(&names, &name_count) != 0 || name_count == 0) { if (nsigner_list(&names, &name_count) != 0 || name_count == 0) {
tui_print("No running n_signer found. Start `nsigner` and try again."); tui_print("No running n_signer found. Start `nsigner` and try again.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -220,14 +262,15 @@ void menu_login(void) {
for (idx = 0; idx < name_count; idx++) { for (idx = 0; idx < name_count; idx++) {
tui_print(" [%d] @%s", idx, names[idx]); tui_print(" [%d] @%s", idx, names[idx]);
} }
tui_get_line("Select (0) >", pickbuf, (int)sizeof(pickbuf)); (void)tui_end_frame_with_prompt("Select (0) >", pickbuf, (int)sizeof(pickbuf));
selected = (pickbuf[0] != '\0') ? atoi(pickbuf) : 0; selected = (pickbuf[0] != '\0') ? atoi(pickbuf) : 0;
if (selected < 0 || selected >= name_count) { if (selected < 0 || selected >= name_count) {
selected = 0; selected = 0;
} }
} }
tui_get_line("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf)); tui_begin_frame(&login_signer_local_view);
(void)tui_end_frame_with_prompt("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') { if (idxbuf[0] != '\0') {
nostr_index = atoi(idxbuf); nostr_index = atoi(idxbuf);
if (nostr_index < 0) { if (nostr_index < 0) {
@@ -238,6 +281,7 @@ void menu_login(void) {
selector.nostr_index = nostr_index; selector.nostr_index = nostr_index;
selector.role[0] = '\0'; selector.role[0] = '\0';
tui_begin_frame(&login_signer_local_view);
tui_print("Waiting for n_signer approval..."); tui_print("Waiting for n_signer approval...");
if (nsigner_get_public_key(names[selected], &selector, pubkey_hex) != 0) { if (nsigner_get_public_key(names[selected], &selector, pubkey_hex) != 0) {
int i; int i;
@@ -246,7 +290,7 @@ void menu_login(void) {
free(names[i]); free(names[i]);
} }
free(names); free(names);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -296,18 +340,21 @@ void menu_login(void) {
nt_nsigner_auth_ctx_t auth = {0}; nt_nsigner_auth_ctx_t auth = {0};
snprintf(endpoint_url, sizeof(endpoint_url), "%s", (saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : ""); snprintf(endpoint_url, sizeof(endpoint_url), "%s", (saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : "");
tui_get_line("Signer URL (http://<npub>.fips:8080) >", endpoint_url, (int)sizeof(endpoint_url)); tui_begin_frame(&login_signer_url_view);
(void)tui_end_frame_with_prompt("Signer URL (http://<npub>.fips:8080) >", endpoint_url, (int)sizeof(endpoint_url));
if (endpoint_url[0] == '\0') { if (endpoint_url[0] == '\0') {
if (saved_endpoint && saved_endpoint[0] != '\0') { if (saved_endpoint && saved_endpoint[0] != '\0') {
snprintf(endpoint_url, sizeof(endpoint_url), "%s", saved_endpoint); snprintf(endpoint_url, sizeof(endpoint_url), "%s", saved_endpoint);
} else { } else {
tui_begin_frame(&login_signer_url_view);
tui_print("No URL provided."); tui_print("No URL provided.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
} }
tui_get_line("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf)); tui_begin_frame(&login_signer_url_view);
(void)tui_end_frame_with_prompt("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') { if (idxbuf[0] != '\0') {
nostr_index = atoi(idxbuf); nostr_index = atoi(idxbuf);
if (nostr_index < 0) { if (nostr_index < 0) {
@@ -318,13 +365,14 @@ void menu_login(void) {
selector.nostr_index = nostr_index; selector.nostr_index = nostr_index;
selector.role[0] = '\0'; selector.role[0] = '\0';
tui_begin_frame(&login_signer_url_view);
tui_print("Connecting to n_signer URL: %s", endpoint_url); tui_print("Connecting to n_signer URL: %s", endpoint_url);
tui_print("Waiting for n_signer approval..."); tui_print("Waiting for n_signer approval...");
auth.enabled = 1; auth.enabled = 1;
if (getrandom(auth.privkey, sizeof(auth.privkey), 0) != (ssize_t)sizeof(auth.privkey)) { if (getrandom(auth.privkey, sizeof(auth.privkey), 0) != (ssize_t)sizeof(auth.privkey)) {
tui_print("Failed to generate URL auth key."); tui_print("Failed to generate URL auth key.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
snprintf(auth.label, sizeof(auth.label), "%s", "nostr_terminal"); snprintf(auth.label, sizeof(auth.label), "%s", "nostr_terminal");
@@ -332,21 +380,21 @@ void menu_login(void) {
session_fd = nsigner_session_open_url(endpoint_url); session_fd = nsigner_session_open_url(endpoint_url);
if (session_fd < 0) { if (session_fd < 0) {
tui_print("Failed to open persistent connection to n_signer URL."); tui_print("Failed to open persistent connection to n_signer URL.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
if (nsigner_session_get_public_key(&session_fd, endpoint_url, &selector, &auth, pubkey_hex) != 0) { if (nsigner_session_get_public_key(&session_fd, endpoint_url, &selector, &auth, pubkey_hex) != 0) {
nsigner_session_close(session_fd); nsigner_session_close(session_fd);
tui_print("Failed to get public key from n_signer URL (denied or error)."); tui_print("Failed to get public key from n_signer URL (denied or error).");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
if (signer_init_nsigner_url_with_session_auth(endpoint_url, &selector, session_fd, &auth) != 0) { if (signer_init_nsigner_url_with_session_auth(endpoint_url, &selector, session_fd, &auth) != 0) {
nsigner_session_close(session_fd); nsigner_session_close(session_fd);
tui_print("Failed to initialize URL signer mode."); tui_print("Failed to initialize URL signer mode.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -389,17 +437,20 @@ void menu_login(void) {
snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault"); snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault");
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc"); snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
tui_get_line("Target signer qube (nsigner-vault) >", target_qube, (int)sizeof(target_qube)); tui_begin_frame(&login_signer_qrexec_view);
(void)tui_end_frame_with_prompt("Target signer qube (nsigner-vault) >", target_qube, (int)sizeof(target_qube));
if (target_qube[0] == '\0') { if (target_qube[0] == '\0') {
snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault"); snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault");
} }
tui_get_line("Qrexec service (qubes.NsignerRpc) >", service_name, (int)sizeof(service_name)); tui_begin_frame(&login_signer_qrexec_view);
(void)tui_end_frame_with_prompt("Qrexec service (qubes.NsignerRpc) >", service_name, (int)sizeof(service_name));
if (service_name[0] == '\0') { if (service_name[0] == '\0') {
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc"); snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
} }
tui_get_line("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf)); tui_begin_frame(&login_signer_qrexec_view);
(void)tui_end_frame_with_prompt("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') { if (idxbuf[0] != '\0') {
nostr_index = atoi(idxbuf); nostr_index = atoi(idxbuf);
if (nostr_index < 0) { if (nostr_index < 0) {
@@ -410,18 +461,19 @@ void menu_login(void) {
selector.nostr_index = nostr_index; selector.nostr_index = nostr_index;
selector.role[0] = '\0'; selector.role[0] = '\0';
tui_begin_frame(&login_signer_qrexec_view);
tui_print("Calling qrexec service %s in %s...", service_name, target_qube); tui_print("Calling qrexec service %s in %s...", service_name, target_qube);
tui_print("Waiting for n_signer approval..."); tui_print("Waiting for n_signer approval...");
if (nsigner_qrexec_get_public_key(target_qube, service_name, &selector, pubkey_hex) != 0) { if (nsigner_qrexec_get_public_key(target_qube, service_name, &selector, pubkey_hex) != 0) {
tui_print("Failed to get public key via qrexec (denied or error)."); tui_print("Failed to get public key via qrexec (denied or error).");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
if (signer_init_nsigner_qrexec(target_qube, service_name, &selector) != 0) { if (signer_init_nsigner_qrexec(target_qube, service_name, &selector) != 0) {
tui_print("Failed to initialize qrexec signer mode."); tui_print("Failed to initialize qrexec signer mode.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -458,27 +510,31 @@ void menu_login(void) {
int account = 0; int account = 0;
if (nostr_generate_mnemonic_and_keys(mnemonic, sizeof(mnemonic), 0, priv, pub) != 0) { if (nostr_generate_mnemonic_and_keys(mnemonic, sizeof(mnemonic), 0, priv, pub) != 0) {
tui_begin_frame(&login_new_account_view);
tui_print("Failed to generate new seed phrase."); tui_print("Failed to generate new seed phrase.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
tui_get_line("Enter seed phrase index (0):", idxbuf, (int)sizeof(idxbuf)); tui_begin_frame(&login_new_account_view);
(void)tui_end_frame_with_prompt("Enter seed phrase index (0):", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') { if (idxbuf[0] != '\0') {
account = atoi(idxbuf); account = atoi(idxbuf);
if (account < 0) { if (account < 0) {
account = 0; account = 0;
} }
if (nostr_derive_keys_from_mnemonic(mnemonic, account, priv, pub) != 0) { if (nostr_derive_keys_from_mnemonic(mnemonic, account, priv, pub) != 0) {
tui_begin_frame(&login_new_account_view);
tui_print("Failed to derive keys from generated mnemonic."); tui_print("Failed to derive keys from generated mnemonic.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
} }
if (menu_set_keys_from_private_bytes(priv, mnemonic) != 0) { if (menu_set_keys_from_private_bytes(priv, mnemonic) != 0) {
tui_begin_frame(&login_new_account_view);
tui_print("Could not initialize keys."); tui_print("Could not initialize keys.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -528,8 +584,9 @@ void menu_login(void) {
(void)state_load_user_info(); (void)state_load_user_info();
return; return;
} }
tui_begin_frame(&login_privkey_view);
tui_print("Invalid nsecHex. Try again."); tui_print("Invalid nsecHex. Try again.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
} }
@@ -540,8 +597,9 @@ void menu_login(void) {
(void)state_load_user_info(); (void)state_load_user_info();
return; return;
} }
tui_begin_frame(&login_privkey_view);
tui_print("Invalid nsec. Try again."); tui_print("Invalid nsec. Try again.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
@@ -562,12 +620,14 @@ void menu_login(void) {
int account = 0; int account = 0;
if (nostr_bip39_mnemonic_validate(input) != 0) { if (nostr_bip39_mnemonic_validate(input) != 0) {
tui_begin_frame(&login_mnemonic_view);
tui_print("Invalid seed phrase. Try again."); tui_print("Invalid seed phrase. Try again.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
tui_get_line("Enter seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf)); tui_begin_frame(&login_mnemonic_view);
(void)tui_end_frame_with_prompt("Enter seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') { if (idxbuf[0] != '\0') {
account = atoi(idxbuf); account = atoi(idxbuf);
if (account < 0) { if (account < 0) {
@@ -577,22 +637,23 @@ void menu_login(void) {
if (nostr_derive_keys_from_mnemonic(input, account, priv, pub) != 0 || if (nostr_derive_keys_from_mnemonic(input, account, priv, pub) != 0 ||
menu_set_keys_from_private_bytes(priv, input) != 0) { menu_set_keys_from_private_bytes(priv, input) != 0) {
tui_begin_frame(&login_mnemonic_view);
tui_print("Failed to derive keys from seed phrase."); tui_print("Failed to derive keys from seed phrase.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue; continue;
} }
tui_print(""); tui_begin_frame(&login_mnemonic_view);
tui_print("Using seed phrase: %s", g_state.seed_phrase); tui_print("Using seed phrase: %s", g_state.seed_phrase);
tui_print(""); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
tui_get_line("Press Enter to continue >", input, (int)sizeof(input));
(void)state_load_user_info(); (void)state_load_user_info();
return; return;
} }
} }
tui_begin_frame(&login_view);
tui_print("Invalid login input. Choose a menu command or enter valid key/mnemonic data."); tui_print("Invalid login input. Choose a menu command or enter valid key/mnemonic data.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} }
} }

View File

@@ -169,6 +169,10 @@ static int notif_cmp_desc_created(const void *a, const void *b) {
} }
void menu_notifications(void) { void menu_notifications(void) {
static const tui_view_t notifications_view = {
"> Main Menu > Notifications",
NULL,
};
const char **read_relays; const char **read_relays;
int relay_count = 0; int relay_count = 0;
time_t now; time_t now;
@@ -194,13 +198,12 @@ void menu_notifications(void) {
g_state.npub_hex, g_state.npub_hex,
since); since);
tui_clear_screen(); tui_begin_frame(&notifications_view);
tui_print("NOTIFICATIONS\n");
tui_print("Querying %d read relay(s)...", relay_count); tui_print("Querying %d read relay(s)...", relay_count);
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) { if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
tui_print("Failed to query notifications (all relays failed)."); tui_print("Failed to query notifications (all relays failed).");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
@@ -271,8 +274,7 @@ void menu_notifications(void) {
qsort(items, (size_t)items_count, sizeof(notif_item_t), notif_cmp_desc_created); qsort(items, (size_t)items_count, sizeof(notif_item_t), notif_cmp_desc_created);
} }
tui_clear_screen(); tui_begin_frame(&notifications_view);
tui_print("NOTIFICATIONS\n");
tui_print("%d reactions, %d replies in last 3 days", reactions, replies); tui_print("%d reactions, %d replies in last 3 days", reactions, replies);
tui_print(""); tui_print("");
@@ -305,5 +307,5 @@ void menu_notifications(void) {
notif_free_items(items, items_count); notif_free_items(items, items_count);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
} }

View File

@@ -162,6 +162,26 @@ static void posts_format_date(long long created_at, char *buf, size_t buf_sz) {
} }
} }
static void posts_print_list_item_responsive(int index,
int kind,
const char *kind_label,
const char *label,
const char *date_buf,
int term_width) {
if (term_width < 80) {
tui_print("[%2d] %s", index, label ? label : "(untitled)");
tui_print(" %s %s", kind_label ? kind_label : "?", date_buf ? date_buf : "");
return;
}
tui_print("[%2d] (%d/%s) %s %s",
index,
kind,
kind_label ? kind_label : "?",
label ? label : "(untitled)",
date_buf ? date_buf : "");
}
static int posts_build_list(const char *filter_json, post_item_t **items_out, int *count_out) { static int posts_build_list(const char *filter_json, post_item_t **items_out, int *count_out) {
const char **read_relays; const char **read_relays;
int relay_count = 0; int relay_count = 0;
@@ -279,6 +299,10 @@ static const char *posts_kind_label(int kind) {
} }
static void posts_view_item(const post_item_t *it) { static void posts_view_item(const post_item_t *it) {
static const tui_view_t post_view = {
"> Main Menu > Posts > View",
NULL,
};
char date_buf[64]; char date_buf[64];
char input[16]; char input[16];
char *decrypted = NULL; char *decrypted = NULL;
@@ -293,8 +317,7 @@ static void posts_view_item(const post_item_t *it) {
decrypted = posts_try_decrypt_nip04(it->pubkey, it->content); decrypted = posts_try_decrypt_nip04(it->pubkey, it->content);
} }
tui_clear_screen(); tui_begin_frame(&post_view);
tui_print("POST VIEW\n");
tui_print("id: %s", it->id); tui_print("id: %s", it->id);
tui_print("kind: %d (%s)", it->kind, posts_kind_label(it->kind)); tui_print("kind: %d (%s)", it->kind, posts_kind_label(it->kind));
tui_print("date: %s", date_buf); tui_print("date: %s", date_buf);
@@ -312,19 +335,35 @@ static void posts_view_item(const post_item_t *it) {
} }
free(decrypted); free(decrypted);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
} }
static void posts_delete_item(const post_item_t *it) { static void posts_delete_item(const post_item_t *it) {
static const tui_view_t post_delete = {
"> Main Menu > Posts > Delete",
NULL,
};
cJSON *tags; cJSON *tags;
cJSON *e_tag; cJSON *e_tag;
char input[16]; char input[16];
char yn[8];
int posted; int posted;
if (!it || !it->id) { if (!it || !it->id) {
return; return;
} }
tui_begin_frame(&post_delete);
tui_print("Delete this post from relays?");
tui_print("id: %.24s...", it->id);
(void)tui_end_frame_with_prompt("Confirm delete [y/n] >", yn, (int)sizeof(yn));
if (!(yn[0] == 'y' || yn[0] == 'Y')) {
tui_begin_frame(&post_delete);
tui_print("Delete canceled.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return;
}
tags = cJSON_CreateArray(); tags = cJSON_CreateArray();
if (!tags) { if (!tags) {
return; return;
@@ -343,16 +382,21 @@ static void posts_delete_item(const post_item_t *it) {
posted = publish_signed_event_with_report("Delete", 5, "", tags, 8000); posted = publish_signed_event_with_report("Delete", 5, "", tags, 8000);
cJSON_Delete(tags); cJSON_Delete(tags);
tui_begin_frame(&post_delete);
if (posted < 0) { if (posted < 0) {
tui_print("Delete publish failed."); tui_print("Delete publish failed.");
} else { } else {
tui_print("Deletion published."); tui_print("Deletion published.");
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} }
static void posts_edit_item(const post_item_t *it) { static void posts_edit_item(const post_item_t *it) {
static const tui_view_t post_edit = {
"> Main Menu > Posts > Edit",
NULL,
};
char *initial = NULL; char *initial = NULL;
char *edited = NULL; char *edited = NULL;
char *out_content = NULL; char *out_content = NULL;
@@ -374,8 +418,9 @@ static void posts_edit_item(const post_item_t *it) {
} }
if (!initial) { if (!initial) {
tui_begin_frame(&post_edit);
tui_print("Failed to prepare editor content."); tui_print("Failed to prepare editor content.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return; return;
} }
@@ -383,23 +428,24 @@ static void posts_edit_item(const post_item_t *it) {
free(initial); free(initial);
if (!edited) { if (!edited) {
tui_begin_frame(&post_edit);
tui_print("Edit canceled."); tui_print("Edit canceled.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return; return;
} }
if (it->kind == 30024 || it->kind == 30078) { if (it->kind == 30024 || it->kind == 30078) {
if (posts_encrypt_nip04_for_self(edited, &out_content) != 0) { if (posts_encrypt_nip04_for_self(edited, &out_content) != 0) {
tui_begin_frame(&post_edit);
tui_print("Failed to encrypt updated content."); tui_print("Failed to encrypt updated content.");
free(edited); free(edited);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return; return;
} }
} else { } else {
out_content = posts_strdup(edited); out_content = posts_strdup(edited);
if (!out_content) { if (!out_content) {
free(edited); free(edited);
tui_get_line(">", input, (int)sizeof(input));
return; return;
} }
} }
@@ -408,8 +454,9 @@ static void posts_edit_item(const post_item_t *it) {
if (!tags_copy) { if (!tags_copy) {
free(edited); free(edited);
free(out_content); free(out_content);
tui_begin_frame(&post_edit);
tui_print("Failed to duplicate tags."); tui_print("Failed to duplicate tags.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return; return;
} }
@@ -418,33 +465,39 @@ static void posts_edit_item(const post_item_t *it) {
free(edited); free(edited);
free(out_content); free(out_content);
tui_begin_frame(&post_edit);
if (posted < 0) { if (posted < 0) {
tui_print("Post update failed."); tui_print("Post update failed.");
} else { } else {
tui_print("Post updated."); tui_print("Post updated.");
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} }
static void posts_kind_menu(const char *title, const char *filter_json) { static void posts_kind_menu(const char *title, const char *filter_json) {
static const tui_view_t posts_kind_view = {
"> Main Menu > Posts > List",
NULL,
};
post_item_t *items = NULL; post_item_t *items = NULL;
int items_count = 0; int items_count = 0;
char input[32]; char input[32];
tui_clear_screen();
tui_print("%s\n", title);
if (posts_build_list(filter_json, &items, &items_count) != 0) { if (posts_build_list(filter_json, &items, &items_count) != 0) {
tui_begin_frame(&posts_kind_view);
tui_print("%s", title ? title : "Posts");
tui_print("Failed to query posts (all relays failed)."); tui_print("Failed to query posts (all relays failed).");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
while (1) { while (1) {
int i; int i;
int term_width = 0;
tui_clear_screen(); tui_begin_frame(&posts_kind_view);
tui_print("%s\n", title); tui_print("%s", title ? title : "Posts");
tui_get_terminal_size(&term_width, NULL);
for (i = 0; i < items_count; i++) { for (i = 0; i < items_count; i++) {
char date_buf[64]; char date_buf[64];
const char *label; const char *label;
@@ -454,12 +507,12 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
? items[i].title ? items[i].title
: ((items[i].dtag && items[i].dtag[0] != '\0') ? items[i].dtag : "(untitled)"); : ((items[i].dtag && items[i].dtag[0] != '\0') ? items[i].dtag : "(untitled)");
tui_print("[%2d] (%d/%s) %s %s", posts_print_list_item_responsive(i + 1,
i + 1, items[i].kind,
items[i].kind, posts_kind_label(items[i].kind),
posts_kind_label(items[i].kind), label,
label, date_buf,
date_buf); term_width);
} }
if (items_count == 0) { if (items_count == 0) {
@@ -467,11 +520,11 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
} }
tui_print(""); tui_print("");
tui_print("^_V^:iew <n>"); tui_print_menu_item("^_V^:iew <n>");
tui_print("^_E^:dit <n>"); tui_print_menu_item("^_E^:dit <n>");
tui_print("^_D^:elete <n>"); tui_print_menu_item("^_D^:elete <n>");
tui_print("^_B^:ack"); tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'b' || input[0] == 'B' || if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' || input[0] == 'x' || input[0] == 'X' ||
@@ -502,19 +555,22 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
} }
void menu_posts(void) { void menu_posts(void) {
static const tui_view_t posts_view = {
"> Main Menu > Posts",
NULL,
};
char input[32]; char input[32];
char filter[512]; char filter[512];
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&posts_view);
tui_print("POSTS\n"); tui_print_menu_item("^_S^:ecret blog (kind 30024)");
tui_print("^_S^:ecret blog (kind 30024)"); tui_print_menu_item("^_P^:ublic blog (kind 30023)");
tui_print("^_P^:ublic blog (kind 30023)"); tui_print_menu_item("^_E^:ncrypted data (kind 30078)");
tui_print("^_E^:ncrypted data (kind 30078)"); tui_print_menu_item("^_A^:ll posts");
tui_print("^_A^:ll posts"); tui_print_menu_item("^_X^:Exit");
tui_print("^_B^:ack");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'b' || input[0] == 'B' || if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' || input[0] == 'x' || input[0] == 'X' ||

View File

@@ -16,6 +16,10 @@ static int profile_publish_kind0(const char *content_json) {
} }
void menu_profile(void) { void menu_profile(void) {
static const tui_view_t profile_view = {
"> Main Menu > Profile",
NULL,
};
char menu_sel[32]; char menu_sel[32];
char edit_buf[512]; char edit_buf[512];
@@ -24,8 +28,7 @@ void menu_profile(void) {
const char *fields[] = {"name", "about", "picture", "banner", "website", "lud16", "nip05"}; const char *fields[] = {"name", "about", "picture", "banner", "website", "lud16", "nip05"};
int i; int i;
tui_clear_screen(); tui_begin_frame(&profile_view);
tui_print("PROFILE\n");
if (g_signer.kind == NT_SIGNER_NSIGNER) { if (g_signer.kind == NT_SIGNER_NSIGNER) {
tui_print("Signer: n_signer (@%s)", g_signer.socket_name); tui_print("Signer: n_signer (@%s)", g_signer.socket_name);
@@ -69,12 +72,12 @@ void menu_profile(void) {
tui_print("%-18s %s", fields[i], (cJSON_IsString(v) && v->valuestring) ? v->valuestring : ""); tui_print("%-18s %s", fields[i], (cJSON_IsString(v) && v->valuestring) ? v->valuestring : "");
} }
tui_print("\n"); tui_print("");
tui_print("^_M^:odify account"); tui_print_menu_item("^_M^:odify account");
tui_print("^_P^:ost changes and exit."); tui_print_menu_item("^_P^:ost changes and exit.");
tui_print("E^_x^:it without saving"); tui_print_menu_item("E^_x^:it without saving");
tui_get_line(">", menu_sel, (int)sizeof(menu_sel)); (void)tui_end_frame_with_prompt(">", menu_sel, (int)sizeof(menu_sel));
if (menu_sel[0] == 'm' || menu_sel[0] == 'M') { if (menu_sel[0] == 'm' || menu_sel[0] == 'M') {
for (i = 0; i < (int)(sizeof(fields) / sizeof(fields[0])); i++) { for (i = 0; i < (int)(sizeof(fields) / sizeof(fields[0])); i++) {

View File

@@ -95,7 +95,21 @@ static void relays_print_nip11_summary(const char *nip11_json) {
cJSON_Delete(doc); cJSON_Delete(doc);
} }
static void relays_print_row_responsive(int term_width, int index, const char *url, const char *rw) {
if (term_width < 90) {
tui_print("[%2d] %s", index, url ? url : "");
tui_print(" %s", rw ? rw : "");
return;
}
tui_print("[%2d] %-30s %s", index, url ? url : "", rw ? rw : "");
}
void menu_relays(void) { void menu_relays(void) {
static const tui_view_t relays_view = {
"> Main Menu > Relays",
NULL,
};
cJSON *working = NULL; cJSON *working = NULL;
cJSON *tags = NULL; cJSON *tags = NULL;
char input[512]; char input[512];
@@ -118,9 +132,10 @@ void menu_relays(void) {
while (1) { while (1) {
int i; int i;
int n; int n;
int term_width = 0;
tui_clear_screen(); tui_begin_frame(&relays_view);
tui_print("RELAYS\n"); tui_get_terminal_size(&term_width, NULL);
n = cJSON_GetArraySize(tags); n = cJSON_GetArraySize(tags);
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
@@ -138,17 +153,20 @@ void menu_relays(void) {
rw = t2->valuestring; rw = t2->valuestring;
} }
tui_print("[%2d] %-30s %s", i + 1, (cJSON_IsString(t1) && t1->valuestring) ? t1->valuestring : "", rw); relays_print_row_responsive(term_width,
i + 1,
(cJSON_IsString(t1) && t1->valuestring) ? t1->valuestring : "",
rw);
} }
tui_print("\n\n"); tui_print("");
tui_print("^_A^:dd relay"); tui_print_menu_item("^_A^:dd relay");
tui_print("^_D^:elete relay"); tui_print_menu_item("^_D^:elete relay");
tui_print("^_M^:odify relay"); tui_print_menu_item("^_M^:odify relay");
tui_print("^_P^:ost changes and exit."); tui_print_menu_item("^_P^:ost changes and exit.");
tui_print("E^_x^:it without saving"); tui_print_menu_item("E^_x^:it without saving");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'a' || input[0] == 'A') { if (input[0] == 'a' || input[0] == 'A') {
char relay_url[256]; char relay_url[256];
@@ -157,12 +175,14 @@ void menu_relays(void) {
char write_yn[32]; char write_yn[32];
char *nip11 = NULL; char *nip11 = NULL;
tui_print("\n\nADD RELAY\n"); tui_begin_frame(&relays_view);
tui_get_line("Relay URL >", relay_url, (int)sizeof(relay_url)); tui_print("Add relay");
(void)tui_end_frame_with_prompt("Relay URL >", relay_url, (int)sizeof(relay_url));
if (relay_url[0] == '\0') { if (relay_url[0] == '\0') {
continue; continue;
} }
tui_begin_frame(&relays_view);
tui_print("Gathering relay data ..."); tui_print("Gathering relay data ...");
if (nt_fetch_nip11(relay_url, &nip11) == 0 && nip11) { if (nt_fetch_nip11(relay_url, &nip11) == 0 && nip11) {
relays_print_nip11_summary(nip11); relays_print_nip11_summary(nip11);
@@ -171,14 +191,14 @@ void menu_relays(void) {
tui_print("NIP-11 fetch failed."); tui_print("NIP-11 fetch failed.");
} }
tui_get_line("Add relay [y/n] >", yn, (int)sizeof(yn)); (void)tui_end_frame_with_prompt("Add relay [y/n] >", yn, (int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') { if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON *tag = cJSON_CreateArray(); cJSON *tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("r")); cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url)); cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
tui_get_line("Read from relay [y/n] >", read_yn, (int)sizeof(read_yn)); (void)tui_end_frame_with_prompt("Read from relay [y/n] >", read_yn, (int)sizeof(read_yn));
tui_get_line("Write to relay [y/n] >", write_yn, (int)sizeof(write_yn)); (void)tui_end_frame_with_prompt("Write to relay [y/n] >", write_yn, (int)sizeof(write_yn));
if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) { if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) {
cJSON_AddItemToArray(tag, cJSON_CreateString("write")); cJSON_AddItemToArray(tag, cJSON_CreateString("write"));
@@ -194,7 +214,8 @@ void menu_relays(void) {
cJSON *tag; cJSON *tag;
cJSON *url; cJSON *url;
tui_get_line("relay to delete >", numbuf, (int)sizeof(numbuf)); tui_begin_frame(&relays_view);
(void)tui_end_frame_with_prompt("relay to delete >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1; idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) { if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue; continue;
@@ -202,12 +223,16 @@ void menu_relays(void) {
tag = cJSON_GetArrayItem(tags, idx); tag = cJSON_GetArrayItem(tags, idx);
url = cJSON_GetArrayItem(tag, 1); url = cJSON_GetArrayItem(tag, 1);
tui_get_line("Delete selected relay [y/n] >", yn, (int)sizeof(yn)); (void)tui_end_frame_with_prompt("Delete selected relay [y/n] >", yn, (int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') { if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON_DeleteItemFromArray(tags, idx); cJSON_DeleteItemFromArray(tags, idx);
tui_begin_frame(&relays_view);
if (cJSON_IsString(url) && url->valuestring) { if (cJSON_IsString(url) && url->valuestring) {
tui_print("Deleted %s", url->valuestring); tui_print("Deleted %s", url->valuestring);
} else {
tui_print("Deleted relay.");
} }
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} }
} else if (input[0] == 'm' || input[0] == 'M') { } else if (input[0] == 'm' || input[0] == 'M') {
char numbuf[32]; char numbuf[32];
@@ -218,7 +243,8 @@ void menu_relays(void) {
cJSON *url; cJSON *url;
cJSON *new_tag; cJSON *new_tag;
tui_get_line("# relay to modify >", numbuf, (int)sizeof(numbuf)); tui_begin_frame(&relays_view);
(void)tui_end_frame_with_prompt("# relay to modify >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1; idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) { if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue; continue;
@@ -234,8 +260,8 @@ void menu_relays(void) {
cJSON_AddItemToArray(new_tag, cJSON_CreateString("r")); cJSON_AddItemToArray(new_tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(new_tag, cJSON_CreateString(url->valuestring)); cJSON_AddItemToArray(new_tag, cJSON_CreateString(url->valuestring));
tui_get_line("Read from relay [y/n]?", read_yn, (int)sizeof(read_yn)); (void)tui_end_frame_with_prompt("Read from relay [y/n]?", read_yn, (int)sizeof(read_yn));
tui_get_line("Write to relay [y/n]?", write_yn, (int)sizeof(write_yn)); (void)tui_end_frame_with_prompt("Write to relay [y/n]?", write_yn, (int)sizeof(write_yn));
if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) { if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) {
cJSON_AddItemToArray(new_tag, cJSON_CreateString("write")); cJSON_AddItemToArray(new_tag, cJSON_CreateString("write"));
@@ -247,15 +273,20 @@ void menu_relays(void) {
} else if (input[0] == 'p' || input[0] == 'P') { } else if (input[0] == 'p' || input[0] == 'P') {
int posted = relays_publish_kind10002(working); int posted = relays_publish_kind10002(working);
char *new_json = cJSON_PrintUnformatted(working); char *new_json = cJSON_PrintUnformatted(working);
if (new_json) { if (new_json) {
free(g_state.kind10002_json); free(g_state.kind10002_json);
g_state.kind10002_json = new_json; g_state.kind10002_json = new_json;
state_parse_relay_list(); state_parse_relay_list();
} }
tui_begin_frame(&relays_view);
if (posted < 0) { if (posted < 0) {
tui_print("Relay list publish failed."); tui_print("Relay list publish failed.");
} else {
tui_print("Relay list published.");
} }
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
break; break;
} else if (input[0] == 'x' || input[0] == 'X' || } else if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') { input[0] == 'q' || input[0] == 'Q') {

View File

@@ -301,6 +301,7 @@ static void todo_toggle_item(todo_item_t *items, int count) {
static void todo_delete_item(todo_item_t *items, int *count) { static void todo_delete_item(todo_item_t *items, int *count) {
char input[64]; char input[64];
char yn[8];
int idx; int idx;
int i; int i;
@@ -315,6 +316,12 @@ static void todo_delete_item(todo_item_t *items, int *count) {
return; return;
} }
tui_get_line("Delete selected item [y/n] >", yn, (int)sizeof(yn));
if (!(yn[0] == 'y' || yn[0] == 'Y')) {
tui_print("Delete canceled.");
return;
}
free(items[idx].text); free(items[idx].text);
for (i = idx; i < (*count - 1); i++) { for (i = idx; i < (*count - 1); i++) {
items[i] = items[i + 1]; items[i] = items[i + 1];
@@ -389,27 +396,37 @@ static int todo_publish(const todo_item_t *items, int count) {
} }
void menu_todo(void) { void menu_todo(void) {
static const tui_view_t todo_view = {
"> Main Menu > Todo",
NULL,
};
todo_item_t *items = NULL; todo_item_t *items = NULL;
int count = 0; int count = 0;
char input[32]; char input[32];
if (todo_load_remote(&items, &count) != 0) { if (todo_load_remote(&items, &count) != 0) {
tui_print("Failed to load remote todo list; starting empty.");
items = NULL; items = NULL;
count = 0; count = 0;
} }
while (1) { while (1) {
tui_clear_screen(); tui_begin_frame(&todo_view);
todo_show_list(items, count); if (count == 0) {
tui_print("^_A^:dd item"); tui_print("TODO LIST");
tui_print("^_C^:omplete/toggle item"); tui_print("(empty)");
tui_print("^_D^:elete item"); tui_print("");
tui_print("^_R^:eorder (swap two)"); } else {
tui_print("^_P^:ost/save changes"); todo_show_list(items, count);
tui_print("E^_x^:it without saving"); }
tui_get_line(">", input, (int)sizeof(input)); tui_print_menu_item("^_A^:dd item");
tui_print_menu_item("^_C^:omplete/toggle item");
tui_print_menu_item("^_D^:elete item");
tui_print_menu_item("^_R^:eorder (swap two)");
tui_print_menu_item("^_P^:ost/save changes");
tui_print_menu_item("E^_x^:it without saving");
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') { input[0] == 'q' || input[0] == 'Q') {
@@ -427,7 +444,9 @@ void menu_todo(void) {
} else if (input[0] == 'p' || input[0] == 'P') { } else if (input[0] == 'p' || input[0] == 'P') {
char hold[8]; char hold[8];
(void)todo_publish(items, count); (void)todo_publish(items, count);
tui_get_line(">", hold, (int)sizeof(hold)); tui_begin_frame(&todo_view);
tui_print("Todo publish attempted.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
} }

View File

@@ -8,20 +8,28 @@
#include <string.h> #include <string.h>
void menu_tweet(void) { void menu_tweet(void) {
static const tui_view_t tweet_view = {
"> Main Menu > Tweet",
NULL,
};
char text[4096]; char text[4096];
int posted; int posted;
tui_clear_screen(); tui_begin_frame(&tweet_view);
tui_print("TWEET\n"); tui_print("Compose a short post.");
tui_print("");
tui_get_line("tweet >", text, (int)sizeof(text)); (void)tui_end_frame_with_prompt("tweet >", text, (int)sizeof(text));
if (text[0] == '\0') { if (text[0] == '\0') {
return; return;
} }
posted = publish_signed_event_with_report("Tweet", 1, text, NULL, 8000); posted = publish_signed_event_with_report("Tweet", 1, text, NULL, 8000);
tui_begin_frame(&tweet_view);
if (posted < 0) { if (posted < 0) {
tui_print("Tweet publish failed."); tui_print("Tweet publish failed.");
} else {
tui_print("Tweet published.");
} }
tui_get_line(">", text, (int)sizeof(text)); (void)tui_end_frame_with_prompt("Press Enter to exit >", text, (int)sizeof(text));
} }

View File

@@ -39,35 +39,40 @@ static cJSON *write_make_blog_tags(const char *d_tag_value, const char *title_op
} }
void menu_write(void) { void menu_write(void) {
static const tui_view_t write_view = {
"> Main Menu > Write",
NULL,
};
char *content; char *content;
char input[64]; char input[64];
tui_clear_screen(); tui_begin_frame(&write_view);
tui_print("WRITE\n");
tui_print("Launching external editor..."); tui_print("Launching external editor...");
content = editor_launch(NULL); content = editor_launch(NULL);
if (!content) { if (!content) {
tui_begin_frame(&write_view);
tui_print("No content created."); tui_print("No content created.");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
tui_clear_screen(); tui_begin_frame(&write_view);
tui_print("WRITE RESULT\n");
tui_print("Content length: %d bytes", (int)strlen(content)); tui_print("Content length: %d bytes", (int)strlen(content));
tui_print("\n"); tui_print("");
tui_print("^_T^:weet it"); tui_print_menu_item("^_T^:weet it");
tui_print("^_B^:log post (kind 30023)"); tui_print_menu_item("^_B^:log post (kind 30023)");
tui_print("^_D^:iary entry (kind 30024)"); tui_print_menu_item("^_D^:iary entry (kind 30024)");
tui_print("^_C^:ancel/discard"); tui_print_menu_item("^_X^:Exit without saving");
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 't' || input[0] == 'T') { if (input[0] == 't' || input[0] == 'T') {
int posted = publish_signed_event_with_report("Tweet", 1, content, NULL, 8000); int posted = publish_signed_event_with_report("Tweet", 1, content, NULL, 8000);
tui_begin_frame(&write_view);
if (posted < 0) { if (posted < 0) {
tui_print("Tweet publish failed."); tui_print("Tweet publish failed.");
} else {
tui_print("Tweet published.");
} }
} else if (input[0] == 'b' || input[0] == 'B') { } else if (input[0] == 'b' || input[0] == 'B') {
char title[256]; char title[256];
@@ -76,11 +81,14 @@ void menu_write(void) {
cJSON *tags; cJSON *tags;
int posted; int posted;
tui_get_line("title >", title, (int)sizeof(title)); tui_begin_frame(&write_view);
tui_get_line("slug (d tag) [empty uses title] >", slug, (int)sizeof(slug)); (void)tui_end_frame_with_prompt("title >", title, (int)sizeof(title));
(void)tui_end_frame_with_prompt("slug (d tag) [empty uses title] >", slug, (int)sizeof(slug));
d_val = (slug[0] != '\0') ? slug : title; d_val = (slug[0] != '\0') ? slug : title;
tags = write_make_blog_tags(d_val, title); tags = write_make_blog_tags(d_val, title);
tui_begin_frame(&write_view);
if (!tags) { if (!tags) {
tui_print("Failed to build blog tags."); tui_print("Failed to build blog tags.");
} else { } else {
@@ -88,6 +96,8 @@ void menu_write(void) {
cJSON_Delete(tags); cJSON_Delete(tags);
if (posted < 0) { if (posted < 0) {
tui_print("Blog publish failed."); tui_print("Blog publish failed.");
} else {
tui_print("Blog post published.");
} }
} }
} else if (input[0] == 'd' || input[0] == 'D') { } else if (input[0] == 'd' || input[0] == 'D') {
@@ -98,17 +108,18 @@ void menu_write(void) {
int posted; int posted;
memset(&tmv, 0, sizeof(tmv)); memset(&tmv, 0, sizeof(tmv));
tui_begin_frame(&write_view);
if (!localtime_r(&now, &tmv)) { if (!localtime_r(&now, &tmv)) {
tui_print("Failed to get local date."); tui_print("Failed to get local date.");
free(content); free(content);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
if (strftime(date_d, sizeof(date_d), "%Y%m%d", &tmv) == 0) { if (strftime(date_d, sizeof(date_d), "%Y%m%d", &tmv) == 0) {
tui_print("Failed to format diary date."); tui_print("Failed to format diary date.");
free(content); free(content);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return; return;
} }
@@ -120,16 +131,15 @@ void menu_write(void) {
cJSON_Delete(tags); cJSON_Delete(tags);
if (posted < 0) { if (posted < 0) {
tui_print("Diary publish failed."); tui_print("Diary publish failed.");
} else {
tui_print("Diary entry published.");
} }
} }
} else if (input[0] == 'c' || input[0] == 'C' ||
input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
tui_print("Discarded.");
} else { } else {
tui_begin_frame(&write_view);
tui_print("Discarded."); tui_print("Discarded.");
} }
free(content); free(content);
tui_get_line(">", input, (int)sizeof(input)); (void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
} }

306
src/tui.c
View File

@@ -1,6 +1,8 @@
#include "tui.h" #include "tui.h"
#include <ctype.h> #include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
@@ -13,6 +15,36 @@ static struct termios g_original_termios;
static bool g_termios_saved = false; static bool g_termios_saved = false;
static bool g_raw_enabled = false; static bool g_raw_enabled = false;
static volatile sig_atomic_t g_resize_pending = 0;
static char g_app_title[256] = "NOSTR TERMINAL";
static int g_last_menu_start_col = 0;
static int g_frame_line_count = 0;
static bool g_frame_active = false;
static int tui_visible_len_no_markup(const char *s) {
int len = 0;
if (!s) {
return 0;
}
for (size_t i = 0; s[i] != '\0';) {
if (s[i] == '^' && s[i + 1] == '_') {
i += 2;
continue;
}
if (s[i] == '^' && s[i + 1] == ':') {
i += 2;
continue;
}
len++;
i++;
}
return len;
}
/* Write a string while converting ^_...^: segments to underline. */ /* Write a string while converting ^_...^: segments to underline. */
static void tui_write_with_hotkey_markup(const char *s) { static void tui_write_with_hotkey_markup(const char *s) {
bool in_hotkey = false; bool in_hotkey = false;
@@ -29,6 +61,11 @@ static void tui_write_with_hotkey_markup(const char *s) {
continue; continue;
} }
if (in_hotkey && s[i] == '^' && s[i + 1] == '^') {
i += 2;
continue;
}
if (in_hotkey && s[i] == '^' && s[i + 1] == ':') { if (in_hotkey && s[i] == '^' && s[i + 1] == ':') {
fputs("\x1b[0m", stdout); /* reset */ fputs("\x1b[0m", stdout); /* reset */
in_hotkey = false; in_hotkey = false;
@@ -45,6 +82,86 @@ static void tui_write_with_hotkey_markup(const char *s) {
} }
} }
static int tui_count_newlines(const char *s) {
int count = 0;
if (!s) {
return 0;
}
for (size_t i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
count++;
}
}
return count;
}
static void tui_track_lines(int lines) {
if (g_frame_active && lines > 0) {
g_frame_line_count += lines;
}
}
static void tui_print_repeat_char(char ch, int width) {
if (width <= 0) {
fputc('\n', stdout);
tui_track_lines(1);
return;
}
for (int i = 0; i < width; i++) {
fputc(ch, stdout);
}
fputc('\n', stdout);
tui_track_lines(1);
}
static void tui_print_centered_text(const char *text, int width) {
int len = 0;
if (!text) {
fputc('\n', stdout);
tui_track_lines(1);
return;
}
len = tui_visible_len_no_markup(text);
if (width <= 0) {
fputc('\n', stdout);
tui_track_lines(1);
return;
}
if (len >= width) {
tui_write_with_hotkey_markup(text);
fputc('\n', stdout);
tui_track_lines(1);
return;
}
{
int left = (width - len) / 2;
int right = width - len - left;
for (int i = 0; i < left; i++) {
fputc(' ', stdout);
}
tui_write_with_hotkey_markup(text);
for (int i = 0; i < right; i++) {
fputc(' ', stdout);
}
fputc('\n', stdout);
tui_track_lines(1);
}
}
static void tui_handle_sigwinch(int signum) {
(void)signum;
g_resize_pending = 1;
}
void tui_init(void) { void tui_init(void) {
struct termios raw; struct termios raw;
@@ -81,6 +198,24 @@ void tui_cleanup(void) {
} }
} }
void tui_install_resize_handler(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = tui_handle_sigwinch;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
(void)sigaction(SIGWINCH, &sa, NULL);
}
int tui_consume_resize_flag(void) {
if (g_resize_pending) {
g_resize_pending = 0;
return 1;
}
return 0;
}
void tui_get_terminal_size(int *width, int *height) { void tui_get_terminal_size(int *width, int *height) {
struct winsize ws; struct winsize ws;
int w = 80; int w = 80;
@@ -103,6 +238,14 @@ void tui_get_terminal_size(int *width, int *height) {
} }
} }
void tui_set_app_title(const char *title) {
if (!title || title[0] == '\0') {
return;
}
snprintf(g_app_title, sizeof(g_app_title), "%s", title);
}
void tui_clear_screen(void) { void tui_clear_screen(void) {
int width = 0; int width = 0;
int height = 0; int height = 0;
@@ -126,6 +269,152 @@ void tui_print(const char *fmt, ...) {
tui_write_with_hotkey_markup(buffer); tui_write_with_hotkey_markup(buffer);
fputc('\n', stdout); fputc('\n', stdout);
fflush(stdout); fflush(stdout);
tui_track_lines(tui_count_newlines(buffer) + 1);
}
void tui_print_hr(void) {
int width = 0;
int height = 0;
(void)height;
tui_get_terminal_size(&width, &height);
tui_print_repeat_char('=', width);
fflush(stdout);
}
void tui_print_centered(const char *text) {
int width = 0;
int height = 0;
(void)height;
tui_get_terminal_size(&width, &height);
tui_print_centered_text(text ? text : "", width);
fflush(stdout);
}
int tui_centered_menu_start_col(void) {
int width = 0;
int height = 0;
int title_len = 0;
(void)height;
tui_get_terminal_size(&width, &height);
title_len = tui_visible_len_no_markup(g_app_title);
if (width <= title_len) {
g_last_menu_start_col = 0;
return 0;
}
g_last_menu_start_col = (width - title_len) / 2;
if (g_last_menu_start_col < 0) {
g_last_menu_start_col = 0;
}
return g_last_menu_start_col;
}
void tui_render_top_frame(const tui_view_t *view) {
int width = 0;
int height = 0;
(void)height;
tui_get_terminal_size(&width, &height);
tui_print_repeat_char('=', width);
tui_print_centered_text(g_app_title, width);
tui_print_repeat_char('=', width);
if (view && view->breadcrumb && view->breadcrumb[0] != '\0') {
tui_write_with_hotkey_markup(view->breadcrumb);
} else {
tui_write_with_hotkey_markup("> Main Menu");
}
fputc('\n', stdout);
tui_track_lines(1);
fputc('\n', stdout);
fputc('\n', stdout);
tui_track_lines(2);
fflush(stdout);
tui_centered_menu_start_col();
}
void tui_print_menu_item(const char *fmt, ...) {
char buffer[8192];
va_list ap;
va_start(ap, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);
for (int i = 0; i < g_last_menu_start_col; i++) {
fputc(' ', stdout);
}
tui_write_with_hotkey_markup(buffer);
fputc('\n', stdout);
fflush(stdout);
tui_track_lines(tui_count_newlines(buffer) + 1);
}
void tui_begin_frame(const tui_view_t *view) {
g_frame_active = true;
g_frame_line_count = 0;
tui_clear_screen();
tui_render_top_frame(view);
}
int tui_end_frame_with_prompt(const char *prompt, char *buf, int bufsize) {
int width = 0;
int height = 0;
int filler = 0;
tui_get_terminal_size(&width, &height);
filler = (height - 1) - g_frame_line_count;
if (filler < 0) {
filler = 0;
}
for (int i = 0; i < filler; i++) {
fputc('\n', stdout);
}
if (filler > 0) {
fprintf(stdout, "\033[%dA\r", filler);
}
for (int i = 0; i < g_last_menu_start_col; i++) {
fputc(' ', stdout);
}
fflush(stdout);
g_frame_active = false;
g_frame_line_count = 0;
return tui_get_line(prompt ? prompt : ">", buf, bufsize);
}
void tui_show_splash(void) {
static const tui_view_t splash_view = {
"> Splash",
NULL,
};
char hold[8];
tui_begin_frame(&splash_view);
tui_print("Welcome to Nostr Terminal.");
tui_print("");
tui_print("Continuous TUI mode is enabled.");
tui_print("Frame redraw preserves scrollback by using newline refresh.");
tui_print("");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
} }
int tui_get_line(const char *prompt, char *buf, int bufsize) { int tui_get_line(const char *prompt, char *buf, int bufsize) {
@@ -162,6 +451,9 @@ int tui_get_line(const char *prompt, char *buf, int bufsize) {
unsigned char ch = 0; unsigned char ch = 0;
ssize_t n = read(STDIN_FILENO, &ch, 1); ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n <= 0) { if (n <= 0) {
if (n < 0 && errno == EINTR) {
continue;
}
break; break;
} }
@@ -203,11 +495,19 @@ int tui_get_key(void) {
tui_init(); tui_init();
} }
if (read(STDIN_FILENO, &ch, 1) != 1) { while (1) {
ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n == 1) {
return (int)ch;
}
if (n < 0 && errno == EINTR) {
if (g_resize_pending) {
return TUI_KEY_RESIZE;
}
continue;
}
return -1; return -1;
} }
return (int)ch;
} }
void tui_set_window_title(const char *title) { void tui_set_window_title(const char *title) {