[Concept,12/19] cmd: editenv: Add -e flag for expo-based editing

Message ID 20260130035849.3580212-13-simon.glass@canonical.com
State New
Headers
Series Enhanced command-line editing with undo/redo support |

Commit Message

Simon Glass Jan. 30, 2026, 3:58 a.m. UTC
  Add a Kconfig option CMD_EDITENV_EXPO to enable graphical environment
variable editing using the expo framework. When enabled, users can use
'editenv -e varname' to edit a variable using a textedit widget in a
graphical interface.

The expo-based editor creates a simple scene with a textedit object,
allowing the user to edit the variable value with full cursor movement
support. Press Enter to accept changes or Escape to cancel.

The implementation is in boot/editenv.c controlled by EXPO_EDITENV,
allowing it to be used without CONFIG_CMDLINE. The command support
(CMD_EDITENV_EXPO) depends on EXPO_EDITENV.

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 boot/Kconfig        |   9 +++
 boot/Makefile       |   1 +
 boot/editenv.c      | 191 ++++++++++++++++++++++++++++++++++++++++++++
 cmd/Kconfig         |   9 +++
 cmd/nvedit.c        |  50 ++++++++++--
 include/expo.h      |  64 +++++++++++++++
 test/boot/Makefile  |   1 +
 test/boot/editenv.c |  94 ++++++++++++++++++++++
 8 files changed, 411 insertions(+), 8 deletions(-)
 create mode 100644 boot/editenv.c
 create mode 100644 test/boot/editenv.c
  

Patch

diff --git a/boot/Kconfig b/boot/Kconfig
index 7a8f9862ba7..e117e5b0479 100644
--- a/boot/Kconfig
+++ b/boot/Kconfig
@@ -1012,6 +1012,15 @@  config EXPO_LOG_FILTER
 	  Only objects whose name contains the filter string are logged. This
 	  is useful for debugging specific expo objects.
 
+config EXPO_EDITENV
+	bool "Expo-based environment variable editor"
+	depends on EXPO
+	default y if SANDBOX
+	help
+	  Enable a graphical environment variable editor using expo. This
+	  provides a textedit widget for editing environment variables with
+	  full cursor movement support.
+
 config BOOTMETH_SANDBOX
 	def_bool y
 	depends on SANDBOX
diff --git a/boot/Makefile b/boot/Makefile
index b9129a174c7..b2a475d4917 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -62,6 +62,7 @@  obj-$(CONFIG_$(PHASE_)LOAD_FIT) += common_fit.o
 obj-$(CONFIG_$(PHASE_)EXPO) += expo.o scene.o expo_build.o
 obj-$(CONFIG_$(PHASE_)EXPO_DUMP) += expo_dump.o
 obj-$(CONFIG_$(PHASE_)EXPO) += scene_menu.o scene_textline.o scene_textedit.o scene_txtin.o
+obj-$(CONFIG_$(PHASE_)EXPO_EDITENV) += editenv.o
 obj-$(CONFIG_$(PHASE_)EXPO_TEST) += expo_test.o
 ifdef CONFIG_COREBOOT_SYSINFO
 obj-$(CONFIG_$(PHASE_)EXPO) += expo_build_cb.o
diff --git a/boot/editenv.c b/boot/editenv.c
new file mode 100644
index 00000000000..de7df6fe3fa
--- /dev/null
+++ b/boot/editenv.c
@@ -0,0 +1,191 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Expo-based environment variable editor
+ *
+ * Copyright 2025 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#include <dm.h>
+#include <expo.h>
+#include <video.h>
+#include <video_console.h>
+
+/* IDs for expo objects */
+enum {
+	EDITENV_SCENE = EXPOID_BASE_ID + 1,
+	EDITENV_OBJ_TEXTEDIT,
+	EDITENV_OBJ_LABEL,
+	EDITENV_OBJ_EDIT,
+};
+
+static int editenv_setup(struct expo *exp, struct udevice *dev,
+			 const char *varname, const char *value,
+			 struct editenv_info *info)
+{
+	struct scene_obj_txtedit *ted;
+	struct scene *scn;
+	const char *name;
+	uint font_size;
+	int ret;
+
+	ret = expo_set_display(exp, dev);
+	if (ret)
+		return log_msg_ret("dis", ret);
+
+	ret = vidconsole_get_font_size(exp->cons, NULL, &name, &font_size);
+	if (ret)
+		font_size = 16;
+
+	exp->theme.font_size = font_size;
+	exp->theme.textline_label_margin_x = 10;
+
+	ret = scene_new(exp, "edit", EDITENV_SCENE, &scn);
+	if (ret < 0)
+		return log_msg_ret("scn", ret);
+
+	ret = scene_texted(scn, "textedit", EDITENV_OBJ_TEXTEDIT, 70, &ted);
+	if (ret < 0)
+		return log_msg_ret("ted", ret);
+
+	ret = scene_obj_set_bbox(scn, EDITENV_OBJ_TEXTEDIT, 50, 200, 1300, 400);
+	if (ret < 0)
+		return log_msg_ret("sbb", ret);
+
+	/* Create the label text object */
+	ret = scene_txt_str(scn, "label", EDITENV_OBJ_LABEL, 0, varname, NULL);
+	if (ret < 0)
+		return log_msg_ret("lab", ret);
+
+	ted->tin.label_id = EDITENV_OBJ_LABEL;
+
+	/* Create the edit text object pointing to the textedit buffer */
+	ret = scene_txt_str(scn, "edit", EDITENV_OBJ_EDIT, 0,
+			    abuf_data(&ted->tin.buf), NULL);
+	if (ret < 0)
+		return log_msg_ret("edi", ret);
+
+	ted->tin.edit_id = EDITENV_OBJ_EDIT;
+
+	ret = expo_apply_theme(exp, true);
+	if (ret)
+		return log_msg_ret("thm", ret);
+
+	/* Copy initial value into the textedit buffer */
+	if (value)
+		strlcpy(abuf_data(&ted->tin.buf), value,
+			abuf_size(&ted->tin.buf));
+
+	ret = expo_set_scene_id(exp, EDITENV_SCENE);
+	if (ret)
+		return log_msg_ret("sid", ret);
+
+	/* Set the textedit as highlighted and open for editing */
+	scene_set_highlight_id(scn, EDITENV_OBJ_TEXTEDIT);
+	ret = scene_set_open(scn, EDITENV_OBJ_TEXTEDIT, true);
+	if (ret)
+		return log_msg_ret("ope", ret);
+
+	expo_enter_mode(exp);
+
+	info->exp = exp;
+	info->scn = scn;
+	info->ted = ted;
+
+	ret = scene_arrange(scn);
+	if (ret)
+		return log_msg_ret("arr", ret);
+
+	ret = expo_render(exp);
+	if (ret)
+		return log_msg_ret("ren", ret);
+
+	return 0;
+}
+
+int expo_editenv_init(const char *varname, const char *value,
+		      struct editenv_info *info)
+{
+	struct udevice *dev;
+	struct expo *exp;
+	int ret;
+
+	ret = uclass_first_device_err(UCLASS_VIDEO, &dev);
+	if (ret)
+		return log_msg_ret("vid", ret);
+
+	ret = expo_new("editenv", NULL, &exp);
+	if (ret)
+		return log_msg_ret("exp", ret);
+
+	ret = editenv_setup(exp, dev, varname, value, info);
+	if (ret) {
+		expo_destroy(exp);
+		return log_msg_ret("set", ret);
+	}
+
+	return 0;
+}
+
+int expo_editenv_poll(struct editenv_info *info)
+{
+	struct expo_action act;
+	int ret;
+
+	ret = scene_arrange(info->scn);
+	if (ret)
+		return log_msg_ret("arr", ret);
+
+	ret = expo_render(info->exp);
+	if (ret)
+		return log_msg_ret("ren", ret);
+
+	ret = expo_poll(info->exp, &act);
+	if (ret == -EAGAIN)
+		return -EAGAIN;
+	if (ret)
+		return log_msg_ret("pol", ret);
+
+	if (act.type == EXPOACT_QUIT)
+		return -ECANCELED;
+
+	if (act.type == EXPOACT_CLOSE)
+		return 0;
+
+	return -EAGAIN;
+}
+
+void expo_editenv_uninit(struct editenv_info *info)
+{
+	expo_exit_mode(info->exp);
+	expo_destroy(info->exp);
+}
+
+const char *expo_editenv_result(struct editenv_info *info)
+{
+	return abuf_data(&info->ted->tin.buf);
+}
+
+int expo_editenv(const char *varname, const char *value, char *buf, int size)
+{
+	struct editenv_info info;
+	int ret;
+
+	ret = expo_editenv_init(varname, value, &info);
+	if (ret)
+		return log_msg_ret("ini", ret);
+
+	/* Render and process input */
+	while (1) {
+		ret = expo_editenv_poll(&info);
+		if (ret != -EAGAIN)
+			break;
+	}
+
+	if (!ret)
+		strlcpy(buf, expo_editenv_result(&info), size);
+
+	expo_editenv_uninit(&info);
+
+	return ret;
+}
diff --git a/cmd/Kconfig b/cmd/Kconfig
index 448a6e9fe39..606a34f8869 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -707,6 +707,15 @@  config CMD_EDITENV
 	help
 	  Edit environment variable.
 
+config CMD_EDITENV_EXPO
+	bool "editenv expo support"
+	depends on CMD_EDITENV && EXPO_EDITENV
+	default y if EXPO_EDITENV
+	help
+	  Enable the -e flag for the editenv command, which provides a
+	  graphical editor using the expo framework. This requires a video
+	  console.
+
 config CMD_GREPENV
 	bool "search env"
 	help
diff --git a/cmd/nvedit.c b/cmd/nvedit.c
index f67c268da84..f62b4cca242 100644
--- a/cmd/nvedit.c
+++ b/cmd/nvedit.c
@@ -29,6 +29,7 @@ 
 #include <console.h>
 #include <env.h>
 #include <env_internal.h>
+#include <expo.h>
 #include <log.h>
 #include <search.h>
 #include <errno.h>
@@ -427,31 +428,55 @@  static int do_env_edit(struct cmd_tbl *cmdtp, int flag, int argc,
 		       char *const argv[])
 {
 	char buffer[CONFIG_SYS_CBSIZE];
+	bool use_expo = false;
+	const char *varname;
 	char *init_val;
 
+	if (IS_ENABLED(CONFIG_CMD_EDITENV_EXPO) &&
+	    argc >= 2 && !strcmp(argv[1], "-e")) {
+		use_expo = true;
+		argc--;
+		argv++;
+	}
+
 	if (argc < 2)
 		return CMD_RET_USAGE;
 
+	varname = argv[1];
+
 	/* before import into hashtable */
 	if (!(gd->flags & GD_FLG_ENV_READY))
 		return 1;
 
-	/* Set read buffer to initial value or empty sting */
-	init_val = env_get(argv[1]);
+	/* Set read buffer to initial value or empty string */
+	init_val = env_get(varname);
 	if (init_val)
 		snprintf(buffer, CONFIG_SYS_CBSIZE, "%s", init_val);
 	else
 		buffer[0] = '\0';
 
-	if (cli_readline_into_buffer("edit: ", buffer, 0) < 0)
-		return 1;
+	if (IS_ENABLED(CONFIG_CMD_EDITENV_EXPO) && use_expo) {
+		int ret;
+
+		ret = expo_editenv(varname, init_val, buffer,
+				   CONFIG_SYS_CBSIZE);
+		if (ret == -EAGAIN)
+			return 0;	/* User cancelled, no change */
+		if (ret) {
+			printf("Edit failed (err=%d)\n", ret);
+			return CMD_RET_FAILURE;
+		}
+	} else {
+		if (cli_readline_into_buffer("edit: ", buffer, 0) < 0)
+			return 1;
+	}
 
 	if (buffer[0] == '\0') {
-		const char * const _argv[3] = { "setenv", argv[1], NULL };
+		const char * const _argv[3] = { "setenv", varname, NULL };
 
 		return env_do_env_set(0, 2, (char * const *)_argv, H_INTERACTIVE);
 	} else {
-		const char * const _argv[4] = { "setenv", argv[1], buffer,
+		const char * const _argv[4] = { "setenv", varname, buffer,
 			NULL };
 
 		return env_do_env_set(0, 3, (char * const *)_argv, H_INTERACTIVE);
@@ -1065,7 +1090,7 @@  static struct cmd_tbl cmd_env_sub[] = {
 	U_BOOT_CMD_MKENT(default, 1, 0, do_env_default, "", ""),
 	U_BOOT_CMD_MKENT(delete, CONFIG_SYS_MAXARGS, 0, do_env_delete, "", ""),
 #if defined(CONFIG_CMD_EDITENV)
-	U_BOOT_CMD_MKENT(edit, 2, 0, do_env_edit, "", ""),
+	U_BOOT_CMD_MKENT(edit, 3, 0, do_env_edit, "", ""),
 #endif
 #if defined(CONFIG_CMD_ENV_CALLBACK)
 	U_BOOT_CMD_MKENT(callbacks, 1, 0, do_env_callback, "", ""),
@@ -1141,8 +1166,12 @@  U_BOOT_LONGHELP(env,
 	"      \"-k\": keep variables not defined in default environment\n"
 	"env delete [-f] var [...] - [forcibly] delete variable(s)\n"
 #if defined(CONFIG_CMD_EDITENV)
+#if defined(CONFIG_CMD_EDITENV_EXPO)
+	"env edit [-e] name - edit environment variable (-e for expo)\n"
+#else
 	"env edit name - edit environment variable\n"
 #endif
+#endif
 #if defined(CONFIG_CMD_ENV_EXISTS)
 	"env exists name - tests for existence of variable\n"
 #endif
@@ -1208,9 +1237,14 @@  U_BOOT_CMD(
 
 #if defined(CONFIG_CMD_EDITENV)
 U_BOOT_CMD_COMPLETE(
-	editenv, 2, 0,	do_env_edit,
+	editenv, 3, 0,	do_env_edit,
 	"edit environment variable",
+#if defined(CONFIG_CMD_EDITENV_EXPO)
+	"[-e] name\n"
+	"    -e - use expo (graphical editor)\n"
+#else
 	"name\n"
+#endif
 	"    - edit environment variable 'name'",
 	var_complete
 );
diff --git a/include/expo.h b/include/expo.h
index d63fbd0c8ad..5a35d72c58f 100644
--- a/include/expo.h
+++ b/include/expo.h
@@ -1181,6 +1181,70 @@  int expo_setup_theme(struct expo *exp, ofnode node);
  */
 int expo_apply_theme(struct expo *exp, bool do_objs);
 
+/**
+ * struct editenv_info - Context for environment-variable editing
+ *
+ * @exp: Expo being used
+ * @scn: Scene in the expo
+ * @ted: Textedit object for editing
+ */
+struct editenv_info {
+	struct expo *exp;
+	struct scene *scn;
+	struct scene_obj_txtedit *ted;
+};
+
+/**
+ * expo_editenv_init() - Set up a new editenv expo
+ *
+ * @varname: Name of the variable to edit
+ * @value: Initial value (may be NULL)
+ * @info: Returns info about the editenv state
+ * Return: 0 if OK, -ve on error
+ */
+int expo_editenv_init(const char *varname, const char *value,
+		      struct editenv_info *info);
+
+/**
+ * expo_editenv_poll() - Poll for user input
+ *
+ * @info: Editenv info
+ * Return: 0 if editing is complete, -EAGAIN if more polling is needed,
+ *	-ECANCELED if user quit, other -ve on error
+ */
+int expo_editenv_poll(struct editenv_info *info);
+
+/**
+ * expo_editenv_uninit() - Free resources used by editenv
+ *
+ * @info: Editenv info
+ */
+void expo_editenv_uninit(struct editenv_info *info);
+
+/**
+ * expo_editenv_result() - Get the result string from editenv
+ *
+ * @info: Editenv info
+ * Return: Pointer to the edited string
+ */
+const char *expo_editenv_result(struct editenv_info *info);
+
+/**
+ * expo_editenv() - Edit an environment variable using expo
+ *
+ * Creates a simple expo with a textedit object to edit the variable.
+ * This is a convenience function that calls expo_editenv_init(),
+ * expo_editenv_poll() in a loop, and expo_editenv_uninit().
+ *
+ * @varname: Name of the variable to edit
+ * @value: Initial value (may be NULL)
+ * @buf: Buffer to receive the edited text
+ * @size: Size of buf
+ * Return: 0 if OK and text was edited, -ECANCELED if cancelled, other -ve on
+ *	error
+ */
+int expo_editenv(const char *varname, const char *value, char *buf, int size);
+
 /**
  * expo_build() - Build an expo from an FDT description
  *
diff --git a/test/boot/Makefile b/test/boot/Makefile
index ceb863969dd..329f4acbd52 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -10,6 +10,7 @@  obj-$(CONFIG_$(PHASE_)FIT_PRINT) += fit_print.o
 obj-$(CONFIG_BLK_LUKS) += luks.o
 
 obj-$(CONFIG_EXPO) += expo.o expo_common.o
+obj-$(CONFIG_EXPO_EDITENV) += editenv.o
 obj-$(CONFIG_CEDIT) += cedit.o expo_common.o
 obj-$(CONFIG_UT_BOOTCTL) += bootctl/
 endif
diff --git a/test/boot/editenv.c b/test/boot/editenv.c
new file mode 100644
index 00000000000..02bc025e216
--- /dev/null
+++ b/test/boot/editenv.c
@@ -0,0 +1,94 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Test for expo environment editor
+ *
+ * Copyright 2025 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#include <dm.h>
+#include <env.h>
+#include <expo.h>
+#include <video.h>
+#include <test/ut.h>
+#include <test/video.h>
+#include "bootstd_common.h"
+
+/* Check expo_editenv() basic functionality */
+static int editenv_test_base(struct unit_test_state *uts)
+{
+	char buf[256];
+	int ret;
+
+	/*
+	 * Type "test" then press Enter to accept
+	 * \x0d is Ctrl-M (Enter/carriage return)
+	 */
+	console_in_puts("test\x0d");
+	ret = expo_editenv("myvar", NULL, buf, sizeof(buf));
+	ut_assertok(ret);
+	ut_asserteq_str("test", buf);
+
+	return 0;
+}
+BOOTSTD_TEST(editenv_test_base, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* Check expo_editenv() with initial value - prepend text */
+static int editenv_test_initial(struct unit_test_state *uts)
+{
+	char buf[256];
+	int ret;
+
+	/*
+	 * Start with "world", go to start with Ctrl-A, type "hello ", then
+	 * press Enter
+	 */
+	console_in_puts("\x01hello \x0d");
+	ret = expo_editenv("myvar", "world", buf, sizeof(buf));
+	ut_assertok(ret);
+	ut_asserteq_str("hello world", buf);
+
+	return 0;
+}
+BOOTSTD_TEST(editenv_test_initial, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* Check expo_editenv() escape closes editor (accepts current value) */
+static int editenv_test_escape(struct unit_test_state *uts)
+{
+	char buf[256];
+	int ret;
+
+	/*
+	 * Press Escape immediately - this closes the editor and accepts
+	 * the current (initial) value
+	 */
+	console_in_puts("\x1b");
+	ret = expo_editenv("myvar", "unchanged", buf, sizeof(buf));
+	ut_assertok(ret);
+	ut_asserteq_str("unchanged", buf);
+
+	return 0;
+}
+BOOTSTD_TEST(editenv_test_escape, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);
+
+/* Check expo_editenv() renders correctly */
+static int editenv_test_video(struct unit_test_state *uts)
+{
+	struct udevice *dev;
+	char buf[256];
+	int ret;
+
+	ut_assertok(uclass_first_device_err(UCLASS_VIDEO, &dev));
+
+	/* Type "abc" then press Enter */
+	console_in_puts("abc\x0d");
+	ret = expo_editenv("testvar", "initial", buf, sizeof(buf));
+	ut_assertok(ret);
+	ut_asserteq_str("initialabc", buf);
+
+	/* Check the framebuffer has expected content */
+	ut_asserteq(1029, video_compress_fb(uts, dev, false));
+
+	return 0;
+}
+BOOTSTD_TEST(editenv_test_video, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE);