From patchwork Fri Jan 30 03:58:41 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1793 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org Authentication-Results: mail.u-boot.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=chromium.org header.i=@chromium.org header.a=rsa-sha256 header.s=google header.b=TswcVTf6; dkim-atps=neutral Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 0157F697CE for ; Thu, 29 Jan 2026 20:59:51 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 7awEjNCQ7LNW for ; Thu, 29 Jan 2026 20:59:50 -0700 (MST) Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 75B38697E0 for ; Thu, 29 Jan 2026 20:59:50 -0700 (MST) Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 5AE46697E0 for ; Thu, 29 Jan 2026 20:59:48 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 0EA-u38DSXWm for ; Thu, 29 Jan 2026 20:59:48 -0700 (MST) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.160.52; helo=mail-oa1-f52.google.com; envelope-from=sjg@chromium.org; receiver=u-boot.org Received: from mail-oa1-f52.google.com (mail-oa1-f52.google.com [209.85.160.52]) by mail.u-boot.org (Postfix) with ESMTPS id C6989697F3 for ; Thu, 29 Jan 2026 20:59:45 -0700 (MST) Received: by mail-oa1-f52.google.com with SMTP id 586e51a60fabf-409440b98b5so957552fac.2 for ; Thu, 29 Jan 2026 19:59:45 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chromium.org; s=google; t=1769745584; x=1770350384; darn=u-boot.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=XoYCvNg90jj7D6wBFqHg+UDBaMylMrnoBcdo3PnYr48=; b=TswcVTf6nT8KbCmzFb9qelINAdh2/CmH/Q3sDMn7G9wtNvcWP8czfBEqog0aFuCW9l 0X7eAL2NxJJzera65gz7xtF5uAvpDDXROBxWHtw6KQvTaJymn2z9uMPp8dC+MBd1LQyb LrM0FngXsrGtE4fv4c57UTuWL3JHHdz20CscU= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769745584; x=1770350384; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=XoYCvNg90jj7D6wBFqHg+UDBaMylMrnoBcdo3PnYr48=; b=dPCYTYvVQT41cHAolZ2Lzj2Fk4b85f9IeV6aMvbQO1seZamSgHnOHz9wNpnbWFVlUU sM+QDoUWR3c61qe/h5OXI8VeBKonKe1xgF6S0p0AjNptHtWIj24hDUvTJK4BTNGrFIMb rC/thlgst5Zk4UEVhC+oDInp6sGoBtS0N9NtsfAYcPnCiQ0kyoFmdk2YrpzlXQg27fbI 6fUvv4Th7C9TLMm+OC3fvP9/7Dju6rsxqHPrUIRWhHg7+1R9ADCq2CpAR0ekpdbFJsqd QHz05KZkfst8WvoEvd68Omt585j6cUyQyFOdLqeK4BgNjLAXwaPNJ6Z7Y/uN8bkBvF1P 3zQg== X-Gm-Message-State: AOJu0YyDefekjsYq3jNwCFsOWuvJPafCF7zy55lzc/TmfL5L2EiSJEOS Fv3cVt6t5FYAGc1pnkwgWPxlKh0V+xiNTBYlQYVJ8QeqdImDwIIVyI3s1CHIdhzTJGkOKxxS+d2 Oqf8snQ== X-Gm-Gg: AZuq6aKIqb96smH/oqiPHKdYjjiyf3V9QeLXiNuVYdStJLNyJINftP34rO43PHHK40m OS/vQFoIrSXkiNNcwCrvz9yEyg9NQB5/4uL8b2Z2ENEU7ebkDVYXuacFJihiQOoXwr+hoErBzMm PlVfzjrHZRIY7glczHxhuaX5ZH7k1wayUE9OKWAd87AY3N757BN2L9Guzychq1TUJ0plQzJphPp BOxkicu2ha5zxXuAlXz/y44eL15P1DXJ53uUwaY64gXu2rMHfXzQuLO20OA5gEfLeqbQPJRvE1B YLxFdFYjRk+hk3tSuZwmvC24nit6mredkdFuRG+bMp+fkl0QF26Tn8SucuS0QLNZ1ZYLTQpje+K MJo0E6UxiuZGs9R4XUWrKAuyNku6+ako4AiKu+du3Y2zsAPqIc98wB7sYtlJZDbAOjsNHy76hNW HgnJv+Rcrx3QCBTNHC X-Received: by 2002:a05:6820:1f12:b0:662:fe9f:2259 with SMTP id 006d021491bc7-6630f36eabfmr836935eaf.50.1769745584462; Thu, 29 Jan 2026 19:59:44 -0800 (PST) Received: from chromium.org ([73.34.74.121]) by smtp.gmail.com with ESMTPSA id 006d021491bc7-662f9a4e491sm4128687eaf.16.2026.01.29.19.59.42 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 29 Jan 2026 19:59:43 -0800 (PST) From: Simon Glass X-Google-Original-From: Simon Glass To: U-Boot Concept Date: Thu, 29 Jan 2026 20:58:41 -0700 Message-ID: <20260130035849.3580212-19-simon.glass@canonical.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260130035849.3580212-1-simon.glass@canonical.com> References: <20260130035849.3580212-1-simon.glass@canonical.com> MIME-Version: 1.0 Message-ID-Hash: QYUTIXS6HR6EMEVB52DUFRSZ63KRRCZL X-Message-ID-Hash: QYUTIXS6HR6EMEVB52DUFRSZ63KRRCZL X-MailFrom: sjg@chromium.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , "Claude Opus 4 . 5" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 18/19] cli: Add Ctrl+Y to yank killed text List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 Signed-off-by: Simon Glass --- 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 + */ + +#include +#include +#include +#include +#include + +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 #include #include @@ -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);