]> git.ipfire.org Git - thirdparty/ipxe.git/commitdiff
[form] Add support for dynamically created interactive forms
authorMichael Brown <mcb30@ipxe.org>
Fri, 31 May 2024 09:10:53 +0000 (10:10 +0100)
committerMichael Brown <mcb30@ipxe.org>
Thu, 20 Jun 2024 23:28:46 +0000 (16:28 -0700)
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 <mcb30@ipxe.org>
src/config/config.c
src/config/general.h
src/hci/commands/dynui_cmd.c
src/hci/tui/form_ui.c [new file with mode: 0644]
src/include/ipxe/dynui.h
src/include/ipxe/errfile.h

index 6667123042f4039c18e4b4d509a4bd2e66fd0048..6bcd3c1ca6223a50f81b7ea0d94f6c1cfebcffbc 100644 (file)
@@ -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
index e883e072fad543556a70bdafd8e687a8b7506328..f936e874cc4c426d70b620531ccdea51dd4ed009 100644 (file)
@@ -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 */
index 5aed3d0341d24d8457eed0f12d825bb4d9c3d9b8..d4446dc7cc145f05237cf40d2d5ac3717ca78acb 100644 (file)
@@ -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,12 +289,72 @@ 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 = {
        {
                .name = "menu",
                .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 (file)
index 0000000..6cc28c3
--- /dev/null
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2024 Michael Brown <mbrown@fensystems.co.uk>.
+ *
+ * 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 <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ipxe/ansicol.h>
+#include <ipxe/dynui.h>
+#include <ipxe/jumpscroll.h>
+#include <ipxe/settings.h>
+#include <ipxe/editbox.h>
+#include <ipxe/message.h>
+
+/** 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;
+}
index 5029e5850511e55e284aabb63d3d69c37d272bc2..67eb8b8f8d9a566c031d486ab3c383625a6f2058 100644 (file)
@@ -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 */
index d75661ba756dfa5c193074d79b85a5dd1c9ad96a..fcb4f0e68c70de0366b06f29fd35850ee6ba3da6 100644 (file)
@@ -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 )
 
 /** @} */