@@ -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
@@ -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
new file mode 100644
@@ -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;
+}
@@ -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
@@ -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
);
@@ -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
*
@@ -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
new file mode 100644
@@ -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);