AmendHub

jcs

/

subtext

/

amendments

/

281

*: Add dynamically configured main menu

The list of action->key pairs can be edited from the console menu,
and doubles as a list of each key->label that we can print for the
default main menu shown to the user when no custom menu is defined.

jcs made amendment 281 2 months ago
--- db.h Sat Oct 1 20:14:31 2022 +++ db.h Wed Nov 9 09:56:01 2022 @@ -44,6 +44,7 @@ #define DB_TEXT_PAGE_SYSOP_ID 5 #define DB_TEXT_NO_FREE_NODES_ID 6 #define DB_TEXT_SIGNOFF_ID 7 +#define DB_TEXT_MENU_OPTIONS_ID 8 #define DB_USERNAME_LENGTH 16 --- main.c Sat Oct 1 21:08:28 2022 +++ main.c Wed Nov 9 09:58:42 2022 @@ -22,6 +22,7 @@ #include "db.h" #include "focusable.h" #include "logger.h" +#include "main_menu.h" #include "serial_local.h" #include "session.h" #include "settings.h" @@ -99,6 +100,8 @@ main(void) logger_init(); logger_update_title(); + + main_menu_init(); zone_size = (unsigned long)CurStackBase - (unsigned long)ApplZone; stack_size = (unsigned long)CurStackBase - (unsigned long)ApplLimit; @@ -106,7 +109,7 @@ main(void) logger_printf("Initialized with %ldKB zone, %ldKB stack for " "%d threads, %ldKB heap", zone_size / 1024L, stack_size / 1024L, NUM_UTHREADS, heap_size / 1024L); - + logger_printf("[db] Updating username cache"); user_cache_usernames(); @@ -332,6 +335,9 @@ handle_menu(long menu_id) switch (LoWord(menu_id)) { case BBS_MENU_OPEN_CONSOLE_ID: console_init(); + break; + case BBS_MENU_EDIT_MENUS_ID: + main_menu_edit(); break; } ret = 1; --- main_menu.c Fri Nov 11 21:54:35 2022 +++ main_menu.c Fri Nov 11 23:02:08 2022 @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2022 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <string.h> + +#include "subtext.h" +#include "db.h" +#include "main_menu.h" + +struct main_menu_option *main_menu_options = NULL; + +void +main_menu_init(void) +{ + struct main_menu_option *opts = NULL, *old_opts = NULL; + size_t size; + char *menu = NULL; + bool ok = false; + Handle default_menu; + + size = bile_read_alloc(db->bile, DB_TEXT_TYPE, DB_TEXT_MENU_OPTIONS_ID, + &menu); + if (size == 0 || menu == NULL) { + /* use default menu */ + if (menu != NULL) + xfree(&menu); + default_menu = GetResource('TEXT', MENU_DEFAULTS_ID); + if (default_menu == NULL) + panic("Failed to find TEXT resource %d", MENU_DEFAULTS_ID); + size = GetHandleSize(default_menu); + menu = xmalloc(size, "menu copy"); + HLock(default_menu); + memcpy(menu, *default_menu, size); + HUnlock(default_menu); + ReleaseResource(default_menu); + + bile_write(db->bile, DB_TEXT_TYPE, DB_TEXT_MENU_OPTIONS_ID, + menu, size); + } + + opts = main_menu_parse(menu, size); + free(&menu); + if (opts == NULL) { + main_menu_edit(); + return; + } + + /* swap this atomically */ + old_opts = main_menu_options; + main_menu_options = opts; + if (old_opts != NULL) + xfree(&old_opts); +} + +void +main_menu_edit(void) +{ + view_editor_show(DB_TEXT_MENU_OPTIONS_ID, "Edit: Main Menu options"); +} + +struct main_menu_option * +main_menu_parse(char *opts, size_t len) +{ + char *line, *action, *menu_key, *all_keys, *label; + size_t n, m, linelen, lastsep, linenum, ret_size; + short count, actionid, ret_count; + struct main_menu_option *ret = NULL; + +#define LINESIZE 1024 + line = xmalloc(LINESIZE, "parse_menu line"); + action = xmalloc(LINESIZE, "parse_menu action"); + menu_key = xmalloc(LINESIZE, "parse_menu menu_key"); + all_keys = xmalloc(LINESIZE, "parse_menu all_keys"); + label = xmalloc(LINESIZE, "parse_menu label"); + + ret_count = 0; + ret_size = 0; + + for (n = 0, lastsep = 0, linenum = 0; n <= len; n++) { + if (!(n == len || opts[n] == '\r')) + continue; + + linenum++; + linelen = MIN(n - lastsep, LINESIZE - 1); + if (n == len && linelen) + linelen--; + memcpy(line, opts + lastsep, linelen); + line[linelen] = '\0'; + lastsep = n + 1; + + if (line[0] == '\0' || line[0] == '#' || line[0] == '\r') + continue; + + /* BOARD_SHOW_FIRST:B:Bb:Message Board */ + + /* sscanf won't match %[^:] for an empty section ("A::Aa:aa") */ + action[0] = '\0'; + for (m = 0; m < linelen; m++) { + if (line[m] == ':') { + memcpy(action, line, m); + action[m] = '\0'; + linelen -= m + 1; + memmove(line, line + m + 1, linelen + 1); + break; + } + } + if (action[0] == '\0') { + warn("Error on line %ld: no action found", linenum); + ret_count = 0; + break; + } + + actionid = ACTION_NONE; + if (strcmp(action, "BOARD_SHOW_FIRST") == 0) + actionid = ACTION_BOARD_SHOW_FIRST; + else if (strcmp(action, "BOARD_SHOW_1") == 0) + actionid = ACTION_BOARD_SHOW_1; + else if (strcmp(action, "BOARD_SHOW_2") == 0) + actionid = ACTION_BOARD_SHOW_2; + else if (strcmp(action, "BOARD_SHOW_3") == 0) + actionid = ACTION_BOARD_SHOW_3; + else if (strcmp(action, "BOARD_SHOW_4") == 0) + actionid = ACTION_BOARD_SHOW_4; + else if (strcmp(action, "BOARD_SHOW_5") == 0) + actionid = ACTION_BOARD_SHOW_5; + else if (strcmp(action, "BOARD_SHOW_6") == 0) + actionid = ACTION_BOARD_SHOW_6; + else if (strcmp(action, "BOARD_SHOW_7") == 0) + actionid = ACTION_BOARD_SHOW_7; + else if (strcmp(action, "BOARD_SHOW_8") == 0) + actionid = ACTION_BOARD_SHOW_8; + else if (strcmp(action, "BOARD_SHOW_9") == 0) + actionid = ACTION_BOARD_SHOW_9; + else if (strcmp(action, "BOARD_SHOW_10") == 0) + actionid = ACTION_BOARD_SHOW_10; + else if (strcmp(action, "CHAT") == 0) + actionid = ACTION_CHAT; + else if (strcmp(action, "FILES_MENU") == 0) + actionid = ACTION_FILES_MENU; + else if (strcmp(action, "GOODBYE") == 0) + actionid = ACTION_GOODBYE; + else if (strcmp(action, "RECENT_LOGINS") == 0) + actionid = ACTION_RECENT_LOGINS; + else if (strcmp(action, "MAIL_COMPOSE") == 0) + actionid = ACTION_MAIL_COMPOSE; + else if (strcmp(action, "MAIL_MENU") == 0) + actionid = ACTION_MAIL_MENU; + else if (strcmp(action, "MOTD") == 0) + actionid = ACTION_MOTD; + else if (strcmp(action, "PAGE_SEND_OR_ANSWER") == 0) + actionid = ACTION_PAGE_SEND_OR_ANSWER; + else if (strcmp(action, "SETTINGS_OR_SIGNUP") == 0) + actionid = ACTION_SETTINGS_OR_SIGNUP; + else if (strcmp(action, "SHOW_MENU") == 0) + actionid = ACTION_SHOW_MENU; + else if (strcmp(action, "SYSOP_MENU") == 0) + actionid = ACTION_SYSOP_MENU; + else if (strcmp(action, "WHOS_ONLINE") == 0) + actionid = ACTION_WHOS_ONLINE; + else { + warn("Error on line %ld: invalid action \"%s\"", linenum, + action); + ret_count = 0; + break; + } + + menu_key[0] = '\0'; + for (m = 0; m < linelen; m++) { + if (line[m] == ':') { + memcpy(menu_key, line, m); + menu_key[m] = '\0'; + linelen -= m + 1; + memmove(line, line + m + 1, linelen + 1); + break; + } + } + /* menu_key can be 1 or 0 characters */ + if (strlen(menu_key) > 1) { + warn("Error on line %ld: Menu Key can only be 1 character", + linenum); + ret_count = 0; + break; + } + + all_keys[0] = '\0'; + for (m = 0; m < linelen; m++) { + if (line[m] == ':') { + memcpy(all_keys, line, m); + all_keys[m] = '\0'; + linelen -= m + 1; + memmove(line, line + m + 1, linelen + 1); + break; + } + } + if (all_keys[0] == '\0') { + warn("Error on line %ld: All Keys field cannot be empty", + linenum); + ret_count = 0; + break; + } + + if (line[0] == '\0') { + warn("Error on line %ld: Label cannot be empty", + linenum); + ret_count = 0; + break; + } + strlcpy(label, line, LINESIZE); + + EXPAND_TO_FIT(ret, ret_size, + sizeof(struct main_menu_option) * ret_count, + sizeof(struct main_menu_option), 4); + + ret[ret_count].action = actionid; + ret[ret_count].menu_key = menu_key[0]; + strlcpy(ret[ret_count].all_keys, all_keys, + sizeof(ret[ret_count].all_keys)); + strlcpy(ret[ret_count].label, label, sizeof(ret[ret_count].label)); + ret_count++; + } + + xfree(&line); + xfree(&action); + xfree(&menu_key); + xfree(&all_keys); + xfree(&label); + + if (ret_count == 0) { + xfree(&ret); + return NULL; + } + + EXPAND_TO_FIT(ret, ret_size, + sizeof(struct main_menu_option) * ret_count, + sizeof(struct main_menu_option), 1); + + ret[ret_count].action = ACTION_NONE; + ret[ret_count].menu_key = 0; + ret[ret_count].all_keys[0] = '\0'; + ret[ret_count].label[0] = '\0'; + + return ret; +#undef LINESIZE +} --- main_menu.h Fri Nov 11 21:53:39 2022 +++ main_menu.h Fri Nov 11 21:53:39 2022 @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __MAIN_MENU_H__ +#define __MAIN_MENU_H__ + +enum main_menu_action { + ACTION_NONE = -1, + ACTION_BOARD_SHOW_FIRST, + ACTION_BOARD_SHOW_1, + ACTION_BOARD_SHOW_2, + ACTION_BOARD_SHOW_3, + ACTION_BOARD_SHOW_4, + ACTION_BOARD_SHOW_5, + ACTION_BOARD_SHOW_6, + ACTION_BOARD_SHOW_7, + ACTION_BOARD_SHOW_8, + ACTION_BOARD_SHOW_9, + ACTION_BOARD_SHOW_10, + ACTION_CHAT, + ACTION_FILES_MENU, + ACTION_GOODBYE, + ACTION_MAIL_COMPOSE, + ACTION_MAIL_MENU, + ACTION_MOTD, + ACTION_PAGE_ANSWER, + ACTION_PAGE_SEND, + ACTION_PAGE_SEND_OR_ANSWER, + ACTION_RECENT_LOGINS, + ACTION_SETTINGS_OR_SIGNUP, + ACTION_SETTINGS, + ACTION_SIGNUP, + ACTION_SHOW_MENU, + ACTION_SYSOP_MENU, + ACTION_WHOS_ONLINE +}; + +struct main_menu_option { + short action; + char menu_key; + char all_keys[20]; + char label[200]; +}; + +extern struct main_menu_option *main_menu_options; + +void main_menu_init(void); +void main_menu_edit(void); +struct main_menu_option * main_menu_parse(char *opts, size_t len); + +#endif --- session.c Tue Oct 4 22:52:44 2022 +++ session.c Fri Nov 11 22:15:32 2022 @@ -25,6 +25,7 @@ #include "folder.h" #include "logger.h" #include "mail.h" +#include "main_menu.h" #include "subtext.h" #include "session.h" #include "settings.h" @@ -46,6 +47,7 @@ static const char invalid_option_help[] = "Invalid option (press {{B}}?{{/B}} for help)\r\n"; void session_run(struct uthread *uthread, void *arg); +void session_print_menu(struct session *session, bool short_menu); short session_login(struct session *s); size_t session_log(struct session *session, const char *str); size_t session_vprintf(struct session *session, const char *format, @@ -107,12 +109,15 @@ session_run(struct uthread *uthread, void *arg) struct tm *date_tm; unsigned short c, last_c = 0; bool done = false; - short auth; + short auth, i, action; /* until we negotiate otherwise */ s->terminal_columns = DEFAULT_TERMINAL_COLUMNS; s->terminal_lines = DEFAULT_TERMINAL_LINES; - + + if (main_menu_options == NULL) + panic("No main menu!"); + if (s->node_funcs->setup) { s->node_funcs->setup(s); @@ -183,9 +188,9 @@ session_run(struct uthread *uthread, void *arg) session_print_motd(s, false); -main_menu: - session_output_view_or_printf(s, DB_TEXT_MENU_ID, - "\r\n[ Menu missing! ]\r\n\r\n"); +main_menu: + session_output(s, "\r\n", 2); + session_print_menu(s, false); session_flush(s); while (!done && !s->ending) { @@ -193,8 +198,10 @@ main_menu: session_flush(s); get_another_char: + action = ACTION_NONE; + if (auth == AUTH_USER_SIGNUP) { - c = 's'; + action = ACTION_SIGNUP; auth = AUTH_USER_GUEST; } else c = session_input_char(s); @@ -206,84 +213,128 @@ get_another_char: session_printf(s, "%c\r\n", c); session_flush(s); + + if (action == ACTION_NONE) { + /* check each menu option's all_keys array for matching key */ + for (i = 0; ; i++) { + struct main_menu_option *option = &main_menu_options[i]; + short j; + + if (option->action == ACTION_NONE) + break; + + if (option->menu_key == 0 || option->label == NULL) + continue; + + for (j = 0; option->all_keys[j] != '\0'; j++) { + if (option->all_keys[j] == c) { + action = option->action; + break; + } + } + + if (action != ACTION_NONE) + break; + } + } - /* TODO: make letter->command dynamic from a resource */ - switch (c) { - case 'b': - case 'B': - /* first (or only) board */ + /* change actions for conditionals */ + if (action == ACTION_SETTINGS_OR_SIGNUP) { + if (s->user) + action = ACTION_SETTINGS; + else + action = ACTION_SIGNUP; + } else if (action == ACTION_PAGE_SEND_OR_ANSWER) { + if (s->user && s->user->is_sysop) + action = ACTION_PAGE_ANSWER; + else + action = ACTION_PAGE_SEND; + } + + switch (action) { + case ACTION_BOARD_SHOW_FIRST: board_show(s, 1); break; - case 'c': - case 'C': - /* chat */ + case ACTION_CHAT: chat_start(s, NULL); break; - case 'f': - case 'F': - /* files */ + case ACTION_FILES_MENU: folder_list(s); break; - case 'g': - case 'G': - /* goodbye */ + case ACTION_GOODBYE: session_output_view_or_printf(s, DB_TEXT_SIGNOFF_ID, "Goodbye!\r\n"); session_flush(s); done = true; break; - case 'l': - case 'L': + case ACTION_RECENT_LOGINS: session_recents(s); break; - case 'm': - case 'M': + case ACTION_MAIL_MENU: mail_menu(s); break; - case 'n': - case 'N': - /* send mail */ + case ACTION_MAIL_COMPOSE: mail_compose(s, NULL, NULL, NULL); break; - case 'o': - case 'O': + case ACTION_MOTD: session_print_motd(s, true); break; - case 'p': - case 'P': - if (s->user && s->user->is_sysop) - session_answer_page(s); - else - session_page_sysop(s); + case ACTION_PAGE_ANSWER: + if (!s->user || !s->user->is_sysop) { + session_printf(s, invalid_option_help); + session_flush(s); + break; + } + session_answer_page(s); break; - case 's': - case 'S': + case ACTION_PAGE_SEND: + session_page_sysop(s); + break; + case ACTION_SETTINGS: if (s->user) user_settings_menu(s); - else if ((s->user = signup(s))) { + break; + case ACTION_SIGNUP: + if ((s->user = signup(s))) { session_printf(s, "\r\n" "Welcome, {{B}}%s{{/B}}!\r\n", s->user->username); goto main_menu; } break; - case 'w': - case 'W': - /* who's online */ + case ACTION_WHOS_ONLINE: session_who(s); break; - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '0': - board_show(s, c - '0'); + case ACTION_BOARD_SHOW_1: + board_show(s, 1); break; - case '!': + case ACTION_BOARD_SHOW_2: + board_show(s, 2); + break; + case ACTION_BOARD_SHOW_3: + board_show(s, 3); + break; + case ACTION_BOARD_SHOW_4: + board_show(s, 4); + break; + case ACTION_BOARD_SHOW_5: + board_show(s, 5); + break; + case ACTION_BOARD_SHOW_6: + board_show(s, 6); + break; + case ACTION_BOARD_SHOW_7: + board_show(s, 7); + break; + case ACTION_BOARD_SHOW_8: + board_show(s, 8); + break; + case ACTION_BOARD_SHOW_9: + board_show(s, 9); + break; + case ACTION_BOARD_SHOW_10: + board_show(s, 10); + break; + case ACTION_SYSOP_MENU: if (!s->user || !s->user->is_sysop) { session_printf(s, invalid_option_help); session_flush(s); @@ -291,17 +342,12 @@ get_another_char: } sysop_menu(s); break; - case '?': - if (last_c == '?') { + case ACTION_SHOW_MENU: + if (last_c == c) /* asking twice in a row will print the full menu */ - session_output_view_or_printf(s, DB_TEXT_MENU_ID, - "\r\n[ Menu missing! ]\r\n\r\n"); - } else { - if (session_output_view_or_printf(s, DB_TEXT_SHORTMENU_ID, - NULL) == 0) - session_output_view_or_printf(s, DB_TEXT_MENU_ID, - "\r\n[ Short and long menu missing! ]\r\n\r\n"); - } + session_print_menu(s, false); + else + session_print_menu(s, true); session_flush(s); break; default: @@ -314,6 +360,40 @@ get_another_char: } session_close(s); +} + +void +session_print_menu(struct session *session, bool short_menu) +{ + struct main_menu_option *option = NULL; + size_t size; + short i, j; + char *output = NULL; + + if (short_menu && session_output_view_or_printf(session, + DB_TEXT_SHORTMENU_ID, NULL) != 0) + return; + + if (session_output_view_or_printf(session, DB_TEXT_MENU_ID, NULL) != 0) + return; + + for (i = 0; ; i++) { + option = &main_menu_options[i]; + + if (option->action == ACTION_NONE) + break; + + if (option->menu_key == 0 || option->label == NULL) + continue; + + size = session_expand_template(session, option->label, &output); + if (!size) + continue; + + session_printf(session, "{{B}}%c{{/B}}: %s\r\n", option->menu_key, + output); + xfree(&output); + } } void --- settings.c Thu Sep 8 22:14:05 2022 +++ settings.c Fri Nov 11 23:02:55 2022 @@ -20,6 +20,7 @@ #include "subtext.h" #include "db.h" #include "focusable.h" +#include "main_menu.h" #include "settings.h" #include "tcp.h" /* for long2ip/ip2long */ #include "tetab.h" @@ -42,12 +43,12 @@ void view_editor_close(struct focusable *focusable, Ev void view_editor_save(struct focusable *focusable, EventRecord *event); short -struct_editor(struct session *s, struct struct_field *fields, - size_t nfields, void *data, size_t dsize, void **result, char *title, - char *prompt) +struct_editor(struct session *s, const struct struct_field *fields, + const size_t nfields, void *data, size_t dsize, void **result, + char *title, char *prompt) { Handle ihandle; - struct struct_field *sf; + const struct struct_field *sf; long lval; char co, initial[20]; char *input = NULL, *new_data; @@ -522,12 +523,24 @@ view_editor_save(struct focusable *focusable, EventRec HLock((*(view_editor->te))->hText); len = (*(view_editor->te))->teLength; + + if (view_editor->view_id == DB_TEXT_MENU_OPTIONS_ID) { + if (!main_menu_parse(*(*(view_editor->te))->hText, len)) { + HUnlock((*(view_editor->te))->hText); + HUnlock(view_editor->te); + return; + } + } + bile_write(db->bile, DB_TEXT_TYPE, view_editor->view_id, *(*(view_editor->te))->hText, len); HUnlock((*(view_editor->te))->hText); HUnlock(view_editor->te); + if (view_editor->view_id == DB_TEXT_MENU_OPTIONS_ID) + main_menu_init(); + view_editor_close(focusable, event); } --- settings.h Wed Jun 22 15:25:39 2022 +++ settings.h Tue Nov 8 14:34:51 2022 @@ -35,9 +35,9 @@ struct struct_field { unsigned short max; }; -short struct_editor(struct session *s, struct struct_field *fields, - size_t nfields, void *data, size_t dsize, void **result, char *title, - char *prompt); +short struct_editor(struct session *s, const struct struct_field *fields, + const size_t nfields, void *data, size_t dsize, void **result, + char *title, char *prompt); void view_editor_show(size_t id, char *title); #endif --- subtext.h Sat Oct 1 21:29:19 2022 +++ subtext.h Wed Nov 9 22:04:37 2022 @@ -36,7 +36,8 @@ #define BBS_MENU_ID 130 #define BBS_MENU_VIEWS_ID 1 -#define BBS_MENU_OPEN_CONSOLE_ID 3 +#define BBS_MENU_EDIT_MENUS_ID 2 +#define BBS_MENU_OPEN_CONSOLE_ID 4 #define VIEWS_SUBMENU_ID 132 #define VIEWS_SUBMENU_ISSUE_ID 1 @@ -46,6 +47,8 @@ #define VIEWS_SUBMENU_PAGE_SYSOP_ID 5 #define VIEWS_SUBMENU_NO_FREE_NODES_ID 6 #define VIEWS_SUBMENU_SIGNOFF_ID 7 + +#define MENU_DEFAULTS_ID 128 #define STR_LAST_DB 128