[PATCH 00/19] Enhanced command-line editing with undo/redo support
This series adds enhanced editing features to the U-Boot command line, including multi-level undo/redo, yank/paste, word navigation, and multiline editing support for the expo textedit widget. Key features: - Multi-level undo (Ctrl+Z) and redo (Ctrl+Shift+Z) with configurable depth via CONFIG_CMDLINE_UNDO_COUNT - Yank/paste killed text with Ctrl+Y - Word navigation with Ctrl+Left/Right arrows - Home/End key support in sandbox SDL console - Multiline editing mode where Ctrl+K kills to end of line (not buffer) and Home/End navigate within the current line The series also includes test infrastructure improvements, fixes a memory leak in expo text object destruction, and fixes a buildman bug with merge_config.sh path resolution when using work directories. Simon Glass (19): buildman: Fix merge_config.sh path when using work directories buildman: Add option to build then show summary test: Add ut_check_video() helper function test: Add video_compress_fb_() with message parameter test: Display message at top right during video test delay expo: Free gen.lines alist when destroying text objects expo: Read all available input to keep up with key repeat input: Add Ctrl+arrow key support for sandbox SDL console input: Add Home and End key support for sandbox SDL console input: Add Ctrl+Shift+Z support for redo cli: Add a Kconfig for enhanced editing cmd: editenv: Add -e flag for expo-based editing test: Add editenv test for init/poll/uninit functions expo: Convert BKEY_UP/DOWN to control characters for text input cli: Add Ctrl+Left/Right arrow support for word navigation expo: Add SCENEOF_MULTILINE flag for textedit cli: Make Ctrl+K clear to end of line in multiline mode cli: Add Ctrl+Y to yank killed text cli: Add multi-level undo/redo support arch/sandbox/dts/cros-ec-keyboard.dtsi | 4 + boot/Kconfig | 9 + boot/Makefile | 1 + boot/editenv.c | 193 +++++++++++++++++++ boot/expo.c | 6 +- boot/scene.c | 2 + boot/scene_txtin.c | 73 ++++++- cmd/Kconfig | 47 ++++- cmd/nvedit.c | 50 ++++- common/Makefile | 1 + common/cli_getch.c | 40 ++++ common/cli_readline.c | 239 +++++++++++++++++++++-- common/cli_undo.c | 251 ++++++++++++++++++++++++ doc/develop/expo.rst | 21 ++ doc/usage/cmdline.rst | 68 +++++++ drivers/input/input.c | 54 ++++++ include/cli.h | 230 +++++++++++++++++++++- include/expo.h | 66 +++++++ include/test/test.h | 5 + include/test/video.h | 12 ++ test/boot/Makefile | 1 + test/boot/editenv.c | 253 +++++++++++++++++++++++++ test/boot/expo.c | 63 +++++- test/dm/video.c | 139 +++++++++++++- test/test-main.c | 2 + tools/buildman/buildman.rst | 5 +- tools/buildman/cfgutil.py | 21 +- tools/buildman/cmdline.py | 2 + tools/buildman/control.py | 4 + tools/buildman/test.py | 110 +++++++++++ tools/buildman/test_cfgutil.py | 48 +++++ 31 files changed, 1954 insertions(+), 66 deletions(-) create mode 100644 boot/editenv.c create mode 100644 common/cli_undo.c create mode 100644 test/boot/editenv.c -- 2.43.0 base-commit: a7d65f7fcee089ed97b46765e574279cad40d75f branch: exph
The run_merge_config() function constructs paths for merge_config.sh using out_dir and cfg_file which are relative to the original working directory. However, the commands run with cwd=src_dir (the work directory), so these paths resolve incorrectly. For example, with src_dir='../exph/.bm-work/00' and out_dir='../exph/.bm-work/00/build', the -O flag would pass the full out_dir path. When make runs from src_dir, it interprets this as '../exph/.bm-work/00/../exph/.bm-work/00/build', doubling the path. Fix this by converting out_dir and cfg_file to paths relative to src_dir using os.path.relpath(). This ensures the paths resolve correctly when commands execute from the work directory. Fixes: 635c5f5638a0 ("buildman: Use merge_config.sh for --adjust-cfg") Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/buildman/cfgutil.py | 21 ++++++++++----- tools/buildman/test_cfgutil.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/tools/buildman/cfgutil.py b/tools/buildman/cfgutil.py index cec33e1e62b..060f2762b96 100644 --- a/tools/buildman/cfgutil.py +++ b/tools/buildman/cfgutil.py @@ -368,9 +368,18 @@ def run_merge_config(src_dir, out_dir, cfg_file, adjust_cfg, env): # Create a minimal defconfig from the current .config # This is necessary for 'imply' to work - the full .config has # '# CONFIG_xxx is not set' lines that prevent imply from taking effect - defconfig_path = os.path.join(out_dir or '.', 'defconfig') - make_cmd = ['make', f'O={out_dir}' if out_dir else None, - f'KCONFIG_CONFIG={cfg_file}', 'savedefconfig'] + # + # Convert paths to be relative to src_dir since commands run with + # cwd=src_dir + if src_dir and out_dir: + rel_out_dir = os.path.relpath(out_dir, src_dir) + rel_cfg_file = os.path.relpath(cfg_file, src_dir) + else: + rel_out_dir = out_dir or '.' + rel_cfg_file = cfg_file + defconfig_path = os.path.join(rel_out_dir, 'defconfig') + make_cmd = ['make', f'O={rel_out_dir}' if rel_out_dir != '.' else None, + f'KCONFIG_CONFIG={rel_cfg_file}', 'savedefconfig'] make_cmd = [x for x in make_cmd if x] # Remove None elements result = command.run_one(*make_cmd, cwd=src_dir, env=env, capture=True, capture_stderr=True) @@ -382,10 +391,8 @@ def run_merge_config(src_dir, out_dir, cfg_file, adjust_cfg, env): try: # Run merge_config.sh with the minimal defconfig as base # -O sets output dir; defconfig is the base, fragment is merged - merge_script = os.path.join(src_dir or '.', 'scripts', 'kconfig', - 'merge_config.sh') - out = out_dir or '.' - cmd = [merge_script, '-O', out, defconfig_path, frag_path] + merge_script = os.path.join('scripts', 'kconfig', 'merge_config.sh') + cmd = [merge_script, '-O', rel_out_dir, defconfig_path, frag_path] result = command.run_one(*cmd, cwd=src_dir, env=env, capture=True, capture_stderr=True) finally: diff --git a/tools/buildman/test_cfgutil.py b/tools/buildman/test_cfgutil.py index 47e522d3d6c..b623a4c4f67 100644 --- a/tools/buildman/test_cfgutil.py +++ b/tools/buildman/test_cfgutil.py @@ -180,6 +180,54 @@ class TestAdjustCfg(unittest.TestCase): result) +class TestRunMergeConfig(unittest.TestCase): + """Tests for run_merge_config() function""" + + def test_merge_script_path(self): + """Test that merge_config.sh path is relative to cwd, not absolute""" + from unittest import mock + from u_boot_pylib import command + + # Track commands that were run + commands_run = [] + + def mock_run_one(*args, **kwargs): + commands_run.append((args, kwargs)) + result = command.CommandResult() + result.return_code = 0 + result.stdout = '' + result.stderr = '' + return result + + with mock.patch.object(command, 'run_one', mock_run_one): + with mock.patch('os.path.exists', return_value=True): + with mock.patch('os.unlink'): + # Use a work directory path like buildman does + src_dir = '../branch/.bm-work/00' + cfgutil.run_merge_config( + src_dir, 'build', 'build/.config', + {'LOCALVERSION_AUTO': '~LOCALVERSION_AUTO'}, {}) + + # Find the merge_config.sh command + merge_cmd = None + for args, kwargs in commands_run: + if args and 'merge_config.sh' in args[0]: + merge_cmd = args + merge_cwd = kwargs.get('cwd') + break + + self.assertIsNotNone(merge_cmd, 'merge_config.sh command not found') + + # The script path should be relative, not include src_dir + script_path = merge_cmd[0] + self.assertEqual('scripts/kconfig/merge_config.sh', script_path, + f'Script path should be relative, got: {script_path}') + + # The cwd should be src_dir + self.assertEqual(src_dir, merge_cwd, + f'cwd should be src_dir, got: {merge_cwd}') + + class TestProcessConfig(unittest.TestCase): """Tests for process_config() function""" -- 2.43.0
The -s option shows a summary of existing build results, but requires that a build has already been done. Add a -z/--build-summary option which performs the build first, then shows the summary afterwards. The summary is shown regardless of whether the build succeeds, and the return code reflects the build result. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/buildman/buildman.rst | 5 +- tools/buildman/cmdline.py | 2 + tools/buildman/control.py | 4 ++ tools/buildman/test.py | 110 ++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/tools/buildman/buildman.rst b/tools/buildman/buildman.rst index 603b019540b..831431670d3 100644 --- a/tools/buildman/buildman.rst +++ b/tools/buildman/buildman.rst @@ -111,7 +111,10 @@ Otherwise buildman will perform random actions. Use -n to check what the random actions might be. Buildman effectively has two modes: without -s it builds, with -s it -summarises the results of previous (or active) builds. +summarises the results of previous (or active) builds. You can combine +both with -z (--build-summary), which builds first and then shows the +summary. This is useful when you want to do a build and immediately see +a summary of the results without running buildman twice. If you just want to build the current source tree, leave off the -b flag. This will display results and errors as they happen. You can still look at diff --git a/tools/buildman/cmdline.py b/tools/buildman/cmdline.py index fae1910307b..f4a1a3d018c 100644 --- a/tools/buildman/cmdline.py +++ b/tools/buildman/cmdline.py @@ -185,6 +185,8 @@ def add_after_m(parser): parser.add_argument('-Y', '--filter-migration-warnings', action='store_true', default=False, help='Filter out migration warnings from output') + parser.add_argument('-z', '--build-summary', action='store_true', + default=False, help='Build first, then show a summary of the results') def parse_args(): diff --git a/tools/buildman/control.py b/tools/buildman/control.py index 728389d9a8b..7fc0d20574a 100644 --- a/tools/buildman/control.py +++ b/tools/buildman/control.py @@ -573,6 +573,10 @@ def run_builder(builder, commits, board_selected, display_options, args): fail, warned, excs = builder.build_boards( commits, board_selected, args.keep_outputs, args.verbose, args.fragments) + if args.build_summary: + builder.commits = commits + builder.result_handler.show_summary( + commits, board_selected, args.step) if excs: return 102 if fail: diff --git a/tools/buildman/test.py b/tools/buildman/test.py index 0f4a5b9e543..37930ad9720 100644 --- a/tools/buildman/test.py +++ b/tools/buildman/test.py @@ -1222,6 +1222,116 @@ class TestBuildMisc(TestBuildBase): 'other_board')) +class TestBuildSummary(TestBuildBase): + """Tests for build summary functionality""" + + def test_build_summary(self): + """Test --build-summary option builds then shows summary""" + opts = DisplayOptions( + show_errors=False, show_sizes=False, show_detail=False, + show_bloat=False, show_config=False, show_environment=False, + show_unknown=False, ide=False, list_error_boards=False) + build = builder.Builder(self.toolchains, self.base_dir, None, 1, + 2, self._col, ResultHandler(self._col, opts), + checkout=False) + build._result_handler.set_builder(build) + + # Track calls to build_boards and show_summary + build_boards_called = [] + show_summary_called = [] + + def mock_build_boards(*args, **kwargs): + build_boards_called.append(True) + return False, False, False + + def mock_show_summary(*args, **kwargs): + show_summary_called.append(True) + + build.build_boards = mock_build_boards + build._result_handler.show_summary = mock_show_summary + + # Create args with build_summary=True + class Args: + summary = False + build_summary = True + step = 1 + keep_outputs = False + verbose = False + fragments = '' + ignore_warnings = False + ide = False + filter_dtb_warnings = False + filter_migration_warnings = False + git = '.' + threads = 1 + jobs = 1 + + args = Args() + board_selected = self.brds.get_selected_dict() + + # Mock gnu_make detection + with patch.object(command, 'output', return_value='make'): + control.run_builder(build, self.commits, board_selected, + opts, args) + + # Verify both build_boards and show_summary were called + self.assertEqual(1, len(build_boards_called), + 'build_boards should be called once') + self.assertEqual(1, len(show_summary_called), + 'show_summary should be called once') + + def test_build_summary_with_failures(self): + """Test --build-summary shows summary even when build fails""" + opts = DisplayOptions( + show_errors=False, show_sizes=False, show_detail=False, + show_bloat=False, show_config=False, show_environment=False, + show_unknown=False, ide=False, list_error_boards=False) + build = builder.Builder(self.toolchains, self.base_dir, None, 1, + 2, self._col, ResultHandler(self._col, opts), + checkout=False) + build._result_handler.set_builder(build) + + show_summary_called = [] + + def mock_build_boards(*args, **kwargs): + # Simulate build failure + return True, False, False + + def mock_show_summary(*args, **kwargs): + show_summary_called.append(True) + + build.build_boards = mock_build_boards + build._result_handler.show_summary = mock_show_summary + + class Args: + summary = False + build_summary = True + step = 1 + keep_outputs = False + verbose = False + fragments = '' + ignore_warnings = False + ide = False + filter_dtb_warnings = False + filter_migration_warnings = False + git = '.' + threads = 1 + jobs = 1 + + args = Args() + board_selected = self.brds.get_selected_dict() + + with patch.object(command, 'output', return_value='make'): + ret = control.run_builder(build, self.commits, board_selected, + opts, args) + + # Summary should still be shown even with failures + self.assertEqual(1, len(show_summary_called), + 'show_summary should be called even on failure') + # Return code should indicate failure + self.assertEqual(100, ret) + + class TestBuilderFuncs(TestBuildBase): """Tests for individual Builder methods""" -- 2.43.0
Many video tests need to get the first video device and then call video_compress_fb() on it. Add a convenience function that combines these two operations to reduce duplication. The msg parameter allows tests to describe which frame is being checked, which helps with debugging when a check fails. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- include/test/video.h | 12 ++++++++++++ test/dm/video.c | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/include/test/video.h b/include/test/video.h index 000fd708c86..8676613e6a4 100644 --- a/include/test/video.h +++ b/include/test/video.h @@ -42,4 +42,16 @@ int video_compress_fb(struct unit_test_state *uts, struct udevice *dev, */ int video_check_copy_fb(struct unit_test_state *uts, struct udevice *dev); +/** + * ut_check_video() - Compress the frame buffer and return its size + * + * This is a convenience function that gets the first video device and calls + * video_compress_fb() on it. + * + * @uts: Test state + * @msg: Message describing the frame being checked (or NULL) + * Return: compressed size of the frame buffer, or -ve on error + */ +int ut_check_video(struct unit_test_state *uts, const char *msg); + #endif diff --git a/test/dm/video.c b/test/dm/video.c index 92b2ee9a6e3..b7aa835c86a 100644 --- a/test/dm/video.c +++ b/test/dm/video.c @@ -198,6 +198,18 @@ int video_check_copy_fb(struct unit_test_state *uts, struct udevice *dev) return 0; } +int ut_check_video(struct unit_test_state *uts, const char *msg) +{ + struct udevice *dev; + int ret; + + ret = uclass_first_device_err(UCLASS_VIDEO, &dev); + if (ret) + return ret; + + return video_compress_fb(uts, dev, false); +} + /* * Call this function at any point to halt and show the current display. Be * sure to run the test with the -l flag. -- 2.43.0
Add video_compress_fb_() which takes an additional msg parameter to describe the frame being checked. This helps with debugging when a frame check fails. The existing video_compress_fb() becomes a wrapper that passes NULL for the message. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/dm/video.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/dm/video.c b/test/dm/video.c index b7aa835c86a..421d50df064 100644 --- a/test/dm/video.c +++ b/test/dm/video.c @@ -138,8 +138,8 @@ static int video_write_bmp(struct unit_test_state *uts, struct udevice *dev, return ret; } -int video_compress_fb(struct unit_test_state *uts, struct udevice *dev, - bool use_copy) +static int video_compress_fb_(struct unit_test_state *uts, struct udevice *dev, + bool use_copy, const char *msg) { struct sandbox_state *state = state_get_current(); struct video_priv *priv = dev_get_uclass_priv(dev); @@ -184,6 +184,12 @@ int video_compress_fb(struct unit_test_state *uts, struct udevice *dev, return destlen; } +int video_compress_fb(struct unit_test_state *uts, struct udevice *dev, + bool use_copy) +{ + return video_compress_fb_(uts, dev, use_copy, NULL); +} + int video_check_copy_fb(struct unit_test_state *uts, struct udevice *dev) { struct video_priv *priv = dev_get_uclass_priv(dev); -- 2.43.0
When a message is passed to video_compress_fb_() and a delay is active (via -V flag or LOG_DEBUG), display the message at the top right of the screen. The affected framebuffer region is saved before drawing and restored afterwards, so the message is only visible during the delay. Add save_video() and restore_video() functions in test/dm/video.c to handle saving and restoring framebuffer regions. The save buffer is allocated on first use and stored in uts->video_save, then freed in ut_uninit_state(). Guard the message display with CONFIG_UNIT_TEST since the unit_test_state structure is only available when unit tests are enabled. This helps identify which frame check is being displayed when debugging video tests. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- doc/develop/expo.rst | 7 +++ include/test/test.h | 5 ++ test/dm/video.c | 123 ++++++++++++++++++++++++++++++++++++++++--- test/test-main.c | 2 + 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/doc/develop/expo.rst b/doc/develop/expo.rst index 7ef714be3da..71e227c532d 100644 --- a/doc/develop/expo.rst +++ b/doc/develop/expo.rst @@ -824,6 +824,13 @@ For example, to watch an expo test render with a visible display:: ./u-boot -T -l -V 500 --video_frames /tmp/good -c "ut bootstd expo_render_image" +When using ``-V`` or with ``LOG_DEBUG`` enabled, some video tests call +ut_check_video() to display a message at the top right corner of the screen +identifying the current frame check. This helps identify which assertion is +being displayed when debugging test failures. The message is automatically +removed after the delay, so it does not affect the framebuffer checksums used by +video tests. + The :doc:`../usage/cmd/sb` ``grid`` subcommand can be used to overlay a grid on the display, to help with checking alignment of objects. The grid size defaults to 0x20 pixels but can be specified as a parameter. diff --git a/include/test/test.h b/include/test/test.h index 56e25f6fa9d..c3b251e2cd4 100644 --- a/include/test/test.h +++ b/include/test/test.h @@ -6,6 +6,7 @@ #ifndef __TEST_TEST_H #define __TEST_TEST_H +#include <abuf.h> #include <malloc.h> #include <linux/bitops.h> @@ -100,6 +101,8 @@ struct ut_arg { * @arg_error: Set if ut_str/int/bool() detects a type mismatch * @keep_record: Preserve console recording when ut_fail() is called * @emit_result: Emit result line after each test completes + * @video_ctx: Vidconsole context for test message display (allocated on use) + * @video_save: Saved framebuffer region for video tests * @priv: Private data for tests to use as needed */ struct unit_test_state { @@ -134,6 +137,8 @@ struct unit_test_state { bool arg_error; bool keep_record; bool emit_result; + void *video_ctx; + struct abuf video_save; char priv[UT_PRIV_SIZE]; }; diff --git a/test/dm/video.c b/test/dm/video.c index 421d50df064..f97e2183a64 100644 --- a/test/dm/video.c +++ b/test/dm/video.c @@ -138,8 +138,106 @@ static int video_write_bmp(struct unit_test_state *uts, struct udevice *dev, return ret; } -static int video_compress_fb_(struct unit_test_state *uts, struct udevice *dev, - bool use_copy, const char *msg) +/** + * save_video() - Save a portion of the framebuffer + * + * Saves the top portion of the framebuffer so it can be restored later. + * The buffer is allocated on first use and stored in uts->video_save. + * + * @uts: Unit test state + * @dev: Video device + * @lines: Number of lines to save + * Return: 0 on success, -ve on error + */ +static int save_video(struct unit_test_state *uts, struct udevice *dev, + uint lines) +{ + struct video_priv *priv = dev_get_uclass_priv(dev); + int size; + + size = priv->line_length * lines; + if (size > priv->fb_size) + return -EINVAL; + + /* Allocate/resize the save buffer to exact size needed */ + if (!abuf_realloc(&uts->video_save, size)) + return -ENOMEM; + + memcpy(abuf_data(&uts->video_save), priv->fb, size); + + return 0; +} + +/** + * restore_video() - Restore a saved portion of the framebuffer + * + * Restores the framebuffer region previously saved by save_video(). + * + * @uts: Unit test state + * @dev: Video device + * Return: 0 on success, -ve on error + */ +static int restore_video(struct unit_test_state *uts, struct udevice *dev) +{ + struct video_priv *priv = dev_get_uclass_priv(dev); + + if (!abuf_size(&uts->video_save)) + return -ENOENT; + + memcpy(priv->fb, abuf_data(&uts->video_save), + abuf_size(&uts->video_save)); + + return 0; +} + +/** + * show_test_msg() - Display a test message at the top right of the screen + * + * @uts: Unit test state + * @dev: Video device + * @msg: Message to display + * Return: true if framebuffer was saved and needs restoring, false otherwise + */ +static bool show_test_msg(struct unit_test_state *uts, struct udevice *dev, + const char *msg) +{ + struct video_priv *priv = dev_get_uclass_priv(dev); + struct vidconsole_ctx *ctx = uts->video_ctx; + struct udevice *con; + bool saved = false; + int len = strlen(msg); + int x, ret; + + ret = uclass_first_device_err(UCLASS_VIDEO_CONSOLE, &con); + if (ret) + return false; + + /* Allocate context on first use and select 8x16 font */ + if (!ctx) { + ret = vidconsole_ctx_new(con, (void **)&ctx); + if (ret) + return false; + uts->video_ctx = ctx; + vidconsole_select_font(con, ctx, "8x16", 0); + } + + /* Calculate position at top right */ + x = priv->xsize - (len + 2) * ctx->x_charsize; + + /* Save the affected region (one line) */ + if (!save_video(uts, dev, ctx->y_charsize)) + saved = true; + + /* Draw the message */ + vidconsole_set_cursor_pos(con, ctx, x, 0); + vidconsole_put_string(con, ctx, msg); + video_manual_sync(dev, VIDSYNC_COPY | VIDSYNC_FLUSH); + + return saved; +} + +int video_compress_fb_(struct unit_test_state *uts, struct udevice *dev, + bool use_copy, const char *msg) { struct sandbox_state *state = state_get_current(); struct video_priv *priv = dev_get_uclass_priv(dev); @@ -176,10 +274,21 @@ static int video_compress_fb_(struct unit_test_state *uts, struct udevice *dev, } /* provide a useful delay if -V flag is used or LOG_DEBUG is set */ - if (state->video_test) - mdelay(state->video_test); - else if (_DEBUG) - mdelay(300); + if (state->video_test || _DEBUG) { + int delay = state->video_test ? state->video_test : 300; + bool saved = false; + + if (msg) + saved = show_test_msg(uts, dev, msg); + + mdelay(delay); + + /* Restore the framebuffer region */ + if (saved) { + restore_video(uts, dev); + video_manual_sync(dev, VIDSYNC_COPY | VIDSYNC_FLUSH); + } + } return destlen; } @@ -213,7 +322,7 @@ int ut_check_video(struct unit_test_state *uts, const char *msg) if (ret) return ret; - return video_compress_fb(uts, dev, false); + return video_compress_fb_(uts, dev, false, msg); } /* diff --git a/test/test-main.c b/test/test-main.c index a2c4e32423b..5db35b59760 100644 --- a/test/test-main.c +++ b/test/test-main.c @@ -87,6 +87,8 @@ void ut_uninit_state(struct unit_test_state *uts) os_free(uts->fdt_copy); os_free(uts->other_fdt); } + /* video_ctx is freed when the vidconsole is unbound */ + abuf_uninit(&uts->video_save); } /** -- 2.43.0
Text scene objects (SCENEOBJT_TEXT) have a gen.lines alist that stores line measurement info populated during rendering. This alist was not being freed when the object was destroyed, causing a memory leak. Add cleanup for gen.lines in scene_obj_destroy() for TEXT objects. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/boot/scene.c b/boot/scene.c index ff21b524843..b075697adb6 100644 --- a/boot/scene.c +++ b/boot/scene.c @@ -110,6 +110,8 @@ void scene_obj_destroy(struct scene_obj *obj) else if (obj->type == SCENEOBJT_TEXTLINE || obj->type == SCENEOBJT_TEXTEDIT) scene_txtin_destroy(obj->scene, scene_obj_txtin(obj)); + if (obj->type == SCENEOBJT_TEXT) + alist_uninit(&((struct scene_obj_txt *)obj)->gen.lines); free(obj->name); free(obj); } -- 2.43.0
When the user holds down a key, the terminal sends characters faster than expo can process them if it only reads one character per poll. Change poll_keys() to drain the input buffer by reading all available characters until a complete key is decoded or no more input is available. There are no tests for this feature. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/expo.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/boot/expo.c b/boot/expo.c index 842bacae163..e44c7552f30 100644 --- a/boot/expo.c +++ b/boot/expo.c @@ -498,10 +498,12 @@ static int poll_keys(struct expo *exp) ichar = cli_ch_process(&exp->cch, 0); if (!ichar) { - /* Check once for available input */ - if (tstc()) { + /* Read all available input to keep up with key repeat */ + while (tstc()) { ch = getchar(); ichar = cli_ch_process(&exp->cch, ch); + if (ichar) + break; } if (!ch && get_timer(exp->last_key_ms) >= 10) -- 2.43.0
Track the Ctrl modifier state in the input driver and generate the appropriate escape sequences (ESC [ 1 ; 5 D/C) when Ctrl+Left or Ctrl+Right arrow keys are pressed. This enables word navigation in the sandbox SDL console. There are no tests for this feature. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- drivers/input/input.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/drivers/input/input.c b/drivers/input/input.c index 3f146fb07e6..5206d6f68d6 100644 --- a/drivers/input/input.c +++ b/drivers/input/input.c @@ -28,6 +28,9 @@ enum { /* Special flag ORed with key code to indicate release */ KEY_RELEASE = 1 << 15, KEY_MASK = 0xfff, + + /* Modifier bits for config->modifiers */ + MOD_CTRL = 1 << 0, }; /* @@ -428,6 +431,31 @@ static int input_keycode_to_ansi364(struct input_config *config, int ch_count; int i; + /* Handle Ctrl+arrow keys for word navigation */ + if (config->modifiers & MOD_CTRL) { + const char *seq = NULL; + + switch (keycode) { + case KEY_LEFT: + seq = "[1;5D"; /* Ctrl+Left: backward-word */ + break; + case KEY_RIGHT: + seq = "[1;5C"; /* Ctrl+Right: forward-word */ + break; + } + if (seq) { + ch_count = 0; + output_ch[ch_count++] = 0x1b; + while (*seq) { + if (ch_count < max_chars) + output_ch[ch_count] = *seq; + ch_count++; + seq++; + } + return ch_count; + } + } + for (i = ch_count = 0; i < ARRAY_SIZE(kbd_to_ansi364); i++) { if (keycode != kbd_to_ansi364[i].kbd_scan_code) continue; @@ -483,6 +511,9 @@ static int input_keycodes_to_ascii(struct input_config *config, table = process_modifier(config, key, keycode[i] & KEY_RELEASE); } + /* Track Ctrl state for special key handling */ + if (key == KEY_LEFTCTRL || key == KEY_RIGHTCTRL) + config->modifiers |= MOD_CTRL; } /* Start conversion by looking for the first new keycode (by same). */ -- 2.43.0
Add KEY_HOME and KEY_END to the ANSI escape sequence table so that these keys work in the sandbox SDL console. They generate the escape sequences ESC [ H and ESC [ F respectively, which are then decoded by cli_getch.c to move the cursor to the beginning or end of the line. Also add these keys (plus KEY_DELETE) to the sandbox cros-ec keyboard matrix so they are recognized by the emulated keyboard controller. There are no tests for this feature. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- arch/sandbox/dts/cros-ec-keyboard.dtsi | 4 ++++ drivers/input/input.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/arch/sandbox/dts/cros-ec-keyboard.dtsi b/arch/sandbox/dts/cros-ec-keyboard.dtsi index d885a5ecd22..a8a2c790b9a 100644 --- a/arch/sandbox/dts/cros-ec-keyboard.dtsi +++ b/arch/sandbox/dts/cros-ec-keyboard.dtsi @@ -110,6 +110,10 @@ MATRIX_KEY(0x07, 0x09, KEY_O) MATRIX_KEY(0x07, 0x0b, KEY_UP) MATRIX_KEY(0x07, 0x0c, KEY_LEFT) + + MATRIX_KEY(0x00, 0x07, KEY_HOME) + MATRIX_KEY(0x00, 0x09, KEY_END) + MATRIX_KEY(0x00, 0x0b, KEY_DELETE) >; }; }; diff --git a/drivers/input/input.c b/drivers/input/input.c index 5206d6f68d6..5d265837a25 100644 --- a/drivers/input/input.c +++ b/drivers/input/input.c @@ -188,6 +188,9 @@ static struct { { KEY_LEFT, "[D"}, { KEY_RIGHT, "[C"}, { KEY_DOWN, "[B"}, + { KEY_HOME, "[H"}, + { KEY_END, "[F"}, + { KEY_DELETE, "[3~"}, { KEY_F1, "OP"}, { KEY_F2, "OQ"}, { KEY_F3, "OR"}, -- 2.43.0
Add support for detecting Ctrl+Shift+Z key combination in the input driver and generating the escape sequence ESC [ 1 ; 6 z which is then decoded by cli_getch.c to trigger redo. This requires tracking both Ctrl and Shift modifier state, so add MOD_SHIFT to the modifier flags. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- drivers/input/input.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/drivers/input/input.c b/drivers/input/input.c index 5d265837a25..683569f59a1 100644 --- a/drivers/input/input.c +++ b/drivers/input/input.c @@ -31,6 +31,7 @@ enum { /* Modifier bits for config->modifiers */ MOD_CTRL = 1 << 0, + MOD_SHIFT = 1 << 1, }; /* @@ -459,6 +460,23 @@ static int input_keycode_to_ansi364(struct input_config *config, } } + /* Handle Ctrl+Shift+Z for redo */ + if ((config->modifiers & (MOD_CTRL | MOD_SHIFT)) == + (MOD_CTRL | MOD_SHIFT) && keycode == KEY_Z) { + /* Generate ESC [ 1 ; 6 z for Ctrl+Shift+Z */ + const char *seq = "[1;6z"; + + ch_count = 0; + output_ch[ch_count++] = 0x1b; + while (*seq) { + if (ch_count < max_chars) + output_ch[ch_count] = *seq; + ch_count++; + seq++; + } + return ch_count; + } + for (i = ch_count = 0; i < ARRAY_SIZE(kbd_to_ansi364); i++) { if (keycode != kbd_to_ansi364[i].kbd_scan_code) continue; @@ -514,9 +532,11 @@ static int input_keycodes_to_ascii(struct input_config *config, table = process_modifier(config, key, keycode[i] & KEY_RELEASE); } - /* Track Ctrl state for special key handling */ + /* Track Ctrl and Shift state for special key handling */ if (key == KEY_LEFTCTRL || key == KEY_RIGHTCTRL) config->modifiers |= MOD_CTRL; + if (key == KEY_LEFTSHIFT || key == KEY_RIGHTSHIFT) + config->modifiers |= MOD_SHIFT; } /* Start conversion by looking for the first new keycode (by same). */ -- 2.43.0
Add a new Kconfig option CONFIG_CMDLINE_EDITOR to enable enhanced command-line editing features. This replaces CLI_READLINE_CALLBACK and is enabled by default when EXPO is enabled. Create struct cli_editor_state to hold state for enhanced editing features, initially containing: - putch: callback for character output redirection - line_nav: callback for multi-line navigation (Ctrl-P/N) - multiline: flag for multi-line input mode This struct is embedded in cli_line_state when CMDLINE_EDITOR is enabled. Add cli_editor() accessor function that returns a pointer to the editor state, or NULL if CMDLINE_EDITOR is not enabled. Update cli_readline.c and scene_txtin.c to use the new accessor. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 10 ++++--- cmd/Kconfig | 16 +++++++---- common/cli_readline.c | 12 +++++--- include/cli.h | 66 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index cde9fdb8ccf..79814891cdc 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -112,6 +112,7 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, struct scene_txtin *tin) { struct cli_line_state *cls = &tin->cls; + struct cli_editor_state *ed = cli_editor(cls); const bool open = obj->flags & SCENEOF_OPEN; struct udevice *cons = scn->expo->cons; void *ctx = tin->ctx; @@ -121,7 +122,7 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, if (open) { scene_render_obj(scn, tin->edit_id, ctx); - if (cls->multiline) { + if (ed->multiline) { /* for multiline, set cursor position directly */ struct scene_obj_txt *txt; @@ -260,6 +261,7 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, struct scene_txtin *tin) { struct cli_line_state *cls = &tin->cls; + struct cli_editor_state *ed = cli_editor(cls); struct udevice *cons = scn->expo->cons; struct scene_obj_txt *txt; void *ctx; @@ -286,11 +288,11 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, vidconsole_entry_start(cons, ctx); cli_cread_init(cls, abuf_data(&tin->buf), abuf_size(&tin->buf)); cls->insert = true; - cls->putch = scene_txtin_putch; + ed->putch = scene_txtin_putch; cls->priv = scn; if (obj->type == SCENEOBJT_TEXTEDIT) { - cls->multiline = true; - cls->line_nav = scene_txtin_line_nav; + ed->multiline = true; + ed->line_nav = scene_txtin_line_nav; } cli_cread_add_initial(cls); diff --git a/cmd/Kconfig b/cmd/Kconfig index d8f57c446a5..448a6e9fe39 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -54,13 +54,17 @@ config CMDLINE_EDITING Enable editing and History functions for interactive command line input operations -config CLI_READLINE_CALLBACK - bool "Support a callback for character output" +config CMDLINE_EDITOR + bool "Enhanced command-line editing features" depends on CMDLINE_EDITING - help - Enable a callback for character output during line editing. This - allows redirection of output to a different destination, such as - a vidconsole. This is used by expo to support textline editing. + default y if EXPO + help + Enable enhanced editing features for the command line, including: + - Character-output callback for redirection to vidconsole + - Multi-line navigation callback (Ctrl-P/N) + - Ctrl+Left/Right arrow keys to move by words + - Undo/redo support (Ctrl+Z / Ctrl+Shift+Z) + - Yank/paste of killed text (Ctrl+Y) config CMDLINE_PS_SUPPORT bool "Enable support for changing the command prompt string at run-time" diff --git a/common/cli_readline.c b/common/cli_readline.c index 38f825184df..d2b02933fd7 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -79,8 +79,10 @@ static char *delete_char (char *buffer, char *p, int *colp, int *np, int plen) */ static void cls_putch(struct cli_line_state *cls, int ch) { - if (CONFIG_IS_ENABLED(CLI_READLINE_CALLBACK) && cls->putch) - cls->putch(cls, ch); + struct cli_editor_state *ed = cli_editor(cls); + + if (ed && ed->putch) + ed->putch(cls, ch); else putc(ch); } @@ -299,6 +301,7 @@ static void cread_add_str(struct cli_line_state *cls, char *str, int strsize, int cread_line_process_ch(struct cli_line_state *cls, char ichar) { + struct cli_editor_state *ed; char *buf = cls->buf; /* ichar=0x0 when error occurs in U-Boot getc */ @@ -405,10 +408,11 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) break; case CTL_CH('p'): case CTL_CH('n'): - if (cls->multiline && cls->line_nav) { + ed = cli_editor(cls); + if (ed && ed->multiline && ed->line_nav) { int new_num; - new_num = cls->line_nav(cls, ichar == CTL_CH('p')); + new_num = ed->line_nav(cls, ichar == CTL_CH('p')); if (new_num < 0) { getcmd_cbeep(cls); break; diff --git a/include/cli.h b/include/cli.h index f1e5887fa56..b6a8a6be1dd 100644 --- a/include/cli.h +++ b/include/cli.h @@ -25,6 +25,43 @@ struct cli_ch_state { bool emitting; }; +struct cli_line_state; + +/** + * struct cli_editor_state - state for enhanced editing features + * + * This is only available when CONFIG_CMDLINE_EDITOR is enabled. + * + * @putch: Output a character (NULL to use putc()) + * @line_nav: Handle multi-line navigation (Ctrl-P/N) + * @multiline: true if input may contain multiple lines (enables + * Ctrl-P/N for line navigation instead of history) + */ +struct cli_editor_state { + /** + * @putch: Output a character (NULL to use putc()) + * + * @cls: CLI line state + * @ch: Character to output + */ + void (*putch)(struct cli_line_state *cls, int ch); + + /** + * @line_nav: Handle multi-line navigation (Ctrl-P/N) + * + * @cls: CLI line state + * @up: true for previous line, false for next + * Return: new cursor position, or -ve if at boundary + */ + int (*line_nav)(struct cli_line_state *cls, bool up); + + /** + * @multiline: true if input may contain multiple lines (enables + * Ctrl-P/N for line navigation instead of history) + */ + bool multiline; +}; + /** * struct cli_line_state - state of the line editor * @@ -34,15 +71,10 @@ struct cli_ch_state { * @history: true if history should be accessible * @cmd_complete: true if tab completion should be enabled (requires @prompt to * be set) - * @multiline: true if input may contain multiple lines (enables Ctrl-P/N for - * line navigation instead of history) * @buf: Buffer containing line * @prompt: Prompt for the line - * @putch: Function to call to output a character (NULL to use putc()) - * @line_nav: Function to call for multi-line navigation (Ctrl-P/N). Called with - * @up true for previous line, false for next. Returns new cursor position, - * or -ve if at boundary * @priv: Private data for callbacks + * @ed: Editor state for enhanced features (if CONFIG_CMDLINE_EDITOR) */ struct cli_line_state { uint num; @@ -51,14 +83,30 @@ struct cli_line_state { bool insert; bool history; bool cmd_complete; - bool multiline; char *buf; const char *prompt; - void (*putch)(struct cli_line_state *cls, int ch); - int (*line_nav)(struct cli_line_state *cls, bool up); void *priv; +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) + struct cli_editor_state ed; +#endif }; +/** + * cli_editor() - Get the editor state from a line state + * + * @cls: CLI line state + * Return: Pointer to editor state, or NULL if CONFIG_CMDLINE_EDITOR is not + * enabled + */ +static inline struct cli_editor_state *cli_editor(struct cli_line_state *cls) +{ +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) + return &cls->ed; +#else + return NULL; +#endif +} + /** * Go into the command loop * -- 2.43.0
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 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); -- 2.43.0
Add a test for the expo_editenv_init(), expo_editenv_poll() and expo_editenv_uninit() functions which allow more flexible use of the environment editor. The test uses a helper function editenv_send() to send keys directly to the expo and verify the result. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/boot/editenv.c | 84 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/boot/editenv.c b/test/boot/editenv.c index 02bc025e216..62a33f1ba0f 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -9,11 +9,64 @@ #include <dm.h> #include <env.h> #include <expo.h> +#include <menu.h> #include <video.h> +#include <video_console.h> #include <test/ut.h> #include <test/video.h> #include "bootstd_common.h" +static const char initial[] = + "This is a long string that will wrap to multiple lines " + "when displayed in the textedit widget. It needs to be " + "long enough to span several lines so that the up and down " + "arrow keys can be tested properly.\n" + "The arrow keys should " + "move the cursor between lines in the multiline editor."; + +/** + * editenv_send() - Send a key to the editenv expo + * + * Arranges and renders the scene, sends the key, then checks for any + * resulting action. + * + * @info: Editenv info + * @key: Key to send (ASCII or BKEY_...) + * Return: 0 if OK, 1 if editing is complete, -ECANCELED if user quit, + * other -ve on error + */ +static int editenv_send(struct editenv_info *info, int key) +{ + struct expo_action act; + int ret; + + ret = expo_send_key(info->exp, key); + if (ret) + return ret; + + ret = scene_arrange(info->scn); + if (ret) + return ret; + + ret = expo_render(info->exp); + if (ret) + return ret; + + ret = expo_action_get(info->exp, &act); + if (ret == -EAGAIN) + return 0; + if (ret) + return ret; + + if (act.type == EXPOACT_QUIT) + return -ECANCELED; + + if (act.type == EXPOACT_CLOSE) + return 1; + + return 0; +} + /* Check expo_editenv() basic functionality */ static int editenv_test_base(struct unit_test_state *uts) { @@ -92,3 +145,34 @@ static int editenv_test_video(struct unit_test_state *uts) return 0; } BOOTSTD_TEST(editenv_test_video, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE); + +/* Check the init/poll/uninit functions work correctly */ +static int editenv_test_funcs(struct unit_test_state *uts) +{ + struct editenv_info info; + struct udevice *dev, *con; + + ut_assertok(uclass_first_device_err(UCLASS_VIDEO, &dev)); + ut_assertok(uclass_first_device_err(UCLASS_VIDEO_CONSOLE, &con)); + + /* Set font size to 30 */ + ut_assertok(vidconsole_select_font(con, NULL, NULL, 30)); + + ut_assertok(expo_editenv_init("testvar", initial, &info)); + ut_asserteq(16611, ut_check_video(uts, "init")); + + /* Type a character and press Enter to accept */ + ut_assertok(editenv_send(&info, '*')); + ut_asserteq(16689, ut_check_video(uts, "insert")); + + ut_asserteq(1, editenv_send(&info, BKEY_SELECT)); + + /* The '*' should be appended to the initial text */ + ut_assert(strstr(expo_editenv_result(&info), "editor.*")); + ut_asserteq(16689, ut_check_video(uts, "save")); + + expo_editenv_uninit(&info); + + return 0; +} +BOOTSTD_TEST(editenv_test_funcs, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE); -- 2.43.0
The expo keyboard handling converts arrow keys to BKEY_UP and BKEY_DOWN before sending them to scene objects. However, cread_line_process_ch() expects CTL_CH('p') and CTL_CH('n') for line navigation in multiline text editors. Add explicit handling for BKEY_UP and BKEY_DOWN in scene_txtin_send_key() to convert them back to the control characters that cread_line_process_ch() expects. This fixes arrow key navigation in textedit objects. Update the editenv_test_video() to use a longer string and a larger font size so we can test this. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 6 ++++++ test/boot/editenv.c | 26 ++++++++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 79814891cdc..8b49daddd56 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -349,6 +349,12 @@ int scene_txtin_send_key(struct scene_obj *obj, struct scene_txtin *tin, event->select.id = obj->id; scene_txtin_close(scn, tin); break; + case BKEY_UP: + cread_line_process_ch(cls, CTL_CH('p')); + break; + case BKEY_DOWN: + cread_line_process_ch(cls, CTL_CH('n')); + break; default: cread_line_process_ch(cls, key); break; diff --git a/test/boot/editenv.c b/test/boot/editenv.c index 62a33f1ba0f..0f9db54474d 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -124,23 +124,33 @@ static int editenv_test_escape(struct unit_test_state *uts) } BOOTSTD_TEST(editenv_test_escape, UTF_DM | UTF_SCAN_FDT | UTF_CONSOLE); -/* Check expo_editenv() renders correctly */ +/* Check expo_editenv() renders correctly with multiline text and navigation */ static int editenv_test_video(struct unit_test_state *uts) { - struct udevice *dev; - char buf[256]; + struct udevice *dev, *con; + char buf[512]; int ret; ut_assertok(uclass_first_device_err(UCLASS_VIDEO, &dev)); + ut_assertok(uclass_first_device_err(UCLASS_VIDEO_CONSOLE, &con)); - /* Type "abc" then press Enter */ - console_in_puts("abc\x0d"); - ret = expo_editenv("testvar", "initial", buf, sizeof(buf)); + /* Set font size to 30 */ + ut_assertok(vidconsole_select_font(con, NULL, NULL, 30)); + + /* + * Navigate with up arrow, insert text, then press Enter. The up arrow + * should be converted to Ctrl-P by scene_txtin_send_key(). + * \x1b[A is the escape sequence for up arrow + */ + console_in_puts("\x1b[A!\x0d"); + ret = expo_editenv("testvar", initial, buf, sizeof(buf)); ut_assertok(ret); - ut_asserteq_str("initialabc", buf); + + /* The '!' should be inserted one visual line up from the end */ + ut_assert(strstr(buf, "tes!ted")); /* Check the framebuffer has expected content */ - ut_asserteq(1029, video_compress_fb(uts, dev, false)); + ut_asserteq(16829, video_compress_fb(uts, dev, false)); return 0; } -- 2.43.0
Add support for Ctrl+Left and Ctrl+Right arrow keys to move the cursor by word in the command line editor. Ctrl+Left moves backward to the start of the previous word, and Ctrl+Right moves forward to the end of the next word. Decode the escape sequences (ESC[1;5D and ESC[1;5C) arein cli_getch.c and convert then to CTL_CH('r') and CTL_CH('t') respectively. Handle the actual word-movement logic is then handled in cli_readline.c, guarded by CONFIG_CMDLINE_EDITOR Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/cli_getch.c | 25 +++++++++++++++++++++++++ common/cli_readline.c | 31 +++++++++++++++++++++++++++++++ test/boot/editenv.c | 8 ++++++++ 3 files changed, 64 insertions(+) diff --git a/common/cli_getch.c b/common/cli_getch.c index a5ed6eb6fcf..8df1997911a 100644 --- a/common/cli_getch.c +++ b/common/cli_getch.c @@ -114,6 +114,12 @@ static int cli_ch_esc(struct cli_ch_state *cch, int ichar, if (cch->esc_save[2] == '2') act = ESC_SAVE; break; + case ';': + /* Ctrl+arrow: ESC [ 1 ; */ + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && + cch->esc_save[2] == '1') + act = ESC_SAVE; + break; } break; case 4: @@ -122,12 +128,31 @@ static int cli_ch_esc(struct cli_ch_state *cch, int ichar, case '1': act = ESC_SAVE; break; /* bracketed paste */ + case '5': + /* Ctrl+arrow: ESC [ 1 ; 5 */ + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && + cch->esc_save[3] == ';') + act = ESC_SAVE; + break; } break; case 5: if (ichar == '~') { /* bracketed paste */ ichar = 0; act = ESC_CONVERTED; + } else if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && + cch->esc_save[4] == '5') { + /* Ctrl+arrow: ESC [ 1 ; 5 D/C */ + switch (ichar) { + case 'D': /* Ctrl+<- key */ + ichar = CTL_CH('r'); + act = ESC_CONVERTED; + break; /* pass to backward-word handler */ + case 'C': /* Ctrl+-> key */ + ichar = CTL_CH('t'); + act = ESC_CONVERTED; + break; /* pass to forward-word handler */ + } } } diff --git a/common/cli_readline.c b/common/cli_readline.c index d2b02933fd7..4c25e9a04ba 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -333,6 +333,37 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) cls->num--; } break; + case CTL_CH('r'): /* backward-word */ + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && cls->num) { + uint pos = cls->num; + + /* skip spaces before word */ + while (pos > 0 && buf[pos - 1] == ' ') + pos--; + /* skip word characters */ + while (pos > 0 && buf[pos - 1] != ' ') + pos--; + cls_putchars(cls, cls->num - pos, CTL_BACKSPACE); + cls->num = pos; + } + break; + case CTL_CH('t'): /* forward-word */ + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && cls->num < cls->eol_num) { + uint pos = cls->num; + + /* skip spaces after cursor */ + while (pos < cls->eol_num && buf[pos] == ' ') { + cls_putch(cls, buf[pos]); + pos++; + } + /* skip word characters */ + while (pos < cls->eol_num && buf[pos] != ' ') { + cls_putch(cls, buf[pos]); + pos++; + } + cls->num = pos; + } + break; case CTL_CH('d'): if (cls->num < cls->eol_num) { uint wlen; diff --git a/test/boot/editenv.c b/test/boot/editenv.c index 0f9db54474d..20273c53647 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -171,6 +171,14 @@ static int editenv_test_funcs(struct unit_test_state *uts) ut_assertok(expo_editenv_init("testvar", initial, &info)); ut_asserteq(16611, ut_check_video(uts, "init")); + /* Navigate up to previous line */ + ut_assertok(editenv_send(&info, BKEY_UP)); + ut_asserteq(16684, ut_check_video(uts, "up")); + + /* Navigate back down */ + ut_assertok(editenv_send(&info, BKEY_DOWN)); + ut_asserteq(16611, ut_check_video(uts, "down")); + /* Type a character and press Enter to accept */ ut_assertok(editenv_send(&info, '*')); ut_asserteq(16689, ut_check_video(uts, "insert")); -- 2.43.0
Add a new flag SCENEOF_MULTILINE that allows the textedit widget to accept multiline input. When this flag is set, pressing Enter inserts a newline character instead of closing the textedit. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/editenv.c | 2 ++ boot/scene_txtin.c | 21 ++++++++++++++++++++- include/expo.h | 2 ++ test/boot/editenv.c | 22 +++++++++++----------- test/boot/expo.c | 16 ++++++++++++++-- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/boot/editenv.c b/boot/editenv.c index de7df6fe3fa..fd7670263b7 100644 --- a/boot/editenv.c +++ b/boot/editenv.c @@ -47,6 +47,7 @@ static int editenv_setup(struct expo *exp, struct udevice *dev, ret = scene_texted(scn, "textedit", EDITENV_OBJ_TEXTEDIT, 70, &ted); if (ret < 0) return log_msg_ret("ted", ret); + ted->obj.flags |= SCENEOF_MULTILINE; ret = scene_obj_set_bbox(scn, EDITENV_OBJ_TEXTEDIT, 50, 200, 1300, 400); if (ret < 0) @@ -122,6 +123,7 @@ int expo_editenv_init(const char *varname, const char *value, if (ret) { expo_destroy(exp); return log_msg_ret("set", ret); + } return 0; diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 8b49daddd56..ab4fd5056a0 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -342,13 +342,32 @@ int scene_txtin_send_key(struct scene_obj *obj, struct scene_txtin *tin, log_debug("menu quit\n"); } break; - case BKEY_SELECT: + case BKEY_SAVE: if (!open) break; + /* Accept contents even in multiline mode */ event->type = EXPOACT_CLOSE; event->select.id = obj->id; scene_txtin_close(scn, tin); break; + case BKEY_SELECT: + if (!open) + break; + if (obj->flags & SCENEOF_MULTILINE) { + char *buf = cls->buf; + int wlen = cls->eol_num - cls->num; + + /* Insert newline at cursor position */ + memmove(&buf[cls->num + 1], &buf[cls->num], wlen); + buf[cls->num] = '\n'; + cls->num++; + cls->eol_num++; + } else { + event->type = EXPOACT_CLOSE; + event->select.id = obj->id; + scene_txtin_close(scn, tin); + } + break; case BKEY_UP: cread_line_process_ch(cls, CTL_CH('p')); break; diff --git a/include/expo.h b/include/expo.h index 5a35d72c58f..3846b58abdd 100644 --- a/include/expo.h +++ b/include/expo.h @@ -321,6 +321,7 @@ enum scene_obj_align { * @SCENEOF_MANUAL: manually arrange the items associated with this object * @SCENEOF_DIRTY: object has been modified and needs to be redrawn * @SCENEOF_PASSWORD: textline input should show stars instead of characters + * @SCENEOF_MULTILINE: textedit allows multiline input (Enter adds newline) * @SCENEOF_LAST: used just as a check for the size of the flags mask */ enum scene_obj_flags_t { @@ -335,6 +336,7 @@ enum scene_obj_flags_t { SCENEOF_MANUAL = BIT(8), SCENEOF_DIRTY = BIT(9), SCENEOF_PASSWORD = BIT(10), + SCENEOF_MULTILINE = BIT(11), SCENEOF_LAST, /* check for size of flags below */ }; diff --git a/test/boot/editenv.c b/test/boot/editenv.c index 20273c53647..69e543ea51f 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -74,10 +74,10 @@ static int editenv_test_base(struct unit_test_state *uts) int ret; /* - * Type "test" then press Enter to accept - * \x0d is Ctrl-M (Enter/carriage return) + * Type "test" then press Ctrl-S to save + * \x13 is Ctrl-S */ - console_in_puts("test\x0d"); + console_in_puts("test\x13"); ret = expo_editenv("myvar", NULL, buf, sizeof(buf)); ut_assertok(ret); ut_asserteq_str("test", buf); @@ -94,9 +94,9 @@ static int editenv_test_initial(struct unit_test_state *uts) /* * Start with "world", go to start with Ctrl-A, type "hello ", then - * press Enter + * press Ctrl-S to save */ - console_in_puts("\x01hello \x0d"); + console_in_puts("\x01hello \x13"); ret = expo_editenv("myvar", "world", buf, sizeof(buf)); ut_assertok(ret); ut_asserteq_str("hello world", buf); @@ -138,11 +138,11 @@ static int editenv_test_video(struct unit_test_state *uts) ut_assertok(vidconsole_select_font(con, NULL, NULL, 30)); /* - * Navigate with up arrow, insert text, then press Enter. The up arrow - * should be converted to Ctrl-P by scene_txtin_send_key(). - * \x1b[A is the escape sequence for up arrow + * Navigate with up arrow, insert text, then press Ctrl-S to save. + * The up arrow should be converted to Ctrl-P by scene_txtin_send_key(). + * \x1b[A is the escape sequence for up arrow, \x13 is Ctrl-S (save) */ - console_in_puts("\x1b[A!\x0d"); + console_in_puts("\x1b[A!\x13"); ret = expo_editenv("testvar", initial, buf, sizeof(buf)); ut_assertok(ret); @@ -179,11 +179,11 @@ static int editenv_test_funcs(struct unit_test_state *uts) ut_assertok(editenv_send(&info, BKEY_DOWN)); ut_asserteq(16611, ut_check_video(uts, "down")); - /* Type a character and press Enter to accept */ + /* Type a character and press Ctrl-S to save */ ut_assertok(editenv_send(&info, '*')); ut_asserteq(16689, ut_check_video(uts, "insert")); - ut_asserteq(1, editenv_send(&info, BKEY_SELECT)); + ut_asserteq(1, editenv_send(&info, BKEY_SAVE)); /* The '*' should be appended to the initial text */ ut_assert(strstr(expo_editenv_result(&info), "editor.*")); diff --git a/test/boot/expo.c b/test/boot/expo.c index 824fd0cca38..f598b9cb86c 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1703,7 +1703,19 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_assertok(expo_render(exp)); ut_asserteq(21083, video_compress_fb(uts, dev, false)); - /* close the textedit with Enter (BKEY_SELECT) */ + /* set multiline mode and check Enter inserts newline */ + ted->obj.flags |= SCENEOF_MULTILINE; + ut_assertok(expo_send_key(exp, BKEY_SELECT)); + ut_asserteq(90, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + ut_assert(ted->obj.flags & SCENEOF_OPEN); + ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[89]); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21091, video_compress_fb(uts, dev, false)); + + /* clear multiline mode, close the textedit with Enter (BKEY_SELECT) */ + ted->obj.flags &= ~SCENEOF_MULTILINE; ut_assertok(expo_send_key(exp, BKEY_SELECT)); ut_assertok(expo_action_get(exp, &act)); ut_asserteq(EXPOACT_CLOSE, act.type); @@ -1713,7 +1725,7 @@ static int expo_render_textedit(struct unit_test_state *uts) /* check the textedit is closed and text is changed */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); ut_asserteq_str("his\nis the initial contents of the text " - "editor but it is ely that more will be added latr", + "editor but it is ely that more will be added latr\n", abuf_data(&ted->tin.buf)); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); -- 2.43.0
In multiline mode, Ctrl+K (kill to end of line) currently erases all text from the cursor to the end of the buffer. This is not the expected behaviour for multiline editing. Update cread_erase_to_eol() to only erase to the next newline character in multiline mode, preserving text on subsequent lines. Use a parameterized ERASE_TO() macro to share the erase logic between the multiline-aware function (when CMDLINE_EDITOR is enabled) and a simpler version (when disabled), avoiding code growth on boards without CMDLINE_EDITOR Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/cli_readline.c | 114 +++++++++++++++++++++++++++++++++++++++--- test/boot/editenv.c | 17 +++++-- test/boot/expo.c | 33 ++++++++---- 3 files changed, 144 insertions(+), 20 deletions(-) diff --git a/common/cli_readline.c b/common/cli_readline.c index 4c25e9a04ba..847b49450b5 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -231,13 +231,86 @@ void cread_print_hist_list(void) } } -#define BEGINNING_OF_LINE() { \ - while (cls->num) { \ +#define GOTO_LINE_START(target) { \ + while (cls->num > (target)) { \ cls_putch(cls, CTL_BACKSPACE); \ cls->num--; \ } \ } +#define ERASE_TO(erase_to) { \ + if (cls->num < (erase_to)) { \ + uint wlen = (erase_to) - cls->num; \ + \ + /* erase characters on screen */ \ + printf("%*s", wlen, ""); \ + while (wlen--) \ + cls_putch(cls, CTL_BACKSPACE); \ + \ + /* remove characters from buffer */ \ + memmove(&buf[cls->num], &buf[erase_to], \ + cls->eol_num - (erase_to) + 1); \ + cls->eol_num -= (erase_to) - cls->num; \ + } \ +} + +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) +/** + * cread_start_of_line() - Move cursor to start of line + * + * In multiline mode, moves to the character after the previous newline. + * Otherwise moves to position 0. + * + * @cls: CLI line state + */ +static void cread_start_of_line(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + uint target = 0; + + if (ed && ed->multiline) { + char *buf = cls->buf; + uint i; + + /* find previous newline */ + for (i = cls->num; i > 0; i--) { + if (buf[i - 1] == '\n') { + target = i; + break; + } + } + } + GOTO_LINE_START(target); +} +#define BEGINNING_OF_LINE() cread_start_of_line(cls) +#else +#define BEGINNING_OF_LINE() GOTO_LINE_START(0) +#endif + +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) +static void cread_erase_to_eol(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + char *buf = cls->buf; + uint erase_to; + + if (cls->num >= cls->eol_num) + return; + + /* + * In multiline mode, only erase to end of current line (next newline + * or end of buffer) + */ + erase_to = cls->eol_num; + if (ed && ed->multiline) { + char *nl = strchr(&buf[cls->num], '\n'); + + if (nl) + erase_to = nl - buf; + } + ERASE_TO(erase_to); +} +#else static void cread_erase_to_eol(struct cli_line_state *cls) { if (cls->num < cls->eol_num) { @@ -247,15 +320,44 @@ static void cread_erase_to_eol(struct cli_line_state *cls) } while (--cls->eol_num > cls->num); } } +#endif -#define REFRESH_TO_EOL() { \ - if (cls->num < cls->eol_num) { \ - uint wlen = cls->eol_num - cls->num; \ +#define GOTO_LINE_END(target) { \ + if (cls->num < (target)) { \ + uint wlen = (target) - cls->num; \ cls_putnstr(cls, buf + cls->num, wlen); \ - cls->num = cls->eol_num; \ + cls->num = (target); \ } \ } +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) +/** + * cread_end_of_line() - Move cursor to end of line + * + * In multiline mode, moves to the next newline character. + * Otherwise moves to end of buffer. + * + * @cls: CLI line state + */ +static void cread_end_of_line(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + char *buf = cls->buf; + uint target = cls->eol_num; + + if (ed && ed->multiline) { + char *nl = strchr(&buf[cls->num], '\n'); + + if (nl) + target = nl - buf; + } + GOTO_LINE_END(target); +} +#define REFRESH_TO_EOL() cread_end_of_line(cls) +#else +#define REFRESH_TO_EOL() GOTO_LINE_END(cls->eol_num) +#endif + static void cread_add_char(struct cli_line_state *cls, char ichar, int insert, uint *num, uint *eol_num, char *buf, uint len) { diff --git a/test/boot/editenv.c b/test/boot/editenv.c index 69e543ea51f..ab3c6648886 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -179,15 +179,22 @@ static int editenv_test_funcs(struct unit_test_state *uts) ut_assertok(editenv_send(&info, BKEY_DOWN)); ut_asserteq(16611, ut_check_video(uts, "down")); - /* Type a character and press Ctrl-S to save */ + /* Navigate with up arrow and insert '*' */ + ut_assertok(editenv_send(&info, BKEY_UP)); + ut_asserteq(16684, ut_check_video(uts, "up2")); + ut_assertok(editenv_send(&info, '*')); - ut_asserteq(16689, ut_check_video(uts, "insert")); + ut_asserteq(16877, ut_check_video(uts, "insert")); + + /* Use Ctrl-K to kill to end of line (stops at the existing newline) */ + ut_assertok(editenv_send(&info, CTL_CH('k'))); + ut_asserteq(16033, ut_check_video(uts, "kill")); ut_asserteq(1, editenv_send(&info, BKEY_SAVE)); - /* The '*' should be appended to the initial text */ - ut_assert(strstr(expo_editenv_result(&info), "editor.*")); - ut_asserteq(16689, ut_check_video(uts, "save")); + /* The '*' is inserted after "tes", Ctrl-K killed "ted properly." */ + ut_assert(strstr(expo_editenv_result(&info), "tes*\n")); + ut_asserteq(16033, ut_check_video(uts, "save")); expo_editenv_uninit(&info); diff --git a/test/boot/expo.c b/test/boot/expo.c index f598b9cb86c..366183e4a79 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1681,16 +1681,16 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_assertok(expo_render(exp)); ut_asserteq(21211, video_compress_fb(uts, dev, false)); - /* go to start of buffer and delete a character */ + /* go to start of line (multiline Home goes to start of current line) */ ut_assertok(expo_send_key(exp, CTL_CH('a'))); - ut_asserteq(0, ted->tin.cls.num); + ut_asserteq(5, ted->tin.cls.num); ut_asserteq(91, ted->tin.cls.eol_num); ut_assertok(expo_send_key(exp, CTL_CH('d'))); - ut_asserteq(0, ted->tin.cls.num); + ut_asserteq(5, ted->tin.cls.num); ut_asserteq(90, ted->tin.cls.eol_num); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21147, video_compress_fb(uts, dev, false)); + ut_asserteq(21174, video_compress_fb(uts, dev, false)); /* go to end of buffer and backspace */ ut_assertok(expo_send_key(exp, CTL_CH('e'))); @@ -1701,7 +1701,7 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(89, ted->tin.cls.eol_num); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21083, video_compress_fb(uts, dev, false)); + ut_asserteq(21079, video_compress_fb(uts, dev, false)); /* set multiline mode and check Enter inserts newline */ ted->obj.flags |= SCENEOF_MULTILINE; @@ -1712,7 +1712,22 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[89]); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21091, video_compress_fb(uts, dev, false)); + ut_asserteq(21109, video_compress_fb(uts, dev, false)); + + /* go back 5 characters (before the newline) and use Ctrl+K */ + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_asserteq(85, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + + /* Ctrl+K in multiline mode should only delete to the newline */ + ut_assertok(expo_send_key(exp, CTL_CH('k'))); + ut_asserteq(85, ted->tin.cls.num); + ut_asserteq(86, ted->tin.cls.eol_num); + ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[85]); /* clear multiline mode, close the textedit with Enter (BKEY_SELECT) */ ted->obj.flags &= ~SCENEOF_MULTILINE; @@ -1724,12 +1739,12 @@ static int expo_render_textedit(struct unit_test_state *uts) /* check the textedit is closed and text is changed */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); - ut_asserteq_str("his\nis the initial contents of the text " - "editor but it is ely that more will be added latr\n", + ut_asserteq_str("This\ns the initial contents of the text " + "editor but it is ely that more will be added \n", abuf_data(&ted->tin.buf)); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21230, video_compress_fb(uts, dev, false)); + ut_asserteq(21099, video_compress_fb(uts, dev, false)); abuf_uninit(&buf); abuf_uninit(&logo_copy); -- 2.43.0
Add emacs-style yank (paste) functionality to the command-line editor. When CONFIG_CMDLINE_UNDO is enabled, Ctrl+Y inserts the last killed text at the cursor position. Text is saved to the yank buffer during kill operations: - Ctrl+K (kill to end of line) - Ctrl+W (kill word backwards) - Ctrl+U (kill entire line) - Ctrl+X (kill entire line) The yank buffer is separate from the undo buffer, allowing both features to work independently. Yanking text also saves the current state to the undo buffer, so it can be undone with Ctrl+Z. Enable CMDLINE_UNDO by default in sandbox for testing. Add test coverage for Ctrl+Y yank functionality. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- cmd/Kconfig | 9 +++ common/Makefile | 1 + common/cli_readline.c | 56 ++++++++++++++++- common/cli_undo.c | 142 ++++++++++++++++++++++++++++++++++++++++++ include/cli.h | 96 ++++++++++++++++++++++++++++ test/boot/expo.c | 10 ++- 6 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 common/cli_undo.c diff --git a/cmd/Kconfig b/cmd/Kconfig index 606a34f8869..9d96c12fd86 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -66,6 +66,15 @@ config CMDLINE_EDITOR - Undo/redo support (Ctrl+Z / Ctrl+Shift+Z) - Yank/paste of killed text (Ctrl+Y) +config CMDLINE_UNDO + bool "Support undo in command-line editing" + depends on CMDLINE_EDITOR + default y + help + Enable an undo buffer for command-line editing. When enabled, + pressing Ctrl+Z restores the previous state of the edit buffer. + This uses additional memory to store the undo state. + config CMDLINE_PS_SUPPORT bool "Enable support for changing the command prompt string at run-time" depends on HUSH_PARSER diff --git a/common/Makefile b/common/Makefile index 125f768ef53..a9d7a516b56 100644 --- a/common/Makefile +++ b/common/Makefile @@ -10,6 +10,7 @@ obj-y += main.o obj-y += memtop.o obj-y += exports.o obj-y += cli_getch.o cli_simple.o cli_readline.o +obj-$(CONFIG_CMDLINE_UNDO) += cli_undo.o obj-$(CONFIG_HUSH_OLD_PARSER) += cli_hush.o obj-$(CONFIG_HUSH_MODERN_PARSER) += cli_hush_modern.o obj-$(CONFIG_AUTOBOOT) += autoboot.o diff --git a/common/cli_readline.c b/common/cli_readline.c index 847b49450b5..d554b7241c6 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -358,6 +358,8 @@ static void cread_end_of_line(struct cli_line_state *cls) #define REFRESH_TO_EOL() GOTO_LINE_END(cls->eol_num) #endif +/* undo/yank functions are in cli_undo.c when CMDLINE_UNDO is enabled */ + static void cread_add_char(struct cli_line_state *cls, char ichar, int insert, uint *num, uint *eol_num, char *buf, uint len) { @@ -484,9 +486,21 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) cls->eol_num--; } break; - case CTL_CH('k'): + case CTL_CH('k'): { + uint erase_to = cls->eol_num; + + ed = cli_editor(cls); + if (ed && ed->multiline) { + char *nl = strchr(&buf[cls->num], '\n'); + + if (nl) + erase_to = nl - buf; + } + cread_save_undo(cls); + cread_save_yank(cls, &buf[cls->num], erase_to - cls->num); cread_erase_to_eol(cls); break; + } case CTL_CH('e'): REFRESH_TO_EOL(); break; @@ -505,6 +519,8 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) /* now delete chars from base to cls->num */ wlen = cls->num - base; + cread_save_undo(cls); + cread_save_yank(cls, &buf[base], wlen); cls->eol_num -= wlen; memmove(&buf[base], &buf[cls->num], cls->eol_num - base + 1); @@ -517,7 +533,24 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) } break; case CTL_CH('x'): + if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { + cread_save_undo(cls); + cread_save_yank(cls, buf, cls->eol_num); + BEGINNING_OF_LINE(); + cread_erase_to_eol(cls); + } + break; + case CTL_CH('y'): +#if CONFIG_IS_ENABLED(CMDLINE_UNDO) + cread_yank(cls); +#endif + break; + case CTL_CH('z'): + cread_restore_undo(cls); + break; case CTL_CH('u'): + cread_save_undo(cls); + cread_save_yank(cls, buf, cls->eol_num); BEGINNING_OF_LINE(); cread_erase_to_eol(cls); break; @@ -630,6 +663,27 @@ void cli_cread_init(struct cli_line_state *cls, char *buf, uint buf_size) cls->len = buf_size; } +void cli_cread_init_undo(struct cli_line_state *cls, char *buf, uint buf_size) +{ + cli_cread_init(cls, buf, buf_size); + if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { + struct cli_editor_state *ed = cli_editor(cls); + + abuf_init_size(&ed->undo.buf, buf_size); + abuf_init_size(&ed->yank, buf_size); + } +} + +void cli_cread_uninit(struct cli_line_state *cls) +{ + if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { + struct cli_editor_state *ed = cli_editor(cls); + + abuf_uninit(&ed->undo.buf); + abuf_uninit(&ed->yank); + } +} + void cli_cread_add_initial(struct cli_line_state *cls) { int init_len = strlen(cls->buf); diff --git a/common/cli_undo.c b/common/cli_undo.c new file mode 100644 index 00000000000..4aa9a719ebf --- /dev/null +++ b/common/cli_undo.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * CLI undo/yank support + * + * Copyright 2025 Google LLC + * Written by Simon Glass <sjg@chromium.org> + */ + +#include <cli.h> +#include <command.h> +#include <stdio.h> +#include <linux/string.h> +#include <asm/global_data.h> + +DECLARE_GLOBAL_DATA_PTR; + +#define CTL_BACKSPACE ('\b') + +/** + * cls_putch() - Output a character, using callback if available + * + * @cls: CLI line state + * @ch: Character to output + */ +static void cls_putch(struct cli_line_state *cls, int ch) +{ + struct cli_editor_state *ed = cli_editor(cls); + + if (ed && ed->putch) + ed->putch(cls, ch); + else + putc(ch); +} + +static void cls_putnstr(struct cli_line_state *cls, const char *str, size_t n) +{ + while (n-- > 0) + cls_putch(cls, *str++); +} + +/** + * cls_putchars() - Output a character multiple times + * + * @cls: CLI line state + * @count: Number of times to output the character + * @ch: Character to output + */ +static void cls_putchars(struct cli_line_state *cls, int count, int ch) +{ + int i; + + for (i = 0; i < count; i++) + cls_putch(cls, ch); +} + +#define getcmd_cbeep(cls) cls_putch(cls, '\a') + +void cread_save_undo(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + struct cli_undo_state *undo = &ed->undo; + + if (abuf_size(&undo->buf)) { + memcpy(abuf_data(&undo->buf), cls->buf, cls->len); + undo->num = cls->num; + undo->eol_num = cls->eol_num; + } +} + +void cread_restore_undo(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + struct cli_undo_state *undo = &ed->undo; + + if (!abuf_size(&undo->buf)) + return; + + /* go to start of line */ + while (cls->num) { + cls_putch(cls, CTL_BACKSPACE); + cls->num--; + } + + /* erase current content on screen */ + cls_putchars(cls, cls->eol_num, ' '); + cls_putchars(cls, cls->eol_num, CTL_BACKSPACE); + + /* restore from undo buffer */ + memcpy(cls->buf, abuf_data(&undo->buf), cls->len); + cls->eol_num = undo->eol_num; + + /* display restored content */ + cls_putnstr(cls, cls->buf, cls->eol_num); + + /* position cursor */ + cls_putchars(cls, cls->eol_num - undo->num, CTL_BACKSPACE); + cls->num = undo->num; +} + +void cread_save_yank(struct cli_line_state *cls, const char *text, uint len) +{ + struct cli_editor_state *ed = cli_editor(cls); + + if (abuf_size(&ed->yank) && len > 0 && len < cls->len) { + memcpy(abuf_data(&ed->yank), text, len); + ed->yank_len = len; + } +} + +void cread_yank(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + char *buf = cls->buf; + uint i; + + if (!abuf_size(&ed->yank) || !ed->yank_len) + return; + + /* check if there's room */ + if (cls->eol_num + ed->yank_len > cls->len - 1) { + getcmd_cbeep(cls); + return; + } + + cread_save_undo(cls); + + /* make room for yanked text */ + memmove(&buf[cls->num + ed->yank_len], &buf[cls->num], + cls->eol_num - cls->num + 1); + + /* insert yanked text */ + memcpy(&buf[cls->num], abuf_data(&ed->yank), ed->yank_len); + cls->eol_num += ed->yank_len; + + /* display from cursor to end */ + cls_putnstr(cls, &buf[cls->num], cls->eol_num - cls->num); + + /* move cursor to end of inserted text */ + cls->num += ed->yank_len; + for (i = cls->num; i < cls->eol_num; i++) + cls_putch(cls, CTL_BACKSPACE); +} diff --git a/include/cli.h b/include/cli.h index b6a8a6be1dd..3040342de8e 100644 --- a/include/cli.h +++ b/include/cli.h @@ -7,6 +7,7 @@ #ifndef __CLI_H #define __CLI_H +#include <abuf.h> #include <stdbool.h> #include <linux/types.h> @@ -27,6 +28,19 @@ struct cli_ch_state { struct cli_line_state; +/** + * struct cli_undo_state - state for undo buffer + * + * @buf: Buffer for saved state + * @num: Saved cursor position + * @eol_num: Saved end-of-line position + */ +struct cli_undo_state { + struct abuf buf; + uint num; + uint eol_num; +}; + /** * struct cli_editor_state - state for enhanced editing features * @@ -36,6 +50,9 @@ struct cli_line_state; * @line_nav: Handle multi-line navigation (Ctrl-P/N) * @multiline: true if input may contain multiple lines (enables * Ctrl-P/N for line navigation instead of history) + * @undo: Undo ring buffer state + * @yank: Buffer for killed text (for Ctrl+Y yank) + * @yank_len: Length of killed text in yank buffer */ struct cli_editor_state { /** @@ -60,6 +77,15 @@ struct cli_editor_state { * Ctrl-P/N for line navigation instead of history) */ bool multiline; + + /** @undo: Undo state (if CONFIG_CMDLINE_UNDO) */ + struct cli_undo_state undo; + + /** @yank: Buffer for killed text (for Ctrl+Y yank) */ + struct abuf yank; + + /** @yank_len: Length of killed text in yank buffer */ + uint yank_len; }; /** @@ -336,6 +362,24 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar); */ void cli_cread_init(struct cli_line_state *cls, char *buf, uint buf_size); +/** + * cli_cread_init_undo() - Set up a new cread struct with undo support + * + * Like cli_cread_init() but also sets up the undo buffer. + * + * @cls: CLI line state + * @buf: Text buffer containing the initial text + * @buf_size: Buffer size, including nul terminator + */ +void cli_cread_init_undo(struct cli_line_state *cls, char *buf, uint buf_size); + +/** + * cli_cread_uninit() - Free resources allocated by cli_cread_init_undo() + * + * @cls: CLI line state + */ +void cli_cread_uninit(struct cli_line_state *cls); + /** * cli_cread_add_initial() - Output initial buffer contents * @@ -349,4 +393,56 @@ void cli_cread_add_initial(struct cli_line_state *cls); /** cread_print_hist_list() - Print the command-line history list */ void cread_print_hist_list(void); +/* + * Undo/yank functions - implementations in cli_undo.c when CMDLINE_UNDO is enabled + */ +#if CONFIG_IS_ENABLED(CMDLINE_UNDO) +/** + * cread_save_undo() - Save current state for undo + * + * @cls: CLI line state + */ +void cread_save_undo(struct cli_line_state *cls); + +/** + * cread_restore_undo() - Restore previous state from undo buffer + * + * @cls: CLI line state + */ +void cread_restore_undo(struct cli_line_state *cls); + +/** + * cread_save_yank() - Save killed text to yank buffer + * + * @cls: CLI line state + * @text: Text to save + * @len: Length of text + */ +void cread_save_yank(struct cli_line_state *cls, const char *text, uint len); + +/** + * cread_yank() - Insert yanked text at cursor position + * + * @cls: CLI line state + */ +void cread_yank(struct cli_line_state *cls); +#else +static inline void cread_save_undo(struct cli_line_state *cls) +{ +} + +static inline void cread_restore_undo(struct cli_line_state *cls) +{ +} + +static inline void cread_save_yank(struct cli_line_state *cls, const char *text, + uint len) +{ +} + +static inline void cread_yank(struct cli_line_state *cls) +{ +} +#endif + #endif diff --git a/test/boot/expo.c b/test/boot/expo.c index 366183e4a79..5445fed19c1 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1729,6 +1729,12 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(86, ted->tin.cls.eol_num); ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[85]); + /* Ctrl+Y yanks back the killed text "latr" */ + ut_assertok(expo_send_key(exp, CTL_CH('y'))); + ut_asserteq(89, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[89]); + /* clear multiline mode, close the textedit with Enter (BKEY_SELECT) */ ted->obj.flags &= ~SCENEOF_MULTILINE; ut_assertok(expo_send_key(exp, BKEY_SELECT)); @@ -1740,11 +1746,11 @@ static int expo_render_textedit(struct unit_test_state *uts) /* check the textedit is closed and text is changed */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); ut_asserteq_str("This\ns the initial contents of the text " - "editor but it is ely that more will be added \n", + "editor but it is ely that more will be added latr\n", abuf_data(&ted->tin.buf)); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21099, video_compress_fb(uts, dev, false)); + ut_asserteq(21251, video_compress_fb(uts, dev, false)); abuf_uninit(&buf); abuf_uninit(&logo_copy); -- 2.43.0
Convert the single-level undo buffer to a ring buffer supporting multiple undo/redo levels. This allows users to undo multiple editing operations and redo them if needed. Key changes: - Replace single undo state with ring buffer (struct cli_undo_state) - Add redo ring buffer for undone states - Add Ctrl+Shift+Z (via Ctrl+G) for redo operation - Track undo/redo counts separately from buffer allocation - Clear redo history on new edits (standard editor behaviour) The number of undo levels is configurable via CONFIG_CMDLINE_UNDO_COUNT (default 64). Each level stores a complete copy of the edit buffer plus cursor position. Also fix scene_txtin_open() which was allocating the yank buffer twice - once in cli_cread_init_undo() and again directly. Remove the duplicate. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 36 +++++++++++- cmd/Kconfig | 15 +++++ common/cli_getch.c | 19 +++++- common/cli_readline.c | 40 ++++++++++--- common/cli_undo.c | 133 ++++++++++++++++++++++++++++++++++++++---- doc/develop/expo.rst | 14 +++++ doc/usage/cmdline.rst | 68 +++++++++++++++++++++ include/cli.h | 86 ++++++++++++++++++++++++--- test/boot/editenv.c | 62 ++++++++++++++++++-- test/boot/expo.c | 12 +++- 10 files changed, 446 insertions(+), 39 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index ab4fd5056a0..101616acc07 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -11,6 +11,7 @@ #include <cli.h> #include <expo.h> #include <log.h> +#include <malloc.h> #include <menu.h> #include <video_console.h> #include <linux/errno.h> @@ -18,6 +19,12 @@ #include <linux/string.h> #include "scene_internal.h" +#ifdef CONFIG_CMDLINE_UNDO_COUNT +#define UNDO_COUNT CONFIG_CMDLINE_UNDO_COUNT +#else +#define UNDO_COUNT 64 +#endif + int scene_txtin_init(struct scene_txtin *tin, uint size, uint line_chars) { char *buf; @@ -161,6 +168,10 @@ static void scene_txtin_putch(struct cli_line_state *cls, int ch) void scene_txtin_close(struct scene *scn, struct scene_txtin *tin) { + struct cli_line_state *cls = &tin->cls; + + cli_cread_uninit(cls); + /* cursor is not needed now */ vidconsole_readline_end(scn->expo->cons, tin->ctx); } @@ -265,7 +276,7 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, struct udevice *cons = scn->expo->cons; struct scene_obj_txt *txt; void *ctx; - int ret; + int ret, i; ctx = tin->ctx; if (!ctx) { @@ -286,10 +297,31 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, vidconsole_set_cursor_pos(cons, ctx, txt->obj.bbox.x0, txt->obj.bbox.y0); vidconsole_entry_start(cons, ctx); - cli_cread_init(cls, abuf_data(&tin->buf), abuf_size(&tin->buf)); + cli_cread_init_undo(cls, abuf_data(&tin->buf), abuf_size(&tin->buf)); cls->insert = true; ed->putch = scene_txtin_putch; cls->priv = scn; + + /* Initialise undo ring buffer */ + alist_init_struct(&ed->undo.pos, struct cli_undo_pos); + for (i = 0; i < UNDO_COUNT; i++) { + struct cli_undo_pos *pos; + + pos = alist_ensure(&ed->undo.pos, i, struct cli_undo_pos); + abuf_init_size(&pos->buf, abuf_size(&tin->buf)); + } + + /* Initialise redo ring buffer */ + alist_init_struct(&ed->redo.pos, struct cli_undo_pos); + for (i = 0; i < UNDO_COUNT; i++) { + struct cli_undo_pos *pos; + + pos = alist_ensure(&ed->redo.pos, i, struct cli_undo_pos); + abuf_init_size(&pos->buf, abuf_size(&tin->buf)); + } + + /* yank buffer is initialised by cli_cread_init_undo() above */ + if (obj->type == SCENEOBJT_TEXTEDIT) { ed->multiline = true; ed->line_nav = scene_txtin_line_nav; diff --git a/cmd/Kconfig b/cmd/Kconfig index 9d96c12fd86..03e8014786b 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -66,6 +66,8 @@ config CMDLINE_EDITOR - Undo/redo support (Ctrl+Z / Ctrl+Shift+Z) - Yank/paste of killed text (Ctrl+Y) + This uses additional memory to store the undo, redo, and yank buffers. + config CMDLINE_UNDO bool "Support undo in command-line editing" depends on CMDLINE_EDITOR @@ -75,6 +77,19 @@ config CMDLINE_UNDO pressing Ctrl+Z restores the previous state of the edit buffer. This uses additional memory to store the undo state. +config CMDLINE_UNDO_COUNT + int "Number of undo levels" + depends on CMDLINE_UNDO + default 64 + range 1 64 + help + Number of undo/redo levels to support. Each level requires memory + to store a copy of the edit buffer. With multiple levels, + pressing Ctrl+Z repeatedly undoes progressively older changes, and + Ctrl+Shift+Z redoes them. Set to 1 for single-level undo/redo, or + higher for multi-level. Note that any new edit clears the redo + history. + config CMDLINE_PS_SUPPORT bool "Enable support for changing the command prompt string at run-time" depends on HUSH_PARSER diff --git a/common/cli_getch.c b/common/cli_getch.c index 8df1997911a..810128a9fca 100644 --- a/common/cli_getch.c +++ b/common/cli_getch.c @@ -134,14 +134,20 @@ static int cli_ch_esc(struct cli_ch_state *cch, int ichar, cch->esc_save[3] == ';') act = ESC_SAVE; break; + case '6': + /* Ctrl+Shift+key: ESC [ 1 ; 6 */ + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && + cch->esc_save[3] == ';') + act = ESC_SAVE; + break; } break; case 5: if (ichar == '~') { /* bracketed paste */ ichar = 0; act = ESC_CONVERTED; - } else if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && - cch->esc_save[4] == '5') { + } + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && cch->esc_save[4] == '5') { /* Ctrl+arrow: ESC [ 1 ; 5 D/C */ switch (ichar) { case 'D': /* Ctrl+<- key */ @@ -154,6 +160,15 @@ static int cli_ch_esc(struct cli_ch_state *cch, int ichar, break; /* pass to forward-word handler */ } } + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR) && cch->esc_save[4] == '6') { + /* Ctrl+Shift+key: ESC [ 1 ; 6 x */ + switch (ichar) { + case 'z': /* Ctrl+Shift+Z: redo */ + ichar = CTL_CH('g'); + act = ESC_CONVERTED; + break; + } + } } *actp = act; diff --git a/common/cli_readline.c b/common/cli_readline.c index d554b7241c6..fac5080cc07 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -113,6 +113,14 @@ static char hist_data[HIST_MAX][HIST_SIZE + 1]; #endif static char *hist_list[HIST_MAX]; +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) +#ifdef CONFIG_CMDLINE_UNDO_COUNT +#define UNDO_COUNT CONFIG_CMDLINE_UNDO_COUNT +#else +#define UNDO_COUNT 64 +#endif +#endif + #define add_idx_minus_one() ((hist_add_idx == 0) ? hist_max : hist_add_idx-1) /** @@ -358,7 +366,7 @@ static void cread_end_of_line(struct cli_line_state *cls) #define REFRESH_TO_EOL() GOTO_LINE_END(cls->eol_num) #endif -/* undo/yank functions are in cli_undo.c when CMDLINE_UNDO is enabled */ +/* undo/redo/yank functions are in cli_undo.c when CMDLINE_EDITOR is enabled */ static void cread_add_char(struct cli_line_state *cls, char ichar, int insert, uint *num, uint *eol_num, char *buf, uint len) @@ -374,6 +382,9 @@ static void cread_add_char(struct cli_line_state *cls, char ichar, int insert, (*eol_num)++; } + /* new edit invalidates redo history */ + cread_clear_redo(cls); + if (insert) { wlen = *eol_num - *num; if (wlen > 1) @@ -472,6 +483,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) if (cls->num < cls->eol_num) { uint wlen; + cread_save_undo(cls); wlen = cls->eol_num - cls->num - 1; if (wlen) { memmove(&buf[cls->num], &buf[cls->num + 1], @@ -511,6 +523,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) if (cls->num) { uint base, wlen; + cread_save_undo(cls); for (base = cls->num - 1; base >= 0 && buf[base] == ' ';) base--; @@ -519,7 +532,6 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) /* now delete chars from base to cls->num */ wlen = cls->num - base; - cread_save_undo(cls); cread_save_yank(cls, &buf[base], wlen); cls->eol_num -= wlen; memmove(&buf[base], &buf[cls->num], @@ -541,13 +553,18 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) } break; case CTL_CH('y'): -#if CONFIG_IS_ENABLED(CMDLINE_UNDO) +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) cread_yank(cls); #endif break; case CTL_CH('z'): cread_restore_undo(cls); break; + case CTL_CH('g'): +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) + cread_redo(cls); +#endif + break; case CTL_CH('u'): cread_save_undo(cls); cread_save_yank(cls, buf, cls->eol_num); @@ -560,6 +577,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) if (cls->num) { uint wlen; + cread_save_undo(cls); wlen = cls->eol_num - cls->num; cls->num--; memmove(&buf[cls->num], &buf[cls->num + 1], wlen); @@ -605,6 +623,8 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) break; } + cread_save_undo(cls); + /* nuke the current line */ /* first, go home */ BEGINNING_OF_LINE(); @@ -632,6 +652,7 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) buf[cls->num] = '\0'; col = strlen(cls->prompt) + cls->eol_num; num2 = cls->num; + cread_save_undo(cls); if (cmd_auto_complete(cls->prompt, buf, &num2, &col)) { col = num2 - cls->num; cls->num += col; @@ -669,17 +690,22 @@ void cli_cread_init_undo(struct cli_line_state *cls, char *buf, uint buf_size) if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { struct cli_editor_state *ed = cli_editor(cls); - abuf_init_size(&ed->undo.buf, buf_size); abuf_init_size(&ed->yank, buf_size); } } void cli_cread_uninit(struct cli_line_state *cls) { - if (CONFIG_IS_ENABLED(CMDLINE_UNDO)) { + if (CONFIG_IS_ENABLED(CMDLINE_EDITOR)) { struct cli_editor_state *ed = cli_editor(cls); - - abuf_uninit(&ed->undo.buf); + struct cli_undo_pos *pos; + + alist_for_each(pos, &ed->undo.pos) + abuf_uninit(&pos->buf); + alist_uninit(&ed->undo.pos); + alist_for_each(pos, &ed->redo.pos) + abuf_uninit(&pos->buf); + alist_uninit(&ed->redo.pos); abuf_uninit(&ed->yank); } } diff --git a/common/cli_undo.c b/common/cli_undo.c index 4aa9a719ebf..43858d4636d 100644 --- a/common/cli_undo.c +++ b/common/cli_undo.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0+ /* - * CLI undo/yank support + * CLI undo/redo/yank support * * Copyright 2025 Google LLC * Written by Simon Glass <sjg@chromium.org> @@ -55,26 +55,134 @@ static void cls_putchars(struct cli_line_state *cls, int count, int ch) #define getcmd_cbeep(cls) cls_putch(cls, '\a') +void cread_save_redo(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + struct cli_undo_state *redo = &ed->redo; + struct cli_undo_pos *pos; + uint idx; + + if (!redo->pos.alloc) + return; + + /* save at current head position */ + idx = redo->head; + pos = alist_getw(&redo->pos, idx, struct cli_undo_pos); + memcpy(abuf_data(&pos->buf), cls->buf, cls->len); + pos->num = cls->num; + pos->eol_num = cls->eol_num; + + /* advance head (ring buffer) */ + redo->head = (redo->head + 1) % redo->pos.alloc; + + /* track how many redo levels are available */ + if (redo->count < redo->pos.alloc) + redo->count++; +} + +void cread_clear_redo(struct cli_line_state *cls) +{ + struct cli_editor_state *ed = cli_editor(cls); + + ed->redo.count = 0; + ed->redo.head = 0; +} + void cread_save_undo(struct cli_line_state *cls) { struct cli_editor_state *ed = cli_editor(cls); struct cli_undo_state *undo = &ed->undo; + struct cli_undo_pos *pos; + uint idx; - if (abuf_size(&undo->buf)) { - memcpy(abuf_data(&undo->buf), cls->buf, cls->len); - undo->num = cls->num; - undo->eol_num = cls->eol_num; - } + if (!undo->pos.alloc) + return; + + /* save at current head position */ + idx = undo->head; + pos = alist_getw(&undo->pos, idx, struct cli_undo_pos); + memcpy(abuf_data(&pos->buf), cls->buf, cls->len); + pos->num = cls->num; + pos->eol_num = cls->eol_num; + + /* advance head (ring buffer) */ + undo->head = (undo->head + 1) % undo->pos.alloc; + + /* track how many undo levels are available */ + if (undo->count < undo->pos.alloc) + undo->count++; + + /* new edit invalidates redo history */ + cread_clear_redo(cls); } void cread_restore_undo(struct cli_line_state *cls) +{ + struct cli_undo_state *undo = &cli_editor(cls)->undo; + const struct cli_undo_pos *pos; + uint idx; + + if (!undo->pos.alloc || !undo->count) + return; + + /* save current state to redo buffer before restoring */ + cread_save_redo(cls); + + /* move back to previous undo state */ + undo->head = undo->head ? undo->head - 1 : undo->pos.alloc - 1; + undo->count--; + idx = undo->head; + + /* go to start of line */ + while (cls->num) { + cls_putch(cls, CTL_BACKSPACE); + cls->num--; + } + + /* erase current content on screen */ + cls_putchars(cls, cls->eol_num, ' '); + cls_putchars(cls, cls->eol_num, CTL_BACKSPACE); + + /* restore from undo buffer */ + pos = alist_get(&undo->pos, idx, struct cli_undo_pos); + memcpy(cls->buf, abuf_data(&pos->buf), cls->len); + cls->eol_num = pos->eol_num; + + /* display restored content */ + cls_putnstr(cls, cls->buf, cls->eol_num); + + /* position cursor */ + cls_putchars(cls, cls->eol_num - pos->num, CTL_BACKSPACE); + cls->num = pos->num; +} + +void cread_redo(struct cli_line_state *cls) { struct cli_editor_state *ed = cli_editor(cls); struct cli_undo_state *undo = &ed->undo; + struct cli_undo_state *redo = &ed->redo; + struct cli_undo_pos *pos; + const struct cli_undo_pos *rpos; + uint idx; - if (!abuf_size(&undo->buf)) + if (!redo->pos.alloc || !redo->count) return; + /* save current state to undo buffer */ + idx = undo->head; + pos = alist_getw(&undo->pos, idx, struct cli_undo_pos); + memcpy(abuf_data(&pos->buf), cls->buf, cls->len); + pos->num = cls->num; + pos->eol_num = cls->eol_num; + undo->head = (undo->head + 1) % undo->pos.alloc; + if (undo->count < undo->pos.alloc) + undo->count++; + + /* move back to previous redo state */ + redo->head = redo->head ? redo->head - 1 : redo->pos.alloc - 1; + redo->count--; + idx = redo->head; + /* go to start of line */ while (cls->num) { cls_putch(cls, CTL_BACKSPACE); @@ -85,16 +193,17 @@ void cread_restore_undo(struct cli_line_state *cls) cls_putchars(cls, cls->eol_num, ' '); cls_putchars(cls, cls->eol_num, CTL_BACKSPACE); - /* restore from undo buffer */ - memcpy(cls->buf, abuf_data(&undo->buf), cls->len); - cls->eol_num = undo->eol_num; + /* restore from redo buffer */ + rpos = alist_get(&redo->pos, idx, struct cli_undo_pos); + memcpy(cls->buf, abuf_data(&rpos->buf), cls->len); + cls->eol_num = rpos->eol_num; /* display restored content */ cls_putnstr(cls, cls->buf, cls->eol_num); /* position cursor */ - cls_putchars(cls, cls->eol_num - undo->num, CTL_BACKSPACE); - cls->num = undo->num; + cls_putchars(cls, cls->eol_num - rpos->num, CTL_BACKSPACE); + cls->num = rpos->num; } void cread_save_yank(struct cli_line_state *cls, const char *text, uint len) diff --git a/doc/develop/expo.rst b/doc/develop/expo.rst index 71e227c532d..046b464d9f2 100644 --- a/doc/develop/expo.rst +++ b/doc/develop/expo.rst @@ -366,6 +366,9 @@ type "textline" A line of text which can be edited + "textedit" + A multi-line text editor + "box" A rectangle with a given line width (not filled) @@ -446,6 +449,17 @@ max-chars: Specifies the maximum number of characters permitted to be in the textline. The user will be prevented from adding more. +Textedit nodes have the same properties as textline nodes, with the following +differences: + +- The editor supports multiple lines of text +- Pressing Enter inserts a newline instead of closing the editor +- Home/End move to start/end of the current line +- Ctrl+K kills to end of the current line (not entire buffer) +- Up/Down (Ctrl+P/N) navigate between lines + +See :doc:`../usage/cmdline` for a full list of editing keys. + Box nodes have the following additional properties: width diff --git a/doc/usage/cmdline.rst b/doc/usage/cmdline.rst index 58240c5279c..e1525b12884 100644 --- a/doc/usage/cmdline.rst +++ b/doc/usage/cmdline.rst @@ -91,3 +91,71 @@ convenient:: => i2c speed 0x30000 Setting bus speed to 196608 Hz + +Command-line editing +-------------------- + +U-Boot supports command-line editing when `CONFIG_CMDLINE_EDITING` is enabled. +This provides an Emacs-like interface for editing commands before they are +executed. The following key bindings are available: + +Cursor movement +~~~~~~~~~~~~~~~ + +- **Left arrow** or **Ctrl+B**: Move cursor left one character +- **Right arrow** or **Ctrl+F**: Move cursor right one character +- **Ctrl+Left** or **Alt+B**: Move cursor left one word +- **Ctrl+Right** or **Alt+F**: Move cursor right one word +- **Home** or **Ctrl+A**: Move to beginning of line +- **End** or **Ctrl+E**: Move to end of line + +Character deletion +~~~~~~~~~~~~~~~~~~ + +- **Backspace** or **Ctrl+H**: Delete character before cursor +- **Delete** or **Ctrl+D**: Delete character at cursor +- **Ctrl+K**: Kill (delete) from cursor to end of line +- **Ctrl+W**: Kill word before cursor +- **Ctrl+U**: Kill entire line +- **Ctrl+X**: Kill entire line (same as Ctrl+U) + +History +~~~~~~~ + +- **Up arrow** or **Ctrl+P**: Previous command in history +- **Down arrow** or **Ctrl+N**: Next command in history + +Undo, redo, and yank +~~~~~~~~~~~~~~~~~~~~ + +When `CONFIG_CMDLINE_UNDO` is enabled, the following features are available: + +- **Ctrl+Z**: Undo the last edit operation +- **Ctrl+Shift+Z**: Redo the last undone operation +- **Ctrl+Y**: Yank (paste) previously killed text + +Text killed by Ctrl+K, Ctrl+W, Ctrl+U, or Ctrl+X is saved to a yank buffer +and can be pasted with Ctrl+Y. + +The number of undo/redo levels can be configured with `CONFIG_CMDLINE_UNDO_COUNT` +(default 1, maximum 64). Each level saves the complete buffer state, +so higher values use more memory. Note that any new edit clears the redo +history. + +Other +~~~~~ + +- **Tab**: Command and argument completion (if `CONFIG_AUTO_COMPLETE` is enabled) +- **Ctrl+C**: Cancel current input +- **Enter**: Execute command + +Multiline editing +~~~~~~~~~~~~~~~~~ + +In multiline mode (used by expo text editors), some keys have modified +behaviour: + +- **Home/End**: Move to start/end of current line (not entire buffer) +- **Ctrl+K**: Kill to end of current line (not entire buffer) +- **Ctrl+P/N** or **Up/Down**: Navigate between lines +- **Enter**: Insert newline (instead of executing) diff --git a/include/cli.h b/include/cli.h index 3040342de8e..1b23caaa078 100644 --- a/include/cli.h +++ b/include/cli.h @@ -8,6 +8,7 @@ #define __CLI_H #include <abuf.h> +#include <alist.h> #include <stdbool.h> #include <linux/types.h> @@ -29,18 +30,44 @@ struct cli_ch_state { struct cli_line_state; /** - * struct cli_undo_state - state for undo buffer + * struct cli_undo_pos - saved state for a single undo/redo level * - * @buf: Buffer for saved state - * @num: Saved cursor position - * @eol_num: Saved end-of-line position + * Before any editing operation (insert, delete, kill, etc.), the entire + * buffer state is saved so it can be restored on undo. The buffer contents, + * cursor position, and line length are captured together. + * + * @buf: Complete copy of the edit buffer at the time of save + * @num: Cursor position (offset from start of buffer) + * @eol_num: Number of characters in the buffer (end-of-line position) */ -struct cli_undo_state { +struct cli_undo_pos { struct abuf buf; uint num; uint eol_num; }; +/** + * struct cli_undo_state - state for undo/redo ring buffer + * + * This implements a ring buffer for storing undo or redo states. Each state + * consists of a complete copy of the edit buffer plus the cursor position. + * The ring buffer allows multiple levels of undo/redo up to alloc entries. + * + * When saving a new state, it is written at the @head index, then @head + * advances (wrapping at alloc). When restoring, @head moves back and the + * state at that index is restored. The @count tracks how many valid states + * are available for undo/redo. + * + * @pos: List of &struct cli_undo_pos entries + * @head: Index where the next state will be saved (0 to alloc-1) + * @count: Number of valid states available (0 to alloc) + */ +struct cli_undo_state { + struct alist pos; + uint head; + uint count; +}; + /** * struct cli_editor_state - state for enhanced editing features * @@ -51,6 +78,7 @@ struct cli_undo_state { * @multiline: true if input may contain multiple lines (enables * Ctrl-P/N for line navigation instead of history) * @undo: Undo ring buffer state + * @redo: Redo ring buffer state * @yank: Buffer for killed text (for Ctrl+Y yank) * @yank_len: Length of killed text in yank buffer */ @@ -81,6 +109,9 @@ struct cli_editor_state { /** @undo: Undo state (if CONFIG_CMDLINE_UNDO) */ struct cli_undo_state undo; + /** @redo: Redo state (if CONFIG_CMDLINE_UNDO) */ + struct cli_undo_state redo; + /** @yank: Buffer for killed text (for Ctrl+Y yank) */ struct abuf yank; @@ -393,13 +424,14 @@ void cli_cread_add_initial(struct cli_line_state *cls); /** cread_print_hist_list() - Print the command-line history list */ void cread_print_hist_list(void); -/* - * Undo/yank functions - implementations in cli_undo.c when CMDLINE_UNDO is enabled - */ -#if CONFIG_IS_ENABLED(CMDLINE_UNDO) +#if CONFIG_IS_ENABLED(CMDLINE_EDITOR) /** * cread_save_undo() - Save current state for undo * + * Saves the buffer contents and cursor position to the undo ring buffer. + * Each call pushes a new undo state that can be restored with Ctrl+Z. + * Also clears the redo buffer since a new edit invalidates redo history. + * * @cls: CLI line state */ void cread_save_undo(struct cli_line_state *cls); @@ -407,13 +439,29 @@ void cread_save_undo(struct cli_line_state *cls); /** * cread_restore_undo() - Restore previous state from undo buffer * + * Restores the buffer contents and cursor position from the most recent + * undo state. Multiple calls restore progressively older states. The + * current state is saved to the redo buffer before restoring. + * * @cls: CLI line state */ void cread_restore_undo(struct cli_line_state *cls); +/** + * cread_redo() - Redo previously undone change + * + * Restores the buffer contents and cursor position from the redo buffer. + * The current state is saved to the undo buffer before restoring. + * + * @cls: CLI line state + */ +void cread_redo(struct cli_line_state *cls); + /** * cread_save_yank() - Save killed text to yank buffer * + * Saves the specified text so it can be yanked (pasted) later with Ctrl+Y. + * * @cls: CLI line state * @text: Text to save * @len: Length of text @@ -423,9 +471,21 @@ void cread_save_yank(struct cli_line_state *cls, const char *text, uint len); /** * cread_yank() - Insert yanked text at cursor position * + * Inserts the previously killed text at the current cursor position. + * * @cls: CLI line state */ void cread_yank(struct cli_line_state *cls); + +/** + * cread_clear_redo() - Clear the redo buffer + * + * Called when a new edit is made to invalidate the redo history. This should + * be called for any edit operation that modifies the buffer. + * + * @cls: CLI line state + */ +void cread_clear_redo(struct cli_line_state *cls); #else static inline void cread_save_undo(struct cli_line_state *cls) { @@ -435,6 +495,10 @@ static inline void cread_restore_undo(struct cli_line_state *cls) { } +static inline void cread_redo(struct cli_line_state *cls) +{ +} + static inline void cread_save_yank(struct cli_line_state *cls, const char *text, uint len) { @@ -443,6 +507,10 @@ static inline void cread_save_yank(struct cli_line_state *cls, const char *text, static inline void cread_yank(struct cli_line_state *cls) { } + +static inline void cread_clear_redo(struct cli_line_state *cls) +{ +} #endif #endif diff --git a/test/boot/editenv.c b/test/boot/editenv.c index ab3c6648886..9a41d269d17 100644 --- a/test/boot/editenv.c +++ b/test/boot/editenv.c @@ -74,8 +74,8 @@ static int editenv_test_base(struct unit_test_state *uts) int ret; /* - * Type "test" then press Ctrl-S to save - * \x13 is Ctrl-S + * Type "test" then press Ctrl-S to accept (Enter inserts newline in + * multiline mode) */ console_in_puts("test\x13"); ret = expo_editenv("myvar", NULL, buf, sizeof(buf)); @@ -94,7 +94,7 @@ static int editenv_test_initial(struct unit_test_state *uts) /* * Start with "world", go to start with Ctrl-A, type "hello ", then - * press Ctrl-S to save + * press Ctrl-S to accept */ console_in_puts("\x01hello \x13"); ret = expo_editenv("myvar", "world", buf, sizeof(buf)); @@ -190,11 +190,61 @@ static int editenv_test_funcs(struct unit_test_state *uts) ut_assertok(editenv_send(&info, CTL_CH('k'))); ut_asserteq(16033, ut_check_video(uts, "kill")); + /* Test undo - should restore the killed text */ + ut_assertok(editenv_send(&info, CTL_CH('z'))); + ut_asserteq(16877, ut_check_video(uts, "undo")); + + /* Kill again and yank it back - text should be restored */ + ut_assertok(editenv_send(&info, CTL_CH('k'))); + ut_asserteq(16033, ut_check_video(uts, "kill2")); + + ut_assertok(editenv_send(&info, CTL_CH('y'))); + ut_asserteq(16808, ut_check_video(uts, "yank")); + + /* Test Home - should go to start of current line */ + ut_assertok(editenv_send(&info, CTL_CH('a'))); + ut_asserteq(0, info.ted->tin.cls.num); + ut_asserteq(16845, ut_check_video(uts, "home")); + + /* Test End - should go to end of current line */ + ut_assertok(editenv_send(&info, CTL_CH('e'))); + ut_asserteq(16808, ut_check_video(uts, "end")); + + /* Go left two words with Ctrl+R */ + ut_assertok(editenv_send(&info, CTL_CH('r'))); + ut_asserteq(16838, ut_check_video(uts, "left1")); + + ut_assertok(editenv_send(&info, CTL_CH('r'))); + ut_asserteq(16812, ut_check_video(uts, "left2")); + + /* Delete three words with Ctrl+W */ + ut_assertok(editenv_send(&info, CTL_CH('w'))); + ut_asserteq(16691, ut_check_video(uts, "delw1")); + + ut_assertok(editenv_send(&info, CTL_CH('w'))); + ut_asserteq(16445, ut_check_video(uts, "delw2")); + + ut_assertok(editenv_send(&info, CTL_CH('w'))); + ut_asserteq(16118, ut_check_video(uts, "delw3")); + + /* Undo to restore one deleted word */ + ut_assertok(editenv_send(&info, CTL_CH('z'))); + ut_asserteq(16445, ut_check_video(uts, "undo1")); + + /* Type a character - this clears the redo buffer */ + ut_assertok(editenv_send(&info, '!')); + ut_asserteq(16469, ut_check_video(uts, "type")); + + /* Redo (Ctrl+G) should do nothing since typing cleared the redo buffer */ + ut_assertok(editenv_send(&info, CTL_CH('g'))); + ut_asserteq(16469, ut_check_video(uts, "redo")); + + /* Press Ctrl-S to save */ ut_asserteq(1, editenv_send(&info, BKEY_SAVE)); - /* The '*' is inserted after "tes", Ctrl-K killed "ted properly." */ - ut_assert(strstr(expo_editenv_result(&info), "tes*\n")); - ut_asserteq(16033, ut_check_video(uts, "save")); + /* The '*' and '!' are inserted; redo did nothing since it was cleared */ + ut_assert(strstr(expo_editenv_result(&info), "tes*ted")); + ut_asserteq(16469, ut_check_video(uts, "save")); expo_editenv_uninit(&info); diff --git a/test/boot/expo.c b/test/boot/expo.c index 5445fed19c1..7a3285535b5 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1735,6 +1735,16 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(90, ted->tin.cls.eol_num); ut_asserteq('\n', ((char *)abuf_data(&ted->tin.buf))[89]); + /* Ctrl+Z undoes the yank */ + ut_assertok(expo_send_key(exp, CTL_CH('z'))); + ut_asserteq(85, ted->tin.cls.num); + ut_asserteq(86, ted->tin.cls.eol_num); + + /* Ctrl+Shift+Z (internal code 'g') redoes the yank */ + ut_assertok(expo_send_key(exp, CTL_CH('g'))); + ut_asserteq(89, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + /* clear multiline mode, close the textedit with Enter (BKEY_SELECT) */ ted->obj.flags &= ~SCENEOF_MULTILINE; ut_assertok(expo_send_key(exp, BKEY_SELECT)); @@ -1743,7 +1753,7 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(OBJ_TEXTED, act.select.id); ut_assertok(scene_set_open(scn, act.select.id, false)); - /* check the textedit is closed and text is changed */ + /* check the textedit is closed and text is changed (redo restored latr) */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); ut_asserteq_str("This\ns the initial contents of the text " "editor but it is ely that more will be added latr\n", -- 2.43.0
participants (1)
-
Simon Glass