From f417f0b6a56956137d75c77f344d798f6b30a27c Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Fri, 31 May 2024 10:10:53 +0100 Subject: [form] Add support for dynamically created interactive forms Add support for presenting a dynamic user interface as an interactive form, alongside the existing support for presenting a dynamic user interface as a menu. An interactive form may be used to allow a user to input (or edit) values for multiple settings on a single screen, as a user-friendly alternative to prompting for setting values via the "read" command. In the present implementation, all input fields must fit on a single screen (with no scrolling), and the only supported widget type is an editable text box. Signed-off-by: Michael Brown --- src/config/config.c | 3 + src/config/general.h | 1 + src/hci/commands/dynui_cmd.c | 66 ++++++ src/hci/tui/form_ui.c | 544 +++++++++++++++++++++++++++++++++++++++++++ src/include/ipxe/dynui.h | 1 + src/include/ipxe/errfile.h | 1 + 6 files changed, 616 insertions(+) create mode 100644 src/hci/tui/form_ui.c diff --git a/src/config/config.c b/src/config/config.c index 6667123..6bcd3c1 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -227,6 +227,9 @@ REQUIRE_OBJECT ( sanboot_cmd ); #ifdef MENU_CMD REQUIRE_OBJECT ( dynui_cmd ); #endif +#ifdef FORM_CMD +REQUIRE_OBJECT ( dynui_cmd ); +#endif #ifdef LOGIN_CMD REQUIRE_OBJECT ( login_cmd ); #endif diff --git a/src/config/general.h b/src/config/general.h index e883e07..f936e87 100644 --- a/src/config/general.h +++ b/src/config/general.h @@ -145,6 +145,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #define DHCP_CMD /* DHCP management commands */ #define SANBOOT_CMD /* SAN boot commands */ #define MENU_CMD /* Menu commands */ +#define FORM_CMD /* Form commands */ #define LOGIN_CMD /* Login command */ #define SYNC_CMD /* Sync command */ #define SHELL_CMD /* Shell command */ diff --git a/src/hci/commands/dynui_cmd.c b/src/hci/commands/dynui_cmd.c index 5aed3d0..d4446dc 100644 --- a/src/hci/commands/dynui_cmd.c +++ b/src/hci/commands/dynui_cmd.c @@ -126,6 +126,8 @@ struct item_options { static struct option_descriptor item_opts[] = { OPTION_DESC ( "menu", 'm', required_argument, struct item_options, dynui, parse_string ), + OPTION_DESC ( "form", 'f', required_argument, + struct item_options, dynui, parse_string ), OPTION_DESC ( "key", 'k', required_argument, struct item_options, key, parse_key ), OPTION_DESC ( "default", 'd', no_argument, @@ -287,6 +289,62 @@ static int choose_exec ( int argc, char **argv ) { return rc; } +/** "present" options */ +struct present_options { + /** Dynamic user interface name */ + char *dynui; + /** Keep dynamic user interface */ + int keep; +}; + +/** "present" option list */ +static struct option_descriptor present_opts[] = { + OPTION_DESC ( "form", 'f', required_argument, + struct present_options, dynui, parse_string ), + OPTION_DESC ( "keep", 'k', no_argument, + struct present_options, keep, parse_flag ), +}; + +/** "present" command descriptor */ +static struct command_descriptor present_cmd = + COMMAND_DESC ( struct present_options, present_opts, 0, 0, NULL ); + +/** + * The "present" command + * + * @v argc Argument count + * @v argv Argument list + * @ret rc Return status code + */ +static int present_exec ( int argc, char **argv ) { + struct present_options opts; + struct dynamic_ui *dynui; + int rc; + + /* Parse options */ + if ( ( rc = parse_options ( argc, argv, &present_cmd, &opts ) ) != 0 ) + goto err_parse_options; + + /* Identify dynamic user interface */ + if ( ( rc = parse_dynui ( opts.dynui, &dynui ) ) != 0 ) + goto err_parse_dynui; + + /* Show as form */ + if ( ( rc = show_form ( dynui ) ) != 0 ) + goto err_show_form; + + /* Success */ + rc = 0; + + err_show_form: + /* Destroy dynamic user interface, if applicable */ + if ( ! opts.keep ) + destroy_dynui ( dynui ); + err_parse_dynui: + err_parse_options: + return rc; +} + /** Dynamic user interface commands */ struct command dynui_commands[] __command = { { @@ -294,6 +352,10 @@ struct command dynui_commands[] __command = { .exec = dynui_exec, }, { + .name = "form", + .exec = dynui_exec, + }, + { .name = "item", .exec = item_exec, }, @@ -301,4 +363,8 @@ struct command dynui_commands[] __command = { .name = "choose", .exec = choose_exec, }, + { + .name = "present", + .exec = present_exec, + }, }; diff --git a/src/hci/tui/form_ui.c b/src/hci/tui/form_ui.c new file mode 100644 index 0000000..6cc28c3 --- /dev/null +++ b/src/hci/tui/form_ui.c @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2024 Michael Brown . + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * You can also choose to distribute this program under the terms of + * the Unmodified Binary Distribution Licence (as given in the file + * COPYING.UBDL), provided that you have satisfied its requirements. + */ + +FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); + +/** @file + * + * Text widget forms + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** Form title row */ +#define TITLE_ROW 1U + +/** Starting control row */ +#define START_ROW 3U + +/** Ending control row */ +#define END_ROW ( LINES - 3U ) + +/** Instructions row */ +#define INSTRUCTION_ROW ( LINES - 2U ) + +/** Padding between instructions */ +#define INSTRUCTION_PAD " " + +/** Input field width */ +#define INPUT_WIDTH ( COLS / 2U ) + +/** Input field column */ +#define INPUT_COL ( ( COLS - INPUT_WIDTH ) / 2U ) + +/** A form */ +struct form { + /** Dynamic user interface */ + struct dynamic_ui *dynui; + /** Jump scroller */ + struct jump_scroller scroll; + /** Array of form controls */ + struct form_control *controls; +}; + +/** A form control */ +struct form_control { + /** Dynamic user interface item */ + struct dynamic_item *item; + /** Settings block */ + struct settings *settings; + /** Setting */ + struct setting setting; + /** Label row */ + unsigned int row; + /** Editable text box */ + struct edit_box editbox; + /** Modifiable setting name */ + char *name; + /** Modifiable setting value */ + char *value; + /** Most recent error in saving */ + int rc; +}; + +/** + * Allocate form + * + * @v dynui Dynamic user interface + * @ret form Form, or NULL on error + */ +static struct form * alloc_form ( struct dynamic_ui *dynui ) { + struct form *form; + struct form_control *control; + struct dynamic_item *item; + char *name; + size_t len; + + /* Calculate total length */ + len = sizeof ( *form ); + list_for_each_entry ( item, &dynui->items, list ) { + len += sizeof ( *control ); + if ( item->name ) + len += ( strlen ( item->name ) + 1 /* NUL */ ); + } + + /* Allocate and initialise structure */ + form = zalloc ( len ); + if ( ! form ) + return NULL; + control = ( ( ( void * ) form ) + sizeof ( *form ) ); + name = ( ( ( void * ) control ) + + ( dynui->count * sizeof ( *control ) ) ); + form->dynui = dynui; + form->controls = control; + list_for_each_entry ( item, &dynui->items, list ) { + control->item = item; + if ( item->name ) { + control->name = name; + name = ( stpcpy ( name, item->name ) + 1 /* NUL */ ); + } + control++; + } + assert ( ( ( void * ) name ) == ( ( ( void * ) form ) + len ) ); + + return form; +} + +/** + * Free form + * + * @v form Form + */ +static void free_form ( struct form *form ) { + unsigned int i; + + /* Free input value buffers */ + for ( i = 0 ; i < form->dynui->count ; i++ ) + free ( form->controls[i].value ); + + /* Free form */ + free ( form ); +} + +/** + * Assign form rows + * + * @v form Form + * @ret rc Return status code + */ +static int layout_form ( struct form *form ) { + struct form_control *control; + struct dynamic_item *item; + unsigned int labels = 0; + unsigned int inputs = 0; + unsigned int pad_control = 0; + unsigned int pad_label = 0; + unsigned int minimum; + unsigned int remaining; + unsigned int between; + unsigned int row; + unsigned int flags; + unsigned int i; + + /* Count labels and inputs */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + item = control->item; + if ( item->text[0] ) + labels++; + if ( item->name ) { + if ( ! inputs ) + form->scroll.current = i; + inputs++; + if ( item->flags & DYNUI_DEFAULT ) + form->scroll.current = i; + form->scroll.count = ( i + 1 ); + } + } + form->scroll.rows = form->scroll.count; + DBGC ( form, "FORM %p has %d controls (%d labels, %d inputs)\n", + form, form->dynui->count, labels, inputs ); + + /* Refuse to create forms with no inputs */ + if ( ! inputs ) + return -EINVAL; + + /* Calculate minimum number of rows */ + minimum = ( labels + ( inputs * 2 /* edit box and error message */ ) ); + remaining = ( END_ROW - START_ROW ); + DBGC ( form, "FORM %p has %d (of %d) usable rows\n", + form, remaining, LINES ); + if ( minimum > remaining ) + return -ERANGE; + remaining -= minimum; + + /* Insert blank row between controls, if space exists */ + between = ( form->dynui->count - 1 ); + if ( between <= remaining ) { + pad_control = 1; + remaining -= between; + DBGC ( form, "FORM %p padding between controls\n", form ); + } + + /* Insert blank row after label, if space exists */ + if ( labels <= remaining ) { + pad_label = 1; + remaining -= labels; + DBGC ( form, "FORM %p padding after labels\n", form ); + } + + /* Centre on screen */ + DBGC ( form, "FORM %p has %d spare rows\n", form, remaining ); + row = ( START_ROW + ( remaining / 2 ) ); + + /* Position each control */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + item = control->item; + if ( item->text[0] ) { + control->row = row; + row++; /* Label text */ + row += pad_label; + } + if ( item->name ) { + flags = ( ( item->flags & DYNUI_SECRET ) ? + WIDGET_SECRET : 0 ); + init_editbox ( &control->editbox, row, INPUT_COL, + INPUT_WIDTH, flags, &control->value ); + row++; /* Edit box */ + row++; /* Error message (if any) */ + } + row += pad_control; + } + assert ( row <= END_ROW ); + + return 0; +} + +/** + * Draw form + * + * @v form Form + */ +static void draw_form ( struct form *form ) { + struct form_control *control; + unsigned int i; + + /* Clear screen */ + color_set ( CPAIR_NORMAL, NULL ); + erase(); + + /* Draw title, if any */ + attron ( A_BOLD ); + if ( form->dynui->title ) + msg ( TITLE_ROW, "%s", form->dynui->title ); + attroff ( A_BOLD ); + + /* Draw controls */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + + /* Draw label, if any */ + if ( control->row ) + msg ( control->row, "%s", control->item->text ); + + /* Draw input, if any */ + if ( control->name ) + draw_widget ( &control->editbox.widget ); + } + + /* Draw instructions */ + msg ( INSTRUCTION_ROW, "%s", "Ctrl-X - save changes" + INSTRUCTION_PAD "Ctrl-C - discard changes" ); +} + +/** + * Draw (or clear) error messages + * + * @v form Form + */ +static void draw_errors ( struct form *form ) { + struct form_control *control; + unsigned int row; + unsigned int i; + + /* Draw (or clear) errors */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + + /* Skip non-input controls */ + if ( ! control->name ) + continue; + + /* Draw or clear error message as appropriate */ + row = ( control->editbox.widget.row + 1 ); + if ( control->rc != 0 ) { + color_set ( CPAIR_ALERT, NULL ); + msg ( row, " %s ", strerror ( control->rc ) ); + color_set ( CPAIR_NORMAL, NULL ); + } else { + clearmsg ( row ); + } + } +} + +/** + * Parse setting names + * + * @v form Form + * @ret rc Return status code + */ +static int parse_names ( struct form *form ) { + struct form_control *control; + unsigned int i; + int rc; + + /* Parse all setting names */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + + /* Skip labels */ + if ( ! control->name ) { + DBGC ( form, "FORM %p item %d is a label\n", form, i ); + continue; + } + + /* Parse setting name */ + DBGC ( form, "FORM %p item %d is for %s\n", + form, i, control->name ); + if ( ( rc = parse_setting_name ( control->name, + autovivify_child_settings, + &control->settings, + &control->setting ) ) != 0 ) + return rc; + + /* Apply default type if necessary */ + if ( ! control->setting.type ) + control->setting.type = &setting_type_string; + } + + return 0; +} + +/** + * Load current input values + * + * @v form Form + */ +static void load_values ( struct form *form ) { + struct form_control *control; + unsigned int i; + + /* Fetch all current setting values */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + if ( ! control->name ) + continue; + fetchf_setting_copy ( control->settings, &control->setting, + NULL, &control->setting, + &control->value ); + } +} + +/** + * Store current input values + * + * @v form Form + * @ret rc Return status code + */ +static int save_values ( struct form *form ) { + struct form_control *control; + unsigned int i; + int rc = 0; + + /* Store all current setting values */ + for ( i = 0 ; i < form->dynui->count ; i++ ) { + control = &form->controls[i]; + if ( ! control->name ) + continue; + control->rc = storef_setting ( control->settings, + &control->setting, + control->value ); + if ( control->rc != 0 ) + rc = control->rc; + } + + return rc; +} + +/** + * Submit form + * + * @v form Form + * @ret rc Return status code + */ +static int submit_form ( struct form *form ) { + int rc; + + /* Attempt to save values */ + rc = save_values ( form ); + + /* Draw (or clear) errors */ + draw_errors ( form ); + + return rc; +} + +/** + * Form main loop + * + * @v form Form + * @ret rc Return status code + */ +static int form_loop ( struct form *form ) { + struct jump_scroller *scroll = &form->scroll; + struct form_control *control; + struct dynamic_item *item; + unsigned int move; + unsigned int i; + int key; + int rc; + + /* Main loop */ + while ( 1 ) { + + /* Draw current input */ + control = &form->controls[scroll->current]; + draw_widget ( &control->editbox.widget ); + + /* Process keypress */ + key = edit_widget ( &control->editbox.widget, getkey ( 0 ) ); + + /* Handle scroll keys */ + move = jump_scroll_key ( &form->scroll, key ); + + /* Handle special keys */ + switch ( key ) { + case CTRL_C: + case ESC: + /* Cancel form */ + return -ECANCELED; + case KEY_ENTER: + /* Attempt to do the most intuitive thing when + * Enter is pressed. If we are on the last + * input, then submit the form. If we are + * editing an input which failed, then + * resubmit the form. Otherwise, move to the + * next input. + */ + if ( ( control->rc == 0 ) && + ( scroll->current < ( scroll->count - 1 ) ) ) { + move = SCROLL_DOWN; + break; + } + /* fall through */ + case CTRL_X: + /* Submit form */ + if ( ( rc = submit_form ( form ) ) == 0 ) + return 0; + /* If current input is not the problem, move + * to the first input that needs fixing. + */ + if ( control->rc == 0 ) { + for ( i = 0 ; i < form->dynui->count ; i++ ) { + if ( form->controls[i].rc != 0 ) { + scroll->current = i; + break; + } + } + } + break; + default: + /* Move to input with matching shortcut key, if any */ + item = dynui_shortcut ( form->dynui, key ); + if ( item ) { + scroll->current = item->index; + if ( ! item->name ) + move = SCROLL_DOWN; + } + break; + } + + /* Move selection, if applicable */ + while ( move ) { + move = jump_scroll_move ( &form->scroll, move ); + control = &form->controls[scroll->current]; + if ( control->name ) + break; + } + } +} + +/** + * Show form + * + * @v dynui Dynamic user interface + * @ret rc Return status code + */ +int show_form ( struct dynamic_ui *dynui ) { + struct form *form; + int rc; + + /* Allocate and initialise structure */ + form = alloc_form ( dynui ); + if ( ! form ) { + rc = -ENOMEM; + goto err_alloc; + } + + /* Parse setting names and load current values */ + if ( ( rc = parse_names ( form ) ) != 0 ) + goto err_parse_names; + load_values ( form ); + + /* Lay out form on screen */ + if ( ( rc = layout_form ( form ) ) != 0 ) + goto err_layout; + + /* Draw initial form */ + initscr(); + start_color(); + draw_form ( form ); + + /* Run main loop */ + if ( ( rc = form_loop ( form ) ) != 0 ) + goto err_loop; + + err_loop: + color_set ( CPAIR_NORMAL, NULL ); + endwin(); + err_layout: + err_parse_names: + free_form ( form ); + err_alloc: + return rc; +} diff --git a/src/include/ipxe/dynui.h b/src/include/ipxe/dynui.h index 5029e58..67eb8b8 100644 --- a/src/include/ipxe/dynui.h +++ b/src/include/ipxe/dynui.h @@ -61,5 +61,6 @@ extern struct dynamic_item * dynui_shortcut ( struct dynamic_ui *dynui, int key ); extern int show_menu ( struct dynamic_ui *dynui, unsigned long timeout, const char *select, struct dynamic_item **selected ); +extern int show_form ( struct dynamic_ui *dynui ); #endif /* _IPXE_DYNUI_H */ diff --git a/src/include/ipxe/errfile.h b/src/include/ipxe/errfile.h index d75661b..fcb4f0e 100644 --- a/src/include/ipxe/errfile.h +++ b/src/include/ipxe/errfile.h @@ -418,6 +418,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #define ERRFILE_des ( ERRFILE_OTHER | 0x00600000 ) #define ERRFILE_editstring ( ERRFILE_OTHER | 0x00610000 ) #define ERRFILE_widget_ui ( ERRFILE_OTHER | 0x00620000 ) +#define ERRFILE_form_ui ( ERRFILE_OTHER | 0x00630000 ) /** @} */ -- cgit v1.1