aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Brown <mcb30@ipxe.org>2024-05-31 10:10:53 +0100
committerMichael Brown <mcb30@ipxe.org>2024-06-20 16:28:46 -0700
commitf417f0b6a56956137d75c77f344d798f6b30a27c (patch)
treec87b523ab9f64a267555f20e8ffef84d40c6c87d
parent1c3c5e2b22ca31bbf77c39aef51671d0b6e95767 (diff)
downloadipxe-f417f0b6a56956137d75c77f344d798f6b30a27c.zip
ipxe-f417f0b6a56956137d75c77f344d798f6b30a27c.tar.gz
ipxe-f417f0b6a56956137d75c77f344d798f6b30a27c.tar.bz2
[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 <mcb30@ipxe.org>
-rw-r--r--src/config/config.c3
-rw-r--r--src/config/general.h1
-rw-r--r--src/hci/commands/dynui_cmd.c66
-rw-r--r--src/hci/tui/form_ui.c544
-rw-r--r--src/include/ipxe/dynui.h1
-rw-r--r--src/include/ipxe/errfile.h1
6 files changed, 616 insertions, 0 deletions
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 <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;
+}
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 )
/** @} */